فهرست منبع

fix merger conflicts

Abhishek Kondur 2 سال پیش
والد
کامیت
7af9751bdf
89فایلهای تغییر یافته به همراه2569 افزوده شده و 2039 حذف شده
  1. 4 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 58 0
      .github/workflows/branchtest.yml
  3. 38 0
      .github/workflows/deletedroplets.yml
  4. 2 1
      Dockerfile
  5. 1 1
      README.md
  6. 44 44
      auth/nodecallback.go
  7. 2 7
      auth/nodesession.go
  8. 20 14
      cli/cmd/acl/list.go
  9. 9 0
      cli/cmd/commons/globals.go
  10. 11 5
      cli/cmd/dns/list.go
  11. 11 5
      cli/cmd/ext_client/list.go
  12. 0 35
      cli/cmd/keys/create.go
  13. 0 23
      cli/cmd/keys/delete.go
  14. 0 20
      cli/cmd/keys/list.go
  15. 0 28
      cli/cmd/keys/root.go
  16. 13 7
      cli/cmd/network/list.go
  17. 19 13
      cli/cmd/node/list.go
  18. 2 2
      cli/cmd/root.go
  19. 12 5
      cli/cmd/user/list.go
  20. 0 23
      cli/functions/keys.go
  21. 3 2
      compose/docker-compose-emqx.yml
  22. 1 0
      compose/docker-compose.ee.yml
  23. 17 0
      compose/docker-compose.netclient.yml
  24. 1 0
      compose/docker-compose.reference.yml
  25. 2 0
      compose/docker-compose.yml
  26. 54 46
      config/config.go
  27. 1 0
      controllers/controller.go
  28. 1 25
      controllers/docs.go
  29. 29 3
      controllers/enrollmentkeys.go
  30. 27 16
      controllers/ext_client.go
  31. 49 0
      controllers/hosts.go
  32. 35 0
      controllers/legacy.go
  33. 47 38
      controllers/migrate.go
  34. 1 126
      controllers/network.go
  35. 3 116
      controllers/network_test.go
  36. 22 269
      controllers/node.go
  37. 2 2
      controllers/node_test.go
  38. 4 1
      controllers/regex.go
  39. 6 7
      controllers/relay.go
  40. 1 1
      controllers/user.go
  41. 4 0
      controllers/user_test.go
  42. 3 0
      database/database.go
  43. 1 1
      docker/Caddyfile
  44. 3 0
      ee/initialize.go
  45. 41 0
      ee/logic/ext_acls.go
  46. 2 2
      ee/types.go
  47. 12 21
      functions/helpers_test.go
  48. 6 5
      go.mod
  49. 18 13
      go.sum
  50. 1 1
      k8s/client/netclient-daemonset.yaml
  51. 1 1
      k8s/client/netclient.yaml
  52. 1 1
      k8s/server/netmaker-server.yaml
  53. 1 1
      k8s/server/netmaker-ui.yaml
  54. 0 229
      logic/accesskeys.go
  55. 0 21
      logic/accesskeys_test.go
  56. 53 0
      logic/clients.go
  57. 42 6
      logic/extpeers.go
  58. 20 1
      logic/host_test.go
  59. 34 16
      logic/hostactions/hostactions.go
  60. 24 12
      logic/hosts.go
  61. 2 2
      logic/jwts.go
  62. 46 0
      logic/legacy.go
  63. 2 1
      logic/metrics/metrics.go
  64. 4 2
      logic/networks.go
  65. 20 3
      logic/nodes.go
  66. 264 170
      logic/peers.go
  67. 3 1
      logic/pro/networkuser_test.go
  68. 11 1
      logic/telemetry.go
  69. 1 1
      main.go
  70. 2 0
      models/api_host.go
  71. 6 0
      models/enrollment_key.go
  72. 13 12
      models/extclient.go
  73. 36 2
      models/host.go
  74. 25 14
      models/metrics.go
  75. 2 3
      models/migrate.go
  76. 13 11
      models/mqtt.go
  77. 0 1
      models/network.go
  78. 1 0
      models/proxy.go
  79. 16 12
      models/structs.go
  80. 237 0
      mq/emqx.go
  81. 101 90
      mq/handlers.go
  82. 14 8
      mq/mq.go
  83. 42 15
      mq/publishers.go
  84. 8 0
      mq/util.go
  85. 19 19
      release.md
  86. 525 359
      scripts/nm-quick.sh
  87. 309 95
      scripts/nm-upgrade.sh
  88. 27 0
      servercfg/serverconf.go
  89. 1 1
      swagger.yaml

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

@@ -31,6 +31,10 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.18.6
+        - v0.18.5
+        - v0.18.4
+        - v0.18.3
         - v0.18.2
         - v0.18.1
         - v0.18.0

+ 58 - 0
.github/workflows/branchtest.yml

@@ -0,0 +1,58 @@
+name: Deploy and Test Branch
+
+on:
+  workflow_dispatch:
+  pull_request:
+      types: [opened, synchronize, reopened]
+      branches: [develop]
+
+jobs:
+  skip-check:
+    runs-on: ubuntu-latest
+    outputs:
+      skip: ${{ steps.check.outputs.skip }}
+    steps:
+      - id: skip
+        uses: fkirc/skip-duplicate-actions@v5
+        with:
+          concurrent_skipping: 'always'
+  getbranch:
+    runs-on: ubuntu-latest
+    needs: skip-check
+    if: ${{ needs.skip-check.outputs.skip != 'true' }}
+    outputs:
+      netclientbranch: ${{ steps.checkbranch.outputs.netclientbranch }}
+    steps:
+      - name: checkout
+        uses: actions/checkout@v3
+        with:
+          repository: gravitl/netclient
+          ref: develop
+      - name: check if branch exists
+        id: checkbranch
+        run: |
+          if git show-ref ${{ github.head_ref}}; then
+            echo branch exists
+            echo "netclientbranch=${{ github.head_ref }}" >> $GITHUB_OUTPUT
+          else
+            echo branch does not exist
+            echo "netclientbranch=develop" >> $GITHUB_OUTPUT
+          fi
+  
+  terraform:
+    needs: getbranch
+    uses: gravitl/devops/.github/workflows/terraform.yml@master
+    with:
+      netmakerbranch: ${{ github.head_ref }}
+      netclientbranch: ${{ needs.getbranch.outputs.netclientbranch }}
+    secrets: inherit
+
+
+  testbranch:
+    needs: [getbranch, terraform]
+    uses: gravitl/devops/.github/workflows/branchtest.yml@master
+    with:
+      tag: ${{ github.run_id }}-${{ github.run_attempt }}
+      network: terraform
+    secrets: inherit
+    

+ 38 - 0
.github/workflows/deletedroplets.yml

@@ -0,0 +1,38 @@
+name: Delete Droplets
+
+on:
+  workflow_run:
+    workflows: [Deploy and Test Branch]
+    types:
+      - completed
+
+jobs:
+  on-success:
+    runs-on: ubuntu-latest
+    if: ${{ github.event.workflow_run.conclusion == 'success' }}
+    steps:
+      - name: delete droplets
+        run: |
+          sleep 15m
+          curl -X GET \
+            -H "Content-Type: application/json" \
+            -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
+            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG"
+        env:
+          DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
+          TAG: ${{ github.event.workflow_run.run_id }}-${{ github.event.workflow_run.run_attempt }}
+
+  on-failure:
+    runs-on: ubuntu-latest
+    if: ${{ github.event.workflow_run.conclusion == 'failure' }}
+    steps:
+      - name: delete droplets
+        run: |
+          sleep 6h
+          curl -X GET \
+            -H "Content-Type: application/json" \
+            -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
+            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG"
+        env:
+          DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
+          TAG: ${{ github.event.workflow_run.run_id }}-${{ github.event.workflow_run.run_attempt }}

+ 2 - 1
Dockerfile

@@ -4,10 +4,11 @@ ARG tags
 WORKDIR /app
 COPY . .
 
-RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w" -tags ${tags} .
+RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
 FROM alpine:3.16.2
 
+# add a c lib
 # set the working directory
 WORKDIR /root/
 RUN mkdir -p /etc/netclient/config

+ 1 - 1
README.md

@@ -17,7 +17,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.18.2-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.18.6-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" />

+ 44 - 44
auth/nodecallback.go

@@ -106,12 +106,12 @@ func HandleNodeSSOCallback(w http.ResponseWriter, r *http.Request) {
 	var answer string
 	// The registation logic is starting here:
 	// we request access key with 1 use for the required network
-	accessToken, err := requestAccessKey(reqKeyIf.Network, 1, userClaims.getUserName())
-	if err != nil {
-		answer = fmt.Sprintf("Error from the netmaker controller %s", err.Error())
-	} else {
-		answer = fmt.Sprintf("AccessToken: %s", accessToken)
-	}
+	// accessToken, err := requestAccessKey(reqKeyIf.Network, 1, userClaims.getUserName())
+	// if err != nil {
+	// 	answer = fmt.Sprintf("Error from the netmaker controller %s", err.Error())
+	// } else {
+	// 	answer = fmt.Sprintf("AccessToken: %s", accessToken)
+	// }
 	logger.Log(0, "Updating the token for the client request ... ")
 	// Give the user the access token via Pass in the DB
 	reqKeyIf.Pass = answer
@@ -184,44 +184,44 @@ func RegisterNodeSSO(w http.ResponseWriter, r *http.Request) {
 
 // == private ==
 // API to create an access key for a given network with a given name
-func requestAccessKey(network string, uses int, name string) (accessKey string, err error) {
-
-	var sAccessKey models.AccessKey
-	var sNetwork models.Network
-
-	sNetwork, err = logic.GetParentNetwork(network)
-	if err != nil {
-		logger.Log(0, "err calling GetParentNetwork API=%s", err.Error())
-		return "", fmt.Errorf("internal controller error %s", err.Error())
-	}
-	// If a key already exists, we recreate it.
-	// @TODO Is that a preferred handling ? We could also trying to re-use.
-	// can happen if user started log in but did not finish
-	for _, currentkey := range sNetwork.AccessKeys {
-		if currentkey.Name == name {
-			logger.Log(0, "erasing existing AccessKey for: ", name)
-			err = logic.DeleteKey(currentkey.Name, network)
-			if err != nil {
-				logger.Log(0, "err calling CreateAccessKey API ", err.Error())
-				return "", fmt.Errorf("key already exists. Contact admin to resolve")
-			}
-			break
-		}
-	}
-	// Only one usage is needed - for the next time new access key will be required
-	// it will be created next time after another IdP approval
-	sAccessKey.Uses = 1
-	sAccessKey.Name = name
-
-	accessToken, err := logic.CreateAccessKey(sAccessKey, sNetwork)
-	if err != nil {
-		logger.Log(0, "err calling CreateAccessKey API ", err.Error())
-		return "", fmt.Errorf("error from the netmaker controller %s", err.Error())
-	} else {
-		logger.Log(1, "created access key", sAccessKey.Name, "on", network)
-	}
-	return accessToken.AccessString, nil
-}
+// func requestAccessKey(network string, uses int, name string) (accessKey string, err error) {
+
+// 	var sAccessKey models.AccessKey
+// 	var sNetwork models.Network
+
+// 	sNetwork, err = logic.GetParentNetwork(network)
+// 	if err != nil {
+// 		logger.Log(0, "err calling GetParentNetwork API=%s", err.Error())
+// 		return "", fmt.Errorf("internal controller error %s", err.Error())
+// 	}
+// 	// If a key already exists, we recreate it.
+// 	// @TODO Is that a preferred handling ? We could also trying to re-use.
+// 	// can happen if user started log in but did not finish
+// 	for _, currentkey := range sNetwork.AccessKeys {
+// 		if currentkey.Name == name {
+// 			logger.Log(0, "erasing existing AccessKey for: ", name)
+// 			err = logic.DeleteKey(currentkey.Name, network)
+// 			if err != nil {
+// 				logger.Log(0, "err calling CreateAccessKey API ", err.Error())
+// 				return "", fmt.Errorf("key already exists. Contact admin to resolve")
+// 			}
+// 			break
+// 		}
+// 	}
+// 	// Only one usage is needed - for the next time new access key will be required
+// 	// it will be created next time after another IdP approval
+// 	sAccessKey.Uses = 1
+// 	sAccessKey.Name = name
+
+// 	accessToken, err := logic.CreateAccessKey(sAccessKey, sNetwork)
+// 	if err != nil {
+// 		logger.Log(0, "err calling CreateAccessKey API ", err.Error())
+// 		return "", fmt.Errorf("error from the netmaker controller %s", err.Error())
+// 	} else {
+// 		logger.Log(1, "created access key", sAccessKey.Name, "on", network)
+// 	}
+// 	return accessToken.AccessString, nil
+// }
 
 func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) {
 

+ 2 - 7
auth/nodesession.go

@@ -85,7 +85,7 @@ func SessionHandler(conn *websocket.Conn) {
 			}
 			return
 		}
-		user, err := isUserIsAllowed(loginMessage.User, loginMessage.Network, false)
+		_, err = isUserIsAllowed(loginMessage.User, loginMessage.Network, false)
 		if err != nil {
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
@@ -93,12 +93,7 @@ func SessionHandler(conn *websocket.Conn) {
 			}
 			return
 		}
-		accessToken, err := requestAccessKey(loginMessage.Network, 1, user.UserName)
-		if err != nil {
-			req.Pass = fmt.Sprintf("Error from the netmaker controller %s", err.Error())
-		} else {
-			req.Pass = fmt.Sprintf("AccessToken: %s", accessToken)
-		}
+
 		// Give the user the access token via Pass in the DB
 		if err = netcache.Set(stateStr, req); err != nil {
 			logger.Log(0, "machine failed to complete join on network,", loginMessage.Network, "-", err.Error())

+ 20 - 14
cli/cmd/acl/list.go

@@ -3,6 +3,7 @@ package acl
 import (
 	"os"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/guumaster/tablewriter"
@@ -16,23 +17,28 @@ var aclListCmd = &cobra.Command{
 	Long:  `List all ACLs associated with a network`,
 	Run: func(cmd *cobra.Command, args []string) {
 		aclSource := (map[acls.AclID]acls.ACL)(*functions.GetACL(args[0]))
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"From", "To", "Status"})
-		for id, acl := range aclSource {
-			for k, v := range (map[acls.AclID]byte)(acl) {
-				row := []string{string(id), string(k)}
-				switch v {
-				case acls.NotAllowed:
-					row = append(row, "Not Allowed")
-				case acls.NotPresent:
-					row = append(row, "Not Present")
-				case acls.Allowed:
-					row = append(row, "Allowed")
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(aclSource)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"From", "To", "Status"})
+			for id, acl := range aclSource {
+				for k, v := range (map[acls.AclID]byte)(acl) {
+					row := []string{string(id), string(k)}
+					switch v {
+					case acls.NotAllowed:
+						row = append(row, "Not Allowed")
+					case acls.NotPresent:
+						row = append(row, "Not Present")
+					case acls.Allowed:
+						row = append(row, "Allowed")
+					}
+					table.Append(row)
 				}
-				table.Append(row)
 			}
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 9 - 0
cli/cmd/commons/globals.go

@@ -0,0 +1,9 @@
+package commons
+
+// OutputFormat flag defines the output format to stdout (Enum:- json)
+var OutputFormat string
+
+const (
+	// JsonOutput refers to json format output to stdout
+	JsonOutput = "json"
+)

+ 11 - 5
cli/cmd/dns/list.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/guumaster/tablewriter"
@@ -31,12 +32,17 @@ var dnsListCmd = &cobra.Command{
 		} else {
 			data = *functions.GetDNS()
 		}
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"Name", "Network", "IPv4 Address", "IPv6 Address"})
-		for _, d := range data {
-			table.Append([]string{d.Name, d.Network, d.Address, d.Address6})
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"Name", "Network", "IPv4 Address", "IPv6 Address"})
+			for _, d := range data {
+				table.Append([]string{d.Name, d.Network, d.Address, d.Address6})
+			}
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 11 - 5
cli/cmd/ext_client/list.go

@@ -5,6 +5,7 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/guumaster/tablewriter"
@@ -25,12 +26,17 @@ var extClientListCmd = &cobra.Command{
 		} else {
 			data = *functions.GetAllExtClients()
 		}
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"Client ID", "Network", "IPv4 Address", "IPv6 Address", "Enabled", "Last Modified"})
-		for _, d := range data {
-			table.Append([]string{d.ClientID, d.Network, d.Address, d.Address6, strconv.FormatBool(d.Enabled), time.Unix(d.LastModified, 0).String()})
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"Client ID", "Network", "IPv4 Address", "IPv6 Address", "Enabled", "Last Modified"})
+			for _, d := range data {
+				table.Append([]string{d.ClientID, d.Network, d.Address, d.Address6, strconv.FormatBool(d.Enabled), time.Unix(d.LastModified, 0).String()})
+			}
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 0 - 35
cli/cmd/keys/create.go

@@ -1,35 +0,0 @@
-package keys
-
-import (
-	"log"
-	"strconv"
-
-	"github.com/gravitl/netmaker/cli/functions"
-	"github.com/gravitl/netmaker/models"
-	"github.com/spf13/cobra"
-)
-
-var keyName string
-
-var keysCreateCmd = &cobra.Command{
-	Use:   "create [NETWORK NAME] [NUM USES]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Create an access key",
-	Long:  `Create an access key`,
-	Run: func(cmd *cobra.Command, args []string) {
-		keyUses, err := strconv.ParseInt(args[1], 10, 64)
-		if err != nil {
-			log.Fatal(err)
-		}
-		key := &models.AccessKey{Uses: int(keyUses)}
-		if keyName != "" {
-			key.Name = keyName
-		}
-		functions.PrettyPrint(functions.CreateKey(args[0], key))
-	},
-}
-
-func init() {
-	keysCreateCmd.Flags().StringVar(&keyName, "name", "", "Name of the key")
-	rootCmd.AddCommand(keysCreateCmd)
-}

+ 0 - 23
cli/cmd/keys/delete.go

@@ -1,23 +0,0 @@
-package keys
-
-import (
-	"fmt"
-
-	"github.com/gravitl/netmaker/cli/functions"
-	"github.com/spf13/cobra"
-)
-
-var keysDeleteCmd = &cobra.Command{
-	Use:   "delete [NETWORK NAME] [KEY NAME]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Delete a key",
-	Long:  `Delete a key`,
-	Run: func(cmd *cobra.Command, args []string) {
-		functions.DeleteKey(args[0], args[1])
-		fmt.Println("Success")
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(keysDeleteCmd)
-}

+ 0 - 20
cli/cmd/keys/list.go

@@ -1,20 +0,0 @@
-package keys
-
-import (
-	"github.com/gravitl/netmaker/cli/functions"
-	"github.com/spf13/cobra"
-)
-
-var keysListCmd = &cobra.Command{
-	Use:   "list [NETWORK NAME]",
-	Args:  cobra.ExactArgs(1),
-	Short: "List all keys associated with a network",
-	Long:  `List all keys associated with a network`,
-	Run: func(cmd *cobra.Command, args []string) {
-		functions.PrettyPrint(functions.GetKeys(args[0]))
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(keysListCmd)
-}

+ 0 - 28
cli/cmd/keys/root.go

@@ -1,28 +0,0 @@
-package keys
-
-import (
-	"os"
-
-	"github.com/spf13/cobra"
-)
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "keys",
-	Short: "Manage access keys associated with a network",
-	Long:  `Manage access keys associated with a network`,
-}
-
-// GetRoot returns the root subcommand
-func GetRoot() *cobra.Command {
-	return rootCmd
-}
-
-// Execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	err := rootCmd.Execute()
-	if err != nil {
-		os.Exit(1)
-	}
-}

+ 13 - 7
cli/cmd/network/list.go

@@ -4,6 +4,7 @@ import (
 	"os"
 	"time"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/olekukonko/tablewriter"
 	"github.com/spf13/cobra"
@@ -16,14 +17,19 @@ var networkListCmd = &cobra.Command{
 	Args:  cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		networks := functions.GetNetworks()
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"NetId", "Address Range (IPv4)", "Address Range (IPv6)", "Network Last Modified", "Nodes Last Modified"})
-		for _, n := range *networks {
-			networkLastModified := time.Unix(n.NetworkLastModified, 0).Format(time.RFC3339)
-			nodesLastModified := time.Unix(n.NodesLastModified, 0).Format(time.RFC3339)
-			table.Append([]string{n.NetID, n.AddressRange, n.AddressRange6, networkLastModified, nodesLastModified})
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(networks)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"NetId", "Address Range (IPv4)", "Address Range (IPv6)", "Network Last Modified", "Nodes Last Modified"})
+			for _, n := range *networks {
+				networkLastModified := time.Unix(n.NetworkLastModified, 0).Format(time.RFC3339)
+				nodesLastModified := time.Unix(n.NodesLastModified, 0).Format(time.RFC3339)
+				table.Append([]string{n.NetID, n.AddressRange, n.AddressRange6, networkLastModified, nodesLastModified})
+			}
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 19 - 13
cli/cmd/node/list.go

@@ -4,6 +4,7 @@ import (
 	"os"
 	"strconv"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/guumaster/tablewriter"
@@ -23,23 +24,28 @@ var nodeListCmd = &cobra.Command{
 		} else {
 			data = *functions.GetNodes()
 		}
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Ingress", "Relay"})
-		for _, d := range data {
-			addresses := ""
-			if d.Address != "" {
-				addresses += d.Address
-			}
-			if d.Address6 != "" {
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Ingress", "Relay"})
+			for _, d := range data {
+				addresses := ""
 				if d.Address != "" {
-					addresses += ", "
+					addresses += d.Address
+				}
+				if d.Address6 != "" {
+					if d.Address != "" {
+						addresses += ", "
+					}
+					addresses += d.Address6
 				}
-				addresses += d.Address6
+				table.Append([]string{d.ID, addresses, d.Network,
+					strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay)})
 			}
-			table.Append([]string{d.ID, addresses, d.Network,
-				strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay)})
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 2 - 2
cli/cmd/root.go

@@ -4,12 +4,12 @@ import (
 	"os"
 
 	"github.com/gravitl/netmaker/cli/cmd/acl"
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/context"
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/host"
-	"github.com/gravitl/netmaker/cli/cmd/keys"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/network_user"
@@ -42,10 +42,10 @@ func Execute() {
 }
 
 func init() {
+	rootCmd.PersistentFlags().StringVarP(&commons.OutputFormat, "output", "o", "", "List output in specific format (Enum:- json)")
 	// Bind subcommands here
 	rootCmd.AddCommand(network.GetRoot())
 	rootCmd.AddCommand(context.GetRoot())
-	rootCmd.AddCommand(keys.GetRoot())
 	rootCmd.AddCommand(acl.GetRoot())
 	rootCmd.AddCommand(node.GetRoot())
 	rootCmd.AddCommand(dns.GetRoot())

+ 12 - 5
cli/cmd/user/list.go

@@ -5,6 +5,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/guumaster/tablewriter"
 	"github.com/spf13/cobra"
@@ -16,12 +17,18 @@ var userListCmd = &cobra.Command{
 	Short: "List all users",
 	Long:  `List all users`,
 	Run: func(cmd *cobra.Command, args []string) {
-		table := tablewriter.NewWriter(os.Stdout)
-		table.SetHeader([]string{"Name", "Admin", "Networks", "Groups"})
-		for _, d := range *functions.ListUsers() {
-			table.Append([]string{d.UserName, strconv.FormatBool(d.IsAdmin), strings.Join(d.Networks, ", "), strings.Join(d.Groups, ", ")})
+		data := functions.ListUsers()
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			table.SetHeader([]string{"Name", "Admin", "Networks", "Groups"})
+			for _, d := range *data {
+				table.Append([]string{d.UserName, strconv.FormatBool(d.IsAdmin), strings.Join(d.Networks, ", "), strings.Join(d.Groups, ", ")})
+			}
+			table.Render()
 		}
-		table.Render()
 	},
 }
 

+ 0 - 23
cli/functions/keys.go

@@ -1,23 +0,0 @@
-package functions
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/gravitl/netmaker/models"
-)
-
-// GetKeys - fetch all access keys of a network
-func GetKeys(networkName string) *[]models.AccessKey {
-	return request[[]models.AccessKey](http.MethodGet, fmt.Sprintf("/api/networks/%s/keys", networkName), nil)
-}
-
-// CreateKey - create an access key
-func CreateKey(networkName string, key *models.AccessKey) *models.AccessKey {
-	return request[models.AccessKey](http.MethodPost, fmt.Sprintf("/api/networks/%s/keys", networkName), key)
-}
-
-// DeleteKey - delete an access key
-func DeleteKey(networkName, keyName string) {
-	request[string](http.MethodDelete, fmt.Sprintf("/api/networks/%s/keys/%s", networkName, keyName), nil)
-}

+ 3 - 2
compose/docker-compose-emqx.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.18.2
+    image: gravitl/netmaker:v0.18.6
     restart: always
     volumes:
       - dnsconfig:/root/config/dnsconfig
@@ -30,11 +30,12 @@ services:
       VERBOSITY: "1"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
+      DEFAULT_PROXY_MODE: "off"
     ports:
       - "3478:3478/udp"
   netmaker-ui:
     container_name: netmaker-ui
-    image: gravitl/netmaker-ui:v0.18.2
+    image: gravitl/netmaker-ui:v0.18.6
     depends_on:
       - netmaker
     links:

+ 1 - 0
compose/docker-compose.ee.yml

@@ -33,6 +33,7 @@ services:
       METRICS_EXPORTER: "on"
       LICENSE_KEY: "YOUR_LICENSE_KEY"
       NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID"
+      DEFAULT_PROXY_MODE: "off"
     ports:
       - "3478:3478/udp"
   netmaker-ui:

+ 17 - 0
compose/docker-compose.netclient.yml

@@ -0,0 +1,17 @@
+version: "3.4"
+
+services:
+  netclient:
+    container_name: netclient
+    image: 'gravitl/netclient:v0.18.6'
+    hostname: netmaker-1
+    network_mode: host
+    restart: always
+    environment:
+      TOKEN: "TOKEN_VALUE"
+    volumes:
+      - /etc/netclient:/etc/netclient
+    cap_add:
+      - NET_ADMIN
+      - NET_RAW
+      - SYS_MODULE

+ 1 - 0
compose/docker-compose.reference.yml

@@ -38,6 +38,7 @@ services:
       FRONTEND_URL: "" # "https://dashboard.<netmaker base domain>"
       AZURE_TENANT: "" # "<only for azure, you may optionally specify the tenant for the OAuth>"
       OIDC_ISSUER: "" # https://oidc.yourprovider.com - URL of oidc provider
+      DEFAULT_PROXY_MODE: "off" # if ON, all new clients will enable proxy by default if OFF, all new clients will disable proxy by default, if AUTO, stick with the existing logic for NAT detection
     ports:
       - "3478:3478/udp" # the stun port
   netmaker-ui:  # The Netmaker UI Component

+ 2 - 0
compose/docker-compose.yml

@@ -27,6 +27,8 @@ services:
       VERBOSITY: "1"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
+      STUN_PORT: "3478"
+      DEFAULT_PROXY_MODE: "off"
     ports:
       - "3478:3478/udp"
   netmaker-ui:

+ 54 - 46
config/config.go

@@ -32,52 +32,60 @@ type EnvironmentConfig struct {
 
 // ServerConfig - server conf struct
 type ServerConfig struct {
-	CoreDNSAddr          string `yaml:"corednsaddr"`
-	APIConnString        string `yaml:"apiconn"`
-	APIHost              string `yaml:"apihost"`
-	APIPort              string `yaml:"apiport"`
-	Broker               string `yam:"broker"`
-	ServerBrokerEndpoint string `yaml:"serverbrokerendpoint"`
-	BrokerType           string `yaml:"brokertype"`
-	EmqxRestEndpoint     string `yaml:"emqxrestendpoint"`
-	MasterKey            string `yaml:"masterkey"`
-	DNSKey               string `yaml:"dnskey"`
-	AllowedOrigin        string `yaml:"allowedorigin"`
-	NodeID               string `yaml:"nodeid"`
-	RestBackend          string `yaml:"restbackend"`
-	MessageQueueBackend  string `yaml:"messagequeuebackend"`
-	DNSMode              string `yaml:"dnsmode"`
-	DisableRemoteIPCheck string `yaml:"disableremoteipcheck"`
-	Version              string `yaml:"version"`
-	SQLConn              string `yaml:"sqlconn"`
-	Platform             string `yaml:"platform"`
-	Database             string `yaml:"database"`
-	Verbosity            int32  `yaml:"verbosity"`
-	AuthProvider         string `yaml:"authprovider"`
-	OIDCIssuer           string `yaml:"oidcissuer"`
-	ClientID             string `yaml:"clientid"`
-	ClientSecret         string `yaml:"clientsecret"`
-	FrontendURL          string `yaml:"frontendurl"`
-	DisplayKeys          string `yaml:"displaykeys"`
-	AzureTenant          string `yaml:"azuretenant"`
-	Telemetry            string `yaml:"telemetry"`
-	HostNetwork          string `yaml:"hostnetwork"`
-	Server               string `yaml:"server"`
-	PublicIPService      string `yaml:"publicipservice"`
-	MQPassword           string `yaml:"mqpassword"`
-	MQUserName           string `yaml:"mqusername"`
-	MetricsExporter      string `yaml:"metrics_exporter"`
-	BasicAuth            string `yaml:"basic_auth"`
-	LicenseValue         string `yaml:"license_value"`
-	NetmakerAccountID    string `yaml:"netmaker_account_id"`
-	IsEE                 string `yaml:"is_ee"`
-	StunPort             int    `yaml:"stun_port"`
-	StunList             string `yaml:"stun_list"`
-	UsersLimit           int    `yaml:"user_limit"`
-	ClientsLimit         int    `yaml:"client_limit"`
-	NetworksLimit        int    `yaml:"network_limit"`
-	HostsLimit           int    `yaml:"host_limit"`
-	DeployedByOperator   bool   `yaml:"deployed_by_operator"`
+	CoreDNSAddr          string    `yaml:"corednsaddr"`
+	APIConnString        string    `yaml:"apiconn"`
+	APIHost              string    `yaml:"apihost"`
+	APIPort              string    `yaml:"apiport"`
+	Broker               string    `yam:"broker"`
+	ServerBrokerEndpoint string    `yaml:"serverbrokerendpoint"`
+	BrokerType           string    `yaml:"brokertype"`
+	EmqxRestEndpoint     string    `yaml:"emqxrestendpoint"`
+	MasterKey            string    `yaml:"masterkey"`
+	DNSKey               string    `yaml:"dnskey"`
+	AllowedOrigin        string    `yaml:"allowedorigin"`
+	NodeID               string    `yaml:"nodeid"`
+	RestBackend          string    `yaml:"restbackend"`
+	MessageQueueBackend  string    `yaml:"messagequeuebackend"`
+	DNSMode              string    `yaml:"dnsmode"`
+	DisableRemoteIPCheck string    `yaml:"disableremoteipcheck"`
+	Version              string    `yaml:"version"`
+	SQLConn              string    `yaml:"sqlconn"`
+	Platform             string    `yaml:"platform"`
+	Database             string    `yaml:"database"`
+	Verbosity            int32     `yaml:"verbosity"`
+	AuthProvider         string    `yaml:"authprovider"`
+	OIDCIssuer           string    `yaml:"oidcissuer"`
+	ClientID             string    `yaml:"clientid"`
+	ClientSecret         string    `yaml:"clientsecret"`
+	FrontendURL          string    `yaml:"frontendurl"`
+	DisplayKeys          string    `yaml:"displaykeys"`
+	AzureTenant          string    `yaml:"azuretenant"`
+	Telemetry            string    `yaml:"telemetry"`
+	HostNetwork          string    `yaml:"hostnetwork"`
+	Server               string    `yaml:"server"`
+	PublicIPService      string    `yaml:"publicipservice"`
+	MQPassword           string    `yaml:"mqpassword"`
+	MQUserName           string    `yaml:"mqusername"`
+	MetricsExporter      string    `yaml:"metrics_exporter"`
+	BasicAuth            string    `yaml:"basic_auth"`
+	LicenseValue         string    `yaml:"license_value"`
+	NetmakerAccountID    string    `yaml:"netmaker_account_id"`
+	IsEE                 string    `yaml:"is_ee"`
+	StunPort             int       `yaml:"stun_port"`
+	StunList             string    `yaml:"stun_list"`
+	Proxy                string    `yaml:"proxy"`
+	DefaultProxyMode     ProxyMode `yaml:"defaultproxymode"`
+	UsersLimit           int       `yaml:"user_limit"`
+	ClientsLimit         int       `yaml:"client_limit"`
+	NetworksLimit        int       `yaml:"network_limit"`
+	HostsLimit           int       `yaml:"host_limit"`
+	DeployedByOperator   bool      `yaml:"deployed_by_operator"`
+}
+
+// ProxyMode - default proxy mode for server
+type ProxyMode struct {
+	Set   bool
+	Value bool
 }
 
 // SQLConfig - Generic SQL Config

+ 1 - 0
controllers/controller.go

@@ -27,6 +27,7 @@ var HttpHandlers = []interface{}{
 	loggerHandlers,
 	hostHandlers,
 	enrollmentKeyHandlers,
+	legacyHandlers,
 }
 
 // HandleRESTRequests - handles the rest requests

+ 1 - 25
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.18.2
+//	Version: 0.18.6
 //	Host: netmaker.io
 //
 //	Consumes:
@@ -203,27 +203,6 @@ type networkBodyResponse struct {
 	Network models.Network `json:"network"`
 }
 
-// swagger:parameters createAccessKey
-type accessKeyBodyParam struct {
-	// Access Key
-	// in: body
-	AccessKey models.AccessKey `json:"access_key"`
-}
-
-// swagger:response accessKeyBodyResponse
-type accessKeyBodyResponse struct {
-	// Access Key
-	// in: body
-	AccessKey models.AccessKey `json:"access_key"`
-}
-
-// swagger:response accessKeySliceBodyResponse
-type accessKeySliceBodyResponse struct {
-	// Access Keys
-	// in: body
-	AccessKey []models.AccessKey `json:"access_key"`
-}
-
 // swagger:parameters updateNetworkACL getNetworkACL
 type aclContainerBodyParam struct {
 	// ACL Container
@@ -373,9 +352,6 @@ func useUnused() bool {
 	_ = networkPathParam{}
 	_ = networkAccessKeyNamePathParam{}
 	_ = networkBodyResponse{}
-	_ = accessKeyBodyParam{}
-	_ = accessKeyBodyResponse{}
-	_ = accessKeySliceBodyResponse{}
 	_ = aclContainerBodyParam{}
 	_ = aclContainerResponse{}
 	_ = nodeSliceResponse{}

+ 29 - 3
controllers/enrollmentkeys.go

@@ -160,8 +160,13 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// version check
-	if !logic.IsVersionComptatible(newHost.Version) || newHost.TrafficKeyPublic == nil {
-		err := fmt.Errorf("incompatible netclient")
+	if !logic.IsVersionComptatible(newHost.Version) {
+		err := fmt.Errorf("bad client version on register: %s", newHost.Version)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if newHost.TrafficKeyPublic == nil && newHost.OS != models.OS_Types.IoT {
+		err := fmt.Errorf("missing traffic key")
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
@@ -177,9 +182,21 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid enrollment key"), "badrequest"))
 		return
 	}
+	hostPass := newHost.HostPass
 	if !hostExists {
 		// 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 {
+				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 {
+				logger.Log(0, "failed to add host ACL rules to EMQX: ", err.Error())
+				return
+			}
+		}
 		if err = logic.CreateHost(&newHost); err != nil {
 			logger.Log(0, "host", newHost.ID.String(), newHost.Name, "failed registration -", err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -200,9 +217,18 @@ 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,
+	}
 	logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
 	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(&server)
+	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
 	go checkNetRegAndHostUpdate(enrollmentKey.Networks, &newHost)
 }

+ 27 - 16
controllers/ext_client.go

@@ -17,6 +17,7 @@ import (
 	"github.com/gravitl/netmaker/models/promodels"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/skip2/go-qrcode"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
 func extClientHandlers(r *mux.Router) {
@@ -214,7 +215,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if network.DefaultKeepalive != 0 {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 	}
-	gwendpoint := host.EndpointIP.String() + ":" + strconv.Itoa(logic.GetPeerListenPort(host))
+	gwendpoint := host.EndpointIP.String() + ":" + strconv.Itoa(host.ListenPort)
 	newAllowedIPs := network.AddressRange
 	if newAllowedIPs != "" && network.AddressRange6 != "" {
 		newAllowedIPs += ","
@@ -317,16 +318,22 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	var extclient models.ExtClient
-	var CustomExtClient models.CustomExtClient
-
-	err := json.NewDecoder(r.Body).Decode(&CustomExtClient)
+	var customExtClient models.CustomExtClient
 
+	err := json.NewDecoder(r.Body).Decode(&customExtClient)
 	if err == nil {
-		if CustomExtClient.ClientID != "" && !validName(CustomExtClient.ClientID) {
+		if customExtClient.ClientID != "" && !validName(customExtClient.ClientID) {
 			logic.ReturnErrorResponse(w, r, logic.FormatError(errInvalidExtClientID, "badrequest"))
 			return
 		}
-		extclient.ClientID = CustomExtClient.ClientID
+		extclient.ClientID = customExtClient.ClientID
+		if len(customExtClient.PublicKey) > 0 {
+			if _, err := wgtypes.ParseKey(customExtClient.PublicKey); err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errInvalidExtClientPubKey, "badrequest"))
+				return
+			}
+			extclient.PublicKey = customExtClient.PublicKey
+		}
 	}
 
 	extclient.Network = networkName
@@ -350,16 +357,20 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		listenPort = host.ProxyListenPort
 	}
 	extclient.IngressGatewayEndpoint = host.EndpointIP.String() + ":" + strconv.FormatInt(int64(listenPort), 10)
-
 	extclient.Enabled = true
 	parentNetwork, err := logic.GetNetwork(networkName)
 	if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 	}
-	// check pro settings
 
-	err = logic.CreateExtClient(&extclient)
-	if err != nil {
+	if err := logic.SetClientDefaultACLs(&extclient); err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to assign ACLs to new ext client on network [%s]: %v", networkName, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	if err = logic.CreateExtClient(&extclient); err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to create new ext client on network [%s]: %v", networkName, err))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -380,7 +391,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 				logger.Log(0, "failed to associate client", extclient.ClientID, "to user", userID)
 			}
 			extclient.OwnerID = userID
-			if _, err := logic.UpdateExtClient(extclient.ClientID, extclient.Network, extclient.Enabled, &extclient); err != nil {
+			if _, err := logic.UpdateExtClient(extclient.ClientID, extclient.Network, extclient.Enabled, &extclient, extclient.ACLs); err != nil {
 				logger.Log(0, "failed to add owner id", userID, "to client", extclient.ClientID)
 			}
 		}
@@ -389,8 +400,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	logger.Log(0, r.Header.Get("user"), "created new ext client on network", networkName)
 	w.WriteHeader(http.StatusOK)
 	go func() {
-		err = mq.PublishPeerUpdate()
-		if err != nil {
+		if err := mq.PublishPeerUpdate(); err != nil {
 			logger.Log(1, "error setting ext peers on "+nodeid+": "+err.Error())
 		}
 		if err := mq.PublishExtCLientDNS(&extclient); err != nil {
@@ -474,10 +484,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	// == END PRO ==
 
-	var changedEnabled = newExtClient.Enabled != oldExtClient.Enabled // indicates there was a change in enablement
+	var changedEnabled = (newExtClient.Enabled != oldExtClient.Enabled) || // indicates there was a change in enablement
+		len(newExtClient.ACLs) != len(oldExtClient.ACLs)
 	// extra var need as logic.Update changes oldExtClient
 	currentClient := oldExtClient
-	newclient, err := logic.UpdateExtClient(newExtClient.ClientID, params["network"], newExtClient.Enabled, &oldExtClient)
+	newclient, err := logic.UpdateExtClient(newExtClient.ClientID, params["network"], newExtClient.Enabled, &oldExtClient, newExtClient.ACLs)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to update ext client [%s], network [%s]: %v",
@@ -567,7 +578,7 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	go func() {
-		if err := mq.PublishPeerUpdate(); err != nil {
+		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
 			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())
 		}
 		if err = mq.PublishDeleteExtClientDNS(&extclient); err != nil {

+ 49 - 0
controllers/hosts.go

@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -26,6 +27,7 @@ func hostHandlers(r *mux.Router) {
 	r.HandleFunc("/api/hosts/{hostid}/relay", logic.SecurityCheck(false, http.HandlerFunc(createHostRelay))).Methods(http.MethodPost)
 	r.HandleFunc("/api/hosts/{hostid}/relay", logic.SecurityCheck(false, http.HandlerFunc(deleteHostRelay))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/adm/authenticate", authenticateHost).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/host", authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 }
 
 // swagger:route GET /api/hosts hosts getHosts
@@ -53,6 +55,53 @@ func getHosts(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiHosts)
 }
 
+// swagger:route GET /api/v1/host pull pullHost
+//
+// Used by clients for "pull" command
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: pull
+func pull(w http.ResponseWriter, r *http.Request) {
+
+	hostID := r.Header.Get(hostIDHeader) // return JSON/API formatted keys
+	if len(hostID) == 0 {
+		logger.Log(0, "no host authorized to pull")
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no host authorized to pull"), "internal"))
+		return
+	}
+	host, err := logic.GetHost(hostID)
+	if err != nil {
+		logger.Log(0, "no host found during pull", hostID)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	hPU, err := logic.GetPeerUpdateForHost(context.Background(), "", host, nil, nil)
+	if err != nil {
+		logger.Log(0, "could not pull peers for host", hostID)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	serverConf := servercfg.GetServerInfo()
+	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+		serverConf.MQUserName = hostID
+	}
+	response := models.HostPull{
+		Host:         *host,
+		ServerConfig: serverConf,
+		Peers:        hPU.Peers,
+		PeerIDs:      hPU.PeerIDs,
+	}
+
+	logger.Log(1, hostID, "completed a pull")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(&response)
+}
+
 // swagger:route PUT /api/hosts/{hostid} hosts updateHost
 //
 // Updates a Netclient host on Netmaker server.

+ 35 - 0
controllers/legacy.go

@@ -0,0 +1,35 @@
+package controller
+
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+)
+
+func legacyHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/legacy/nodes", logic.SecurityCheck(true, http.HandlerFunc(wipeLegacyNodes))).Methods(http.MethodDelete)
+}
+
+// swagger:route DELETE /api/v1/legacy/nodes nodes wipeLegacyNodes
+//
+// Delete all legacy nodes from DB.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: wipeLegacyNodesResponse
+func wipeLegacyNodes(w http.ResponseWriter, r *http.Request) {
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
+	if err := logic.RemoveAllLegacyNodes(); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		logger.Log(0, "error occurred when removing legacy nodes", err.Error())
+	}
+	logger.Log(0, r.Header.Get("user"), "wiped legacy nodes")
+	logic.ReturnSuccessResponse(w, r, "wiped all legacy nodes")
+}

+ 47 - 38
controllers/migrate.go

@@ -2,19 +2,17 @@ package controller
 
 import (
 	"encoding/json"
-	"io"
 	"net/http"
-	"strings"
 
-	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/crypto/bcrypt"
 )
 
-// swagger:route PUT /api/nodes/{network}/{nodeid}/migrate nodes migrateNode
+// swagger:route PUT /api/v1/nodes/migrate nodes migrateNode
 //
 // Used to migrate a legacy node.
 //
@@ -26,7 +24,6 @@ import (
 //			Responses:
 //				200: nodeJoinResponse
 func migrate(w http.ResponseWriter, r *http.Request) {
-	// we decode our body request params
 	data := models.MigrationData{}
 	err := json.NewDecoder(r.Body).Decode(&data)
 	if err != nil {
@@ -34,45 +31,57 @@ func migrate(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	params := mux.Vars(r)
-	//check authorization
-	record, err := database.FetchRecord(database.NODES_TABLE_NAME, data.LegacyNodeID)
-	if err != nil {
-		logger.Log(0, "no record for legacy node", data.LegacyNodeID, err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	var legacyNode models.LegacyNode
-	if err = json.Unmarshal([]byte(record), &legacyNode); err != nil {
-		logger.Log(0, "error decoding legacy node", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
+
+	var networksToAdd = []string{}
+	for i := range data.LegacyNodes {
+		legacyNode := data.LegacyNodes[i]
+		record, err := database.FetchRecord(database.NODES_TABLE_NAME, legacyNode.ID)
+		if err != nil {
+			logger.Log(0, "no record for legacy node", legacyNode.ID, err.Error())
+			continue
+		} else {
+			var oldLegacyNode models.LegacyNode
+			if err = json.Unmarshal([]byte(record), &oldLegacyNode); err != nil {
+				logger.Log(0, "error decoding legacy node", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+				continue
+			}
+			if err := bcrypt.CompareHashAndPassword([]byte(oldLegacyNode.Password), []byte(legacyNode.Password)); err != nil {
+				logger.Log(0, "error decoding legacy password", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+				continue
+			}
+			networksToAdd = append(networksToAdd, oldLegacyNode.Network)
+			_ = database.DeleteRecord(database.NODES_TABLE_NAME, oldLegacyNode.ID)
+		}
 	}
-	if err := bcrypt.CompareHashAndPassword([]byte(legacyNode.Password), []byte(data.Password)); err != nil {
-		logger.Log(0, "error decoding legacy password", err.Error())
+	if len(networksToAdd) == 0 {
+		logger.Log(0, "no valid networks to migrate for host", data.NewHost.Name)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
 		return
 	}
-	network, err := logic.GetNetwork(params["network"])
-	if err != nil {
-		logger.Log(0, "error retrieving network:  ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
+	if !logic.HostExists(&data.NewHost) {
+		logic.CheckHostPorts(&data.NewHost)
+		if err = logic.CreateHost(&data.NewHost); err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
 	}
-	key, err := logic.CreateAccessKey(models.AccessKey{}, network)
-	if err != nil {
-		logger.Log(0, "error creating key:  ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+	key, keyErr := logic.RetrievePublicTrafficKey()
+	if keyErr != nil {
+		logger.Log(0, "error retrieving key:", keyErr.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	data.JoinData.Key = key.Value
-	payload, err := json.Marshal(data.JoinData)
-	if err != nil {
-		logger.Log(0, "error encoding data:  ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
+	server := servercfg.GetServerInfo()
+	server.TrafficKey = key
+	response := models.RegisterResponse{
+		ServerConf:    server,
+		RequestedHost: data.NewHost,
 	}
-	r.Body = io.NopCloser(strings.NewReader(string(payload)))
-	r.ContentLength = int64(len(string(payload)))
-	createNode(w, r)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(&response)
+	logger.Log(0, "successfully migrated host", data.NewHost.Name, data.NewHost.ID.String())
+	// notify host of changes, peer and node updates
+	go checkNetRegAndHostUpdate(networksToAdd, &data.NewHost)
 }

+ 1 - 126
controllers/network.go

@@ -25,9 +25,6 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(false, http.HandlerFunc(updateNetwork))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/networks/{networkname}/keyupdate", logic.SecurityCheck(true, http.HandlerFunc(keyUpdate))).Methods(http.MethodPost)
-	r.HandleFunc("/api/networks/{networkname}/keys", logic.SecurityCheck(false, http.HandlerFunc(createAccessKey))).Methods(http.MethodPost)
-	r.HandleFunc("/api/networks/{networkname}/keys", logic.SecurityCheck(false, http.HandlerFunc(getAccessKeys))).Methods(http.MethodGet)
-	r.HandleFunc("/api/networks/{networkname}/keys/{name}", logic.SecurityCheck(false, http.HandlerFunc(deleteAccessKey))).Methods(http.MethodDelete)
 	// ACLs
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
@@ -72,12 +69,6 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 	}
-	if !servercfg.IsDisplayKeys() {
-		for i, net := range allnetworks {
-			net.AccessKeys = logic.RemoveKeySensitiveInfo(net.AccessKeys)
-			allnetworks[i] = net
-		}
-	}
 
 	logger.Log(2, r.Header.Get("user"), "fetched networks.")
 	w.WriteHeader(http.StatusOK)
@@ -107,9 +98,7 @@ func getNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !servercfg.IsDisplayKeys() {
-		network.AccessKeys = logic.RemoveKeySensitiveInfo(network.AccessKeys)
-	}
+
 	logger.Log(2, r.Header.Get("user"), "fetched network", netname)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
@@ -426,117 +415,3 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
 }
-
-// swagger:route POST /api/networks/{networkname}/keys networks createAccessKey
-//
-// Create a network access key.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: accessKeyBodyResponse
-//
-// BEGIN KEY MANAGEMENT SECTION
-func createAccessKey(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	var accesskey models.AccessKey
-	// start here
-	netname := params["networkname"]
-	network, err := logic.GetParentNetwork(netname)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to get network info: ",
-			err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	err = json.NewDecoder(r.Body).Decode(&accesskey)
-	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
-	}
-	key, err := logic.CreateAccessKey(accesskey, network)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to create access key: ",
-			err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-
-	// do not allow access key creations view API with user names
-	if _, err = logic.GetUser(key.Name); err == nil {
-		logger.Log(0, "access key creation with invalid name attempted by", r.Header.Get("user"))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("cannot create access key with user name"), "badrequest"))
-		logic.DeleteKey(key.Name, network.NetID)
-		return
-	}
-
-	logger.Log(1, r.Header.Get("user"), "created access key", accesskey.Name, "on", netname)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(key)
-}
-
-// swagger:route GET /api/networks/{networkname}/keys networks getAccessKeys
-//
-// Get network access keys for a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: accessKeySliceBodyResponse
-func getAccessKeys(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	network := params["networkname"]
-	keys, err := logic.GetKeys(network)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), fmt.Sprintf("failed to get keys for network [%s]: %v",
-			network, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	if !servercfg.IsDisplayKeys() {
-		keys = logic.RemoveKeySensitiveInfo(keys)
-	}
-	logger.Log(2, r.Header.Get("user"), "fetched access keys on network", network)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(keys)
-}
-
-// swagger:route DELETE /api/networks/{networkname}/keys/{name} networks deleteAccessKey
-//
-// Delete a network access key.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200:
-//				*: stringJSONResponse
-//
-// delete key. Has to do a little funky logic since it's not a collection item
-func deleteAccessKey(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	keyname := params["name"]
-	netname := params["networkname"]
-	err := logic.DeleteKey(keyname, netname)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), fmt.Sprintf("failed to delete key [%s] for network [%s]: %v",
-			keyname, netname, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	logger.Log(1, r.Header.Get("user"), "deleted access key", keyname, "on network,", netname)
-	w.WriteHeader(http.StatusOK)
-}

+ 3 - 116
controllers/network_test.go

@@ -40,6 +40,8 @@ func TestMain(m *testing.M) {
 			logger.Log(3, "received node update", update.Action)
 		}
 	}()
+	os.Exit(m.Run())
+
 }
 
 func TestCreateNetwork(t *testing.T) {
@@ -84,118 +86,6 @@ func TestDeleteNetwork(t *testing.T) {
 	})
 }
 
-func TestCreateKey(t *testing.T) {
-	createNet()
-	keys, _ := logic.GetKeys("skynet")
-	for _, key := range keys {
-		logic.DeleteKey(key.Name, "skynet")
-	}
-	var accesskey models.AccessKey
-	var network models.Network
-	network.NetID = "skynet"
-	t.Run("NameTooLong", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Name = "ThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfailThisisareallylongkeynamethatwillfail"
-		_, err = logic.CreateAccessKey(accesskey, network)
-		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'max' tag")
-	})
-	t.Run("BlankName", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Name = ""
-		key, err := logic.CreateAccessKey(accesskey, network)
-		assert.Nil(t, err)
-		assert.NotEqual(t, "", key.Name)
-	})
-	t.Run("InvalidValue", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Value = "bad-value"
-		_, err = logic.CreateAccessKey(accesskey, network)
-		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Value' failed on the 'alphanum' tag")
-	})
-	t.Run("BlankValue", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Name = "mykey"
-		accesskey.Value = ""
-		key, err := logic.CreateAccessKey(accesskey, network)
-		assert.Nil(t, err)
-		assert.NotEqual(t, "", key.Value)
-		assert.Equal(t, accesskey.Name, key.Name)
-	})
-	t.Run("ValueTooLong", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Name = "keyname"
-		accesskey.Value = "AccessKeyValuethatistoolong"
-		_, err = logic.CreateAccessKey(accesskey, network)
-		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Value' failed on the 'max' tag")
-	})
-	t.Run("BlankUses", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Uses = 0
-		accesskey.Value = ""
-		key, err := logic.CreateAccessKey(accesskey, network)
-		assert.Nil(t, err)
-		assert.Equal(t, 1, key.Uses)
-	})
-	t.Run("DuplicateKey", func(t *testing.T) {
-		network, err := logic.GetNetwork("skynet")
-		assert.Nil(t, err)
-		accesskey.Name = "mykey"
-		_, err = logic.CreateAccessKey(accesskey, network)
-		assert.NotNil(t, err)
-		assert.EqualError(t, err, "duplicate AccessKey Name")
-	})
-}
-
-func TestGetKeys(t *testing.T) {
-	deleteAllNetworks()
-	createNet()
-	network, err := logic.GetNetwork("skynet")
-	assert.Nil(t, err)
-	var key models.AccessKey
-	key.Name = "mykey"
-	_, err = logic.CreateAccessKey(key, network)
-	assert.Nil(t, err)
-	t.Run("KeyExists", func(t *testing.T) {
-		keys, err := logic.GetKeys(network.NetID)
-		assert.Nil(t, err)
-		assert.NotEqual(t, models.AccessKey{}, keys)
-	})
-	t.Run("NonExistantKey", func(t *testing.T) {
-		err := logic.DeleteKey("mykey", "skynet")
-		assert.Nil(t, err)
-		keys, err := logic.GetKeys(network.NetID)
-		assert.Nil(t, err)
-		assert.Equal(t, []models.AccessKey(nil), keys)
-	})
-}
-func TestDeleteKey(t *testing.T) {
-	createNet()
-	network, err := logic.GetNetwork("skynet")
-	assert.Nil(t, err)
-	var key models.AccessKey
-	key.Name = "mykey"
-	_, err = logic.CreateAccessKey(key, network)
-	assert.Nil(t, err)
-	t.Run("ExistingKey", func(t *testing.T) {
-		err := logic.DeleteKey("mykey", "skynet")
-		assert.Nil(t, err)
-	})
-	t.Run("NonExistantKey", func(t *testing.T) {
-		err := logic.DeleteKey("mykey", "skynet")
-		assert.NotNil(t, err)
-		assert.Equal(t, "key mykey does not exist", err.Error())
-	})
-}
-
 func TestSecurityCheck(t *testing.T) {
 	//these seem to work but not sure it the tests are really testing the functionality
 
@@ -325,10 +215,7 @@ func TestIpv6Network(t *testing.T) {
 
 func deleteAllNetworks() {
 	deleteAllNodes()
-	nets, _ := logic.GetNetworks()
-	for _, net := range nets {
-		logic.DeleteNetwork(net.NetID)
-	}
+	database.DeleteAllRecords(database.NETWORKS_TABLE_NAME)
 }
 
 func createNet() {

+ 22 - 269
controllers/node.go

@@ -1,8 +1,8 @@
 package controller
 
 import (
+	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -19,13 +19,14 @@ import (
 	"golang.org/x/crypto/bcrypt"
 )
 
+var hostIDHeader = "host-id"
+
 func nodeHandlers(r *mux.Router) {
 
 	r.HandleFunc("/api/nodes", authorize(false, false, "user", http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}", authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(false, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPut)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/migrate", migrate).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", authorize(false, true, "user", http.HandlerFunc(createRelay))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", authorize(false, true, "user", http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
@@ -34,8 +35,8 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(false, http.HandlerFunc(createIngressGateway))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(false, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}", nodeauth(checkFreeTierLimits(node_l, http.HandlerFunc(createNode)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
 
 // swagger:route POST /api/nodes/adm/{network}/authenticate nodes authenticate
@@ -146,52 +147,6 @@ func authenticate(response http.ResponseWriter, request *http.Request) {
 	response.Write(successJSONResponse)
 }
 
-// auth middleware for api calls from nodes where node is has not yet joined the server (register, join)
-func nodeauth(next http.Handler) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		bearerToken := r.Header.Get("Authorization")
-		var tokenSplit = strings.Split(bearerToken, " ")
-		var token = ""
-		if len(tokenSplit) < 2 {
-			errorResponse := models.ErrorResponse{
-				Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
-			}
-			logic.ReturnErrorResponse(w, r, errorResponse)
-			return
-		} else {
-			token = tokenSplit[1]
-		}
-		found := false
-		networks, err := logic.GetNetworks()
-		if err != nil {
-			logger.Log(0, "no networks", err.Error())
-			errorResponse := models.ErrorResponse{
-				Code: http.StatusNotFound, Message: "no networks",
-			}
-			logic.ReturnErrorResponse(w, r, errorResponse)
-			return
-		}
-		for _, network := range networks {
-			for _, key := range network.AccessKeys {
-				if key.Value == token {
-					found = true
-					break
-				}
-			}
-		}
-		if !found {
-			logger.Log(0, "valid access key not found")
-			errorResponse := models.ErrorResponse{
-				Code: http.StatusUnauthorized, Message: "You are unauthorized to access this endpoint.",
-			}
-			logic.ReturnErrorResponse(w, r, errorResponse)
-			return
-		}
-
-		next.ServeHTTP(w, r)
-	}
-}
-
 // The middleware for most requests to the API
 // They all pass  through here first
 // This will validate the JWT (or check for master token)
@@ -199,7 +154,7 @@ func nodeauth(next http.Handler) http.HandlerFunc {
 // even if it's technically ok
 // This is kind of a poor man's RBAC. There's probably a better/smarter way.
 // TODO: Consider better RBAC implementations
-func authorize(nodesAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
+func authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
 			Code: http.StatusUnauthorized, Message: logic.Unauthorized_Msg,
@@ -231,11 +186,11 @@ func authorize(nodesAllowed, networkCheck bool, authNetwork string, next http.Ha
 				logic.ReturnErrorResponse(w, r, errorResponse)
 				return
 			}
-			//check if node instead of user
-			if nodesAllowed {
+			// check if host instead of user
+			if hostAllowed {
 				// TODO --- should ensure that node is only operating on itself
-				if _, _, _, err := logic.VerifyToken(authToken); err == nil {
-
+				if hostID, _, _, err := logic.VerifyHostToken(authToken); err == nil {
+					r.Header.Set(hostIDHeader, hostID)
 					// this indicates request is from a node
 					// used for failover - if a getNode comes from node, this will trigger a metrics wipe
 					next.ServeHTTP(w, r)
@@ -291,6 +246,7 @@ func authorize(nodesAllowed, networkCheck bool, authNetwork string, next http.Ha
 					} else {
 						isAuthorized = (nodeID == params["netid"])
 					}
+				case "host":
 				case "user":
 					isAuthorized = true
 				default:
@@ -433,7 +389,7 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	hostPeerUpdate, err := logic.GetPeerUpdateForHost(node.Network, host, nil)
+	hostPeerUpdate, err := logic.GetPeerUpdateForHost(context.Background(), node.Network, host, nil, nil)
 	if err != nil && !database.IsEmptyRecord(err) {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", host.ID.String(), err))
@@ -441,6 +397,10 @@ 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,
@@ -461,218 +421,6 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(response)
 }
 
-// swagger:route POST /api/nodes/{network} nodes createNode
-//
-// Create a node on a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeGetResponse
-func createNode(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-
-	var params = mux.Vars(r)
-
-	var errorResponse = models.ErrorResponse{
-		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
-	}
-	networkName := params["network"]
-	networkexists, err := logic.NetworkExists(networkName)
-
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to fetch network [%s] info: %v", networkName, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	} else if !networkexists {
-		errorResponse = models.ErrorResponse{
-			Code: http.StatusNotFound, Message: "W1R3: Network does not exist! ",
-		}
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("network [%s] does not exist", networkName))
-		logic.ReturnErrorResponse(w, r, errorResponse)
-		return
-	}
-
-	//get data from body of request
-	data := models.JoinData{}
-	err = json.NewDecoder(r.Body).Decode(&data)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-
-	if !logic.IsVersionComptatible(data.Host.Version) {
-		err := errors.New("incompatible netclient version")
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-
-	data.Node.Network = networkName
-
-	networkSettings, err := logic.GetNetworkSettings(networkName)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to get network [%s] settings: %v", networkName, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	data.Node.NetworkSettings(networkSettings)
-	keyName, validKey := logic.IsKeyValid(networkName, data.Key)
-	if !validKey {
-		errorResponse = models.ErrorResponse{
-			Code: http.StatusUnauthorized, Message: "W1R3: Key invalid, or none provided.",
-		}
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create node on network [%s]: %s",
-				data.Node.Network, errorResponse.Message))
-		logic.ReturnErrorResponse(w, r, errorResponse)
-		return
-	}
-	logic.DecrimentKey(networkName, data.Key)
-	user, err := pro.GetNetworkUser(networkName, promodels.NetworkUserID(keyName))
-	if err == nil {
-		if user.ID != "" {
-			logger.Log(1, "associating new node with user", keyName)
-			data.Node.OwnerID = string(user.ID)
-		}
-	}
-
-	key, keyErr := logic.RetrievePublicTrafficKey()
-	if keyErr != nil {
-		logger.Log(0, "error retrieving key: ", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	if key == nil {
-		logger.Log(0, "error: server traffic key is nil")
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	if data.Host.TrafficKeyPublic == nil {
-		logger.Log(0, "error: node traffic key is nil")
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	server := servercfg.GetServerInfo()
-	server.TrafficKey = key
-	data.Node.Server = servercfg.GetServer()
-	if !logic.HostExists(&data.Host) {
-		logic.CheckHostPorts(&data.Host)
-		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-			// create EMQX credentials for host if it doesn't exists
-			if err := mq.CreateEmqxUser(data.Host.ID.String(), data.Host.HostPass, false); err != nil {
-				logger.Log(0, "failed to add host credentials to EMQX: ", data.Host.ID.String(), err.Error())
-				return
-			}
-		}
-	}
-	if err := logic.CreateHost(&data.Host); err != nil {
-		if errors.Is(err, logic.ErrHostExists) {
-			logger.Log(3, "host exists .. no need to create")
-			host, err := logic.GetHost(data.Host.ID.String())
-			if err != nil {
-				logger.Log(0, r.Header.Get("user"), "failed to find host:", err.Error())
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-				return
-			}
-			logic.UpdateHostFromClient(&data.Host, host) // update the in memory struct values
-			err = logic.UpsertHost(host)
-			if err != nil {
-				logger.Log(0, r.Header.Get("user"),
-					fmt.Sprintf("failed to update host [ %s ]: %v", host.ID.String(), err))
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-				return
-			}
-			data.Host = *host
-		} else {
-			logger.Log(0, "error creating host", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-			return
-		}
-	}
-	err = logic.AssociateNodeToHost(&data.Node, &data.Host)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create node on network [%s]: %s",
-				networkName, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	// check if key belongs to a user
-	// if so add to their netuser data
-	// if it fails remove the node and fail request
-	if user != nil {
-		var updatedUserNode bool
-		user.Nodes = append(user.Nodes, data.Node.ID.String()) // add new node to user
-		if err = pro.UpdateNetworkUser(networkName, user); err == nil {
-			logger.Log(1, "added node", data.Node.ID.String(), data.Host.Name, "to user", string(user.ID))
-			updatedUserNode = true
-		}
-		if !updatedUserNode { // user was found but not updated, so delete node
-			logger.Log(0, "failed to add node to user", keyName)
-			logic.DeleteNode(&data.Node, true)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-	}
-	hostPeerUpdate, err := logic.GetPeerUpdateForHost(networkName, &data.Host, nil)
-	if err != nil && !database.IsEmptyRecord(err) {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", data.Host.ID.String(), err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	data.Host.HostPass = "" // client should not change password after join
-	// concealing hash
-	response := models.NodeJoinResponse{
-		Node:         data.Node,
-		ServerConfig: server,
-		Host:         data.Host,
-		Peers:        hostPeerUpdate.Peers,
-	}
-	logger.Log(1, r.Header.Get("user"), "created new node", data.Host.Name, "on network", networkName)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(response)
-
-	go func() {
-		if err := mq.PublishPeerUpdate(); err != nil {
-			logger.Log(1, "failed a peer update after creation of node", data.Host.Name)
-		}
-	}()
-	//runForceServerUpdate(&data.Node, true)
-	go func() {
-		dns := models.DNSUpdate{
-			Action: models.DNSInsert,
-			Name:   data.Host.Name + "." + data.Node.Network,
-		}
-		if data.Node.Address.IP != nil {
-			dns.Address = data.Node.Address.IP.String()
-			//publish new node dns entry to all nodes on network
-			if err := mq.PublishDNSUpdate(data.Node.Network, dns); err != nil {
-				logger.Log(1, "failed to publish dns update on node creation", err.Error())
-			}
-		}
-		if data.Node.Address6.IP != nil {
-			dns.Address = data.Node.Address6.IP.String()
-			//publish new node dns entry to all nodes on network
-			if err := mq.PublishDNSUpdate(data.Node.Network, dns); err != nil {
-				logger.Log(1, "failed to publish dns update on node creation", err.Error())
-			}
-		}
-		//publish add dns records for network to new node
-		if err := mq.PublishAllDNS(&data.Node); err != nil {
-			logger.Log(1, "failed to publish dns update on node creation", err.Error())
-		}
-	}()
-}
-
 // == EGRESS ==
 
 // swagger:route POST /api/nodes/{network}/{nodeid}/creategateway nodes createEgressGateway
@@ -975,8 +723,13 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	fromNode := r.Header.Get("requestfrom") == "node"
 	node, err := logic.GetNodeByID(nodeid)
 	if err != nil {
-		logger.Log(0, "error retrieving node to delete", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		if logic.CheckAndRemoveLegacyNode(nodeid) {
+			logger.Log(0, "removed legacy node", nodeid)
+			logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
+		} else {
+			logger.Log(0, "error retrieving node to delete", err.Error())
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		}
 		return
 	}
 	if r.Header.Get("ismaster") != "yes" {

+ 2 - 2
controllers/node_test.go

@@ -113,12 +113,12 @@ func TestGetNetworkNodes(t *testing.T) {
 	t.Run("BadNet", func(t *testing.T) {
 		node, err := logic.GetNetworkNodes("badnet")
 		assert.Nil(t, err)
-		assert.Nil(t, node)
+		assert.Equal(t, []models.Node{}, node)
 	})
 	t.Run("NoNodes", func(t *testing.T) {
 		node, err := logic.GetNetworkNodes("skynet")
 		assert.Nil(t, err)
-		assert.Nil(t, node)
+		assert.Equal(t, []models.Node{}, node)
 	})
 	t.Run("Success", func(t *testing.T) {
 		createTestNode()

+ 4 - 1
controllers/regex.go

@@ -5,7 +5,10 @@ import (
 	"regexp"
 )
 
-var errInvalidExtClientID = errors.New("ext client ID must be alphanumderic and/or dashes")
+var (
+	errInvalidExtClientPubKey = errors.New("incorrect ext client public key")
+	errInvalidExtClientID     = errors.New("ext client ID must be alphanumderic and/or dashes")
+)
 
 // allow only dashes and alphaneumeric for ext client and node names
 func validName(name string) bool {

+ 6 - 7
controllers/relay.go

@@ -115,7 +115,7 @@ func createHostRelay(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	relay.HostID = params["hostid"]
-	relayHost, _, err := logic.CreateHostRelay(relay)
+	relayHost, relayedHosts, err := logic.CreateHostRelay(relay)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to create relay on host [%s]: %v", relay.HostID, err))
@@ -131,15 +131,14 @@ func createHostRelay(w http.ResponseWriter, r *http.Request) {
 	}
 	logger.Log(1, r.Header.Get("user"), "created relay on host", relay.HostID)
 	go func(relayHostID string) {
-		relatedhosts := logic.GetRelatedHosts(relayHostID)
-		for _, relatedHost := range relatedhosts {
-			relatedHost.ProxyEnabled = true
-			logic.UpsertHost(&relatedHost)
+		for _, relayedHost := range relayedHosts {
+			relayedHost.ProxyEnabled = true
+			logic.UpsertHost(&relayedHost)
 			if err := mq.HostUpdate(&models.HostUpdate{
 				Action: models.UpdateHost,
-				Host:   relatedHost,
+				Host:   relayedHost,
 			}); err != nil {
-				logger.Log(0, "failed to send host update: ", relatedHost.ID.String(), err.Error())
+				logger.Log(0, "failed to send host update: ", relayedHost.ID.String(), err.Error())
 			}
 		}
 		if err := mq.PublishPeerUpdate(); err != nil {

+ 1 - 1
controllers/user.go

@@ -467,5 +467,5 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// Start handling the session
-	go auth.SessionHandler(conn)
+	// go auth.SessionHandler(conn)
 }

+ 4 - 0
controllers/user_test.go

@@ -186,6 +186,7 @@ func TestGetUsers(t *testing.T) {
 		assert.Equal(t, []models.ReturnUser(nil), admin)
 	})
 	t.Run("UserExisits", func(t *testing.T) {
+		user.UserName = "anotheruser"
 		if err := logic.CreateUser(&adminUser); err != nil {
 			t.Error(err)
 		}
@@ -281,6 +282,9 @@ func TestVerifyAuthRequest(t *testing.T) {
 		assert.EqualError(t, err, "error retrieving user from db: could not find any records")
 	})
 	t.Run("Non-Admin", func(t *testing.T) {
+		user.IsAdmin = false
+		user.Password = "somepass"
+		user.UserName = "nonadmin"
 		if err := logic.CreateUser(&user); err != nil {
 			t.Error(err)
 		}

+ 3 - 0
database/database.go

@@ -59,6 +59,8 @@ const (
 	HOSTS_TABLE_NAME = "hosts"
 	// ENROLLMENT_KEYS_TABLE_NAME - table name for enrollmentkeys
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
+	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
+	HOST_ACTIONS_TABLE_NAME = "hostactions"
 
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
@@ -141,6 +143,7 @@ func createTables() {
 	createTable(CACHE_TABLE_NAME)
 	createTable(HOSTS_TABLE_NAME)
 	createTable(ENROLLMENT_KEYS_TABLE_NAME)
+	createTable(HOST_ACTIONS_TABLE_NAME)
 }
 
 func createTable(tableName string) error {

+ 1 - 1
docker/Caddyfile

@@ -1,6 +1,6 @@
 {
         # ZeroSSL account
-        acme_ca https://acme.zerossl.com/v2/DV90
+        # acme_ca https://acme.zerossl.com/v2/DV90
         email YOUR_EMAIL
 }
 

+ 3 - 0
ee/initialize.go

@@ -36,6 +36,9 @@ func InitEE() {
 	logic.EnterpriseFailoverFunc = eelogic.SetFailover
 	logic.EnterpriseResetFailoverFunc = eelogic.ResetFailover
 	logic.EnterpriseResetAllPeersFailovers = eelogic.WipeAffectedFailoversOnly
+	logic.DenyClientNodeAccess = eelogic.DenyClientNode
+	logic.IsClientNodeAllowed = eelogic.IsClientNodeAllowed
+	logic.AllowClientNodeAccess = eelogic.RemoveDeniedNodeFromClient
 }
 
 func resetFailover() {

+ 41 - 0
ee/logic/ext_acls.go

@@ -0,0 +1,41 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+// DenyClientNode - add a denied node to an ext client's list
+func DenyClientNode(ec *models.ExtClient, clientOrNodeID string) (ok bool) {
+	if ec == nil || len(clientOrNodeID) == 0 {
+		return
+	}
+	if ec.ACLs == nil {
+		ec.ACLs = map[string]struct{}{}
+	}
+	ok = true
+	ec.ACLs[clientOrNodeID] = struct{}{}
+	return
+}
+
+// IsClientNodeAllowed - checks if given ext client and node are allowed to communicate
+func IsClientNodeAllowed(ec *models.ExtClient, clientOrNodeID string) bool {
+	if ec == nil || len(clientOrNodeID) == 0 {
+		return false
+	}
+	if ec.ACLs == nil {
+		return true
+	}
+	_, ok := ec.ACLs[clientOrNodeID]
+	return ok
+}
+
+// RemoveDeniedNodeFromClient - removes a node id from set of denied nodes
+func RemoveDeniedNodeFromClient(ec *models.ExtClient, clientOrNodeID string) bool {
+	if ec.ACLs == nil {
+		return true
+	}
+	_, ok := ec.ACLs[clientOrNodeID]
+	if !ok {
+		return false
+	}
+	delete(ec.ACLs, clientOrNodeID)
+	return true
+}

+ 2 - 2
ee/types.go

@@ -3,7 +3,7 @@ package ee
 import "fmt"
 
 const (
-	api_endpoint               = "https://466b-94-204-98-143.in.ngrok.io/api/v1/license/validate"
+	api_endpoint               = "https://api.staging.accounts.netmaker.io/api/v1/license/validate"
 	license_cache_key          = "license_response_cache"
 	license_validation_err_msg = "invalid license"
 	server_id_key              = "nm-server-id"
@@ -18,7 +18,7 @@ type LicenseKey struct {
 	LimitServers  int    `json:"limit_servers"`
 	LimitUsers    int    `json:"limit_users"`
 	LimitHosts    int    `json:"limit_hosts"`
-	LimitNetworks int    ` json:"limit_networks"`
+	LimitNetworks int    `json:"limit_networks"`
 	LimitClients  int    `json:"limit_clients"`
 	Metadata      string `json:"metadata"`
 	IsActive      bool   `json:"is_active"` // yes if active

+ 12 - 21
functions/helpers_test.go

@@ -3,12 +3,14 @@ package functions
 import (
 	"context"
 	"encoding/json"
+	"os"
 	"testing"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
 )
 
 var (
@@ -39,40 +41,29 @@ func TestMain(m *testing.M) {
 			logger.Log(3, "received node update", update.Action)
 		}
 	}()
+	os.Exit(m.Run())
+
 }
 
 func TestNetworkExists(t *testing.T) {
 	database.DeleteRecord(database.NETWORKS_TABLE_NAME, testNetwork.NetID)
-	defer database.CloseDB()
 	exists, err := logic.NetworkExists(testNetwork.NetID)
-	if err == nil {
-		t.Fatalf("expected error, received nil")
-	}
-	if exists {
-		t.Fatalf("expected false")
-	}
+	assert.NotNil(t, err)
+	assert.False(t, exists)
 
 	err = logic.SaveNetwork(testNetwork)
-	if err != nil {
-		t.Fatalf("failed to save test network in databse: %s", err)
-	}
+	assert.Nil(t, err)
 	exists, err = logic.NetworkExists(testNetwork.NetID)
-	if err != nil {
-		t.Fatalf("expected nil, received err: %s", err)
-	}
-	if !exists {
-		t.Fatalf("expected network to exist in database")
-	}
+	assert.Nil(t, err)
+	assert.True(t, exists)
 
 	err = database.DeleteRecord(database.NETWORKS_TABLE_NAME, testNetwork.NetID)
-	if err != nil {
-		t.Fatalf("expected nil, failed to delete test network: %s", err)
-	}
+	assert.Nil(t, err)
 }
 
 func TestGetAllExtClients(t *testing.T) {
-	defer database.CloseDB()
-	database.DeleteRecord(database.EXT_CLIENT_TABLE_NAME, testExternalClient.ClientID)
+	err := database.DeleteRecord(database.EXT_CLIENT_TABLE_NAME, testExternalClient.ClientID)
+	assert.Nil(t, err)
 
 	extClients, err := GetAllExtClients()
 	if err == nil {

+ 6 - 5
go.mod

@@ -15,11 +15,11 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.2
 	github.com/txn2/txeh v1.3.0
-	golang.org/x/crypto v0.6.0
-	golang.org/x/net v0.6.0 // indirect
-	golang.org/x/oauth2 v0.5.0
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/crypto v0.7.0
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/oauth2 v0.6.0
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220324164955-056925b7df31
 	google.golang.org/protobuf v1.28.1 // indirect
@@ -51,6 +51,7 @@ require (
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 )

+ 18 - 13
go.sum

@@ -17,6 +17,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -68,10 +69,12 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
 github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
@@ -98,6 +101,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
 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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -107,7 +111,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/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -150,8 +155,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -167,11 +172,11 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
-golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
-golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -193,8 +198,8 @@ golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBc
 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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@@ -204,8 +209,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.18.2
+        image: gravitl/netclient:v0.18.6
         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.18.2
+        image: gravitl/netclient:v0.18.6
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

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

@@ -79,7 +79,7 @@ spec:
           value: "Kubernetes"
         - name: VERBOSITY
           value: "3"
-        image: gravitl/netmaker:v0.18.2
+        image: gravitl/netmaker:v0.18.6
         imagePullPolicy: Always
         name: netmaker
         ports:

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

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

+ 0 - 229
logic/accesskeys.go

@@ -1,229 +0,0 @@
-package logic
-
-import (
-	"crypto/rand"
-	"encoding/base64"
-	"encoding/json"
-	"errors"
-	"math/big"
-	"strings"
-	"sync"
-
-	validator "github.com/go-playground/validator/v10"
-	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/servercfg"
-)
-
-// CreateAccessKey - create access key
-func CreateAccessKey(accesskey models.AccessKey, network models.Network) (models.AccessKey, error) {
-
-	if accesskey.Name == "" {
-		accesskey.Name = genKeyName()
-	}
-
-	if accesskey.Value == "" {
-		accesskey.Value = GenKey()
-	}
-	if accesskey.Uses == 0 {
-		accesskey.Uses = 1
-	}
-
-	checkkeys, err := GetKeys(network.NetID)
-	if err != nil {
-		return models.AccessKey{}, errors.New("could not retrieve network keys")
-	}
-
-	for _, key := range checkkeys {
-		if key.Name == accesskey.Name {
-			return models.AccessKey{}, errors.New("duplicate AccessKey Name")
-		}
-	}
-
-	netID := network.NetID
-
-	var accessToken models.AccessToken
-
-	accessToken.APIConnString = servercfg.GetAPIConnString()
-	accessToken.ClientConfig.Network = netID
-	accessToken.ClientConfig.Key = accesskey.Value
-
-	tokenjson, err := json.Marshal(accessToken)
-	if err != nil {
-		return accesskey, err
-	}
-
-	accesskey.AccessString = base64.StdEncoding.EncodeToString([]byte(tokenjson))
-
-	//validate accesskey
-	v := validator.New()
-	err = v.Struct(accesskey)
-	if err != nil {
-		for _, e := range err.(validator.ValidationErrors) {
-			logger.Log(1, "validator", e.Error())
-		}
-		return models.AccessKey{}, err
-	}
-
-	network.AccessKeys = append(network.AccessKeys, accesskey)
-	data, err := json.Marshal(&network)
-	if err != nil {
-		return models.AccessKey{}, err
-	}
-	if err = database.Insert(network.NetID, string(data), database.NETWORKS_TABLE_NAME); err != nil {
-		return models.AccessKey{}, err
-	}
-
-	return accesskey, nil
-}
-
-// DeleteKey - deletes a key
-func DeleteKey(keyname, netname string) error {
-	network, err := GetParentNetwork(netname)
-	if err != nil {
-		return err
-	}
-	//basically, turn the list of access keys into the list of access keys before and after the item
-	//have not done any error handling for if there's like...1 item. I think it works? need to test.
-	found := false
-	var updatedKeys []models.AccessKey
-	for _, currentkey := range network.AccessKeys {
-		if currentkey.Name == keyname {
-			found = true
-		} else {
-			updatedKeys = append(updatedKeys, currentkey)
-		}
-	}
-	if !found {
-		return errors.New("key " + keyname + " does not exist")
-	}
-	network.AccessKeys = updatedKeys
-	data, err := json.Marshal(&network)
-	if err != nil {
-		return err
-	}
-	if err := database.Insert(network.NetID, string(data), database.NETWORKS_TABLE_NAME); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// GetKeys - fetches keys for network
-func GetKeys(net string) ([]models.AccessKey, error) {
-
-	record, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, net)
-	if err != nil {
-		return []models.AccessKey{}, err
-	}
-	network, err := ParseNetwork(record)
-	if err != nil {
-		return []models.AccessKey{}, err
-	}
-	return network.AccessKeys, nil
-}
-
-// DecrimentKey - decriments key uses
-func DecrimentKey(networkName string, keyvalue string) {
-
-	var network models.Network
-
-	network, err := GetParentNetwork(networkName)
-	if err != nil {
-		return
-	}
-
-	for i := len(network.AccessKeys) - 1; i >= 0; i-- {
-
-		currentkey := network.AccessKeys[i]
-		if currentkey.Value == keyvalue {
-			network.AccessKeys[i].Uses--
-			if network.AccessKeys[i].Uses < 1 {
-				network.AccessKeys = append(network.AccessKeys[:i],
-					network.AccessKeys[i+1:]...)
-				break
-			}
-		}
-	}
-
-	if newNetworkData, err := json.Marshal(&network); err != nil {
-		logger.Log(2, "failed to decrement key")
-		return
-	} else {
-		database.Insert(network.NetID, string(newNetworkData), database.NETWORKS_TABLE_NAME)
-	}
-}
-
-// IsKeyValid - check if key is valid
-func IsKeyValid(networkname string, keyvalue string) (string, bool) {
-
-	network, err := GetParentNetwork(networkname)
-	if err != nil {
-		return "", false
-	}
-	accesskeys := network.AccessKeys
-
-	var key models.AccessKey
-	foundkey := false
-	isvalid := false
-
-	for i := len(accesskeys) - 1; i >= 0; i-- {
-		currentkey := accesskeys[i]
-		if currentkey.Value == keyvalue {
-			key = currentkey
-			foundkey = true
-		}
-	}
-	if foundkey {
-		if key.Uses > 0 {
-			isvalid = true
-		}
-	}
-	return key.Name, isvalid
-}
-
-// RemoveKeySensitiveInfo - remove sensitive key info
-func RemoveKeySensitiveInfo(keys []models.AccessKey) []models.AccessKey {
-	var returnKeys []models.AccessKey
-	for _, key := range keys {
-		key.Value = models.PLACEHOLDER_KEY_TEXT
-		key.AccessString = models.PLACEHOLDER_TOKEN_TEXT
-		returnKeys = append(returnKeys, key)
-	}
-	return returnKeys
-}
-
-const (
-	maxr string = "ff578f57c15bb743beaa77d27637e02b598dffa9aebd15889187fe6eb3bdca516c3fa1a52eabef31f33b4b8c2e5b5524f1aa4f3329393912f40dbbe23d7f39723e0be05b6696b11f8eea0abe365a11d9f2735ac7e5b4e015ab19b35b84893685b37a9a0a62a566d6571d7e00d4241687f5c804f37cde9bf311c0781f51cc007c5a01a94f6cfcecea640b8e9ab7bd43e73e5df5d0e1eeb4d9b6cc44be67b7cad80808b17869561b579ffe0bbdeca5c83139e458000000000000000000000000000000000000000000000000000000000000000"
-)
-
-var (
-	uno        sync.Once
-	maxentropy *big.Int
-)
-
-func init() {
-	uno.Do(func() {
-		maxentropy, _ = new(big.Int).SetString(maxr, 16)
-	})
-}
-
-// == private methods ==
-
-func genKeyName() string {
-	entropy, _ := rand.Int(rand.Reader, maxentropy)
-	return strings.Join([]string{"key", entropy.Text(16)[:16]}, "-")
-}
-
-// GenKey - generates random key of length 16
-func GenKey() string {
-	entropy, _ := rand.Int(rand.Reader, maxentropy)
-	return entropy.Text(16)[:16]
-}
-
-// GenPassWord - generates random password of length 64
-func GenPassWord() string {
-	entropy, _ := rand.Int(rand.Reader, maxentropy)
-	return entropy.Text(62)[:64]
-}

+ 0 - 21
logic/accesskeys_test.go

@@ -1,21 +0,0 @@
-package logic
-
-import "testing"
-
-func Test_genKeyName(t *testing.T) {
-	for i := 0; i < 100; i++ {
-		kname := genKeyName()
-		if len(kname) != 20 {
-			t.Fatalf("improper length of key name, expected 20 got :%d", len(kname))
-		}
-	}
-}
-
-func Test_genKey(t *testing.T) {
-	for i := 0; i < 100; i++ {
-		kname := GenKey()
-		if len(kname) != 16 {
-			t.Fatalf("improper length of key name, expected 16 got :%d", len(kname))
-		}
-	}
-}

+ 53 - 0
logic/clients.go

@@ -0,0 +1,53 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+// functions defined here, handle client ACLs, should be set on ee
+
+var (
+	// DenyClientNodeAccess - function to handle adding a node to an ext client's denied node set
+	DenyClientNodeAccess = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+	// IsClientNodeAllowed - function to check if an ext client's denied node set contains a node ID
+	IsClientNodeAllowed = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+	// AllowClientNodeAccess - function to handle removing a node ID from ext client's denied nodes, thus allowing it
+	AllowClientNodeAccess = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+)
+
+// SetClientDefaultACLs - set's a client's default ACLs based on network and nodes in network
+func SetClientDefaultACLs(ec *models.ExtClient) error {
+	if !isEE {
+		return nil
+	}
+	networkNodes, err := GetNetworkNodes(ec.Network)
+	if err != nil {
+		return err
+	}
+	network, err := GetNetwork(ec.Network)
+	if err != nil {
+		return err
+	}
+	for i := range networkNodes {
+		currNode := networkNodes[i]
+		if network.DefaultACL == "no" || currNode.DefaultACL == "no" {
+			DenyClientNodeAccess(ec, currNode.ID.String())
+		}
+	}
+	return nil
+}
+
+// SetClientACLs - overwrites an ext client's ACL
+func SetClientACLs(ec *models.ExtClient, newACLs map[string]struct{}) {
+	if ec == nil || newACLs == nil || !isEE {
+		return
+	}
+	ec.ACLs = newACLs
+}
+
+// IsClientNodeAllowedByID - checks if a given ext client ID + nodeID are allowed
+func IsClientNodeAllowedByID(clientID, networkName, clientOrNodeID string) bool {
+	client, err := GetExtClient(clientID, networkName)
+	if err != nil {
+		return false
+	}
+	return IsClientNodeAllowed(&client, clientOrNodeID)
+}

+ 42 - 6
logic/extpeers.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"fmt"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -114,17 +115,34 @@ func GetExtClient(clientid string, network string) (models.ExtClient, error) {
 	return extclient, err
 }
 
+// GetExtClient - gets a single ext client on a network
+func GetExtClientByPubKey(publicKey string, network string) (*models.ExtClient, error) {
+	netClients, err := GetNetworkExtClients(network)
+	if err != nil {
+		return nil, err
+	}
+	for i := range netClients {
+		ec := netClients[i]
+		if ec.PublicKey == publicKey {
+			return &ec, nil
+		}
+	}
+
+	return nil, fmt.Errorf("no client found")
+}
+
 // CreateExtClient - creates an extclient
 func CreateExtClient(extclient *models.ExtClient) error {
 
-	if extclient.PrivateKey == "" {
+	if len(extclient.PublicKey) == 0 {
 		privateKey, err := wgtypes.GeneratePrivateKey()
 		if err != nil {
 			return err
 		}
-
 		extclient.PrivateKey = privateKey.String()
 		extclient.PublicKey = privateKey.PublicKey().String()
+	} else {
+		extclient.PrivateKey = "[ENTER PRIVATE KEY]"
 	}
 
 	parentNetwork, err := GetNetwork(extclient.Network)
@@ -156,7 +174,6 @@ func CreateExtClient(extclient *models.ExtClient) error {
 	}
 
 	extclient.LastModified = time.Now().Unix()
-
 	key, err := GetRecordKey(extclient.ClientID, extclient.Network)
 	if err != nil {
 		return err
@@ -172,15 +189,34 @@ func CreateExtClient(extclient *models.ExtClient) error {
 }
 
 // UpdateExtClient - only supports name changes right now
-func UpdateExtClient(newclientid string, network string, enabled bool, client *models.ExtClient) (*models.ExtClient, error) {
-
+func UpdateExtClient(newclientid string, network string, enabled bool, client *models.ExtClient, newACLs map[string]struct{}) (*models.ExtClient, error) {
 	err := DeleteExtClient(network, client.ClientID)
 	if err != nil {
 		return client, err
 	}
+	if newclientid != client.ClientID {
+		//name change only
+		client.ClientID = newclientid
+		client.LastModified = time.Now().Unix()
+		data, err := json.Marshal(&client)
+		if err != nil {
+			return nil, err
+		}
+		key, err := GetRecordKey(client.ClientID, client.Network)
+		if err != nil {
+			return nil, err
+		}
+		if err = database.Insert(key, string(data), database.EXT_CLIENT_TABLE_NAME); err != nil {
+			return client, err
+		}
+		return client, nil
+	}
 	client.ClientID = newclientid
 	client.Enabled = enabled
-	CreateExtClient(client)
+	SetClientACLs(client, newACLs)
+	if err = CreateExtClient(client); err != nil {
+		return client, err
+	}
 	return client, err
 }
 

+ 20 - 1
logic/host_test.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"os"
 	"testing"
 
 	"github.com/google/uuid"
@@ -12,7 +13,7 @@ import (
 	"github.com/matryer/is"
 )
 
-func TestCheckPorts(t *testing.T) {
+func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	peerUpdate := make(chan *models.Node)
@@ -24,6 +25,10 @@ func TestCheckPorts(t *testing.T) {
 		}
 	}()
 
+	os.Exit(m.Run())
+}
+
+func TestCheckPorts(t *testing.T) {
 	h := models.Host{
 		ID:              uuid.New(),
 		EndpointIP:      net.ParseIP("192.168.1.1"),
@@ -36,10 +41,16 @@ func TestCheckPorts(t *testing.T) {
 		ListenPort:      51830,
 		ProxyListenPort: 51730,
 	}
+	//not sure why this initialization is required but without it
+	// RemoveHost returns database is closed
+	database.InitializeDatabase()
+	RemoveHost(&h)
 	CreateHost(&h)
 	t.Run("no change", func(t *testing.T) {
 		is := is.New(t)
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51830)
 		is.Equal(testHost.ProxyListenPort, 51730)
 	})
@@ -47,6 +58,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ListenPort = 51821
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51822)
 		is.Equal(testHost.ProxyListenPort, 51730)
 	})
@@ -54,6 +67,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ProxyListenPort = 65535
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51822)
 		is.Equal(testHost.ProxyListenPort, minPort)
 	})
@@ -61,6 +76,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ListenPort = maxPort
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, minPort)
 		is.Equal(testHost.ProxyListenPort, minPort+1)
 	})
@@ -68,6 +85,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ProxyListenPort = 51821
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, minPort)
 		is.Equal(testHost.ProxyListenPort, 51822)
 	})

+ 34 - 16
logic/hostactions/hostactions.go

@@ -1,37 +1,55 @@
 package hostactions
 
 import (
-	"sync"
+	"encoding/json"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
 )
 
-// nodeActionHandler - handles the storage of host action updates
-var nodeActionHandler sync.Map
-
 // AddAction - adds a host action to a host's list to be retrieved from broker update
 func AddAction(hu models.HostUpdate) {
-	currentRecords, ok := nodeActionHandler.Load(hu.Host.ID.String())
-	if !ok { // no list exists yet
-		nodeActionHandler.Store(hu.Host.ID.String(), []models.HostUpdate{hu})
-	} else { // list exists, append to it
-		currentList := currentRecords.([]models.HostUpdate)
-		currentList = append(currentList, hu)
-		nodeActionHandler.Store(hu.Host.ID.String(), currentList)
+	hostID := hu.Host.ID.String()
+	currentRecords, err := database.FetchRecord(database.HOST_ACTIONS_TABLE_NAME, hostID)
+	if err != nil {
+		if database.IsEmptyRecord(err) { // no list exists yet
+			newEntry, err := json.Marshal([]models.HostUpdate{hu})
+			if err != nil {
+				return
+			}
+			_ = database.Insert(hostID, string(newEntry), database.HOST_ACTIONS_TABLE_NAME)
+		}
+		return
+	}
+	var currentList []models.HostUpdate
+	if err := json.Unmarshal([]byte(currentRecords), &currentList); err != nil {
+		return
+	}
+	currentList = append(currentList, hu)
+	newData, err := json.Marshal(currentList)
+	if err != nil {
+		return
 	}
+	_ = database.Insert(hostID, string(newData), database.HOST_ACTIONS_TABLE_NAME)
 }
 
 // GetAction - gets an action if exists
-// TODO: may need to move to DB rather than sync map for HA
 func GetAction(id string) *models.HostUpdate {
-	currentRecords, ok := nodeActionHandler.Load(id)
-	if !ok {
+	currentRecords, err := database.FetchRecord(database.HOST_ACTIONS_TABLE_NAME, id)
+	if err != nil {
+		return nil
+	}
+	var currentList []models.HostUpdate
+	if err = json.Unmarshal([]byte(currentRecords), &currentList); err != nil {
 		return nil
 	}
-	currentList := currentRecords.([]models.HostUpdate)
 	if len(currentList) > 0 {
 		hu := currentList[0]
-		nodeActionHandler.Store(hu.Host.ID.String(), currentList[1:])
+		newData, err := json.Marshal(currentList[1:])
+		if err != nil {
+			newData, _ = json.Marshal([]models.HostUpdate{})
+		}
+		_ = database.Insert(id, string(newData), database.HOST_ACTIONS_TABLE_NAME)
 		return &hu
 	}
 	return nil

+ 24 - 12
logic/hosts.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
@@ -103,6 +104,15 @@ func CreateHost(h *models.Host) error {
 		return err
 	}
 	h.HostPass = string(hash)
+	// if another server has already updated proxyenabled, leave it alone
+	if !h.ProxyEnabledSet {
+		log.Println("checking default proxy", servercfg.GetServerConfig().DefaultProxyMode)
+		if servercfg.GetServerConfig().DefaultProxyMode.Set {
+			h.ProxyEnabledSet = true
+			h.ProxyEnabled = servercfg.GetServerConfig().DefaultProxyMode.Value
+			log.Println("set proxy enabled to ", h.ProxyEnabled)
+		}
+	}
 	checkForZombieHosts(h)
 	return UpsertHost(h)
 }
@@ -175,6 +185,10 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 	if newHost.Name != "" {
 		currHost.Name = newHost.Name
 	}
+	if len(newHost.NatType) > 0 && newHost.NatType != currHost.NatType {
+		currHost.NatType = newHost.NatType
+		sendPeerUpdate = true
+	}
 
 	return
 }
@@ -215,7 +229,6 @@ func UpdateHostNetwork(h *models.Host, network string, add bool) (*models.Node,
 			} else {
 				return nil, errors.New("host already part of network " + network)
 			}
-
 		}
 	}
 	if !add {
@@ -369,13 +382,13 @@ func GetRelatedHosts(hostID string) []models.Host {
 // with the same endpoint have different listen ports
 // in the case of 64535 hosts or more with same endpoint, ports will not be changed
 func CheckHostPorts(h *models.Host) {
-	portsInUse := make(map[int]bool)
+	portsInUse := make(map[int]bool, 0)
 	hosts, err := GetAllHosts()
 	if err != nil {
 		return
 	}
 	for _, host := range hosts {
-		if host.ID == h.ID {
+		if host.ID.String() == h.ID.String() {
 			//skip self
 			continue
 		}
@@ -387,12 +400,18 @@ func CheckHostPorts(h *models.Host) {
 	}
 	// iterate until port is not found or max iteration is reached
 	for i := 0; portsInUse[h.ListenPort] && i < maxPort-minPort+1; i++ {
-		updatePort(&h.ListenPort)
+		h.ListenPort++
+		if h.ListenPort > maxPort {
+			h.ListenPort = minPort
+		}
 	}
 	// allocate h.ListenPort so it is unavailable to h.ProxyListenPort
 	portsInUse[h.ListenPort] = true
 	for i := 0; portsInUse[h.ProxyListenPort] && i < maxPort-minPort+1; i++ {
-		updatePort(&h.ProxyListenPort)
+		h.ProxyListenPort++
+		if h.ProxyListenPort > maxPort {
+			h.ProxyListenPort = minPort
+		}
 	}
 }
 
@@ -416,10 +435,3 @@ func GetHostByNodeID(id string) *models.Host {
 	}
 	return nil
 }
-
-func updatePort(p *int) {
-	*p++
-	if *p > maxPort {
-		*p = minPort
-	}
-}

+ 2 - 2
logic/jwts.go

@@ -129,8 +129,8 @@ func VerifyUserToken(tokenString string) (username string, networks []string, is
 	return "", nil, false, err
 }
 
-// VerifyToken - [nodes] Only
-func VerifyToken(tokenString string) (hostID string, mac string, network string, err error) {
+// VerifyHostToken - [hosts] Only
+func VerifyHostToken(tokenString string) (hostID string, mac string, network string, err error) {
 	claims := &models.Claims{}
 
 	// this may be a stupid way of serving up a master key

+ 46 - 0
logic/legacy.go

@@ -0,0 +1,46 @@
+package logic
+
+import (
+	"encoding/json"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
+)
+
+// IsLegacyNode - checks if a node is legacy or not
+func IsLegacyNode(nodeID string) bool {
+	record, err := database.FetchRecord(database.NODES_TABLE_NAME, nodeID)
+	if err != nil {
+		return false
+	}
+	var currentNode models.Node
+	var legacyNode models.LegacyNode
+	currentNodeErr := json.Unmarshal([]byte(record), &currentNode)
+	legacyNodeErr := json.Unmarshal([]byte(record), &legacyNode)
+	return currentNodeErr != nil && legacyNodeErr == nil
+}
+
+// CheckAndRemoveLegacyNode - checks for legacy node and removes
+func CheckAndRemoveLegacyNode(nodeID string) bool {
+	if IsLegacyNode(nodeID) {
+		if err := database.DeleteRecord(database.NODES_TABLE_NAME, nodeID); err == nil {
+			return true
+		}
+	}
+	return false
+}
+
+// RemoveAllLegacyNodes - fetches all legacy nodes from DB and removes
+func RemoveAllLegacyNodes() error {
+	records, err := database.FetchRecords(database.NODES_TABLE_NAME)
+	if err != nil {
+		return err
+	}
+	for k := range records {
+		if CheckAndRemoveLegacyNode(k) {
+			logger.Log(0, "removed legacy node", k)
+		}
+	}
+	return nil
+}

+ 2 - 1
logic/metrics/metrics.go

@@ -10,7 +10,7 @@ import (
 )
 
 // Collect - collects metrics
-func Collect(iface, server, network string, peerMap models.PeerMap) (*models.Metrics, error) {
+func Collect(iface, server, network string, peerMap models.PeerMap, proxy bool) (*models.Metrics, error) {
 	var metrics models.Metrics
 	metrics.Connectivity = make(map[string]models.Metric)
 	var wgclient, err = wgctrl.New()
@@ -45,6 +45,7 @@ func Collect(iface, server, network string, peerMap models.PeerMap) (*models.Met
 		newMetric.TotalSent = int64(proxyMetrics.TrafficSent)
 		newMetric.Latency = int64(proxyMetrics.LastRecordedLatency)
 		newMetric.Connected = proxyMetrics.NodeConnectionStatus[id]
+		newMetric.CollectedByProxy = proxy
 		if newMetric.Connected {
 			newMetric.Uptime = 1
 		}

+ 4 - 2
logic/networks.go

@@ -159,7 +159,6 @@ func GetNetworkSettings(networkname string) (models.Network, error) {
 	if err = json.Unmarshal([]byte(networkData), &network); err != nil {
 		return models.Network{}, err
 	}
-	network.AccessKeys = []models.AccessKey{}
 	return network, nil
 }
 
@@ -599,10 +598,13 @@ func networkNodesUpdateAction(networkName string, action string) error {
 		return err
 	}
 
-	for _, value := range collections {
+	for k, value := range collections {
 		var node models.Node
 		err := json.Unmarshal([]byte(value), &node)
 		if err != nil {
+			if IsLegacyNode(k) { // ignore legacy nodes
+				continue
+			}
 			fmt.Println("error in node address assignment!")
 			return err
 		}

+ 20 - 3
logic/nodes.go

@@ -32,17 +32,34 @@ const (
 
 // GetNetworkNodes - gets the nodes of a network
 func GetNetworkNodes(network string) ([]models.Node, error) {
-	var nodes []models.Node
 	allnodes, err := GetAllNodes()
 	if err != nil {
 		return []models.Node{}, err
 	}
-	for _, node := range allnodes {
+
+	return GetNetworkNodesMemory(allnodes, network), nil
+}
+
+// GetNetworkNodesMemory - gets all nodes belonging to a network from list in memory
+func GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node {
+	var nodes = []models.Node{}
+	for i := range allNodes {
+		node := allNodes[i]
 		if node.Network == network {
 			nodes = append(nodes, node)
 		}
 	}
-	return nodes, nil
+	return nodes
+}
+
+// UpdateNodeCheckin - updates the checkin time of a node
+func UpdateNodeCheckin(node *models.Node) error {
+	node.SetLastCheckIn()
+	data, err := json.Marshal(node)
+	if err != nil {
+		return err
+	}
+	return database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 }
 
 // UpdateNode - takes a node and updates another node with it's values

+ 264 - 170
logic/peers.go

@@ -1,6 +1,7 @@
 package logic
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net"
@@ -15,8 +16,15 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
+var (
+	// PeerUpdateCtx context to send to host peer updates
+	PeerUpdateCtx context.Context
+	// PeerUpdateStop - the cancel for PeerUpdateCtx
+	PeerUpdateStop context.CancelFunc
+)
+
 // GetProxyUpdateForHost - gets the proxy update for host
-func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error) {
+func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.ProxyManagerPayload, error) {
 	proxyPayload := models.ProxyManagerPayload{
 		Action: models.ProxyUpdate,
 	}
@@ -39,7 +47,7 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 		relayPeersMap := make(map[string]models.RelayedConf)
 		for _, relayedHost := range relayedHosts {
 			relayedHost := relayedHost
-			payload, err := GetPeerUpdateForHost("", &relayedHost, nil)
+			payload, err := GetPeerUpdateForHost(ctx, "", &relayedHost, nil, nil)
 			if err == nil {
 				relayedEndpoint, udpErr := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayedHost.EndpointIP, GetPeerListenPort(&relayedHost)))
 				if udpErr == nil {
@@ -63,6 +71,9 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 		if err != nil {
 			continue
 		}
+		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
+			continue
+		}
 		currentPeers, err := GetNetworkNodes(node.Network)
 		if err != nil {
 			continue
@@ -72,6 +83,9 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 				//skip yourself
 				continue
 			}
+			if !peer.Connected || peer.PendingDelete || peer.Action == models.NODE_DELETE {
+				continue
+			}
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
 				continue
@@ -82,6 +96,7 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 				currPeerConf = models.PeerConf{
 					Proxy:            peerHost.ProxyEnabled,
 					PublicListenPort: int32(GetPeerListenPort(peerHost)),
+					ProxyListenPort:  GetProxyListenPort(peerHost),
 				}
 			}
 
@@ -115,11 +130,24 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 	return proxyPayload, nil
 }
 
+// ResetPeerUpdateContext - kills any current peer updates and resets the context
+func ResetPeerUpdateContext() {
+	if PeerUpdateCtx != nil && PeerUpdateStop != nil {
+		PeerUpdateStop() // tell any current peer updates to stop
+	}
+
+	PeerUpdateCtx, PeerUpdateStop = context.WithCancel(context.Background())
+}
+
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
-func GetPeerUpdateForHost(network string, host *models.Host, deletedNode *models.Node) (models.HostPeerUpdate, error) {
+func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host, deletedNode *models.Node, deletedClient *models.ExtClient) (models.HostPeerUpdate, error) {
 	if host == nil {
 		return models.HostPeerUpdate{}, errors.New("host is nil")
 	}
+	allNodes, err := GetAllNodes()
+	if err != nil {
+		return models.HostPeerUpdate{}, err
+	}
 	// track which nodes are deleted
 	// after peer calculation, if peer not in list, add delete config of peer
 	hostPeerUpdate := models.HostPeerUpdate{
@@ -131,10 +159,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, deletedNode *models
 		IngressInfo: models.IngressInfo{
 			ExtPeers: make(map[string]models.ExtClientInfo),
 		},
-		EgressInfo: make(map[string]models.EgressInfo),
-		PeerIDs:    make(models.PeerMap, 0),
-		Peers:      []wgtypes.PeerConfig{},
-		NodePeers:  []wgtypes.PeerConfig{},
+		EgressInfo:      make(map[string]models.EgressInfo),
+		PeerIDs:         make(models.PeerMap, 0),
+		Peers:           []wgtypes.PeerConfig{},
+		NodePeers:       []wgtypes.PeerConfig{},
+		HostNetworkInfo: models.HostInfoMap{},
 	}
 
 	logger.Log(1, "peer update for host", host.ID.String())
@@ -148,197 +177,216 @@ func GetPeerUpdateForHost(network string, host *models.Host, deletedNode *models
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 		}
-		currentPeers, err := GetNetworkNodes(node.Network)
-		if err != nil {
-			return models.HostPeerUpdate{}, err
-		}
+		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		var nodePeerMap map[string]models.PeerRouteInfo
 		if node.IsIngressGateway || node.IsEgressGateway {
 			nodePeerMap = make(map[string]models.PeerRouteInfo)
 		}
 		for _, peer := range currentPeers {
-			peer := peer
-			if peer.ID.String() == node.ID.String() {
-				logger.Log(2, "peer update, skipping self")
-				//skip yourself
-				continue
-			}
-			var peerConfig wgtypes.PeerConfig
-			peerHost, err := GetHost(peer.HostID.String())
-			if err != nil {
-				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
-				return models.HostPeerUpdate{}, err
-			}
-
-			peerConfig.PublicKey = peerHost.PublicKey
-			peerConfig.PersistentKeepaliveInterval = &peer.PersistentKeepalive
-			peerConfig.ReplaceAllowedIPs = true
-			uselocal := false
-			if host.EndpointIP.String() == peerHost.EndpointIP.String() {
-				//peer is on same network
-				// set to localaddress
-				uselocal = true
-				if node.LocalAddress.IP == nil {
-					// use public endpint
-					uselocal = false
+			select {
+			case <-ctx.Done():
+				logger.Log(2, "cancelled peer update for host", host.Name, host.ID.String())
+				return models.HostPeerUpdate{}, fmt.Errorf("peer update cancelled")
+			default:
+				peer := peer
+				if peer.ID.String() == node.ID.String() {
+					logger.Log(2, "peer update, skipping self")
+					//skip yourself
+					continue
 				}
-				if node.LocalAddress.String() == peer.LocalAddress.String() {
-					uselocal = false
+				var peerConfig wgtypes.PeerConfig
+				peerHost, err := GetHost(peer.HostID.String())
+				if err != nil {
+					logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+					return models.HostPeerUpdate{}, err
 				}
-			}
-			peerConfig.Endpoint = &net.UDPAddr{
-				IP:   peerHost.EndpointIP,
-				Port: GetPeerListenPort(peerHost),
-			}
 
-			if uselocal {
-				peerConfig.Endpoint.IP = peer.LocalAddress.IP
-			}
-			allowedips := GetAllowedIPs(&node, &peer, nil)
-			if peer.IsIngressGateway {
-				for _, entry := range peer.IngressGatewayRange {
-					_, cidr, err := net.ParseCIDR(string(entry))
-					if err == nil {
-						allowedips = append(allowedips, *cidr)
+				peerConfig.PublicKey = peerHost.PublicKey
+				peerConfig.PersistentKeepaliveInterval = &peer.PersistentKeepalive
+				peerConfig.ReplaceAllowedIPs = true
+				uselocal := false
+				if host.EndpointIP.String() == peerHost.EndpointIP.String() {
+					// peer is on same network
+					// set to localaddress
+					uselocal = true
+					if node.LocalAddress.IP == nil {
+						// use public endpint
+						uselocal = false
+					}
+					if node.LocalAddress.String() == peer.LocalAddress.String() {
+						uselocal = false
 					}
 				}
-			}
-			if peer.IsEgressGateway {
-				allowedips = append(allowedips, getEgressIPs(&node, &peer)...)
-			}
-			if peer.Action != models.NODE_DELETE &&
-				!peer.PendingDelete &&
-				peer.Connected &&
-				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
-				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
-				peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
-			}
+				peerConfig.Endpoint = &net.UDPAddr{
+					IP:   peerHost.EndpointIP,
+					Port: peerHost.ListenPort,
+				}
 
-			if node.IsIngressGateway || node.IsEgressGateway {
+				if uselocal {
+					peerConfig.Endpoint.IP = peer.LocalAddress.IP
+				}
+				allowedips := GetAllowedIPs(&node, &peer, nil)
 				if peer.IsIngressGateway {
-					_, extPeerIDAndAddrs, err := getExtPeers(&peer)
-					if err == nil {
-						for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
-							extPeerIdAndAddr := extPeerIdAndAddr
-							nodePeerMap[extPeerIdAndAddr.ID] = models.PeerRouteInfo{
-								PeerAddr: net.IPNet{
-									IP:   net.ParseIP(extPeerIdAndAddr.Address),
-									Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
-								},
-								PeerKey: extPeerIdAndAddr.ID,
-								Allow:   true,
-							}
+					for _, entry := range peer.IngressGatewayRange {
+						_, cidr, err := net.ParseCIDR(string(entry))
+						if err == nil {
+							allowedips = append(allowedips, *cidr)
 						}
 					}
 				}
-				if node.IsIngressGateway && peer.IsEgressGateway {
-					hostPeerUpdate.IngressInfo.EgressRanges = append(hostPeerUpdate.IngressInfo.EgressRanges,
-						peer.EgressGatewayRanges...)
+				if peer.IsEgressGateway {
+					allowedips = append(allowedips, getEgressIPs(&node, &peer)...)
 				}
-				nodePeerMap[peerHost.PublicKey.String()] = models.PeerRouteInfo{
-					PeerAddr: net.IPNet{
-						IP:   net.ParseIP(peer.PrimaryAddress()),
-						Mask: getCIDRMaskFromAddr(peer.PrimaryAddress()),
-					},
-					PeerKey: peerHost.PublicKey.String(),
-					Allow:   true,
+				if peer.Action != models.NODE_DELETE &&
+					!peer.PendingDelete &&
+					peer.Connected &&
+					nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+					(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
+					peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
 				}
-			}
 
-			var nodePeer wgtypes.PeerConfig
-			if _, ok := hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()]; !ok {
-				hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()] = make(map[string]models.IDandAddr)
-				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
-				peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
-				hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
-					ID:      peer.ID.String(),
-					Address: peer.PrimaryAddress(),
-					Name:    peerHost.Name,
-					Network: peer.Network,
-				}
-				nodePeer = peerConfig
-			} else {
-				peerAllowedIPs := hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs
-				peerAllowedIPs = append(peerAllowedIPs, allowedips...)
-				hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs = peerAllowedIPs
-				hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
-					ID:      peer.ID.String(),
-					Address: peer.PrimaryAddress(),
-					Name:    peerHost.Name,
-					Network: peer.Network,
-				}
-				nodePeer = hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]]
-			}
-
-			if node.Network == network { // add to peers map for metrics
-				hostPeerUpdate.PeerIDs[peerHost.PublicKey.String()] = models.IDandAddr{
-					ID:      peer.ID.String(),
-					Address: peer.PrimaryAddress(),
-					Name:    peerHost.Name,
-					Network: peer.Network,
-				}
-				hostPeerUpdate.NodePeers = append(hostPeerUpdate.NodePeers, nodePeer)
-			}
-		}
-		var extPeers []wgtypes.PeerConfig
-		var extPeerIDAndAddrs []models.IDandAddr
-		if node.IsIngressGateway {
-			extPeers, extPeerIDAndAddrs, err = getExtPeers(&node)
-			if err == nil {
-				for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
-					extPeerIdAndAddr := extPeerIdAndAddr
-					nodePeerMap[extPeerIdAndAddr.ID] = models.PeerRouteInfo{
+				if node.IsIngressGateway || node.IsEgressGateway {
+					if peer.IsIngressGateway {
+						_, extPeerIDAndAddrs, err := getExtPeers(&peer)
+						if err == nil {
+							for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
+								extPeerIdAndAddr := extPeerIdAndAddr
+								nodePeerMap[extPeerIdAndAddr.ID] = models.PeerRouteInfo{
+									PeerAddr: net.IPNet{
+										IP:   net.ParseIP(extPeerIdAndAddr.Address),
+										Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
+									},
+									PeerKey: extPeerIdAndAddr.ID,
+									Allow:   true,
+									ID:      extPeerIdAndAddr.ID,
+								}
+							}
+						}
+					}
+					if node.IsIngressGateway && peer.IsEgressGateway {
+						hostPeerUpdate.IngressInfo.EgressRanges = append(hostPeerUpdate.IngressInfo.EgressRanges,
+							peer.EgressGatewayRanges...)
+					}
+					nodePeerMap[peerHost.PublicKey.String()] = models.PeerRouteInfo{
 						PeerAddr: net.IPNet{
-							IP:   net.ParseIP(extPeerIdAndAddr.Address),
-							Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
+							IP:   net.ParseIP(peer.PrimaryAddress()),
+							Mask: getCIDRMaskFromAddr(peer.PrimaryAddress()),
 						},
-						PeerKey: extPeerIdAndAddr.ID,
+						PeerKey: peerHost.PublicKey.String(),
 						Allow:   true,
+						ID:      peer.ID.String(),
 					}
 				}
-				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...)
-				for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
-					extPeerIdAndAddr := extPeerIdAndAddr
-					hostPeerUpdate.HostPeerIDs[extPeerIdAndAddr.ID] = make(map[string]models.IDandAddr)
-					hostPeerUpdate.HostPeerIDs[extPeerIdAndAddr.ID][extPeerIdAndAddr.ID] = models.IDandAddr{
-						ID:      extPeerIdAndAddr.ID,
-						Address: extPeerIdAndAddr.Address,
-						Name:    extPeerIdAndAddr.Name,
-						Network: node.Network,
+
+				peerProxyPort := GetProxyListenPort(peerHost)
+				var nodePeer wgtypes.PeerConfig
+				if _, ok := hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()]; !ok {
+					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()] = make(map[string]models.IDandAddr)
+					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
+					peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
+					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
+						ID:              peer.ID.String(),
+						Address:         peer.PrimaryAddress(),
+						Name:            peerHost.Name,
+						Network:         peer.Network,
+						ProxyListenPort: peerProxyPort,
 					}
-					hostPeerUpdate.IngressInfo.ExtPeers[extPeerIdAndAddr.ID] = models.ExtClientInfo{
-						Masquerade: true,
-						IngGwAddr: net.IPNet{
-							IP:   net.ParseIP(node.PrimaryAddress()),
-							Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
-						},
-						Network: node.PrimaryNetworkRange(),
-						ExtPeerAddr: net.IPNet{
-							IP:   net.ParseIP(extPeerIdAndAddr.Address),
-							Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
-						},
-						ExtPeerKey: extPeerIdAndAddr.ID,
-						Peers:      nodePeerMap,
+					hostPeerUpdate.HostNetworkInfo[peerHost.PublicKey.String()] = models.HostNetworkInfo{
+						Interfaces:      peerHost.Interfaces,
+						ProxyListenPort: peerProxyPort,
+					}
+					nodePeer = peerConfig
+				} else {
+					peerAllowedIPs := hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs
+					peerAllowedIPs = append(peerAllowedIPs, allowedips...)
+					hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs = peerAllowedIPs
+					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
+						ID:              peer.ID.String(),
+						Address:         peer.PrimaryAddress(),
+						Name:            peerHost.Name,
+						Network:         peer.Network,
+						ProxyListenPort: GetProxyListenPort(peerHost),
 					}
-					if node.Network == network {
-						hostPeerUpdate.PeerIDs[extPeerIdAndAddr.ID] = extPeerIdAndAddr
-						hostPeerUpdate.NodePeers = append(hostPeerUpdate.NodePeers, extPeers...)
+					hostPeerUpdate.HostNetworkInfo[peerHost.PublicKey.String()] = models.HostNetworkInfo{
+						Interfaces:      peerHost.Interfaces,
+						ProxyListenPort: peerProxyPort,
 					}
+					nodePeer = hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]]
+				}
+
+				if node.Network == network { // add to peers map for metrics
+					hostPeerUpdate.PeerIDs[peerHost.PublicKey.String()] = models.IDandAddr{
+						ID:              peer.ID.String(),
+						Address:         peer.PrimaryAddress(),
+						Name:            peerHost.Name,
+						Network:         peer.Network,
+						ProxyListenPort: peerHost.ProxyListenPort,
+					}
+					hostPeerUpdate.NodePeers = append(hostPeerUpdate.NodePeers, nodePeer)
 				}
-			} else if !database.IsEmptyRecord(err) {
-				logger.Log(1, "error retrieving external clients:", err.Error())
 			}
-		}
-		if node.IsEgressGateway {
-			hostPeerUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
-				EgressID: node.ID.String(),
-				Network:  node.PrimaryNetworkRange(),
-				EgressGwAddr: net.IPNet{
-					IP:   net.ParseIP(node.PrimaryAddress()),
-					Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
-				},
-				GwPeers:     nodePeerMap,
-				EgressGWCfg: node.EgressGatewayRequest,
+			var extPeers []wgtypes.PeerConfig
+			var extPeerIDAndAddrs []models.IDandAddr
+			if node.IsIngressGateway {
+				extPeers, extPeerIDAndAddrs, err = getExtPeers(&node)
+				if err == nil {
+					for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
+						extPeerIdAndAddr := extPeerIdAndAddr
+						nodePeerMap[extPeerIdAndAddr.ID] = models.PeerRouteInfo{
+							PeerAddr: net.IPNet{
+								IP:   net.ParseIP(extPeerIdAndAddr.Address),
+								Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
+							},
+							PeerKey: extPeerIdAndAddr.ID,
+							Allow:   true,
+							ID:      extPeerIdAndAddr.ID,
+						}
+					}
+					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...)
+					for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
+						extPeerIdAndAddr := extPeerIdAndAddr
+						hostPeerUpdate.HostPeerIDs[extPeerIdAndAddr.ID] = make(map[string]models.IDandAddr)
+						hostPeerUpdate.HostPeerIDs[extPeerIdAndAddr.ID][extPeerIdAndAddr.ID] = models.IDandAddr{
+							ID:      extPeerIdAndAddr.ID,
+							Address: extPeerIdAndAddr.Address,
+							Name:    extPeerIdAndAddr.Name,
+							Network: node.Network,
+						}
+
+						hostPeerUpdate.IngressInfo.ExtPeers[extPeerIdAndAddr.ID] = models.ExtClientInfo{
+							Masquerade: true,
+							IngGwAddr: net.IPNet{
+								IP:   net.ParseIP(node.PrimaryAddress()),
+								Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
+							},
+							Network: node.PrimaryNetworkRange(),
+							ExtPeerAddr: net.IPNet{
+								IP:   net.ParseIP(extPeerIdAndAddr.Address),
+								Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
+							},
+							ExtPeerKey: extPeerIdAndAddr.ID,
+							Peers:      filterNodeMapForClientACLs(extPeerIdAndAddr.ID, node.Network, nodePeerMap),
+						}
+						if node.Network == network {
+							hostPeerUpdate.PeerIDs[extPeerIdAndAddr.ID] = extPeerIdAndAddr
+							hostPeerUpdate.NodePeers = append(hostPeerUpdate.NodePeers, extPeers...)
+						}
+					}
+				} else if !database.IsEmptyRecord(err) {
+					logger.Log(1, "error retrieving external clients:", err.Error())
+				}
+			}
+			if node.IsEgressGateway {
+				hostPeerUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
+					EgressID: node.ID.String(),
+					Network:  node.PrimaryNetworkRange(),
+					EgressGwAddr: net.IPNet{
+						IP:   net.ParseIP(node.PrimaryAddress()),
+						Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
+					},
+					GwPeers:     nodePeerMap,
+					EgressGWCfg: node.EgressGatewayRequest,
+				}
 			}
 		}
 	}
@@ -360,6 +408,16 @@ func GetPeerUpdateForHost(network string, host *models.Host, deletedNode *models
 		hostPeerUpdate.NodePeers[i] = peer
 	}
 
+	if deletedClient != nil {
+		key, err := wgtypes.ParseKey(deletedClient.PublicKey)
+		if err == nil {
+			hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, wgtypes.PeerConfig{
+				PublicKey: key,
+				Remove:    true,
+			})
+		}
+	}
+
 	return hostPeerUpdate, nil
 }
 
@@ -376,6 +434,15 @@ func GetPeerListenPort(host *models.Host) int {
 	return peerPort
 }
 
+// GetProxyListenPort - fetches the proxy listen port
+func GetProxyListenPort(host *models.Host) int {
+	proxyPort := host.ProxyListenPort
+	if host.PublicListenPort != 0 {
+		proxyPort = host.PublicListenPort
+	}
+	return proxyPort
+}
+
 func getExtPeers(node *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, error) {
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
@@ -388,6 +455,7 @@ func getExtPeers(node *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, e
 		return peers, idsAndAddr, err
 	}
 	for _, extPeer := range extPeers {
+		extPeer := extPeer
 		pubkey, err := wgtypes.ParseKey(extPeer.PublicKey)
 		if err != nil {
 			logger.Log(1, "error parsing ext pub key:", err.Error())
@@ -619,3 +687,29 @@ func getCIDRMaskFromAddr(addr string) net.IPMask {
 	}
 	return cidr
 }
+
+// accounts for ext client ACLs
+func filterNodeMapForClientACLs(publicKey, network string, nodePeerMap map[string]models.PeerRouteInfo) map[string]models.PeerRouteInfo {
+	if !isEE {
+		return nodePeerMap
+	}
+	if nodePeerMap == nil {
+		return map[string]models.PeerRouteInfo{}
+	}
+
+	if len(publicKey) == 0 || len(network) == 0 {
+		return nodePeerMap
+	}
+
+	client, err := GetExtClientByPubKey(publicKey, network)
+	if err != nil {
+		return nodePeerMap
+	}
+	for k := range nodePeerMap {
+		currNodePeer := nodePeerMap[k]
+		if _, ok := client.ACLs[currNodePeer.ID]; ok {
+			delete(nodePeerMap, k)
+		}
+	}
+	return nodePeerMap
+}

+ 3 - 1
logic/pro/networkuser_test.go

@@ -1,6 +1,7 @@
 package pro
 
 import (
+	"os"
 	"testing"
 
 	"github.com/google/uuid"
@@ -13,6 +14,7 @@ import (
 func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
+	os.Exit(m.Run())
 }
 
 func TestNetworkUserLogic(t *testing.T) {
@@ -33,7 +35,7 @@ func TestNetworkUserLogic(t *testing.T) {
 	}
 
 	clients := []models.ExtClient{
-		models.ExtClient{
+		{
 			ClientID: "coolclient",
 		},
 	}

+ 11 - 1
logic/telemetry.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
@@ -86,7 +87,7 @@ func fetchTelemetryData() (telemetryData, error) {
 	data.Networks = getDBLength(database.NETWORKS_TABLE_NAME)
 	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
-	//data.Servers = GetServerCount()
+	data.Servers = getServerCount()
 	nodes, err := GetAllNodes()
 	if err == nil {
 		data.Nodes = len(nodes)
@@ -95,6 +96,15 @@ func fetchTelemetryData() (telemetryData, error) {
 	return data, err
 }
 
+// getServerCount returns number of servers from database
+func getServerCount() int {
+	data, err := database.FetchRecords(database.SERVER_UUID_TABLE_NAME)
+	if err != nil {
+		logger.Log(0, "errror retrieving server data", err.Error())
+	}
+	return len(data)
+}
+
 // setTelemetryTimestamp - Give the entry in the DB a new timestamp
 func setTelemetryTimestamp(telRecord *models.Telemetry) error {
 	lastsend := time.Now().Unix()

+ 1 - 1
main.go

@@ -27,7 +27,7 @@ import (
 	stunserver "github.com/gravitl/netmaker/stun-server"
 )
 
-var version = "v0.18.2"
+var version = "v0.18.6"
 
 // Start DB Connection and start API Request Handler
 func main() {

+ 2 - 0
models/api_host.go

@@ -112,6 +112,8 @@ func (a *ApiHost) ConvertAPIHostToNMHost(currentHost *Host) *Host {
 	h.IsRelayed = a.IsRelayed
 	h.ProxyEnabled = a.ProxyEnabled
 	h.IsDefault = a.IsDefault
+	h.NatType = currentHost.NatType
+	h.TurnEndpoint = currentHost.TurnEndpoint
 
 	return &h
 }

+ 6 - 0
models/enrollment_key.go

@@ -34,6 +34,12 @@ type APIEnrollmentKey struct {
 	Tags          []string `json:"tags"`
 }
 
+// RegisterResponse - the response to a successful enrollment register
+type RegisterResponse struct {
+	ServerConf    ServerConfig `json:"server_config"`
+	RequestedHost Host         `json:"requested_host"`
+}
+
 // EnrollmentKey.IsValid - checks if the key is still valid to use
 func (k *EnrollmentKey) IsValid() bool {
 	if k == nil {

+ 13 - 12
models/extclient.go

@@ -2,16 +2,17 @@ package models
 
 // ExtClient - struct for external clients
 type ExtClient struct {
-	ClientID               string `json:"clientid" bson:"clientid"`
-	Description            string `json:"description" bson:"description"`
-	PrivateKey             string `json:"privatekey" bson:"privatekey"`
-	PublicKey              string `json:"publickey" bson:"publickey"`
-	Network                string `json:"network" bson:"network"`
-	Address                string `json:"address" bson:"address"`
-	Address6               string `json:"address6" bson:"address6"`
-	IngressGatewayID       string `json:"ingressgatewayid" bson:"ingressgatewayid"`
-	IngressGatewayEndpoint string `json:"ingressgatewayendpoint" bson:"ingressgatewayendpoint"`
-	LastModified           int64  `json:"lastmodified" bson:"lastmodified"`
-	Enabled                bool   `json:"enabled" bson:"enabled"`
-	OwnerID                string `json:"ownerid" bson:"ownerid"`
+	ClientID               string              `json:"clientid" bson:"clientid"`
+	Description            string              `json:"description" bson:"description"`
+	PrivateKey             string              `json:"privatekey" bson:"privatekey"`
+	PublicKey              string              `json:"publickey" bson:"publickey"`
+	Network                string              `json:"network" bson:"network"`
+	Address                string              `json:"address" bson:"address"`
+	Address6               string              `json:"address6" bson:"address6"`
+	IngressGatewayID       string              `json:"ingressgatewayid" bson:"ingressgatewayid"`
+	IngressGatewayEndpoint string              `json:"ingressgatewayendpoint" bson:"ingressgatewayendpoint"`
+	LastModified           int64               `json:"lastmodified" bson:"lastmodified"`
+	Enabled                bool                `json:"enabled" bson:"enabled"`
+	OwnerID                string              `json:"ownerid" bson:"ownerid"`
+	ACLs                   map[string]struct{} `json:"acls,omitempty" bson:"acls,omitempty"`
 }

+ 36 - 2
models/host.go

@@ -2,11 +2,40 @@ package models
 
 import (
 	"net"
+	"net/netip"
 
 	"github.com/google/uuid"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
+// OS_Types - list of OS types Netmaker cares about
+var OS_Types = struct {
+	Linux   string
+	Windows string
+	Mac     string
+	FreeBSD string
+	IoT     string
+}{
+	Linux:   "linux",
+	Windows: "windows",
+	Mac:     "darwin",
+	FreeBSD: "freebsd",
+	IoT:     "iot",
+}
+
+// NAT_Types - the type of NAT in which a HOST currently resides (simplified)
+var NAT_Types = struct {
+	Public     string
+	Symmetric  string
+	Asymmetric string
+	Double     string
+}{
+	Public:     "public",
+	Symmetric:  "symmetric",
+	Asymmetric: "asymmetric",
+	Double:     "double",
+}
+
 // WIREGUARD_INTERFACE name of wireguard interface
 const WIREGUARD_INTERFACE = "netmaker"
 
@@ -29,7 +58,7 @@ type Host struct {
 	MTU              int              `json:"mtu" yaml:"mtu"`
 	PublicKey        wgtypes.Key      `json:"publickey" yaml:"publickey"`
 	MacAddress       net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
-	TrafficKeyPublic []byte           `json:"traffickeypublic" yaml:"trafficekeypublic"`
+	TrafficKeyPublic []byte           `json:"traffickeypublic" yaml:"traffickeypublic"`
 	InternetGateway  net.UDPAddr      `json:"internetgateway" yaml:"internetgateway"`
 	Nodes            []string         `json:"nodes" yaml:"nodes"`
 	IsRelayed        bool             `json:"isrelayed" yaml:"isrelayed"`
@@ -37,13 +66,16 @@ type Host struct {
 	IsRelay          bool             `json:"isrelay" yaml:"isrelay"`
 	RelayedHosts     []string         `json:"relay_hosts" yaml:"relay_hosts"`
 	Interfaces       []Iface          `json:"interfaces" yaml:"interfaces"`
-	DefaultInterface string           `json:"defaultinterface" yaml:"defautlinterface"`
+	DefaultInterface string           `json:"defaultinterface" yaml:"defaultinterface"`
 	EndpointIP       net.IP           `json:"endpointip" yaml:"endpointip"`
 	ProxyEnabled     bool             `json:"proxy_enabled" yaml:"proxy_enabled"`
+	ProxyEnabledSet  bool             `json:"proxy_enabled_updated" yaml:"proxy_enabled_updated"`
 	IsDocker         bool             `json:"isdocker" yaml:"isdocker"`
 	IsK8S            bool             `json:"isk8s" yaml:"isk8s"`
 	IsStatic         bool             `json:"isstatic" yaml:"isstatic"`
 	IsDefault        bool             `json:"isdefault" yaml:"isdefault"`
+	NatType          string           `json:"nat_type,omitempty" yaml:"nat_type,omitempty"`
+	TurnEndpoint     *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
 }
 
 // FormatBool converts a boolean to a [yes|no] string
@@ -78,6 +110,8 @@ const (
 	Acknowledgement = "ACK"
 	// RequestAck - request an ACK
 	RequestAck = "REQ_ACK"
+	// CheckIn - update last check in times and public address and interfaces
+	CheckIn = "CHECK_IN"
 )
 
 // HostUpdate - struct for host update

+ 25 - 14
models/metrics.go

@@ -15,24 +15,35 @@ type Metrics struct {
 
 // Metric - holds a metric for data between nodes
 type Metric struct {
-	NodeName      string        `json:"node_name" bson:"node_name" yaml:"node_name"`
-	Uptime        int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
-	TotalTime     int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
-	Latency       int64         `json:"latency" bson:"latency" yaml:"latency"`
-	TotalReceived int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
-	TotalSent     int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
-	ActualUptime  time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
-	PercentUp     float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
-	Connected     bool          `json:"connected" bson:"connected" yaml:"connected"`
+	NodeName         string        `json:"node_name" bson:"node_name" yaml:"node_name"`
+	Uptime           int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
+	TotalTime        int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
+	Latency          int64         `json:"latency" bson:"latency" yaml:"latency"`
+	TotalReceived    int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
+	TotalSent        int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
+	ActualUptime     time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
+	PercentUp        float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
+	Connected        bool          `json:"connected" bson:"connected" yaml:"connected"`
+	CollectedByProxy bool          `json:"collected_by_proxy" bson:"collected_by_proxy" yaml:"collected_by_proxy"`
 }
 
 // IDandAddr - struct to hold ID and primary Address
 type IDandAddr struct {
-	ID       string `json:"id" bson:"id" yaml:"id"`
-	Address  string `json:"address" bson:"address" yaml:"address"`
-	Name     string `json:"name" bson:"name" yaml:"name"`
-	IsServer string `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
-	Network  string `json:"network" bson:"network" yaml:"network" validate:"network"`
+	ID              string `json:"id" bson:"id" yaml:"id"`
+	Address         string `json:"address" bson:"address" yaml:"address"`
+	Name            string `json:"name" bson:"name" yaml:"name"`
+	IsServer        string `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
+	Network         string `json:"network" bson:"network" yaml:"network" validate:"network"`
+	ProxyListenPort int    `json:"proxy_listen_port" yaml:"proxy_listen_port"`
+}
+
+// HostInfoMap - map of host public keys to host networking info
+type HostInfoMap map[string]HostNetworkInfo
+
+// HostNetworkInfo - holds info related to host networking (used for client side peer calculations)
+type HostNetworkInfo struct {
+	Interfaces      []Iface `json:"interfaces" yaml:"interfaces"`
+	ProxyListenPort int     `json:"proxy_listen_port" yaml:"proxy_listen_port"`
 }
 
 // PeerMap - peer map for ids and addresses in metrics

+ 2 - 3
models/migrate.go

@@ -2,7 +2,6 @@ package models
 
 // MigrationData struct needed to create new v0.18.0 node from v.0.17.X node
 type MigrationData struct {
-	JoinData     JoinData
-	LegacyNodeID string
-	Password     string
+	NewHost     Host
+	LegacyNodes []LegacyNode
 }

+ 13 - 11
models/mqtt.go

@@ -8,17 +8,18 @@ import (
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host          Host                 `json:"host" bson:"host" yaml:"host"`
-	Server        string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs   []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers     []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers         []wgtypes.PeerConfig
-	HostPeerIDs   HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
-	ProxyUpdate   ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
-	EgressInfo    map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
-	IngressInfo   IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
-	PeerIDs       PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	Host            Host                 `json:"host" bson:"host" yaml:"host"`
+	Server          string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers           []wgtypes.PeerConfig
+	HostPeerIDs     HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
+	ProxyUpdate     ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
+	EgressInfo      map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
+	IngressInfo     IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
+	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
 }
 
 // IngressInfo - struct for ingress info
@@ -41,6 +42,7 @@ type PeerRouteInfo struct {
 	PeerAddr net.IPNet `json:"peer_addr" yaml:"peer_addr"`
 	PeerKey  string    `json:"peer_key" yaml:"peer_key"`
 	Allow    bool      `json:"allow" yaml:"allow"`
+	ID       string    `json:"id,omitempty" yaml:"id,omitempty"`
 }
 
 // ExtClientInfo - struct for ext. client and it's peers

+ 0 - 1
models/network.go

@@ -19,7 +19,6 @@ type Network struct {
 	NodeLimit           int32                 `json:"nodelimit" bson:"nodelimit"`
 	DefaultPostDown     string                `json:"defaultpostdown" bson:"defaultpostdown"`
 	DefaultKeepalive    int32                 `json:"defaultkeepalive" bson:"defaultkeepalive" validate:"omitempty,max=1000"`
-	AccessKeys          []AccessKey           `json:"accesskeys" bson:"accesskeys"`
 	AllowManualSignUp   string                `json:"allowmanualsignup" bson:"allowmanualsignup" validate:"checkyesorno"`
 	IsIPv4              string                `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
 	IsIPv6              string                `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`

+ 1 - 0
models/proxy.go

@@ -37,6 +37,7 @@ type RelayedConf struct {
 type PeerConf struct {
 	Proxy            bool         `json:"proxy"`
 	PublicListenPort int32        `json:"public_listen_port"`
+	ProxyListenPort  int          `json:"proxy_listen_port"`
 	IsExtClient      bool         `json:"is_ext_client"`
 	Address          net.IP       `json:"address"`
 	ExtInternalIp    net.IP       `json:"ext_internal_ip"`

+ 16 - 12
models/structs.go

@@ -8,12 +8,17 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
-const PLACEHOLDER_KEY_TEXT = "ACCESS_KEY"
-const PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
+const (
+	// PLACEHOLDER_KEY_TEXT - access key placeholder text if option turned off
+	PLACEHOLDER_KEY_TEXT = "ACCESS_KEY"
+	// PLACEHOLDER_TOKEN_TEXT - access key token placeholder text if option turned off
+	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
+)
 
 // CustomExtClient - struct for CustomExtClient params
 type CustomExtClient struct {
-	ClientID string `json:"clientid"`
+	ClientID  string `json:"clientid"`
+	PublicKey string `json:"publickey,omitempty"`
 }
 
 // AuthParams - struct for auth params
@@ -97,15 +102,6 @@ type SuccessResponse struct {
 	Response interface{}
 }
 
-// AccessKey - access key struct
-type AccessKey struct {
-	Name         string     `json:"name" bson:"name" validate:"omitempty,max=345"`
-	Value        string     `json:"value" bson:"value" validate:"omitempty,alphanum,max=16"`
-	AccessString string     `json:"accessstring" bson:"accessstring"`
-	Uses         int        `json:"uses" bson:"uses" validate:"numeric,min=0"`
-	Expiration   *time.Time `json:"expiration" bson:"expiration"`
-}
-
 // DisplayKey - what is displayed for key
 type DisplayKey struct {
 	Name string `json:"name" bson:"name"`
@@ -203,6 +199,14 @@ type TrafficKeys struct {
 	Server []byte `json:"server" bson:"server" yaml:"server"`
 }
 
+// HostPull - response of a host's pull
+type HostPull struct {
+	Host         Host                 `json:"host" yaml:"host"`
+	Peers        []wgtypes.PeerConfig `json:"peers" yaml:"peers"`
+	ServerConfig ServerConfig         `json:"server_config" yaml:"server_config"`
+	PeerIDs      PeerMap              `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+}
+
 // NodeGet - struct for a single node get response
 type NodeGet struct {
 	Node         Node                 `json:"node" bson:"node" yaml:"node"`

+ 237 - 0
mq/emqx.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"sync"
 
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -29,6 +30,17 @@ type (
 		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) {
@@ -152,3 +164,228 @@ func CreateEmqxDefaultAuthenticator() error {
 	}
 	return nil
 }
+
+// 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
+	}
+	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 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/rules/users/"+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/rules/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("dns/all/%s/%s", hostID, serverName),
+				Permission: "allow",
+				Action:     "all",
+			},
+			{
+				Topic:      fmt.Sprintf("dns/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/rules/users/"+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 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/rules/users/"+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
+}

+ 101 - 90
mq/handlers.go

@@ -1,8 +1,10 @@
 package mq
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
+	"math"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -21,94 +23,6 @@ func DefaultHandler(client mqtt.Client, msg mqtt.Message) {
 	logger.Log(0, "MQTT Message: Topic: ", string(msg.Topic()), " Message: ", string(msg.Payload()))
 }
 
-// Ping message Handler -- handles ping topic from client nodes
-func Ping(client mqtt.Client, msg mqtt.Message) {
-	id, err := getID(msg.Topic())
-	if err != nil {
-		logger.Log(0, "error getting node.ID sent on ping topic ")
-		return
-	}
-	node, err := logic.GetNodeByID(id)
-	if err != nil {
-		logger.Log(3, "mq-ping error getting node: ", err.Error())
-		node, err := logic.GetNodeByID(id)
-		if err != nil {
-			logger.Log(3, "mq-ping error getting node: ", err.Error())
-			if database.IsEmptyRecord(err) {
-				h := logic.GetHostByNodeID(id) // check if a host is still associated
-				if h != nil {                  // inform host that node should be removed
-					fakeNode := models.Node{}
-					fakeNode.ID, _ = uuid.Parse(id)
-					fakeNode.Action = models.NODE_DELETE
-					fakeNode.PendingDelete = true
-					if err := NodeUpdate(&fakeNode); err != nil {
-						logger.Log(0, "failed to inform host", h.Name, h.ID.String(), "to remove node", id, err.Error())
-					}
-				}
-			}
-			return
-		}
-		decrypted, decryptErr := decryptMsg(&node, msg.Payload())
-		if decryptErr != nil {
-			logger.Log(0, "error decrypting when updating node ", node.ID.String(), decryptErr.Error())
-			return
-		}
-		var checkin models.NodeCheckin
-		if err := json.Unmarshal(decrypted, &checkin); err != nil {
-			logger.Log(1, "error unmarshaling payload ", err.Error())
-			return
-		}
-		host, err := logic.GetHost(node.HostID.String())
-		if err != nil {
-			logger.Log(0, "error retrieving host for node ", node.ID.String(), err.Error())
-			return
-		}
-		node.SetLastCheckIn()
-		host.Version = checkin.Version
-		node.Connected = checkin.Connected
-		host.Interfaces = checkin.Ifaces
-		for i := range host.Interfaces {
-			host.Interfaces[i].AddressString = host.Interfaces[i].Address.String()
-		}
-		if err := logic.UpdateNode(&node, &node); err != nil {
-			logger.Log(0, "error updating node", node.ID.String(), " on checkin", err.Error())
-			return
-		}
-
-		return
-	}
-	decrypted, decryptErr := decryptMsg(&node, msg.Payload())
-	if decryptErr != nil {
-		logger.Log(0, "error decrypting when updating node ", node.ID.String(), decryptErr.Error())
-		return
-	}
-	var checkin models.NodeCheckin
-	if err := json.Unmarshal(decrypted, &checkin); err != nil {
-		logger.Log(1, "error unmarshaling payload ", err.Error())
-		return
-	}
-	host, err := logic.GetHost(node.HostID.String())
-	if err != nil {
-		logger.Log(0, "error retrieving host for node ", node.ID.String(), err.Error())
-		return
-	}
-	node.SetLastCheckIn()
-	host.Version = checkin.Version
-	node.Connected = checkin.Connected
-	host.Interfaces = checkin.Ifaces
-	for i := range host.Interfaces {
-		host.Interfaces[i].AddressString = host.Interfaces[i].Address.String()
-	}
-	if err := logic.UpdateNode(&node, &node); err != nil {
-		logger.Log(0, "error updating node", node.ID.String(), " on checkin", err.Error())
-		return
-	}
-
-	logger.Log(3, "ping processed for node", node.ID.String())
-	// --TODO --set client version once feature is implemented.
-	//node.SetClientVersion(msg.Payload())
-}
-
 // UpdateNode  message Handler -- handles updates from client nodes
 func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 	id, err := getID(msg.Topic())
@@ -177,6 +91,8 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	logger.Log(3, fmt.Sprintf("recieved host update: %s\n", hostUpdate.Host.ID.String()))
 	var sendPeerUpdate bool
 	switch hostUpdate.Action {
+	case models.CheckIn:
+		sendPeerUpdate = handleHostCheckin(&hostUpdate.Host, currentHost)
 	case models.Acknowledgement:
 		hu := hostactions.GetAction(currentHost.ID.String())
 		if hu != nil {
@@ -184,10 +100,20 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				logger.Log(0, "failed to send new node to host", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
 				return
 			} else {
-				if err = PublishSingleHostPeerUpdate(currentHost, nil); err != nil {
+				if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+					if err = AppendNodeUpdateACL(hu.Host.ID.String(), hu.Node.Network, hu.Node.ID.String(), servercfg.GetServer()); err != nil {
+						logger.Log(0, "failed to add ACLs for EMQX node", err.Error())
+						return
+					}
+				}
+				if err = PublishSingleHostPeerUpdate(context.Background(), currentHost, nil, nil); err != nil {
 					logger.Log(0, "failed peers publish after join acknowledged", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
 					return
 				}
+				if err = handleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
+					logger.Log(0, "failed to send dns update after node,", hu.Node.ID.String(), ", added to host", hu.Host.Name, err.Error())
+					return
+				}
 			}
 		}
 	case models.UpdateHost:
@@ -278,7 +204,7 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 			logger.Log(2, "updating peers after node", currentNode.ID.String(), currentNode.Network, "detected connectivity issues")
 			host, err := logic.GetHost(currentNode.HostID.String())
 			if err == nil {
-				if err = PublishSingleHostPeerUpdate(host, nil); err != nil {
+				if err = PublishSingleHostPeerUpdate(context.Background(), host, nil, nil); err != nil {
 					logger.Log(0, "failed to publish update after failover peer change for node", currentNode.ID.String(), currentNode.Network)
 				}
 			}
@@ -361,6 +287,21 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 		oldMetric := oldMetrics.Connectivity[k]
 		currMetric.TotalTime += oldMetric.TotalTime
 		currMetric.Uptime += oldMetric.Uptime // get the total uptime for this connection
+		if currMetric.CollectedByProxy {
+			currMetric.TotalReceived += oldMetric.TotalReceived
+			currMetric.TotalSent += oldMetric.TotalSent
+		} else {
+			if currMetric.TotalReceived < oldMetric.TotalReceived {
+				currMetric.TotalReceived += oldMetric.TotalReceived
+			} else {
+				currMetric.TotalReceived += int64(math.Abs(float64(currMetric.TotalReceived) - float64(oldMetric.TotalReceived)))
+			}
+			if currMetric.TotalSent < oldMetric.TotalSent {
+				currMetric.TotalSent += oldMetric.TotalSent
+			} else {
+				currMetric.TotalSent += int64(math.Abs(float64(currMetric.TotalSent) - float64(oldMetric.TotalSent)))
+			}
+		}
 		if currMetric.Uptime == 0 || currMetric.TotalTime == 0 {
 			currMetric.PercentUp = 0
 		} else {
@@ -404,3 +345,73 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 	}
 	return shouldUpdate
 }
+
+func handleNewNodeDNS(host *models.Host, node *models.Node) error {
+	dns := models.DNSUpdate{
+		Action: models.DNSInsert,
+		Name:   host.Name + "." + node.Network,
+	}
+	if node.Address.IP != nil {
+		dns.Address = node.Address.IP.String()
+		if err := PublishDNSUpdate(node.Network, dns); err != nil {
+			return err
+		}
+	} else if node.Address6.IP != nil {
+		dns.Address = node.Address6.IP.String()
+		if err := PublishDNSUpdate(node.Network, dns); err != nil {
+			return err
+		}
+	}
+	if err := PublishAllDNS(node); err != nil {
+		return err
+	}
+	return nil
+}
+
+func handleHostCheckin(h, currentHost *models.Host) bool {
+	if h == nil {
+		return false
+	}
+
+	for i := range currentHost.Nodes {
+		currNodeID := currentHost.Nodes[i]
+		node, err := logic.GetNodeByID(currNodeID)
+		if err != nil {
+			if database.IsEmptyRecord(err) {
+				fakeNode := models.Node{}
+				fakeNode.ID, _ = uuid.Parse(currNodeID)
+				fakeNode.Action = models.NODE_DELETE
+				fakeNode.PendingDelete = true
+				if err := NodeUpdate(&fakeNode); err != nil {
+					logger.Log(0, "failed to inform host", currentHost.Name, currentHost.ID.String(), "to remove node", currNodeID, err.Error())
+				}
+			}
+			continue
+		}
+		if err := logic.UpdateNodeCheckin(&node); err != nil {
+			logger.Log(0, "error updating node", node.ID.String(), " on checkin", err.Error())
+		}
+	}
+
+	for i := range h.Interfaces {
+		h.Interfaces[i].AddressString = h.Interfaces[i].Address.String()
+	}
+	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) ||
+		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
+		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
+		h.DefaultInterface != currentHost.DefaultInterface
+	if ifaceDelta { // only save if something changes
+		currentHost.EndpointIP = h.EndpointIP
+		currentHost.Interfaces = h.Interfaces
+		currentHost.DefaultInterface = h.DefaultInterface
+		currentHost.NatType = h.NatType
+		if err := logic.UpsertHost(currentHost); err != nil {
+			logger.Log(0, "failed to update host after check-in", h.Name, h.ID.String(), err.Error())
+			return false
+		}
+		logger.Log(1, "updated host after check-in", currentHost.Name, currentHost.ID.String())
+	}
+
+	logger.Log(2, "check-in processed for host", h.Name, h.ID.String())
+	return ifaceDelta
+}

+ 14 - 8
mq/mq.go

@@ -2,6 +2,7 @@ package mq
 
 import (
 	"context"
+	"fmt"
 	"log"
 	"time"
 
@@ -49,27 +50,32 @@ func SetupMQTT() {
 		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)
+		}
 	}
 	opts := mqtt.NewClientOptions()
 	setMqOptions(servercfg.GetMqUserName(), servercfg.GetMqPassword(), opts)
 	opts.SetOnConnectHandler(func(client mqtt.Client) {
-		if token := client.Subscribe("ping/#", 2, mqtt.MessageHandler(Ping)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
-			client.Disconnect(240)
-			logger.Log(0, "ping subscription failed")
-		}
-		if token := client.Subscribe("update/#", 0, mqtt.MessageHandler(UpdateNode)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
+		serverName := servercfg.GetServer()
+		if token := client.Subscribe(fmt.Sprintf("update/%s/#", serverName), 0, mqtt.MessageHandler(UpdateNode)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
 			client.Disconnect(240)
 			logger.Log(0, "node update subscription failed")
 		}
-		if token := client.Subscribe("host/serverupdate/#", 0, mqtt.MessageHandler(UpdateHost)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
+		if token := client.Subscribe(fmt.Sprintf("host/serverupdate/%s/#", serverName), 0, mqtt.MessageHandler(UpdateHost)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
 			client.Disconnect(240)
 			logger.Log(0, "host update subscription failed")
 		}
-		if token := client.Subscribe("signal/#", 0, mqtt.MessageHandler(ClientPeerUpdate)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
+		if token := client.Subscribe(fmt.Sprintf("signal/%s/#", serverName), 0, mqtt.MessageHandler(ClientPeerUpdate)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
 			client.Disconnect(240)
 			logger.Log(0, "node client subscription failed")
 		}
-		if token := client.Subscribe("metrics/#", 0, mqtt.MessageHandler(UpdateMetrics)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
+		if token := client.Subscribe(fmt.Sprintf("metrics/%s/#", serverName), 0, mqtt.MessageHandler(UpdateMetrics)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
 			client.Disconnect(240)
 			logger.Log(0, "node metrics subscription failed")
 		}

+ 42 - 15
mq/publishers.go

@@ -1,6 +1,7 @@
 package mq
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -23,10 +24,10 @@ func PublishPeerUpdate() error {
 		logger.Log(1, "err getting all hosts", err.Error())
 		return err
 	}
+	logic.ResetPeerUpdateContext()
 	for _, host := range hosts {
 		host := host
-		err = PublishSingleHostPeerUpdate(&host, nil)
-		if err != nil {
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, nil); err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
 	}
@@ -45,9 +46,32 @@ func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
 		logger.Log(1, "err getting all hosts", err.Error())
 		return err
 	}
+	logic.ResetPeerUpdateContext()
 	for _, host := range hosts {
 		host := host
-		if err = PublishSingleHostPeerUpdate(&host, delNode); err != nil {
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, delNode, nil); err != nil {
+			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+		}
+	}
+	return err
+}
+
+// PublishDeletedClientPeerUpdate --- determines and publishes a peer update
+// to all the hosts with a deleted ext client to account for
+func PublishDeletedClientPeerUpdate(delClient *models.ExtClient) error {
+	if !servercfg.IsMessageQueueBackend() {
+		return nil
+	}
+
+	hosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(1, "err getting all hosts", err.Error())
+		return err
+	}
+	logic.ResetPeerUpdateContext()
+	for _, host := range hosts {
+		host := host
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, delClient); err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
 	}
@@ -55,24 +79,28 @@ func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
 }
 
 // PublishSingleHostPeerUpdate --- determines and publishes a peer update to one host
-func PublishSingleHostPeerUpdate(host *models.Host, deletedNode *models.Node) error {
+func PublishSingleHostPeerUpdate(ctx context.Context, host *models.Host, deletedNode *models.Node, deletedClient *models.ExtClient) error {
 
-	peerUpdate, err := logic.GetPeerUpdateForHost("", host, deletedNode)
+	peerUpdate, err := logic.GetPeerUpdateForHost(ctx, "", host, deletedNode, deletedClient)
 	if err != nil {
 		return err
 	}
 	if len(peerUpdate.Peers) == 0 { // no peers to send
 		return nil
 	}
+	proxyUpdate, err := logic.GetProxyUpdateForHost(ctx, host)
+	if err != nil {
+		return err
+	}
+	proxyUpdate.Server = servercfg.GetServer()
 	if host.ProxyEnabled {
-		proxyUpdate, err := logic.GetProxyUpdateForHost(host)
-		if err != nil {
-			return err
-		}
 		proxyUpdate.Action = models.ProxyUpdate
-		peerUpdate.ProxyUpdate = proxyUpdate
+	} else {
+		proxyUpdate.Action = models.NoProxy
 	}
 
+	peerUpdate.ProxyUpdate = proxyUpdate
+
 	data, err := json.Marshal(&peerUpdate)
 	if err != nil {
 		return err
@@ -422,13 +450,12 @@ func sendPeers() {
 
 		//collectServerMetrics(networks[:])
 	}
-
-	for _, host := range hosts {
-		if force {
+	if force {
+		logic.ResetPeerUpdateContext()
+		for _, host := range hosts {
 			host := host
 			logger.Log(2, "sending scheduled peer update (5 min)")
-			err = PublishSingleHostPeerUpdate(&host, nil)
-			if err != nil {
+			if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, nil); err != nil {
 				logger.Log(1, "error publishing peer updates for host: ", host.ID.String(), " Err: ", err.Error())
 			}
 		}

+ 8 - 0
mq/util.go

@@ -12,6 +12,10 @@ import (
 )
 
 func decryptMsgWithHost(host *models.Host, msg []byte) ([]byte, error) {
+	if host.OS == models.OS_Types.IoT { // just pass along IoT messages
+		return msg, nil
+	}
+
 	trafficKey, trafficErr := logic.RetrievePrivateTrafficKey() // get server private key
 	if trafficErr != nil {
 		return nil, trafficErr
@@ -41,6 +45,10 @@ func decryptMsg(node *models.Node, msg []byte) ([]byte, error) {
 }
 
 func encryptMsg(host *models.Host, msg []byte) ([]byte, error) {
+	if host.OS == models.OS_Types.IoT {
+		return msg, nil
+	}
+
 	// fetch server public key to be certain hasn't changed in transit
 	trafficKey, trafficErr := logic.RetrievePrivateTrafficKey()
 	if trafficErr != nil {

+ 19 - 19
release.md

@@ -1,27 +1,27 @@
-# Netmaker v0.18.2
+# Netmaker v0.18.6
 
-## **Do not attempt upgrade from 0.17.x quite yet**
+## **Wait till out of pre-release to fully upgrade**
 
 ## whats new
-- Enrollment Keys, give the ability for an admin to enroll clients into multiple networks, can be unlimited, time, or usage based
-- EMQX broker support and better MQTT support in general
-  - Now you must specify BROKER_ENDPOINT
-  - Also specify SERVER_BROKER_ENDPOINT, if not provided server will connect to broker over BROKER_ENDPOINT
-  - Thsi gives ability for user to specify any broker endpoint and use any protocal on clients desired, such as, `mqtts://mybroker.com:8083`
-    (we will still default to wss)
+- Logic for ext client ACLs (not really usable until new UI is finished)
+- Default proxy mode, enables users to determine if all Hosts should have proxy enabled/disabled/auto by default
+  - specify with DEFAULT_PROXY_MODE="on/off/auto" 
     
 ## whats fixed
-- Fixed default ACL behavior, should work as expected
-- Peer calculations enhancement
-- main routines share a context and docker stop/ctrl+c give expected results now
-- Github workflow edits
-- Removed Deprecated Local Network Range from client + server
+- Proxy Peer calculation improvements
+- DNS is populated correctly after registration by enrollment key
+- Migrate is functional for Windows/Mac **note** Ports may be set to 0 after an upgrade, can be adjusted via UI to fix
+- Interface data is sent on netclient register
+- Upgrade script
+- Latency issue with Node <-> Node Metrics
+- Ports set from server for Hosts on register/join are actually used
 
 ## known issues
-- EnrollmentKeys may not function as intended in an HA setup
-- If a host does not receive a message to delete a node, it could become orphaned and un-deletable
-- Network interface routes may be removed after sometime/unintended network update
-- Upgrade script does not handle clients
 - Caddy does not handle netmaker exporter well for EE
-- Incorrect latency on metrics (EE)
-- Swagger docs not up to date
+- Migration causes a listen port of 0 for upgraded hosts
+- Docker clients can not re-join after deletion
+- Innacurate Ext Client Metrics 
+- Issue with Mac + IPv6 addressing
+- Nodes on same local network may not always connect
+- List populates egress ranges twice
+- If you do NOT set STUN_LIST on server, it could lead to strange behavior on client

+ 525 - 359
scripts/nm-quick.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-LATEST="v0.18.2"
+LATEST="v0.18.5"
 
 print_logo() {(
 cat << "EOF"
@@ -30,100 +30,201 @@ unset INSTALL_TYPE
 unset BUILD_TYPE
 unset BUILD_TAG
 unset IMAGE_TAG
+unset AUTO_BUILD
 
-usage () {(
-    echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag]"
+# usage - displays usage instructions
+usage () {
+    echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag] [-a auto]"
     echo "  -e      if specified, will install netmaker EE"
     echo "  -b      type of build; options:"
 	echo "          \"version\" - will install a specific version of Netmaker using remote git and dockerhub"
 	echo "          \"local\": - will install by cloning repo and and building images from git"
-	echo "          \"branch\": - will install a specific branch using remote git and dockerhub "
+	echo "          \"branch\": - will install a specific branch using remote git and dockerhub"
     echo "  -t      tag of build; if buildtype=version, tag=version. If builtype=branch or builtype=local, tag=branch"
+    echo "  -a      auto-build; skip prompts and use defaults, if none provided"
     echo "examples:"
-	echo "          nm-quick.sh -e -b version -t v0.18.2"
+	echo "          nm-quick.sh -e -b version -t $LATEST"
 	echo "          nm-quick.sh -e -b local -t feature_v0.17.2_newfeature"	
 	echo "          nm-quick.sh -e -b branch -t develop"
     exit 1
-)}
+}
 
-while getopts evb:t: flag
+while getopts evab:t: flag
 do
-    case "${flag}" in
-        e) 
+	case "${flag}" in
+		e) 
 			INSTALL_TYPE="ee"
 			;;
 		v) 
 			usage
 			exit 0
 			;;
-        b) 
+		a) 
+			AUTO_BUILD="on"
+			;;			
+		b) 
 			BUILD_TYPE=${OPTARG}
 			if [[ ! "$BUILD_TYPE" =~ ^(version|local|branch)$ ]]; then
-    			echo "error: $BUILD_TYPE is invalid"
+				echo "error: $BUILD_TYPE is invalid"
 				echo "valid options: version, local, branch"
 				usage
 				exit 1
 			fi
 			;;
-        t) 
+		t) 
 			BUILD_TAG=${OPTARG}
 			;;
-    esac
+	esac
 done
 
-if [ -z "$BUILD_TYPE" ]; then
-	BUILD_TYPE="version"
-	BUILD_TAG=$LATEST
-fi
+# print_logo - prints the netmaker logo
+print_logo() {
+cat << "EOF"
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+                                                                                         
+ __   __     ______     ______   __    __     ______     __  __     ______     ______    
+/\ "-.\ \   /\  ___\   /\__  _\ /\ "-./  \   /\  __ \   /\ \/ /    /\  ___\   /\  == \   
+\ \ \-.  \  \ \  __\   \/_/\ \/ \ \ \-./\ \  \ \  __ \  \ \  _"-.  \ \  __\   \ \  __<   
+ \ \_\\"\_\  \ \_____\    \ \_\  \ \_\ \ \_\  \ \_\ \_\  \ \_\ \_\  \ \_____\  \ \_\ \_\ 
+  \/_/ \/_/   \/_____/     \/_/   \/_/  \/_/   \/_/\/_/   \/_/\/_/   \/_____/   \/_/ /_/ 
+                                                                                                                                                                                                 
 
-if [ -z "$BUILD_TAG" ] && [ "$BUILD_TYPE" = "version" ]; then
-	BUILD_TAG=$LATEST
-fi
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+EOF
+}
 
-if [ -z "$BUILD_TAG" ] && [ ! -z "$BUILD_TYPE" ]; then
-	echo "error: must specify build tag when build type \"$BUILD_TYPE\" is specified"
-	usage		
-	exit 1
-fi
+# set_buildinfo - sets the information based on script input for how the installation should be run
+set_buildinfo() {
 
-IMAGE_TAG=$(sed 's/\//-/g' <<< "$BUILD_TAG")
+	if [ -z "$BUILD_TYPE" ]; then
+		BUILD_TYPE="version"
+		BUILD_TAG=$LATEST
+	fi
 
-if [ "$1" = "ce" ]; then
-	INSTALL_TYPE="ce"
-elif [ "$1" = "ee" ]; then
-	INSTALL_TYPE="ee"
-fi
+	if [ -z "$BUILD_TAG" ] && [ "$BUILD_TYPE" = "version" ]; then
+		BUILD_TAG=$LATEST
+	fi
 
-if [ -z "$INSTALL_TYPE" ]; then
-	echo "-----------------------------------------------------"
-	echo "Would you like to install Netmaker Community Edition (CE), or Netmaker Enterprise Edition (EE)?"
-	echo "EE will require you to create an account at https://dashboard.license.netmaker.io"
-	echo "-----------------------------------------------------"
-	select install_option in "Community Edition" "Enterprise Edition"; do
-	case $REPLY in
-		1)
-		echo "installing Netmaker CE"
+	if [ -z "$BUILD_TAG" ] && [ ! -z "$BUILD_TYPE" ]; then
+		echo "error: must specify build tag when build type \"$BUILD_TYPE\" is specified"
+		usage		
+		exit 1
+	fi
+
+	IMAGE_TAG=$(sed 's/\//-/g' <<< "$BUILD_TAG")
+
+	if [ "$1" = "ce" ]; then
 		INSTALL_TYPE="ce"
-		break
-		;;      
-		2)
-		echo "installing Netmaker EE"
+	elif [ "$1" = "ee" ]; then
 		INSTALL_TYPE="ee"
-		break
-		;;
-		*) echo "invalid option $REPLY";;
-	esac
-	done
-fi
-echo "-----------Build Options-----------------------------"
-echo "    EE or CE: $INSTALL_TYPE";
-echo "  Build Type: $BUILD_TYPE";
-echo "   Build Tag: $BUILD_TAG";
-echo "   Image Tag: $IMAGE_TAG";
-echo "-----------------------------------------------------"
+	fi
 
-print_logo
+	if [ "$AUTO_BUILD" = "on" ] && [ -z "$INSTALL_TYPE" ]; then
+		INSTALL_TYPE="ce"
+	elif [ -z "$INSTALL_TYPE" ]; then
+		echo "-----------------------------------------------------"
+		echo "Would you like to install Netmaker Community Edition (CE), or Netmaker Enterprise Edition (EE)?"
+		echo "EE will require you to create an account at https://dashboard.license.netmaker.io"
+		echo "-----------------------------------------------------"
+		select install_option in "Community Edition" "Enterprise Edition"; do
+		case $REPLY in
+			1)
+			echo "installing Netmaker CE"
+			INSTALL_TYPE="ce"
+			break
+			;;      
+			2)
+			echo "installing Netmaker EE"
+			INSTALL_TYPE="ee"
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
+	echo "-----------Build Options-----------------------------"
+	echo "    EE or CE: $INSTALL_TYPE";
+	echo "  Build Type: $BUILD_TYPE";
+	echo "   Build Tag: $BUILD_TAG";
+	echo "   Image Tag: $IMAGE_TAG";
+	echo "-----------------------------------------------------"
+
+}
 
+# install_yq - install yq if not present
+install_yq() {
+	if ! command -v yq &> /dev/null; then
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+		chmod +x /usr/bin/yq
+	fi
+	set +e
+	if ! command -v yq &> /dev/null; then
+		set -e
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
+		chmod +x /usr/bin/yq
+	fi
+	set -e
+	if ! command -v yq &> /dev/null; then
+		echo "failed to install yq. Please install yq and try again."
+		echo "https://github.com/mikefarah/yq/#install"
+		exit 1
+	fi	
+}
+
+# setup_netclient - adds netclient to docker-compose
+setup_netclient() {
+
+	set +e
+	netclient uninstall
+	set -e
+
+	wget -O netclient https://github.com/gravitl/netclient/releases/download/$LATEST/netclient_linux_amd64
+	chmod +x netclient
+	./netclient install
+	netclient register -t $TOKEN
+
+	echo "waiting for client to become available"
+	wait_seconds 10 
+}
+
+# configure_netclient - configures server's netclient as a default host and an ingress gateway
+configure_netclient() {
+
+	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
+	echo "register complete. New node ID: $NODE_ID"
+	HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
+	echo "making host a default"
+	echo "Host ID: $HOST_ID"
+	# set as a default host
+	set +e
+	nmctl host update $HOST_ID --default
+	sleep 5
+	nmctl node create_ingress netmaker $NODE_ID
+	set -e
+}
+
+# setup_nmctl - pulls nmctl and makes it executable
+setup_nmctl() {
+
+	wget -O /usr/bin/nmctl https://github.com/gravitl/netmaker/releases/download/$LATEST/nmctl_linux_amd64
+
+    chmod +x /usr/bin/nmctl
+    echo "using server api.$NETMAKER_BASE_DOMAIN"
+    echo "using master key $MASTER_KEY"
+    nmctl context set default --endpoint="https://api.$NETMAKER_BASE_DOMAIN" --master_key="$MASTER_KEY"
+    nmctl context use default
+    RESP=$(nmctl network list)
+    if [[ $RESP == *"unauthorized"* ]]; then
+        echo "Unable to properly configure NMCTL, exiting..."
+        exit 1
+    fi
+}
+
+# wait_seconds - wait for the specified period of time
 wait_seconds() {(
   for ((a=1; a <= $1; a++))
   do
@@ -132,7 +233,11 @@ wait_seconds() {(
   done
 )}
 
+# confirm - get user input to confirm that they want to perform the next step
 confirm() {(
+  if [ "$AUTO_BUILD" = "on" ]; then
+	return 0
+  fi
   while true; do
       read -p 'Does everything look right? [y/n]: ' yn
       case $yn in
@@ -143,6 +248,7 @@ confirm() {(
   done
 )}
 
+# local_install_setup - builds artifacts based on specified branch locally to use in install
 local_install_setup() {(
 	rm -rf netmaker-tmp
 	mkdir netmaker-tmp
@@ -165,92 +271,82 @@ local_install_setup() {(
 	rm -rf netmaker-tmp
 )}
 
-echo "checking dependencies..."
-
-OS=$(uname)
-
-if [ -f /etc/debian_version ]; then
-	dependencies="git wireguard wireguard-tools jq docker.io docker-compose"
-	update_cmd='apt update'
-	install_cmd='apt-get install -y'
-elif [ -f /etc/alpine-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='apk update'
-	install_cmd='apk --update add'
-elif [ -f /etc/centos-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='yum update'
-	install_cmd='yum install -y'
-elif [ -f /etc/fedora-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='dnf update'
-	install_cmd='dnf install -y'
-elif [ -f /etc/redhat-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='yum update'
-	install_cmd='yum install -y'
-elif [ -f /etc/arch-release ]; then
-    	dependecies="git wireguard-tools jq docker.io docker-compose"
-	update_cmd='pacman -Sy'
-	install_cmd='pacman -S --noconfirm'
-elif [ "${OS}" = "FreeBSD" ]; then
-	dependencies="git wireguard wget jq docker.io docker-compose"
-	update_cmd='pkg update'
-	install_cmd='pkg install -y'
-elif [ -f /etc/turris-version ]; then
-	dependencies="git wireguard-tools bash jq docker.io docker-compose"
-	OS="TurrisOS"
-	update_cmd='opkg update'	
-	install_cmd='opkg install'
-elif [ -f /etc/openwrt_release ]; then
-	dependencies="git wireguard-tools bash jq docker.io docker-compose"
-	OS="OpenWRT"
-	update_cmd='opkg update'	
-	install_cmd='opkg install'
-else
-	install_cmd=''
-fi
+# install_dependencies - install necessary packages to run netmaker 
+install_dependencies() {
+	echo "checking dependencies..."
+
+	OS=$(uname)
+	if [ -f /etc/debian_version ]; then
+		dependencies="git wireguard wireguard-tools dnsutils jq docker.io docker-compose"
+		update_cmd='apt update'
+		install_cmd='apt-get install -y'
+	elif [ -f /etc/alpine-release ]; then
+		dependencies="git wireguard jq docker.io docker-compose"
+		update_cmd='apk update'
+		install_cmd='apk --update add'
+	elif [ -f /etc/centos-release ]; then
+		dependencies="git wireguard jq bind-utils docker.io docker-compose"
+		update_cmd='yum update'
+		install_cmd='yum install -y'
+	elif [ -f /etc/fedora-release ]; then
+		dependencies="git wireguard bind-utils jq docker.io docker-compose"
+		update_cmd='dnf update'
+		install_cmd='dnf install -y'
+	elif [ -f /etc/redhat-release ]; then
+		dependencies="git wireguard jq docker.io bind-utils docker-compose"
+		update_cmd='yum update'
+		install_cmd='yum install -y'
+	elif [ -f /etc/arch-release ]; then
+			dependecies="git wireguard-tools dnsutils jq docker.io docker-compose"
+		update_cmd='pacman -Sy'
+		install_cmd='pacman -S --noconfirm'
+	elif [ "${OS}" = "FreeBSD" ]; then
+		dependencies="git wireguard wget jq docker.io docker-compose"
+		update_cmd='pkg update'
+		install_cmd='pkg install -y'
+	elif [ -f /etc/turris-version ]; then
+		dependencies="git wireguard-tools bash jq docker.io docker-compose"
+		OS="TurrisOS"
+		update_cmd='opkg update'	
+		install_cmd='opkg install'
+	elif [ -f /etc/openwrt_release ]; then
+		dependencies="git wireguard-tools bash jq docker.io docker-compose"
+		OS="OpenWRT"
+		update_cmd='opkg update'	
+		install_cmd='opkg install'
+	else
+		install_cmd=''
+	fi
 
-if [ -z "${install_cmd}" ]; then
-        echo "OS unsupported for automatic dependency install"
-	exit 1
-fi
+	if [ -z "${install_cmd}" ]; then
+			echo "OS unsupported for automatic dependency install"
+		exit 1
+	fi
 
-set -- $dependencies
+	set -- $dependencies
 
-${update_cmd}
+	${update_cmd}
 
-while [ -n "$1" ]; do
-	if [ "${OS}" = "FreeBSD" ]; then
-		is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
-		if [ "$is_installed" != "" ]; then
-			echo "  " $1 is installed
-		else
-			echo "  " $1 is not installed. Attempting install.
-			${install_cmd} $1
-			sleep 5
+	while [ -n "$1" ]; do
+		if [ "${OS}" = "FreeBSD" ]; then
 			is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
 			if [ "$is_installed" != "" ]; then
 				echo "  " $1 is installed
-			elif [ -x "$(command -v $1)" ]; then
-				echo "  " $1 is installed
 			else
-				echo "  " FAILED TO INSTALL $1
-				echo "  " This may break functionality.
-			fi
-		fi	
-	else
-		if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
-			is_installed=$(opkg list-installed $1 | grep $1)
-		else
-			is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
-		fi
-		if [ "${is_installed}" != "" ]; then
-			echo "    " $1 is installed
+				echo "  " $1 is not installed. Attempting install.
+				${install_cmd} $1
+				sleep 5
+				is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
+				if [ "$is_installed" != "" ]; then
+					echo "  " $1 is installed
+				elif [ -x "$(command -v $1)" ]; then
+					echo "  " $1 is installed
+				else
+					echo "  " FAILED TO INSTALL $1
+					echo "  " This may break functionality.
+				fi
+			fi	
 		else
-			echo "    " $1 is not installed. Attempting install.
-			${install_cmd} $1
-			sleep 5
 			if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 				is_installed=$(opkg list-installed $1 | grep $1)
 			else
@@ -258,290 +354,360 @@ while [ -n "$1" ]; do
 			fi
 			if [ "${is_installed}" != "" ]; then
 				echo "    " $1 is installed
-			elif [ -x "$(command -v $1)" ]; then
-				echo "  " $1 is installed
 			else
-				echo "  " FAILED TO INSTALL $1
-				echo "  " This may break functionality.
+				echo "    " $1 is not installed. Attempting install.
+				${install_cmd} $1
+				sleep 5
+				if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
+					is_installed=$(opkg list-installed $1 | grep $1)
+				else
+					is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+				fi
+				if [ "${is_installed}" != "" ]; then
+					echo "    " $1 is installed
+				elif [ -x "$(command -v $1)" ]; then
+					echo "  " $1 is installed
+				else
+					echo "  " FAILED TO INSTALL $1
+					echo "  " This may break functionality.
+				fi
 			fi
 		fi
+		shift
+	done
+
+	echo "-----------------------------------------------------"
+	echo "dependency check complete"
+	echo "-----------------------------------------------------"
+} 
+set -e
+
+# set_install_vars - sets the variables that will be used throughout installation
+set_install_vars() {
+
+	IP_ADDR=$(dig -4 myip.opendns.com @resolver1.opendns.com +short)
+	if [ "$IP_ADDR" = "" ]; then
+		IP_ADDR=$(curl -s ifconfig.me)
 	fi
-	shift
-done
 
-echo "-----------------------------------------------------"
-echo "dependency check complete"
-echo "-----------------------------------------------------"
+	NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
+	COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
+	SERVER_PUBLIC_IP=$IP_ADDR
+	MASTER_KEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+	DOMAIN_TYPE=""
+	echo "-----------------------------------------------------"
+	echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
+	echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_PUBLIC_IP"
+	echo "-----------------------------------------------------"
 
-wait_seconds 3
+	if [ "$AUTO_BUILD" = "on" ]; then
+			DOMAIN_TYPE="auto"
+	else
+		select domain_option in "Auto Generated ($NETMAKER_BASE_DOMAIN)" "Custom Domain (e.x: netmaker.example.com)"; do
+		case $REPLY in
+			1)
+			echo "using $NETMAKER_BASE_DOMAIN for base domain"
+			DOMAIN_TYPE="auto"
+			break
+			;;      
+			2)
+			read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_PUBLIC_IP first): " domain
+			NETMAKER_BASE_DOMAIN=$domain
+			echo "using $NETMAKER_BASE_DOMAIN"
+			DOMAIN_TYPE="custom"
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
 
+	wait_seconds 2
 
-if [ "$BUILD_TYPE" = "local" ]; then
-	local_install_setup
-fi
+	echo "-----------------------------------------------------"
+	echo "The following subdomains will be used:"
+	echo "          dashboard.$NETMAKER_BASE_DOMAIN"
+	echo "                api.$NETMAKER_BASE_DOMAIN"
+	echo "             broker.$NETMAKER_BASE_DOMAIN"
 
-set -e
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		echo "         prometheus.$NETMAKER_BASE_DOMAIN"
+		echo "  netmaker-exporter.$NETMAKER_BASE_DOMAIN"
+		echo "            grafana.$NETMAKER_BASE_DOMAIN"
+	fi
 
-IP_ADDR=$(dig -4 myip.opendns.com @resolver1.opendns.com +short)
-if [ "$IP_ADDR" = "" ]; then
-	IP_ADDR=$(curl -s ifconfig.me)
-fi
+	echo "-----------------------------------------------------"
 
-NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
-COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
-SERVER_PUBLIC_IP=$IP_ADDR
-MASTER_KEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
-DOMAIN_TYPE=""
-echo "-----------------------------------------------------"
-echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
-echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_PUBLIC_IP"
-echo "-----------------------------------------------------"
-select domain_option in "Auto Generated ($NETMAKER_BASE_DOMAIN)" "Custom Domain (e.x: netmaker.example.com)"; do
-  case $REPLY in
-    1)
-      echo "using $NETMAKER_BASE_DOMAIN for base domain"
-      DOMAIN_TYPE="auto"
-	  break
-      ;;      
-    2)
-      read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_PUBLIC_IP first): " domain
-      NETMAKER_BASE_DOMAIN=$domain
-      echo "using $NETMAKER_BASE_DOMAIN"
-      DOMAIN_TYPE="custom"
-      break
-      ;;
-    *) echo "invalid option $REPLY";;
-  esac
-done
+	if [[ "$DOMAIN_TYPE" == "custom" ]]; then
+		echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_PUBLIC_IP"
+		confirm
+	fi
 
-wait_seconds 2
+	wait_seconds 1
 
-echo "-----------------------------------------------------"
-echo "The following subdomains will be used:"
-echo "          dashboard.$NETMAKER_BASE_DOMAIN"
-echo "                api.$NETMAKER_BASE_DOMAIN"
-echo "             broker.$NETMAKER_BASE_DOMAIN"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
 
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	echo "         prometheus.$NETMAKER_BASE_DOMAIN"
-	echo "  netmaker-exporter.$NETMAKER_BASE_DOMAIN"
-	echo "            grafana.$NETMAKER_BASE_DOMAIN"
-fi
+		echo "-----------------------------------------------------"
+		echo "Provide Details for EE installation:"
+		echo "    1. Log into https://dashboard.license.netmaker.io"
+		echo "    2. Copy License Key Value: https://dashboard.license.netmaker.io/license-keys"
+		echo "    3. Retrieve Account ID: https://dashboard.license.netmaker.io/user"
+		echo "    4. note email address"
+		echo "-----------------------------------------------------"
+		unset LICENSE_KEY
+		while [ -z "$LICENSE_KEY" ]; do
+			read -p "License Key: " LICENSE_KEY
+		done
+		unset ACCOUNT_ID
+		while [ -z ${ACCOUNT_ID} ]; do
+			read -p "Account ID: " ACCOUNT_ID
+		done
+	fi
+
+	unset GET_EMAIL
+	unset RAND_EMAIL
+	RAND_EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
+	if [ -z $AUTO_BUILD ]; then
+		read -p "Email Address for Domain Registration (click 'enter' to use $RAND_EMAIL): " GET_EMAIL
+	fi
+	if [ -z "$GET_EMAIL" ]; then
+	echo "using rand email"
+	EMAIL="$RAND_EMAIL"
+	else
+	EMAIL="$GET_EMAIL"
+	fi
 
-echo "-----------------------------------------------------"
+	wait_seconds 1
+
+	unset GET_MQ_USERNAME
+	unset GET_MQ_PASSWORD
+	unset CONFIRM_MQ_PASSWORD
+	echo "Enter Credentials For MQ..."
+	if [ -z $AUTO_BUILD ]; then
+		read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
+	fi
+	if [ -z "$GET_MQ_USERNAME" ]; then
+	echo "using default username for mq"
+	MQ_USERNAME="netmaker"
+	else
+	MQ_USERNAME="$GET_MQ_USERNAME"
+	fi
+
+	MQ_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+
+	if [ -z $AUTO_BUILD ]; then  
+		select domain_option in "Auto Generated Password" "Input Your Own Password"; do
+			case $REPLY in
+			1)
+			echo "using random password for mq"
+			break
+			;;      
+			2)
+			while true
+			do
+				echo "Enter your Password For MQ: " 
+				read -s GET_MQ_PASSWORD
+				echo "Enter your password again to confirm: "
+				read -s CONFIRM_MQ_PASSWORD
+				if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
+					echo "wrong password entered, try again..."
+					continue
+				fi
+				MQ_PASSWORD="$GET_MQ_PASSWORD"
+				echo "MQ Password Saved Successfully!!"
+				break
+			done
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
+
+	wait_seconds 2
+
+	echo "-----------------------------------------------------------------"
+	echo "                SETUP ARGUMENTS"
+	echo "-----------------------------------------------------------------"
+	echo "        domain: $NETMAKER_BASE_DOMAIN"
+	echo "         email: $EMAIL"
+	echo "     public ip: $SERVER_PUBLIC_IP"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		echo "       license: $LICENSE_KEY"
+		echo "    account id: $ACCOUNT_ID"
+	fi
+	echo "-----------------------------------------------------------------"
+	echo "Confirm Settings for Installation"
+	echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
 
-if [[ "$DOMAIN_TYPE" == "custom" ]]; then
-	echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_PUBLIC_IP"
 	confirm
-fi
 
-wait_seconds 1
+}
 
-if [ "$INSTALL_TYPE" = "ee" ]; then
+# install_netmaker - sets the config files and starts docker-compose
+install_netmaker() {
 
-	echo "-----------------------------------------------------"
-	echo "Provide Details for EE installation:"
-	echo "    1. Log into https://dashboard.license.netmaker.io"
-	echo "    2. Copy License Key Value: https://dashboard.license.netmaker.io/license-keys"
-	echo "    3. Retrieve Account ID: https://dashboard.license.netmaker.io/user"
-	echo "    4. note email address"
-	echo "-----------------------------------------------------"
-	unset LICENSE_KEY
-	while [ -z "$LICENSE_KEY" ]; do
-		read -p "License Key: " LICENSE_KEY
-	done
-	unset ACCOUNT_ID
-	while [ -z ${ACCOUNT_ID} ]; do
-		read -p "Account ID: " ACCOUNT_ID
-	done
+	echo "-----------------------------------------------------------------"
+	echo "Beginning installation..."
+	echo "-----------------------------------------------------------------"
 
-fi
+	wait_seconds 3
 
-unset GET_EMAIL
-unset RAND_EMAIL
-RAND_EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
-read -p "Email Address for Domain Registration (click 'enter' to use $RAND_EMAIL): " GET_EMAIL
-if [ -z "$GET_EMAIL" ]; then
-  echo "using rand email"
-  EMAIL="$RAND_EMAIL"
-else
-  EMAIL="$GET_EMAIL"
-fi
+	echo "Pulling config files..."
 
-wait_seconds 1
-
-unset GET_MQ_USERNAME
-unset GET_MQ_PASSWORD
-unset CONFIRM_MQ_PASSWORD
-echo "Enter Credentials For MQ..."
-read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
-if [ -z "$GET_MQ_USERNAME" ]; then
-  echo "using default username for mq"
-  MQ_USERNAME="netmaker"
-else
-  MQ_USERNAME="$GET_MQ_USERNAME"
-fi
 
-select domain_option in "Auto Generated Password" "Input Your Own Password"; do
-	case $REPLY in
-	1)
-	echo "generating random password for mq"
-	MQ_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
-	break
-	;;      
-    2)
-	while true
-    do
-        echo "Enter your Password For MQ: " 
-        read -s GET_MQ_PASSWORD
-        echo "Enter your password again to confirm: "
-        read -s CONFIRM_MQ_PASSWORD
-        if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
-            echo "wrong password entered, try again..."
-            continue
-        fi
-		MQ_PASSWORD="$GET_MQ_PASSWORD"
-        echo "MQ Password Saved Successfully!!"
-        break
-    done
-      break
-      ;;
-    *) echo "invalid option $REPLY";;
-  esac
-done
+	COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.yml" 
+	CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.ee.yml" 
+		CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile-EE"
+	fi
+	if [ ! "$BUILD_TYPE" = "local" ]; then
+		wget -O /root/docker-compose.yml $COMPOSE_URL && wget -O /root/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/mosquitto.conf && wget -O /root/Caddyfile $CADDY_URL
+		wget -O /root/wait.sh https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/wait.sh
+	fi
 
+	chmod +x /root/wait.sh
+	mkdir -p /etc/netmaker
 
-wait_seconds 2
+	echo "Setting docker-compose and Caddyfile..."
 
-echo "-----------------------------------------------------------------"
-echo "                SETUP ARGUMENTS"
-echo "-----------------------------------------------------------------"
-echo "        domain: $NETMAKER_BASE_DOMAIN"
-echo "         email: $EMAIL"
-echo "     public ip: $SERVER_PUBLIC_IP"
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	echo "       license: $LICENSE_KEY"
-	echo "    account id: $ACCOUNT_ID"
-fi
-echo "-----------------------------------------------------------------"
-echo "Confirm Settings for Installation"
-echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+	sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
+	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/Caddyfile
+	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/docker-compose.yml
+	sed -i "s/REPLACE_MASTER_KEY/$MASTER_KEY/g" /root/docker-compose.yml
+	sed -i "s/YOUR_EMAIL/$EMAIL/g" /root/Caddyfile
+	sed -i "s/REPLACE_MQ_PASSWORD/$MQ_PASSWORD/g" /root/docker-compose.yml
+	sed -i "s/REPLACE_MQ_USERNAME/$MQ_USERNAME/g" /root/docker-compose.yml 
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		sed -i "s~YOUR_LICENSE_KEY~$LICENSE_KEY~g" /root/docker-compose.yml
+		sed -i "s/YOUR_ACCOUNT_ID/$ACCOUNT_ID/g" /root/docker-compose.yml
+	fi
 
-confirm
+	if [ "$BUILD_TYPE" = "version" ] && [ "$INSTALL_TYPE" = "ee" ]; then
+		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG-ee/g" /root/docker-compose.yml
+	else
+		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
+	fi
 
+	if [ "$BUILD_TYPE" = "local" ]; then
+		sed -i "s/REPLACE_UI_IMAGE_TAG/$LATEST/g" /root/docker-compose.yml
+	else
+		sed -i "s/REPLACE_UI_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
+	fi
 
-echo "-----------------------------------------------------------------"
-echo "Beginning installation..."
-echo "-----------------------------------------------------------------"
+	echo "Starting containers..."
 
-wait_seconds 3
+	docker-compose -f /root/docker-compose.yml up -d
 
-echo "Pulling config files..."
+	wait_seconds 2
 
+}
 
-COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.yml" 
-CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile"
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.ee.yml" 
-	CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile-EE"
-fi
-if [ ! "$BUILD_TYPE" = "local" ]; then
-	wget -O /root/docker-compose.yml $COMPOSE_URL && wget -O /root/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/mosquitto.conf && wget -O /root/Caddyfile $CADDY_URL
-	wget -O /root/wait.sh https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/wait.sh
-fi
+# test_connection - tests to make sure Caddy has proper SSL certs
+test_connection() {
 
-chmod +x /root/wait.sh
-mkdir -p /etc/netmaker
-
-echo "Setting docker-compose and Caddyfile..."
-
-sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
-sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/Caddyfile
-sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/docker-compose.yml
-sed -i "s/REPLACE_MASTER_KEY/$MASTER_KEY/g" /root/docker-compose.yml
-sed -i "s/YOUR_EMAIL/$EMAIL/g" /root/Caddyfile
-sed -i "s/REPLACE_MQ_PASSWORD/$MQ_PASSWORD/g" /root/docker-compose.yml
-sed -i "s/REPLACE_MQ_USERNAME/$MQ_USERNAME/g" /root/docker-compose.yml 
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	sed -i "s~YOUR_LICENSE_KEY~$LICENSE_KEY~g" /root/docker-compose.yml
-	sed -i "s/YOUR_ACCOUNT_ID/$ACCOUNT_ID/g" /root/docker-compose.yml
-fi
+	echo "Testing Caddy setup (please be patient, this may take 1-2 minutes)"
+	for i in 1 2 3 4 5 6 7 8
+	do
+	curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
 
-if [ "$BUILD_TYPE" = "version" ] && [ "$INSTALL_TYPE" = "ee" ]; then
-	sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG-ee/g" /root/docker-compose.yml
-else
-	sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-fi
+	if [[ "$i" == 8 ]]; then
+	echo "    Caddy is having an issue setting up certificates, please investigate (docker logs caddy)"
+	echo "    Exiting..."
+	exit 1
+	elif [[ "$curlresponse" == *"failed to verify the legitimacy of the server"* ]]; then
+	echo "    Certificates not yet configured, retrying..."
 
-if [ "$BUILD_TYPE" = "local" ]; then
-	sed -i "s/REPLACE_UI_IMAGE_TAG/$LATEST/g" /root/docker-compose.yml
-else
-	sed -i "s/REPLACE_UI_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-fi
+	elif [[ "$curlresponse" == *"left intact"* ]]; then
+	echo "    Certificates ok"
+	break
+	else
+	secs=$(($i*5+10))
+	echo "    Issue establishing connection...retrying in $secs seconds..."       
+	fi
+	sleep $secs
+	done
 
-echo "Starting containers..."
+}
 
-docker-compose -f /root/docker-compose.yml up -d
+# setup_mesh - sets up a default mesh network on the server
+setup_mesh() {
 
-sleep 2
+	wait_seconds 5
 
-test_connection() {
+	echo "Creating netmaker network (10.101.0.0/16)"
+
+	nmctl network create --name netmaker --ipv4_addr 10.101.0.0/16
+
+	wait_seconds 5
+
+	echo "Creating netmaker enrollment key"
+
+	tokenJson=$(nmctl enrollment_key create --unlimited --networks netmaker)
+	TOKEN=$(jq -r '.token' <<< ${tokenJson})
+
+	wait_seconds 3
 
-echo "Testing Caddy setup (please be patient, this may take 1-2 minutes)"
-for i in 1 2 3 4 5 6 7 8
-do
-curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
-
-if [[ "$i" == 8 ]]; then
-  echo "    Caddy is having an issue setting up certificates, please investigate (docker logs caddy)"
-  echo "    Exiting..."
-  exit 1
-elif [[ "$curlresponse" == *"failed to verify the legitimacy of the server"* ]]; then
-  echo "    Certificates not yet configured, retrying..."
-
-elif [[ "$curlresponse" == *"left intact"* ]]; then
-  echo "    Certificates ok"
-  break
-else
-  secs=$(($i*5+10))
-  echo "    Issue establishing connection...retrying in $secs seconds..."       
-fi
-sleep $secs
-done
 }
 
+# print_success - prints a success message upon completion
+print_success() {
+	echo "-----------------------------------------------------------------"
+	echo "-----------------------------------------------------------------"
+	echo "Netmaker setup is now complete. You are ready to begin using Netmaker."
+	echo "Visit dashboard.$NETMAKER_BASE_DOMAIN to log in"
+	echo "-----------------------------------------------------------------"
+	echo "-----------------------------------------------------------------"
+}
 
-setup_mesh() {( set -e
+# 1. print netmaker logo
+print_logo
 
-wait_seconds 15
+# 2. setup the build instructions
+set_buildinfo
 
-echo "Creating netmaker network (10.101.0.0/16)"
+set +e
 
-curl -s -o /dev/null -d '{"addressrange":"10.101.0.0/16","netid":"netmaker"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks
+# 3. install necessary packages
+install_dependencies
 
-wait_seconds 5
+# 4. install yq if necessary
+install_yq
 
-echo "Creating netmaker access key"
+# 5. if running a local build, clone git and build artifacts
+if [ "$BUILD_TYPE" = "local" ]; then
+	local_install_setup
+fi
 
-curlresponse=$(curl -s -d '{"uses":99999,"name":"netmaker-key"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks/netmaker/keys)
-ACCESS_TOKEN=$(jq -r '.accessstring' <<< ${curlresponse})
+set -e
 
-wait_seconds 3
+# 6. get user input for variables
+set_install_vars
 
-)}
+# 7. get and set config files, startup docker-compose
+install_netmaker
 
 set +e
+
+# 8. make sure Caddy certs are working
 test_connection
 
-wait_seconds 3
+# 9. install the netmaker CLI
+setup_nmctl
 
+# 10. create a default mesh network for netmaker
 setup_mesh
 
-echo "-----------------------------------------------------------------"
-echo "-----------------------------------------------------------------"
-echo "Netmaker setup is now complete. You are ready to begin using Netmaker."
-echo "Visit dashboard.$NETMAKER_BASE_DOMAIN to log in"
-echo "-----------------------------------------------------------------"
-echo "-----------------------------------------------------------------"
+set -e
+
+# 11. add netclient to docker-compose and start it up
+setup_netclient
+
+# 12. make the netclient a default host and ingress gw
+configure_netclient
+
+# 13. print success message
+print_success
 
 # cp -f /etc/skel/.bashrc /root/.bashrc

+ 309 - 95
scripts/nm-upgrade.sh

@@ -1,5 +1,10 @@
 #!/bin/bash
 
+LATEST="v0.18.5"
+INSTALL_PATH="/root"
+
+trap restore_old_netmaker_instructions
+
 # check_version - make sure current version is 0.17.1 before continuing
 check_version() {
   IMG_TAG=$(yq -r '.services.netmaker.image' docker-compose.yml)
@@ -13,6 +18,51 @@ check_version() {
   fi
 }
 
+backup_v17_files() {
+  mkdir $INSTALL_PATH/netmaker_0.17.1_backup
+  cp $INSTALL_PATH/docker-compose.yml  $INSTALL_PATH/netmaker_0.17.1_backup/docker-compose.yml
+  cp $INSTALL_PATH/Caddyfile $INSTALL_PATH/netmaker_0.17.1_backup/Caddyfile
+  cp $INSTALL_PATH/mosquitto.conf %INSTALL_PATH/netmaker_0.17.1_backup/mosquitto.conf
+  cp $INSTALL_PATH/wait.sh $INSTALL_PATH/netmaker_0.17.1_backup/wait.sh
+}
+
+backup_volumes() {
+  cp -r /var/lib/docker/volumes/root_caddy_conf/ /var/lib/docker/volumes/root_caddy_conf-backup/
+  cp -r /var/lib/docker/volumes/root_caddy_data/ /var/lib/docker/volumes/root_caddy_data-backup/
+  cp -r /var/lib/docker/volumes/root_dnsconfig/ /var/lib/docker/volumes/root_dnsconfig-backup/
+  cp -r /var/lib/docker/volumes/root_mosquitto_data/ /var/lib/docker/volumes/root_mosquitto_data-backup/
+  cp -r /var/lib/docker/volumes/root_mosquitto_logs/ /var/lib/docker/volumes/root_mosquitto_logs-backup/
+  cp -r /var/lib/docker/volumes/root_sqldata/ /var/lib/docker/volumes/root_sqldata-backup/
+}
+
+restore_old_netmaker_instructions() {
+  echo "There was a problem with the installation. Your config files and volumes have been backed up."
+  echo "To restore Netmaker back to v0.17.1, copy all the netmaker volume backups (caddy_conf-backup, caddy_data-backup, dnsconfig-backup, mosquitto_data-backup, mosquitto_logs-backup, and sqldata-backup) back to their regular names with out the -backup."
+  echo "Your config files should be located in ${INSALL_PATH}/netmaker_0.17.1_backup. Simply run cp ${INSALL_PATH}/netmaker_0.17.1_backup/* . (include the .) and run docker-compose up -d."
+  echo "Your netmaker should be back to v0.17.1"
+}
+
+get_install_path() {
+  echo "-----------------------------------------------------"
+  echo "Is your docker-compose located in $INSTALL_PATH ?"
+  echo "-----------------------------------------------------"
+  select install_option in "yes" "no (enter manually)"; do
+    case $REPLY in
+      1)
+        echo "using $INSTALL_PATH for an installation path."
+      break
+        ;;      
+      2)
+        read -p "Enter path where your docker-compose is located: " install_path
+        SERVER_HTTP_HOST=$install_path
+        echo "using $INSTALL_PATH"
+        break
+        ;;
+      *) echo "invalid option $REPLY";;
+    esac
+  done
+}
+
 # wait_seconds - wait a number of seconds, print a log
 wait_seconds() {
   for ((a=1; a <= $1; a++))
@@ -37,29 +87,24 @@ confirm() {
 # install_dependencies - install system dependencies necessary for script to run
 install_dependencies() {
   OS=$(uname)
-  is_ubuntu=$(sudo cat /etc/lsb-release | grep "Ubuntu")
-  if [ "${is_ubuntu}" != "" ]; then
-    dependencies="yq jq wireguard jq docker.io docker-compose"
-    update_cmd='apt update'
-    install_cmd='snap install'
-  elif [ -f /etc/debian_version ]; then
-    dependencies="yq jq wireguard jq docker.io docker-compose"
+  if [ -f /etc/debian_version ]; then
+    dependencies="jq wireguard jq dnsutils docker-compose"
     update_cmd='apt update'
     install_cmd='apt install -y'
   elif [ -f /etc/centos-release ]; then
-    dependencies="wireguard jq docker.io docker-compose"
+    dependencies="wireguard jq bind-utils docker-compose"
     update_cmd='yum update'
     install_cmd='yum install -y'
   elif [ -f /etc/fedora-release ]; then
-    dependencies="wireguard jq docker.io docker-compose"
+    dependencies="wireguard jq bind-utils docker-compose"
     update_cmd='dnf update'
     install_cmd='dnf install -y'
   elif [ -f /etc/redhat-release ]; then
-    dependencies="wireguard jq docker.io docker-compose"
+    dependencies="wireguard jq bind-utils docker-compose"
     update_cmd='yum update'
     install_cmd='yum install -y'
   elif [ -f /etc/arch-release ]; then
-        dependecies="wireguard-tools jq docker.io docker-compose netclient"
+        dependencies="wireguard-tools jq dnsutils docker-compose netclient"
     update_cmd='pacman -Sy'
     install_cmd='pacman -S --noconfirm'
   else
@@ -69,6 +114,14 @@ install_dependencies() {
 
   set -- $dependencies
 
+  if command -v docker >/dev/null 2>&1 ; then
+    echo "Docker found"
+    echo "version: $(docker version)"
+  else
+    echo "Docker not found. adding to dependencies"
+    $dependencies += " docker.io"
+  fi
+
   ${update_cmd}
 
   set +e
@@ -103,6 +156,26 @@ install_dependencies() {
   echo "-----------------------------------------------------"
 }
 
+# install_yq - install yq if not present
+install_yq() {
+	if ! command -v yq &> /dev/null; then
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+		chmod +x /usr/bin/yq
+	fi
+	set +e
+	if ! command -v yq &> /dev/null; then
+		set -e
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
+		chmod +x /usr/bin/yq
+	fi
+	set -e
+	if ! command -v yq &> /dev/null; then
+		echo "failed to install yq. Please install yq and try again."
+		echo "https://github.com/mikefarah/yq/#install"
+		exit 1
+	fi	
+}
+
 # collect_server_settings - retrieve server settings from existing compose file
 collect_server_settings() {
   MASTER_KEY=$(yq -r .services.netmaker.environment.MASTER_KEY docker-compose.yml)
@@ -185,9 +258,9 @@ collect_server_settings() {
     esac
   done
 
-  STUN_NAME="stun.$SERVER_NAME"
+  STUN_DOMAIN="stun.$SERVER_NAME"
   echo "-----------------------------------------------------"
-  echo "Netmaker v0.18.2 requires a new DNS entry for $STUN_NAME."
+  echo "Netmaker v0.18 requires a new DNS entry for $STUN_DOMAIN."
   echo "Please confirm this is added to your DNS provider before continuing"
   echo "(note: this is not required if using an nip.io address)"
   echo "-----------------------------------------------------"
@@ -199,6 +272,7 @@ collect_node_settings() {
   curl -s -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://$SERVER_HTTP_HOST/api/nodes | jq -c '[ .[] | select(.isserver=="yes") ]' > nodejson.tmp
   NODE_LEN=$(jq length nodejson.tmp)
   HAS_INGRESS="no"
+  HAS_RELAY="no"
   if [ "$NODE_LEN" -gt 0 ]; then
       echo "===SERVER NODES==="
       for i in $(seq 1 $NODE_LEN); do
@@ -236,26 +310,145 @@ collect_node_settings() {
       echo "WARNING: Your server contains an Ingress Gateway. After upgrading, existing Ext Clients will be lost and must be recreated. Please confirm that you would like to continue."
       confirm
   fi
+  if [[ $HAS_RELAY == "yes" ]]; then
+      echo "WARNING: Your server contains a Relay. After upgrading, relay will be unset. Relay functionality has been moved to the 'host' level, and must be reconfigured once all machines are upgraded."
+      confirm
+  fi
+
+}
+
+# setup_caddy - updates Caddy with new info
+setup_caddy() {
+
+  echo "backing up Caddyfile to ${INSTALL_PATH}/Caddyfile.backup"
+  cp $INSTALL_PATH/Caddyfile $INSTALL_PATH/Caddyfile.backup
+
+  if grep -wq "acme.zerossl.com/v2/DV90" Caddyfile; then 
+      echo "zerossl already set, continuing" 
+  else 
+    echo "editing Caddyfile"
+    sed -i '0,/email/{s~email~acme_ca https://acme.zerossl.com/v2/DV90\n\t&~}' $INSTALL_PATH/Caddyfile
+  fi
+
+cat <<EOT >> $INSTALL_PATH/Caddyfile
+
+# STUN
+https://$STUN_DOMAIN {
+  reverse_proxy netmaker:3478
+}
+EOT
+
+}
+
+# set_mq_credentials - sets mq credentials
+set_mq_credentials() {
+
+  unset GET_MQ_USERNAME
+  unset GET_MQ_PASSWORD
+  unset CONFIRM_MQ_PASSWORD
+  echo "Enter Credentials For MQ..."
+  read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
+  if [ -z "$GET_MQ_USERNAME" ]; then
+    echo "using default username for mq"
+    MQ_USERNAME="netmaker"
+  else
+    MQ_USERNAME="$GET_MQ_USERNAME"
+  fi
+
+  select domain_option in "Auto Generated Password" "Input Your Own Password"; do
+    case $REPLY in
+    1)
+    echo "generating random password for mq"
+    MQ_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+    break
+    ;;      
+      2)
+    while true
+      do
+          echo "Enter your Password For MQ: " 
+          read -s GET_MQ_PASSWORD
+          echo "Enter your password again to confirm: "
+          read -s CONFIRM_MQ_PASSWORD
+          if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
+              echo "wrong password entered, try again..."
+              continue
+          fi
+      MQ_PASSWORD="$GET_MQ_PASSWORD"
+          echo "MQ Password Saved Successfully!!"
+          break
+      done
+        break
+        ;;
+      *) echo "invalid option $REPLY";;
+    esac
+  done
 }
 
 # set_compose - set compose file with proper values
 set_compose() {
 
-  # DEV_TEMP - Temporary instructions for testing
-  sed -i "s/v0.17.1/testing/g" /root/docker-compose.yml
+  set_mq_credentials
+
+  echo "retrieving updated wait script and mosquitto conf"  
+  rm $INSTALL_PATH/wait.sh
+  rm $INSTALL_PATH/mosquitto.conf
+
+  wget -O $INSTALL_PATH/wait.sh https://raw.githubusercontent.com/gravitl/netmaker/master/docker/wait.sh
+
+  chmod +x $INSTALL_PATH/wait.sh
+
+  wget -O $INSTALL_PATH/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/master/docker/mosquitto.conf
+
+  chmod +x $INSTALL_PATH/mosquitto.conf
+
+  # DEV_TEMP
+  sed -i "s/v0.17.1/$LATEST/g" $INSTALL_PATH/docker-compose.yml
+
+  STUN_PORT=3478
 
   # RELEASE_REPLACE - Use this once release is ready
-  #sed -i "s/v0.17.1/v0.18.2/g" /root/docker-compose.yml
-  yq ".services.netmaker.environment.SERVER_NAME = \"$SERVER_NAME\"" -i /root/docker-compose.yml
-  yq ".services.netmaker.environment += {\"BROKER_NAME\": \"$BROKER_NAME\"}" -i /root/docker-compose.yml  
-  yq ".services.netmaker.environment += {\"STUN_NAME\": \"$STUN_NAME\"}" -i /root/docker-compose.yml  
-  yq ".services.netmaker.environment += {\"STUN_PORT\": \"3478\"}" -i /root/docker-compose.yml  
-  yq ".services.netmaker.ports += \"3478:3478/udp\"" -i /root/docker-compose.yml
+
+  #sed -i "s/v0.17.1/v0.18.6/g" /root/docker-compose.yml
+  yq ".services.netmaker.environment.SERVER_NAME = \"$SERVER_NAME\"" -i $INSTALL_PATH/docker-compose.yml
+  yq ".services.netmaker.environment += {\"BROKER_ENDPOINT\": \"wss://$BROKER_NAME\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.environment += {\"SERVER_BROKER_ENDPOINT\": \"ws://mq:1883\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.environment += {\"STUN_LIST\": \"$STUN_DOMAIN:$STUN_PORT,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.environment += {\"MQ_PASSWORD\": \"$MQ_PASSWORD\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.environment += {\"MQ_USERNAME\": \"$MQ_USERNAME\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.environment += {\"STUN_PORT\": \"$STUN_PORT\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.netmaker.ports += \"3478:3478/udp\"" -i $INSTALL_PATH/docker-compose.yml
+
+  yq ".services.mq.environment += {\"MQ_PASSWORD\": \"$MQ_PASSWORD\"}" -i $INSTALL_PATH/docker-compose.yml  
+  yq ".services.mq.environment += {\"MQ_USERNAME\": \"$MQ_USERNAME\"}" -i $INSTALL_PATH/docker-compose.yml  
+
+
+  #remove unnecessary ports
+  yq eval 'del( .services.netmaker.ports[] | select(. == "51821*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.mq.ports[] | select(. == "8883*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.mq.ports[] | select(. == "1883*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.mq.expose[] | select(. == "8883*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.mq.expose[] | select(. == "1883*") )' -i $INSTALL_PATH/docker-compose.yml
+
+  # delete unnecessary compose sections
+  yq eval 'del(.services.netmaker.cap_add)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.sysctls)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.MQ_ADMIN_PASSWORD)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.MQ_HOST)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.MQ_PORT)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.MQ_SERVER_PORT)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.PORT_FORWARD_SERVICES)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.CLIENT_MODE)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.netmaker.environment.HOST_NETWORK)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del(.services.mq.environment.NETMAKER_SERVER_HOST)' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.netmaker.volumes[] | select(. == "mosquitto_data*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .services.mq.volumes[] | select(. == "mosquitto_data*") )' -i $INSTALL_PATH/docker-compose.yml
+  yq eval 'del( .volumes.mosquitto_data )' -i $INSTALL_PATH/docker-compose.yml
+
 }
 
 # start_containers - run docker-compose up -d
 start_containers() {
-  docker-compose -f /root/docker-compose.yml up -d
+  docker-compose -f $INSTALL_PATH/docker-compose.yml up -d
 }
 
 # test_caddy - make sure caddy is working
@@ -283,59 +476,42 @@ test_caddy() {
   done
 }
 
-# setup_netclient - installs netclient locally
+# setup_netclient - adds netclient to docker-compose
 setup_netclient() {
 
-# DEV_TEMP - Temporary instructions for testing
-wget https://fileserver.netmaker.org/testing/netclient
-chmod +x netclient
-./netclient install
-
-# RELEASE_REPLACE - Use this once release is ready
-# if [ -f /etc/debian_version ]; then
-#     curl -sL 'https://apt.netmaker.org/gpg.key' | sudo tee /etc/apt/trusted.gpg.d/netclient.asc
-#     curl -sL 'https://apt.netmaker.org/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/netclient.list
-#     sudo apt update
-#     sudo apt install netclient
-# elif [ -f /etc/centos-release ]; then
-#     curl -sL 'https://rpm.netmaker.org/gpg.key' | sudo tee /tmp/gpg.key
-#     curl -sL 'https://rpm.netmaker.org/netclient-repo' | sudo tee /etc/yum.repos.d/netclient.repo
-#     sudo rpm --import /tmp/gpg.key
-#     sudo dnf check-update
-#     sudo dnf install netclient
-# elif [ -f /etc/fedora-release ]; then
-#     curl -sL 'https://rpm.netmaker.org/gpg.key' | sudo tee /tmp/gpg.key
-#     curl -sL 'https://rpm.netmaker.org/netclient-repo' | sudo tee /etc/yum.repos.d/netclient.repo
-#     sudo rpm --import /tmp/gpg.key
-#     sudo dnf check-update
-#     sudo dnf install netclient
-# elif [ -f /etc/redhat-release ]; then
-#     curl -sL 'https://rpm.netmaker.org/gpg.key' | sudo tee /tmp/gpg.key
-#     curl -sL 'https://rpm.netmaker.org/netclient-repo' | sudo tee /etc/yum.repos.d/netclient.repo
-#     sudo rpm --import /tmp/gpg.key
-#     sudo dnf check-update(
-#     sudo dnf install netclient
-# elif [ -f /etc/arch-release ]; then
-#     yay -S netclient
-# else
-# 	echo "OS not supported for automatic install"
-#     exit 1
-# fi
-
-# if [ -z "${install_cmd}" ]; then
-#         echo "OS unsupported for automatic dependency install"
-# 	exit 1
-# fi
+	set +e
+	netclient uninstall
+	HAS_APT=false
+  set -e
+  if command -v apt >/dev/null; then
+    HAS_APT=true
+  fi
+  set +e
+
+  if  [ "$HAS_APT" = "true" ]; then
+    curl -sL 'https://apt.netmaker.org/gpg.key' | sudo tee /etc/apt/trusted.gpg.d/netclient.asc
+    curl -sL 'https://apt.netmaker.org/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/netclient.list
+    sudo apt update
+    sudo apt install netclient
+  else
+     wget -O /tmp/netclient https://github.com/gravitl/netclient/releases/download/$LATEST/netclient_linux_amd64 
+
+	  chmod +x /tmp/netclient
+	  /tmp/netclient install
+  fi
+
+	netclient register -t $KEY
+
+	echo "waiting for client to become available"
+	wait_seconds 10 
+
 }
 
 # setup_nmctl - pulls nmctl and makes it executable
 setup_nmctl() {
 
-  # DEV_TEMP - Temporary instructions for testing
-  wget https://fileserver.netmaker.org/testing/nmctl
- 
-  # RELEASE_REPLACE - Use this once release is ready
-  # wget https://github.com/gravitl/netmaker/releases/download/v0.17.1/nmctl
+    wget -O nmctl https://github.com/gravitl/netmaker/releases/download/$LATEST/nmctl_linux_amd64
+  
     chmod +x nmctl
     echo "using server $SERVER_HTTP_HOST"
     echo "using master key $MASTER_KEY"
@@ -350,10 +526,16 @@ setup_nmctl() {
 
 # join_networks - joins netclient into the networks using old settings
 join_networks() {
-  NODE_LEN=$(jq length nodejson.tmp)
-  HAS_INGRESS="no"
+  NODE_LEN=$(jq length nodejson.tmp)  
   if [ "$NODE_LEN" -gt 0 ]; then
       for i in $(seq 1 $NODE_LEN); do
+          HAS_INGRESS="no"
+          HAS_EGRESS="no"
+          EGRESS_RANGES=""
+          HAS_RELAY="no"
+          RELAY_ADDRS=""
+          HAS_FAILOVER="no"
+
           NUM=$(($i-1))
           NETWORK=$(jq -r ".[$NUM].network" ./nodejson.tmp)
           echo "  joining network $NETWORK with following settings. Please confirm:"
@@ -364,7 +546,14 @@ join_networks() {
           echo "       is egress: $(jq -r ".[$NUM].isegressgateway" ./nodejson.tmp)"
           if [[ $(jq -r ".[$NUM].isegressgateway" ./nodejson.tmp) == "yes" ]]; then
               HAS_EGRESS="yes"
-              echo "          egress range: $(jq -r ".[$NUM].egressgatewayranges" ./nodejson.tmp)"
+              echo "          egress ranges: $(jq -r ".[$NUM].egressgatewayranges" ./nodejson.tmp | tr -d '[]\n"[:space:]')"
+              EGRESS_RANGES=$(jq -r ".[$NUM].egressgatewayranges" ./nodejson.tmp | tr -d '[]\n"[:space:]')
+              EGRESS_RANGES=${EGRESS_RANGES//0.0.0.0\/0/0.0.0.0\/5,8.0.0.0\/7,11.0.0.0\/8,12.0.0.0\/6,16.0.0.0\/4,32.0.0.0\/3,64.0.0.0\/2,128.0.0.0\/3,160.0.0.0\/5,168.0.0.0\/6,172.0.0.0\/12,172.32.0.0\/11,172.64.0.0\/10,172.128.0.0\/9,173.0.0.0\/8,174.0.0.0\/7,176.0.0.0\/4,192.0.0.0\/9,192.128.0.0\/11,192.160.0.0\/13,192.169.0.0\/16,192.170.0.0\/15,192.172.0.0\/14,192.176.0.0\/12,192.192.0.0\/10,193.0.0.0\/8,194.0.0.0\/7,196.0.0.0\/6,200.0.0.0\/5,208.0.0.0\/4}
+              EGRESS_RANGES=${EGRESS_RANGES//0::\/0/}
+              EGRESS_RANGES=${EGRESS_RANGES//,,/,}
+              EGRESS_RANGES=`echo $EGRESS_RANGES | sed 's/,*$//g'`
+              EGRESS_RANGES=`echo $EGRESS_RANGES | sed 's/^,*//g'`
+
           fi
           echo "      is ingress: $(jq -r ".[$NUM].isingressgateway" ./nodejson.tmp)"
           if [[ $(jq -r ".[$NUM].isingressgateway" ./nodejson.tmp) == "yes" ]]; then
@@ -382,27 +571,26 @@ join_networks() {
           echo "  ------------"
 
           confirm
-          echo "running command: ./nmctl keys create $NETWORK 1"
-          KEY_JSON=$(./nmctl keys create $NETWORK 1)          
-          KEY=$(echo $KEY_JSON | jq -r .accessstring)
 
-          echo "join key created: $KEY"
+          if [[ $NUM -eq 0 ]]; then 
+            echo "running command: ./nmctl enrollment_key create --uses 1 --networks $NETWORK"
+          	KEY_JSON=$(./nmctl enrollment_key create --uses 1 --networks $NETWORK)
+          	KEY=$(jq -r '.token' <<< ${KEY_JSON})
 
+            echo "enrollment key created: $KEY"
+
+            setup_netclient
+          else
+            HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
+            ./nmctl host add_network $HOST_ID $NETWORK
+          fi
           NAME=$(jq -r ".[$NUM].name" ./nodejson.tmp)
           ADDRESS=$(jq -r ".[$NUM].address" ./nodejson.tmp)
           ADDRESS6=$(jq -r ".[$NUM].address6" ./nodejson.tmp)
- 
 
-          if [[ ! -z "$ADDRESS6" ]]; then
-            echo "joining with command: netclient join -t $KEY --name=$NAME --address=$ADDRESS --address6=$ADDRESS6
-"
-            confirm
-            netclient join -t $KEY --name=$NAME --address=$ADDRESS --address6=$ADDRESS6
-          else
-            echo "joining with command: netclient join -t $KEY --name=$NAME --address=$ADDRESS"          
-            confirm
-            netclient join -t $KEY --name=$NAME --address=$ADDRESS
-          fi
+          echo "wait 10 seconds for netclient to be ready"
+          sleep 10
+
           NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .$NETWORK.commonnode.id)
           echo "join complete. New node ID: $NODE_ID"
           if [[ $NUM -eq 0 ]]; then
@@ -410,13 +598,17 @@ join_networks() {
             echo "For first join, making host a default"
             echo "Host ID: $HOST_ID"
             # set as a default host
-            # TODO - this command is not working
+            set +e
             ./nmctl host update $HOST_ID --default
+            sleep 2
+            set -e            
           fi
 
           # create an egress if necessary
           if [[ $HAS_EGRESS == "yes" ]]; then
-            echo "Egress is currently unimplemented. Wait for 0.18.2"
+            echo "creating egress"            
+            ./nmctl node create_egress $NETWORK $NODE_ID $EGRESS_RANGES
+            sleep 2
           fi
 
           echo "HAS INGRESS: $HAS_INGRESS"
@@ -425,16 +617,19 @@ join_networks() {
             if [[ $HAS_FAILOVER == "yes" ]]; then
               echo "creating ingress and failover..."
               ./nmctl node create_ingress $NETWORK $NODE_ID --failover
+              sleep 2
             else
               echo "creating ingress..."
               ./nmctl node create_ingress $NETWORK $NODE_ID
+              sleep 2
             fi
           fi
 
           # relay
           if [[ $HAS_RELAY == "yes" ]]; then
-            echo "creating relay..."
-            ./nmctl node create_relay $NETWORK $NODE_ID $RELAY_ADDRS
+            echo "cannot recreate relay; relay functionality moved to host"
+            # ./nmctl node create_relay $NETWORK $NODE_ID $RELAY_ADDRS
+            # sleep 2
           fi
 
       done
@@ -447,7 +642,7 @@ join_networks() {
 cat << "EOF"
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-The Netmaker Upgrade Script: Upgrading to v0.18.2 so you don't have to!
+The Netmaker Upgrade Script: Upgrading to v0.18 so you don't have to!
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 EOF
@@ -459,9 +654,23 @@ if [ $(id -u) -ne 0 ]; then
    exit 1
 fi
 
+set +e
+
+#backup volumes and v0.17.1 configs in case of failure.
+backup_volumes
+backup_v17_files
+
+# get the installation path for docker-compose.yml and other config files
+get_install_path
+
 echo "...installing dependencies for script"
 install_dependencies
 
+echo "...installing yq if necessary"
+install_yq
+
+set -e
+
 echo "...confirming version is correct"
 check_version
 
@@ -475,7 +684,10 @@ echo "...retrieving current server node settings"
 collect_node_settings
 
 echo "...backing up docker compose to docker-compose.yml.backup"
-cp /root/docker-compose.yml /root/docker-compose.yml.backup
+cp $INSTALL_PATH/docker-compose.yml $INSTALL_PATH/docker-compose.yml.backup
+
+echo "...setting Caddyfile values"
+setup_caddy
 
 echo "...setting docker-compose values"
 set_compose
@@ -483,6 +695,10 @@ set_compose
 echo "...starting containers"
 start_containers
 
+echo "...remove old mosquitto data"
+# TODO - yq is not removing volume from docker compose
+# docker volume rm root_mosquitto_data
+
 wait_seconds 3
 
 echo "..testing Caddy proxy"
@@ -493,11 +709,9 @@ echo "..testing Netmaker health"
 # netmaker_health_check
 # wait_seconds 2
 
-echo "...setting up netclient (this may take a minute, be patient)"
-setup_netclient
 wait_seconds 2
 
-echo "...join networks"
+echo "...setup netclient"
 join_networks
 
 echo "-----------------------------------------------------------------"

+ 27 - 0
servercfg/serverconf.go

@@ -80,6 +80,7 @@ func GetServerConfig() config.ServerConfig {
 	if Is_EE {
 		cfg.IsEE = "yes"
 	}
+	cfg.DefaultProxyMode = GetDefaultProxyMode()
 
 	return cfg
 }
@@ -673,6 +674,32 @@ func DeployedByOperator() bool {
 	return config.Config.Server.DeployedByOperator
 }
 
+// GetDefaultProxyMode - default proxy mode for a server
+func GetDefaultProxyMode() config.ProxyMode {
+	var (
+		mode config.ProxyMode
+		def  string
+	)
+	if os.Getenv("DEFAULT_PROXY_MODE") != "" {
+		def = os.Getenv("DEFAULT_PROXY_MODE")
+	} else if config.Config.Server.DefaultProxyMode.Set {
+		return config.Config.Server.DefaultProxyMode
+	}
+	switch strings.ToUpper(def) {
+	case "ON":
+		mode.Set = true
+		mode.Value = true
+	case "OFF":
+		mode.Set = true
+		mode.Value = false
+	// AUTO or any other value
+	default:
+		mode.Set = false
+	}
+	return mode
+
+}
+
 // parseStunList - turn string into slice of StunServers
 func parseStunList(stunString string) ([]models.StunServer, error) {
 	var err error

+ 1 - 1
swagger.yaml

@@ -704,7 +704,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.18.2
+    version: 0.18.6
 paths:
     /api/dns:
         get: