Browse Source

Merge branch 'develop' into GRA-1277-public-keys

0xdcarns 2 years ago
parent
commit
0ceecfb5b7
100 changed files with 2895 additions and 1755 deletions
  1. 2 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 0 50
      .github/workflows/buildandrelease.yml
  3. 46 0
      .github/workflows/packages.yml
  4. 5 2
      .github/workflows/publish-docker.yml
  5. 32 0
      .github/workflows/pull-request.yml
  6. 0 29
      .github/workflows/purgeGHCR.yml
  7. 50 0
      .github/workflows/release-assets.yml
  8. 39 0
      .github/workflows/release-branch.yml
  9. 49 0
      .github/workflows/release.yml
  10. 41 0
      .github/workflows/upgraderelease.yml
  11. 34 0
      .goreleaser.prerelease.yaml
  12. 4 0
      .goreleaser.update.yaml
  13. 7 6
      .goreleaser.yaml
  14. 2 1
      Dockerfile
  15. 1 1
      README.md
  16. 20 14
      cli/cmd/acl/list.go
  17. 9 0
      cli/cmd/commons/globals.go
  18. 11 5
      cli/cmd/dns/list.go
  19. 47 0
      cli/cmd/enrollment_key/create.go
  20. 23 0
      cli/cmd/enrollment_key/delete.go
  21. 20 0
      cli/cmd/enrollment_key/list.go
  22. 28 0
      cli/cmd/enrollment_key/root.go
  23. 11 5
      cli/cmd/ext_client/list.go
  24. 0 4
      cli/cmd/network/create.go
  25. 0 1
      cli/cmd/network/flags.go
  26. 13 7
      cli/cmd/network/list.go
  27. 0 4
      cli/cmd/network/update.go
  28. 19 13
      cli/cmd/node/list.go
  29. 4 0
      cli/cmd/root.go
  30. 12 5
      cli/cmd/user/list.go
  31. 22 0
      cli/functions/enrollment_keys.go
  32. 0 2
      cli/samples/network.json
  33. 0 4
      cli/samples/node.json
  34. 84 0
      compose/docker-compose-emqx.yml
  35. 16 20
      compose/docker-compose.ee.yml
  36. 17 0
      compose/docker-compose.netclient.yml
  37. 3 4
      compose/docker-compose.reference.yml
  38. 5 6
      compose/docker-compose.yml
  39. 49 42
      config/config.go
  40. 4 4
      config/config_test.go
  41. 6 11
      controllers/controller.go
  42. 1 1
      controllers/docs.go
  43. 247 0
      controllers/enrollmentkeys.go
  44. 26 15
      controllers/ext_client.go
  45. 9 8
      controllers/hosts.go
  46. 35 0
      controllers/legacy.go
  47. 46 38
      controllers/migrate.go
  48. 3 4
      controllers/network_test.go
  49. 28 12
      controllers/node.go
  50. 2 2
      controllers/node_test.go
  51. 4 1
      controllers/regex.go
  52. 4 0
      controllers/user_test.go
  53. 6 0
      database/database.go
  54. 1 1
      docker/Caddyfile
  55. 3 0
      ee/initialize.go
  56. 41 0
      ee/logic/ext_acls.go
  57. 12 21
      functions/helpers_test.go
  58. 9 8
      go.mod
  59. 24 19
      go.sum
  60. 1 1
      k8s/client/netclient-daemonset.yaml
  61. 1 1
      k8s/client/netclient.yaml
  62. 5 7
      k8s/server/netmaker-server.yaml
  63. 1 1
      k8s/server/netmaker-ui.yaml
  64. 0 2
      logic/accesskeys_test.go
  65. 53 0
      logic/clients.go
  66. 221 0
      logic/enrollmentkey.go
  67. 206 0
      logic/enrollmentkey_test.go
  68. 25 6
      logic/extpeers.go
  69. 20 10
      logic/host_test.go
  70. 56 0
      logic/hostactions/hostactions.go
  71. 39 11
      logic/hosts.go
  72. 46 0
      logic/legacy.go
  73. 2 1
      logic/metrics/metrics.go
  74. 19 34
      logic/nodes.go
  75. 282 205
      logic/peers.go
  76. 3 1
      logic/pro/networkuser_test.go
  77. 11 1
      logic/telemetry.go
  78. 25 38
      main.go
  79. 0 3
      models/api_node.go
  80. 65 0
      models/enrollment_key.go
  81. 13 12
      models/extclient.go
  82. 22 2
      models/host.go
  83. 25 14
      models/metrics.go
  84. 2 3
      models/migrate.go
  85. 13 11
      models/mqtt.go
  86. 0 4
      models/network.go
  87. 0 14
      models/node.go
  88. 1 0
      models/proxy.go
  89. 28 17
      models/structs.go
  90. 154 0
      mq/emqx.go
  91. 257 170
      mq/handlers.go
  92. 17 0
      mq/mq.go
  93. 38 12
      mq/publishers.go
  94. 8 0
      mq/util.go
  95. 0 9
      netclient/global_settings/globalsettings.go
  96. 0 78
      netclient/ncutils/iface.go
  97. 0 596
      netclient/ncutils/netclientutils.go
  98. 0 39
      netclient/ncutils/netclientutils_darwin.go
  99. 0 50
      netclient/ncutils/netclientutils_freebsd.go
  100. 0 32
      netclient/ncutils/netclientutils_linux.go

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

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

+ 0 - 50
.github/workflows/buildandrelease.yml

@@ -1,50 +0,0 @@
-name: Build and Release
-
-on:
-  workflow_dispatch:
-    inputs:
-      version:
-        description: 'Netmaker version'
-        required: true
-  release:
-    types: [published]
-
-jobs:
-  version:
-    runs-on: ubuntu-latest
-    outputs:
-      tag: ${{ steps.version.outputs.package_version }}
-      version: ${{ steps.version.outputs.version }}
-    steps:
-      - name: Get Version Number
-        id: version
-        run: |
-          if [[ -n "${{ github.event.inputs.version }}" ]]; then
-            NETMAKER_VERSION=${{ github.event.inputs.version }}
-          else
-            NETMAKER_VERSION=$(curl -fsSL https://api.github.com/repos/gravitl/netmaker/tags | grep 'name' | head -1 | cut -d'"' -f4)
-          fi
-          echo "VERSION=${NETMAKER_VERSION}" >> $GITHUB_OUTPUT
-          # remove everything but digits and . for package (deb, rpm, etc) versions
-          PACKAGE_VERSION=$(echo ${NETMAKER_VERSION} | tr -cd '[:digit:].')
-          echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_OUTPUT
-  netmaker-nmctl:
-    runs-on: ubuntu-latest
-    needs: version
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          ref: release_${{ needs.version.outputs.version }}
-          fetch-depth: 0
-      - run: git fetch --force --tags
-      - name: Setup go
-        uses: actions/setup-go@v3
-        with:
-          go-version: 1.19
-      - name: GoReleaser
-        uses: goreleaser/goreleaser-action@v4
-        with:
-          args: release --clean
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 46 - 0
.github/workflows/packages.yml

@@ -0,0 +1,46 @@
+name: Build linux packages
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "netmaker version"
+        required: true
+  release:
+    types: [released]
+
+jobs:
+
+  packages:
+    runs-on: ubuntu-latest
+    steps:
+      - name: setup ssh
+        run: |
+          mkdir -p ~/.ssh/
+          echo "$SSH_KEY" > ~/.ssh/id_devops
+          chmod 600 ~/.ssh/id_devops
+          cat >>~/.ssh/config <<END
+          Host *.clustercat.com
+            User root
+            IdentityFile ~/.ssh/id_devops
+            StrictHostKeyChecking no
+          END
+        env:
+          SSH_KEY: ${{ secrets.TESTING_SSH_KEY }}
+      - name: set version
+        run: |
+          if [[ -n "${{ github.event.inputs.version }}" ]]; then
+            # remove everything but digits and . for package (deb, rpm, etc) versions
+            VERSION=$(echo ${{ github.event.inputs.version }} | tr -cd '[:digit:].')
+          else
+            VERSION=$(echo ${{ github.ref.name }} | tr -cd '[:digit:].')
+          fi
+            echo "VERSION=${VERSION}" >> $GITHUB_ENV
+            echo ${VERSION}
+      - name: apt/rpm
+        run: |
+          ssh fileserver.clustercat.com "cd packages/nmcli; export VERSION=${{ ENV.version }}; export REVISION=0; ./buildall.sh "

+ 5 - 2
.github/workflows/publish-docker.yml

@@ -6,8 +6,11 @@ on:
       tag:
         description: 'docker tag'
         required: true
-  release:
-    types: [published]
+  workflow_call:
+    inputs:
+      tag:
+        type: string
+        required: true
 
 jobs:
   docker:

+ 32 - 0
.github/workflows/pull-request.yml

@@ -0,0 +1,32 @@
+# creates a PR from release branch to master
+name: Create Release PR to master
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "netmaker version"
+        required: true
+  release:
+    types: [released]
+
+jobs:
+  pr-to-main:
+    runs-on: ubuntu-latest
+    steps:
+      - name: create pr
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          gh api --method POST \
+          -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28'  \
+          /repos/${{ github.repository }}/pulls \
+          -f title='${{ github.event.inputs.version }}' \
+          -f head='release_${{ github.event.inputs.version }}' \
+          -f base="master"
+
+

+ 0 - 29
.github/workflows/purgeGHCR.yml

@@ -1,29 +0,0 @@
-name: Purge untagged images from GHCR
-
-on: 
-    workflow_dispatch:
-    schedule:
-      - cron: '1 1 1 * *'
-jobs:
-  purge:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-      - name: Prune Netmaker
-        uses: vlaurin/action-ghcr-prune@main
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          organization: gravitl
-          container: netmaker
-          dry-run: false # Dry-run first, then change to `false`
-          untagged: true
-      - name: Prune Netclient
-        uses: vlaurin/action-ghcr-prune@main
-        with:
-          token: ${{ secrets.GITHUB_TOKEN }}
-          organization: gravitl
-          container: netclient
-          dry-run: false # Dry-run first, then change to `false`
-          untagged: true
-          

+ 50 - 0
.github/workflows/release-assets.yml

@@ -0,0 +1,50 @@
+# generates release assets and uploads to release 
+name: Upload Release Assets
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+      prerelease:
+        required: true
+        type: boolean
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "netmaker version"
+        required: true
+      prerelease:
+        required: true
+        type: boolean
+        description: "prerelease"
+jobs:
+  release-assets:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          ref: release_${{ github.event.inputs.version}}
+          fetch-depth: 0
+      - name: Get Tags
+        run: |
+          git fetch --force --tags
+      - name: Setup go
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.19
+      - name: GoReleaser (full release)
+        if: ${{ github.event.inputs.prerelease == 'false'}}
+        uses: goreleaser/goreleaser-action@v4
+        with:
+          args: release --clean --release-notes release.md
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: GoReleaser (prerelease)
+        if: ${{ github.event.inputs.prerelease == 'false'}}
+        uses: goreleaser/goreleaser-action@v4
+        with:
+          args: release --clean --release-notes release.md -f .goreleaser.prerelease.yaml
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 39 - 0
.github/workflows/release-branch.yml

@@ -0,0 +1,39 @@
+# creates new branch (release_{{ version }} with tag {{ version }} from develop 
+# will fail if branch or tag already exists on github
+name: Create Release Branch
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "netmaker version"
+        required: true
+jobs:
+  release-branch:
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout develop
+        uses: actions/checkout@v3
+        with:
+          ref: develop
+      - name: setup go
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.19
+      - name: setup git
+        run: |
+          git config user.name "Github Actions"
+          git config user.emaail "[email protected]"
+      - name: create release Branch
+        run: |
+          git switch -c release_${{ github.event.inputs.version }}
+          git tag -f ${{ github.event.inputs.version }}
+          #push branch
+          git push origin release_${{ github.event.inputs.version }}
+          #push tag
+          git push origin ${{ github.event.inputs.version }}
+  

+ 49 - 0
.github/workflows/release.yml

@@ -0,0 +1,49 @@
+# creates a release from develop
+# creates release branch, generates release assets, publishes docker image and copies release.md to release
+# if formal release, linux packages are generated and a PR from release branch to master is created 
+name: Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "new version number"
+        required: true
+      prerelease:
+        required: true
+        type: boolean
+        description: Is this a pre-release
+
+jobs:
+
+  release-branch:
+    uses: ./.github/workflows/release-branch.yml
+    with:
+      version: ${{ github.event.inputs.version }}
+  
+  release-assets:
+    needs: release-branch
+    uses: ./.github/workflows/release-assets.yml
+    with:
+      version: ${{ github.event.inputs.version }}
+      prerelease: ${{ github.event.inputs.prerelease == 'true' }}
+
+  docker:
+    needs: release-branch
+    uses: ./.github/workflows/publish-docker.yml
+    with:
+      tag: ${{ github.event.inputs.version }}
+
+  packages:
+    if: ${{ github.event.inputs.prerelease == 'false' }}
+    needs: release-branch
+    uses: ./.github/workflows/packages.yml
+    with:
+      version: ${{ github.event.inputs.version }}
+
+  pull-request:
+    if: ${{ github.event.inputs.prerelease == 'false' }}
+    needs: release-branch
+    uses: ./.github/workflows/pull-request.yml
+    with:
+      version: ${{ github.event.inputs.version }}

+ 41 - 0
.github/workflows/upgraderelease.yml

@@ -0,0 +1,41 @@
+name: UpgradeRelease
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "release/version to update"
+        required: true
+
+jobs:
+
+  update:
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        uses: actions/checkout@v3
+        with:
+          ref: release_${{ github.event.inputs.version }}
+          fetch-depth: 0
+      - run: |
+          git fetch --force --tags
+      - name: Setup go
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.19
+      - name: goreleaser
+        uses: goreleaser/goreleaser-action@v4
+        with:
+          args: release --clean --release-notes release.md -f .goreleaser.update.yaml
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN  }}
+
+  packages:
+    uses: ./.github/workflows/packages.yml
+    with:
+      version: ${{ github.event.inputs.version }}
+
+  pull-request:
+    uses: ./.github/workflows/pull-request.yml
+    with:
+      version: ${{ github.event.inputs.version }}

+ 34 - 0
.goreleaser.prerelease.yaml

@@ -0,0 +1,34 @@
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod tidy
+builds:
+  - main: ./
+    env:
+      - CGO_ENABLED=1
+    ldflags:
+      - -s -w
+    targets:
+      - linux_amd64
+    binary: '{{ .ProjectName }}'
+
+  - main: ./cli
+    id: 'nmctl'
+    env:
+      - CGO_ENABLED=0
+    ldflags:
+      - -s -w
+    targets:
+      - linux_amd64
+      - linux_arm64
+      - darwin_amd64
+      - darwin_arm64
+      - freebsd_amd64
+      - windows_amd64
+    binary: 'nmctl'
+archives:
+  - format: binary
+    name_template: '{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}'
+release:
+  prerelease: true
+    

+ 4 - 0
.goreleaser.update.yaml

@@ -0,0 +1,4 @@
+builds:
+  - skip: true
+release:
+  prerelease: false

+ 7 - 6
.goreleaser.yaml

@@ -4,16 +4,16 @@ before:
     - go mod tidy
 builds:
   - main: ./
-    id: "netmaker"
     env:
       - CGO_ENABLED=1
     ldflags:
       - -s -w
     targets:
       - linux_amd64
-    binary: netmaker
+    binary: '{{ .ProjectName }}'
+
   - main: ./cli
-    id: "nmctl"
+    id: 'nmctl'
     env:
       - CGO_ENABLED=0
     ldflags:
@@ -25,9 +25,10 @@ builds:
       - darwin_arm64
       - freebsd_amd64
       - windows_amd64
-    binary: nmctl
+    binary: 'nmctl'
 archives:
   - format: binary
     name_template: '{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}'
-changelog:
-  skip: true
+release:
+  prerelease: false
+    

+ 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.4-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" />

+ 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()
 	},
 }
 

+ 47 - 0
cli/cmd/enrollment_key/create.go

@@ -0,0 +1,47 @@
+package enrollment_key
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var (
+	expiration    int
+	usesRemaining int
+	networks      string
+	unlimited     bool
+	tags          string
+)
+
+var enrollmentKeyCreateCmd = &cobra.Command{
+	Use:   "create",
+	Args:  cobra.NoArgs,
+	Short: "Create an enrollment key",
+	Long:  `Create an enrollment key`,
+	Run: func(cmd *cobra.Command, args []string) {
+		enrollKey := &models.APIEnrollmentKey{
+			Expiration:    int64(expiration),
+			UsesRemaining: usesRemaining,
+			Unlimited:     unlimited,
+		}
+		if networks != "" {
+			enrollKey.Networks = strings.Split(networks, ",")
+		}
+		if tags != "" {
+			enrollKey.Tags = strings.Split(tags, ",")
+		}
+		functions.PrettyPrint(functions.CreateEnrollmentKey(enrollKey))
+	},
+}
+
+func init() {
+	enrollmentKeyCreateCmd.Flags().IntVar(&expiration, "expiration", 0, "Expiration time of the key in UNIX timestamp format")
+	enrollmentKeyCreateCmd.Flags().IntVar(&usesRemaining, "uses", 0, "Number of times this key can be used")
+	enrollmentKeyCreateCmd.Flags().StringVar(&networks, "networks", "", "Comma-separated list of networks which the enrollment key can access")
+	enrollmentKeyCreateCmd.Flags().BoolVar(&unlimited, "unlimited", false, "Should the key have unlimited uses ?")
+	enrollmentKeyCreateCmd.Flags().StringVar(&tags, "tags", "", "Comma-separated list of any additional tags")
+	rootCmd.AddCommand(enrollmentKeyCreateCmd)
+}

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

@@ -0,0 +1,23 @@
+package enrollment_key
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var enrollmentKeyDeleteCmd = &cobra.Command{
+	Use:   "delete keyID",
+	Args:  cobra.ExactArgs(1),
+	Short: "Delete an enrollment key",
+	Long:  `Delete an enrollment key`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteEnrollmentKey(args[0])
+		fmt.Println("Enrollment key ", args[0], " deleted")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(enrollmentKeyDeleteCmd)
+}

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

@@ -0,0 +1,20 @@
+package enrollment_key
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var enrollmentKeyListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List enrollment keys",
+	Long:  `List enrollment keys`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetEnrollmentKeys())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(enrollmentKeyListCmd)
+}

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

@@ -0,0 +1,28 @@
+package enrollment_key
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "enrollment_key",
+	Short: "Manage Enrollment Keys",
+	Long:  `Manage Enrollment Keys`,
+}
+
+// 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)
+	}
+}

+ 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 - 4
cli/cmd/network/create.go

@@ -35,9 +35,6 @@ var networkCreateCmd = &cobra.Command{
 			if udpHolePunch {
 				network.DefaultUDPHolePunch = "yes"
 			}
-			if localNetwork {
-				network.IsLocal = "yes"
-			}
 			if defaultACL {
 				network.DefaultACL = "yes"
 			}
@@ -62,7 +59,6 @@ func init() {
 	networkCreateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the network")
 	networkCreateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the network")
 	networkCreateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
-	networkCreateCmd.Flags().BoolVar(&localNetwork, "local", false, "Is the network local (LAN) ?")
 	networkCreateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
 	networkCreateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
 	networkCreateCmd.Flags().StringVar(&defaultExtClientDNS, "ext_client_dns", "", "IPv4 address of DNS server to be used by external clients")

+ 0 - 1
cli/cmd/network/flags.go

@@ -6,7 +6,6 @@ var (
 	address                   string
 	address6                  string
 	udpHolePunch              bool
-	localNetwork              bool
 	defaultACL                bool
 	defaultInterface          string
 	defaultListenPort         int

+ 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()
 	},
 }
 

+ 0 - 4
cli/cmd/network/update.go

@@ -38,9 +38,6 @@ var networkUpdateCmd = &cobra.Command{
 			if udpHolePunch {
 				network.DefaultUDPHolePunch = "yes"
 			}
-			if localNetwork {
-				network.IsLocal = "yes"
-			}
 			if defaultACL {
 				network.DefaultACL = "yes"
 			}
@@ -63,7 +60,6 @@ func init() {
 	networkUpdateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the network")
 	networkUpdateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the network")
 	networkUpdateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
-	networkUpdateCmd.Flags().BoolVar(&localNetwork, "local", false, "Is the network local (LAN) ?")
 	networkUpdateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
 	networkUpdateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
 	networkUpdateCmd.Flags().StringVar(&defaultExtClientDNS, "ext_client_dns", "", "IPv4 address of DNS server to be used by external clients")

+ 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()
 	},
 }
 

+ 4 - 0
cli/cmd/root.go

@@ -4,8 +4,10 @@ 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"
@@ -41,6 +43,7 @@ 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())
@@ -55,4 +58,5 @@ func init() {
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(network_user.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
+	rootCmd.AddCommand(enrollment_key.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()
 	},
 }
 

+ 22 - 0
cli/functions/enrollment_keys.go

@@ -0,0 +1,22 @@
+package functions
+
+import (
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// CreateEnrollmentKey - create an enrollment key
+func CreateEnrollmentKey(key *models.APIEnrollmentKey) *models.EnrollmentKey {
+	return request[models.EnrollmentKey](http.MethodPost, "/api/v1/enrollment-keys", key)
+}
+
+// GetEnrollmentKeys - gets all enrollment keys
+func GetEnrollmentKeys() *[]models.EnrollmentKey {
+	return request[[]models.EnrollmentKey](http.MethodGet, "/api/v1/enrollment-keys", nil)
+}
+
+// DeleteEnrollmentKey - delete an enrollment key
+func DeleteEnrollmentKey(keyID string) {
+	request[any](http.MethodDelete, "/api/v1/enrollment-keys/"+keyID, nil)
+}

+ 0 - 2
cli/samples/network.json

@@ -10,11 +10,9 @@
     "defaultinterface": "nm-test3",
     "accesskeys": [],
     "allowmanualsignup": "no",
-    "islocal": "no",
     "isipv4": "yes",
     "isipv6": "no",
     "ispointtosite": "no",
-    "localrange": "",
     "defaultudpholepunch": "yes",
     "defaultextclientdns": "",
     "defaultmtu": 1280,

+ 0 - 4
cli/samples/node.json

@@ -18,11 +18,9 @@
       "defaultkeepalive": 20,
       "accesskeys": [],
       "allowmanualsignup": "no",
-      "islocal": "no",
       "isipv4": "no",
       "isipv6": "yes",
       "ispointtosite": "no",
-      "localrange": "",
       "defaultudpholepunch": "yes",
       "defaultextclientdns": "",
       "defaultmtu": 1280,
@@ -82,8 +80,6 @@
     "dnson": "no",
     "isserver": "yes",
     "action": "noop",
-    "islocal": "no",
-    "localrange": "",
     "ipforwarding": "yes",
     "os": "linux",
     "mtu": 1280,

+ 84 - 0
compose/docker-compose-emqx.yml

@@ -0,0 +1,84 @@
+version: "3.4"
+
+services:
+  netmaker:
+    container_name: netmaker
+    image: gravitl/netmaker:v0.18.4
+    restart: always
+    volumes:
+      - dnsconfig:/root/config/dnsconfig
+      - sqldata:/root/data
+    environment:
+      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN/mqtt"
+      BROKER_TYPE: "emqx"
+      EMQX_REST_ENDPOINT: "http://mq:18083"
+      SERVER_NAME: "NETMAKER_BASE_DOMAIN"
+      STUN_LIST: "stun.NETMAKER_BASE_DOMAIN:3478,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302"
+      SERVER_HOST: "SERVER_PUBLIC_IP"
+      SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
+      COREDNS_ADDR: "SERVER_PUBLIC_IP"
+      DNS_MODE: "on"
+      SERVER_HTTP_HOST: "api.NETMAKER_BASE_DOMAIN"
+      API_PORT: "8081"
+      MASTER_KEY: "REPLACE_MASTER_KEY"
+      CORS_ALLOWED_ORIGIN: "*"
+      DISPLAY_KEYS: "on"
+      DATABASE: "sqlite"
+      NODE_ID: "netmaker-server-1"
+      SERVER_BROKER_ENDPOINT: "ws://mq:8083/mqtt"
+      STUN_PORT: "3478"      
+      VERBOSITY: "1"
+      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
+      MQ_USERNAME: "REPLACE_MQ_USERNAME"
+      DEFAULT_PROXY_MODE: "auto"
+    ports:
+      - "3478:3478/udp"
+  netmaker-ui:
+    container_name: netmaker-ui
+    image: gravitl/netmaker-ui:v0.18.4
+    depends_on:
+      - netmaker
+    links:
+      - "netmaker:api"
+    restart: always
+    environment:
+      BACKEND_URL: "https://api.NETMAKER_BASE_DOMAIN"
+  caddy:
+    image: caddy:2.6.2
+    container_name: caddy
+    restart: unless-stopped
+    volumes:
+      - /root/Caddyfile:/etc/caddy/Caddyfile
+      - caddy_data:/data
+      - caddy_conf:/config
+    ports:
+      - "80:80"
+      - "443:443"
+  coredns:
+    container_name: coredns
+    image: coredns/coredns
+    command: -conf /root/dnsconfig/Corefile
+    depends_on:
+      - netmaker
+    restart: always
+    volumes:
+      - dnsconfig:/root/dnsconfig
+  mq:
+    container_name: mq
+    image: emqx/emqx:5.0.17
+    restart: unless-stopped
+    environment:
+      EMQX_NAME: "emqx"
+      EMQX_DASHBOARD__DEFAULT_PASSWORD: "REPLACE_MQ_PASSWORD"
+      EMQX_DASHBOARD__DEFAULT_USERNAME: "REPLACE_MQ_USERNAME"
+    ports:
+      - "1883:1883" # MQTT
+      - "8883:8883" # SSL MQTT
+      - "8083:8083" # Websockets
+      - "18083:18083" # Dashboard/REST_API
+volumes:
+  caddy_data: {}
+  caddy_conf: {}
+  sqldata: {}
+  dnsconfig: {}
+  mosquitto_logs: {}

+ 16 - 20
compose/docker-compose.ee.yml

@@ -9,9 +9,11 @@ services:
       - dnsconfig:/root/config/dnsconfig
       - sqldata:/root/data
     environment:
-      BROKER_NAME: "broker.NETMAKER_BASE_DOMAIN"
+      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN/mqtt"
+      BROKER_TYPE: "emqx"
+      EMQX_REST_ENDPOINT: "http://mq:18083"
       SERVER_NAME: "NETMAKER_BASE_DOMAIN"
-      STUN_DOMAIN: "stun.NETMAKER_BASE_DOMAIN"
+      STUN_LIST: "stun.NETMAKER_BASE_DOMAIN:3478,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302"
       SERVER_HOST: "SERVER_PUBLIC_IP"
       SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
       COREDNS_ADDR: "SERVER_PUBLIC_IP"
@@ -23,9 +25,7 @@ services:
       DISPLAY_KEYS: "on"
       DATABASE: "sqlite"
       NODE_ID: "netmaker-server-1"
-      MQ_HOST: "mq"
-      MQ_PORT: "443"
-      MQ_SERVER_PORT: "1883"
+      SERVER_BROKER_ENDPOINT: "ws://mq:8083/mqtt"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       STUN_PORT: "3478"
@@ -33,6 +33,7 @@ services:
       METRICS_EXPORTER: "on"
       LICENSE_KEY: "YOUR_LICENSE_KEY"
       NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID"
+      DEFAULT_PROXY_MODE: "auto"
     ports:
       - "3478:3478/udp"
   netmaker-ui:
@@ -67,21 +68,17 @@ services:
       - dnsconfig:/root/dnsconfig
   mq:
     container_name: mq
-    image: eclipse-mosquitto:2.0.15-openssl
-    depends_on:
-      - netmaker
+    image: emqx/emqx:5.0.17
     restart: unless-stopped
-    command: ["/mosquitto/config/wait.sh"]
     environment:
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
-    volumes:
-      - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf
-      - /root/wait.sh:/mosquitto/config/wait.sh
-      - mosquitto_logs:/mosquitto/log
+      EMQX_NAME: "emqx"
+      EMQX_DASHBOARD__DEFAULT_PASSWORD: "REPLACE_MQ_PASSWORD"
+      EMQX_DASHBOARD__DEFAULT_USERNAME: "REPLACE_MQ_USERNAME"
     ports:
-      - "1883:1883"
-      - "8883:8883"
+      - "1883:1883" # MQTT
+      - "8883:8883" # SSL MQTT
+      - "8083:8083" # Websockets
+      - "18083:18083" # Dashboard/REST_API
   prometheus:
     container_name: prometheus
     image: gravitl/netmaker-prometheus:latest
@@ -115,9 +112,8 @@ services:
     depends_on:
       - netmaker
     environment:
-      MQ_HOST: "mq"
-      MQ_PORT: "443"
-      MQ_SERVER_PORT: "1883"
+      SERVER_BROKER_ENDPOINT: "ws://mq:8083/mqtt"
+      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN/mqtt"
       PROMETHEUS: "on"
       VERBOSITY: "1"
       API_PORT: "8085"

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

@@ -0,0 +1,17 @@
+version: "3.4"
+
+services:
+  netclient:
+    container_name: netclient
+    image: 'gravitl/netclient:v0.18.4'
+    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

+ 3 - 4
compose/docker-compose.reference.yml

@@ -10,7 +10,7 @@ services:
       - sqldata:/root/data
       - shared_certs:/etc/netmaker
     environment: # Necessary capabilities to set iptables when running in container
-      BROKER_NAME: "broker.NETMAKER_BASE_DOMAIN" # The domain/host IP indicating the mq broker address
+      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN" # The domain/host IP indicating the mq broker address
       SERVER_NAME: "NETMAKER_BASE_DOMAIN" # The base domain of netmaker
       SERVER_HOST: "SERVER_PUBLIC_IP" # Set to public IP of machine.
       SERVER_HTTP_HOST: "api.NETMAKER_BASE_DOMAIN" # Overrides SERVER_HOST if set. Useful for making HTTP available via different interfaces/networks.
@@ -26,9 +26,7 @@ services:
       DISPLAY_KEYS: "on" # Show keys permanently in UI (until deleted) as opposed to 1-time display.
       DATABASE: "sqlite" # Database to use - sqlite, postgres, or rqlite
       NODE_ID: "netmaker-server-1" # used for HA - identifies this server vs other servers
-      MQ_HOST: "mq"  # the address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address. If using "host networking", it will find and detect the IP of the mq container.
-      MQ_PORT: "443" # the reachable port of MQ - change if external MQ port changes (port on proxy, not necessarily the one exposed in docker-compose)
-      MQ_SERVER_PORT: "1883" # the reachable port of MQ by the server - change if internal MQ port changes (or use external port if MQ is not on the same machine)
+      SERVER_BROKER_ENDPOINT: ""ws://mq:1883""  # the address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address. If using "host networking", it will find and detect the IP of the mq container.
       MQ_USERNAME: "REPLACE_MQ_USERNAME" # the username to set for MQ access
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD" # the password to set for MQ access
       STUN_PORT: "3478" # the reachable port of STUN on the server
@@ -40,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: "auto" # 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

+ 5 - 6
compose/docker-compose.yml

@@ -9,9 +9,9 @@ services:
       - dnsconfig:/root/config/dnsconfig
       - sqldata:/root/data
     environment:
-      BROKER_NAME: "broker.NETMAKER_BASE_DOMAIN"
+      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN"
       SERVER_NAME: "NETMAKER_BASE_DOMAIN"
-      STUN_DOMAIN: "stun.NETMAKER_BASE_DOMAIN"
+      STUN_LIST: "stun.NETMAKER_BASE_DOMAIN:3478,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302"
       SERVER_HOST: "SERVER_PUBLIC_IP"
       SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
       COREDNS_ADDR: "SERVER_PUBLIC_IP"
@@ -23,13 +23,12 @@ services:
       DISPLAY_KEYS: "on"
       DATABASE: "sqlite"
       NODE_ID: "netmaker-server-1"
-      MQ_HOST: "mq"
-      MQ_PORT: "443"      
-      MQ_SERVER_PORT: "1883"
-      STUN_PORT: "3478"      
+      SERVER_BROKER_ENDPOINT: "ws://mq:1883"
       VERBOSITY: "1"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
+      STUN_PORT: "3478"
+      DEFAULT_PROXY_MODE: "auto"
     ports:
       - "3478:3478/udp"
   netmaker-ui:

+ 49 - 42
config/config.go

@@ -32,48 +32,55 @@ 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"`
-	MQHOST                string `yaml:"mqhost"`
-	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"`
-	MQPort                string `yaml:"mqport"`
-	MQServerPort          string `yaml:"mqserverport"`
-	Server                string `yaml:"server"`
-	Broker                string `yam:"broker"`
-	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"`
-	StunHost              string `yaml:"stun_host"`
-	Proxy                 string `yaml:"proxy"`
+	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"`
+}
+
+// ProxyMode - default proxy mode for server
+type ProxyMode struct {
+	Set   bool
+	Value bool
 }
 
 // SQLConfig - Generic SQL Config

+ 4 - 4
config/config_test.go

@@ -1,7 +1,7 @@
-//Environment file for getting variables
-//Currently the only thing it does is set the master password
-//Should probably have it take over functions from OS such as port and mongodb connection details
-//Reads from the config/environments/dev.yaml file by default
+// Environment file for getting variables
+// Currently the only thing it does is set the master password
+// Should probably have it take over functions from OS such as port and mongodb connection details
+// Reads from the config/environments/dev.yaml file by default
 package config
 
 import (

+ 6 - 11
controllers/controller.go

@@ -4,11 +4,8 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
-	"os/signal"
 	"strings"
 	"sync"
-	"syscall"
 	"time"
 
 	"github.com/gorilla/handlers"
@@ -29,10 +26,12 @@ var HttpHandlers = []interface{}{
 	ipHandlers,
 	loggerHandlers,
 	hostHandlers,
+	enrollmentKeyHandlers,
+	legacyHandlers,
 }
 
 // HandleRESTRequests - handles the rest requests
-func HandleRESTRequests(wg *sync.WaitGroup) {
+func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 	defer wg.Done()
 
 	r := mux.NewRouter()
@@ -58,18 +57,14 @@ func HandleRESTRequests(wg *sync.WaitGroup) {
 	}()
 	logger.Log(0, "REST Server successfully started on port ", port, " (REST)")
 
-	// Relay os.Interrupt to our channel (os.Interrupt = CTRL+C)
-	// Ignore other incoming signals
-	ctx, stop := signal.NotifyContext(context.TODO(), syscall.SIGTERM, os.Interrupt)
-	defer stop()
-
 	// Block main routine until a signal is received
 	// As long as user doesn't press CTRL+C a message is not passed and our main routine keeps running
 	<-ctx.Done()
-
 	// After receiving CTRL+C Properly stop the server
 	logger.Log(0, "Stopping the REST server...")
+	if err := srv.Shutdown(context.TODO()); err != nil {
+		logger.Log(0, "REST shutdown error occurred -", err.Error())
+	}
 	logger.Log(0, "REST Server closed.")
 	logger.DumpFile(fmt.Sprintf("data/netmaker.log.%s", time.Now().Format(logger.TimeFormatDay)))
-	srv.Shutdown(context.TODO())
 }

+ 1 - 1
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.18.2
+//	Version: 0.18.4
 //	Host: netmaker.io
 //
 //	Consumes:

+ 247 - 0
controllers/enrollmentkeys.go

@@ -0,0 +1,247 @@
+package controller
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/hostactions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+func enrollmentKeyHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(createEnrollmentKey))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(getEnrollmentKeys))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(deleteEnrollmentKey))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).Methods(http.MethodPost)
+}
+
+// swagger:route GET /api/v1/enrollment-keys enrollmentKeys getEnrollmentKeys
+//
+// Lists all EnrollmentKeys for admins.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: getEnrollmentKeysSlice
+func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
+	currentKeys, err := logic.GetAllEnrollmentKeys()
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for i := range currentKeys {
+		currentKey := currentKeys[i]
+		if err = logic.Tokenize(currentKey, servercfg.GetAPIHost()); err != nil {
+			logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+	// return JSON/API formatted keys
+	logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(currentKeys)
+}
+
+// swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey
+//
+// Deletes an EnrollmentKey from Netmaker server.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: deleteEnrollmentKeyResponse
+func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	keyID := params["keyID"]
+	err := logic.DeleteEnrollmentKey(keyID)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
+	w.WriteHeader(http.StatusOK)
+}
+
+// swagger:route POST /api/v1/enrollment-keys enrollmentKeys createEnrollmentKey
+//
+// Creates an EnrollmentKey for hosts to use on Netmaker server.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: createEnrollmentKeyResponse
+func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
+
+	var enrollmentKeyBody models.APIEnrollmentKey
+
+	err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
+	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
+	}
+	var newTime time.Time
+	if enrollmentKeyBody.Expiration > 0 {
+		newTime = time.Unix(enrollmentKeyBody.Expiration, 0)
+	}
+
+	newEnrollmentKey, err := logic.CreateEnrollmentKey(enrollmentKeyBody.UsesRemaining, newTime, enrollmentKeyBody.Networks, enrollmentKeyBody.Tags, enrollmentKeyBody.Unlimited)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logger.Log(2, r.Header.Get("user"), "created enrollment key")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(newEnrollmentKey)
+}
+
+// swagger:route POST /api/v1/enrollment-keys/{token} enrollmentKeys handleHostRegister
+//
+// Handles a Netclient registration with server and add nodes accordingly.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: handleHostRegisterResponse
+func handleHostRegister(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	token := params["token"]
+	logger.Log(0, "received registration attempt with token", token)
+	// check if token exists
+	enrollmentKey, err := logic.DeTokenize(token)
+	if err != nil {
+		logger.Log(0, "invalid enrollment key used", token, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// get the host
+	var newHost models.Host
+	if err = json.NewDecoder(r.Body).Decode(&newHost); err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	hostExists := false
+	// check if host already exists
+	if hostExists = logic.HostExists(&newHost); hostExists && len(enrollmentKey.Networks) == 0 {
+		logger.Log(0, "host", newHost.ID.String(), newHost.Name, "attempted to re-register with no networks")
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("host already exists"), "badrequest"))
+		return
+	}
+	// version check
+	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
+	}
+	key, keyErr := logic.RetrievePublicTrafficKey()
+	if keyErr != nil {
+		logger.Log(0, "error retrieving key:", keyErr.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// use the token
+	if ok := logic.TryToUseEnrollmentKey(enrollmentKey); !ok {
+		logger.Log(0, "host", newHost.ID.String(), newHost.Name, "failed registration")
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("invalid enrollment key"), "badrequest"))
+		return
+	}
+	if !hostExists {
+		// register host
+		logic.CheckHostPorts(&newHost)
+		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"))
+			return
+		}
+	} else {
+		// need to revise the list of networks from key
+		// based on the ones host currently has
+		var networksToAdd = []string{}
+		currentNets := logic.GetHostNetworks(newHost.ID.String())
+		for _, newNet := range enrollmentKey.Networks {
+			if !logic.StringSliceContains(currentNets, newNet) {
+				networksToAdd = append(networksToAdd, newNet)
+			}
+		}
+		enrollmentKey.Networks = networksToAdd
+	}
+	// ready the response
+	server := servercfg.GetServerInfo()
+	server.TrafficKey = key
+	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(&response)
+	// notify host of changes, peer and node updates
+	go checkNetRegAndHostUpdate(enrollmentKey.Networks, &newHost)
+}
+
+// run through networks and send a host update
+func checkNetRegAndHostUpdate(networks []string, h *models.Host) {
+	// publish host update through MQ
+	for i := range networks {
+		network := networks[i]
+		if ok, _ := logic.NetworkExists(network); ok {
+			newNode, err := logic.UpdateHostNetwork(h, network, true)
+			if err != nil {
+				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
+				continue
+			}
+			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
+			hostactions.AddAction(models.HostUpdate{
+				Action: models.JoinHostToNetwork,
+				Host:   *h,
+				Node:   *newNode,
+			})
+		}
+	}
+	if servercfg.IsMessageQueueBackend() {
+		mq.HostUpdate(&models.HostUpdate{
+			Action: models.RequestAck,
+			Host:   *h,
+		})
+		if err := mq.PublishPeerUpdate(); err != nil {
+			logger.Log(0, "failed to publish peer update during registration -", err.Error())
+		}
+	}
+}

+ 26 - 15
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) {
@@ -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 {

+ 9 - 8
controllers/hosts.go

@@ -10,8 +10,10 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/crypto/bcrypt"
 )
 
@@ -230,18 +232,17 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	logger.Log(1, "added new node", newNode.ID.String(), "to host", currHost.Name)
-	if err = mq.HostUpdate(&models.HostUpdate{
+	hostactions.AddAction(models.HostUpdate{
 		Action: models.JoinHostToNetwork,
 		Host:   *currHost,
 		Node:   *newNode,
-	}); err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to update host to join network:", hostid, network, err.Error())
+	})
+	if servercfg.IsMessageQueueBackend() {
+		mq.HostUpdate(&models.HostUpdate{
+			Action: models.RequestAck,
+			Host:   *currHost,
+		})
 	}
-	go func() { // notify of peer change
-		if err := mq.PublishPeerUpdate(); err != nil {
-			logger.Log(1, "error publishing peer update ", err.Error())
-		}
-	}()
 
 	logger.Log(2, r.Header.Get("user"), fmt.Sprintf("added host %s to network %s", currHost.Name, network))
 	w.WriteHeader(http.StatusOK)

+ 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")
+}

+ 46 - 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,56 @@ 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) {
+		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)
 }

+ 3 - 4
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) {
@@ -325,10 +327,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() {

+ 28 - 12
controllers/node.go

@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -25,7 +26,6 @@ func nodeHandlers(r *mux.Router) {
 	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)
@@ -36,6 +36,7 @@ func nodeHandlers(r *mux.Router) {
 	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
@@ -433,7 +434,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))
@@ -508,7 +509,7 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if !logic.IsVersionComptatible(data.Host.Version) {
-		err := errors.New("incompatible netclient version")
+		err := errors.New("bad netclient version on node create: " + data.Host.Version)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
@@ -564,6 +565,13 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 	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) {
@@ -589,7 +597,6 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-
 	err = logic.AssociateNodeToHost(&data.Node, &data.Host)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
@@ -616,7 +623,7 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	hostPeerUpdate, err := logic.GetPeerUpdateForHost(networkName, &data.Host, nil)
+	hostPeerUpdate, err := logic.GetPeerUpdateForHost(context.Background(), networkName, &data.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", data.Host.ID.String(), err))
@@ -631,7 +638,7 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 		Host:         data.Host,
 		Peers:        hostPeerUpdate.Peers,
 	}
-	logger.Log(1, r.Header.Get("user"), "created new node", data.Host.Name, "on network", networkName)
+	logger.Log(1, r.Header.Get("user"), "created new node", data.Host.Name, data.Node.ID.String(), "on network", networkName)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(response)
 
@@ -902,7 +909,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		relayedUpdate = true
 	}
 	ifaceDelta := logic.IfaceDelta(&currentNode, newNode)
-
+	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 	if ifaceDelta && servercfg.Is_EE {
 		if err = logic.EnterpriseResetAllPeersFailovers(currentNode.ID, currentNode.Network); err != nil {
 			logger.Log(0, "failed to reset failover lists during node update for node", currentNode.ID.String(), currentNode.Network)
@@ -935,13 +942,17 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	logger.Log(1, r.Header.Get("user"), "updated node", currentNode.ID.String(), "on network", currentNode.Network)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
-
 	runUpdates(newNode, ifaceDelta)
-	go func() {
+	go func(aclUpdate bool, newNode *models.Node) {
+		if aclUpdate {
+			if err := mq.PublishPeerUpdate(); err != nil {
+				logger.Log(0, "error during node ACL update for node", newNode.ID.String())
+			}
+		}
 		if err := mq.PublishReplaceDNS(&currentNode, newNode, host); err != nil {
 			logger.Log(1, "failed to publish dns update", err.Error())
 		}
-	}()
+	}(aclUpdate, newNode)
 }
 
 // swagger:route DELETE /api/nodes/{network}/{nodeid} nodes deleteNode
@@ -965,8 +976,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 {

+ 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)
 		}

+ 6 - 0
database/database.go

@@ -57,6 +57,10 @@ const (
 	CACHE_TABLE_NAME = "cache"
 	// HOSTS_TABLE_NAME - the table name for hosts
 	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
@@ -138,6 +142,8 @@ func createTables() {
 	createTable(USER_GROUPS_TABLE_NAME)
 	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

@@ -38,5 +38,5 @@ https://stun.NETMAKER_BASE_DOMAIN {
 
 # MQ
 wss://broker.NETMAKER_BASE_DOMAIN {
-        reverse_proxy ws://mq:8883
+        reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083`
 }

+ 3 - 0
ee/initialize.go

@@ -40,6 +40,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 setControllerLimits() {

+ 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
+}

+ 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 {

+ 9 - 8
go.mod

@@ -5,7 +5,7 @@ go 1.19
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.2
 	github.com/go-playground/validator/v10 v10.11.2
-	github.com/golang-jwt/jwt/v4 v4.4.3
+	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
@@ -13,13 +13,13 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.16
 	github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.8.1
+	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.4.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
@@ -42,7 +42,7 @@ require (
 
 require (
 	github.com/guumaster/tablewriter v0.0.10
-	github.com/matryer/is v1.4.0
+	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/cobra v1.6.1
 )
@@ -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
 )

+ 24 - 19
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=
@@ -35,8 +36,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
 github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
-github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
-github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
@@ -68,17 +69,19 @@ 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=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
-github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
@@ -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=
@@ -134,8 +139,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/txn2/txeh v1.3.0 h1:vnbv63htVMZCaQgLqVBxKvj2+HHHFUzNW7I183zjg3E=
 github.com/txn2/txeh v1.3.0/go.mod h1:O7M6gUTPeMF+vsa4c4Ipx3JDkOYrruB1Wry8QRsMcw8=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@@ -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.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
-golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
+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.4
         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.4
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

+ 5 - 7
k8s/server/netmaker-server.yaml

@@ -71,17 +71,15 @@ spec:
           value: REPLACE_MASTER_KEY
         - name: CORS_ALLOWED_ORIGIN
           value: '*'
-        - name: MQ_HOST
-          value: "mq"
-        - name: MQ_PORT
-          value: "443"
-        - name: MQ_SERVER_PORT
-          value: "1883"
+        - name: SERVER_BROKER_ENDPOINT
+          value: "ws://mq:1883"
+        - name: BROKER_ENDPOINT
+          value: "wss://broker.NETMAKER_BASE_DOMAIN"
         - name: PLATFORM
           value: "Kubernetes"
         - name: VERBOSITY
           value: "3"
-        image: gravitl/netmaker:v0.18.2
+        image: gravitl/netmaker:v0.18.4
         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.4
         ports:
         - containerPort: 443
         env:

+ 0 - 2
logic/accesskeys_test.go

@@ -5,7 +5,6 @@ import "testing"
 func Test_genKeyName(t *testing.T) {
 	for i := 0; i < 100; i++ {
 		kname := genKeyName()
-		t.Log(kname)
 		if len(kname) != 20 {
 			t.Fatalf("improper length of key name, expected 20 got :%d", len(kname))
 		}
@@ -15,7 +14,6 @@ func Test_genKeyName(t *testing.T) {
 func Test_genKey(t *testing.T) {
 	for i := 0; i < 100; i++ {
 		kname := GenKey()
-		t.Log(kname)
 		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)
+}

+ 221 - 0
logic/enrollmentkey.go

@@ -0,0 +1,221 @@
+package logic
+
+import (
+	b64 "encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+// EnrollmentErrors - struct for holding EnrollmentKey error messages
+var EnrollmentErrors = struct {
+	InvalidCreate      error
+	NoKeyFound         error
+	InvalidKey         error
+	NoUsesRemaining    error
+	FailedToTokenize   error
+	FailedToDeTokenize error
+}{
+	InvalidCreate:      fmt.Errorf("invalid enrollment key created"),
+	NoKeyFound:         fmt.Errorf("no enrollmentkey found"),
+	InvalidKey:         fmt.Errorf("invalid key provided"),
+	NoUsesRemaining:    fmt.Errorf("no uses remaining"),
+	FailedToTokenize:   fmt.Errorf("failed to tokenize"),
+	FailedToDeTokenize: fmt.Errorf("failed to detokenize"),
+}
+
+// CreateEnrollmentKey - creates a new enrollment key in db
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool) (k *models.EnrollmentKey, err error) {
+	newKeyID, err := getUniqueEnrollmentID()
+	if err != nil {
+		return nil, err
+	}
+	k = &models.EnrollmentKey{
+		Value:         newKeyID,
+		Expiration:    time.Time{},
+		UsesRemaining: 0,
+		Unlimited:     unlimited,
+		Networks:      []string{},
+		Tags:          []string{},
+	}
+	if uses > 0 {
+		k.UsesRemaining = uses
+	}
+	if !expiration.IsZero() {
+		k.Expiration = expiration
+	}
+	if len(networks) > 0 {
+		k.Networks = networks
+	}
+	if len(tags) > 0 {
+		k.Tags = tags
+	}
+	if ok := k.Validate(); !ok {
+		return nil, EnrollmentErrors.InvalidCreate
+	}
+	if err = upsertEnrollmentKey(k); err != nil {
+		return nil, err
+	}
+	return
+}
+
+// GetAllEnrollmentKeys - fetches all enrollment keys from DB
+func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
+	currentKeys, err := getEnrollmentKeysMap()
+	if err != nil {
+		return nil, err
+	}
+	var currentKeysList = []*models.EnrollmentKey{}
+	for k := range currentKeys {
+		currentKeysList = append(currentKeysList, currentKeys[k])
+	}
+	return currentKeysList, nil
+}
+
+// GetEnrollmentKey - fetches a single enrollment key
+// returns nil and error if not found
+func GetEnrollmentKey(value string) (*models.EnrollmentKey, error) {
+	currentKeys, err := getEnrollmentKeysMap()
+	if err != nil {
+		return nil, err
+	}
+	if key, ok := currentKeys[value]; ok {
+		return key, nil
+	}
+	return nil, EnrollmentErrors.NoKeyFound
+}
+
+// DeleteEnrollmentKey - delete's a given enrollment key by value
+func DeleteEnrollmentKey(value string) error {
+	_, err := GetEnrollmentKey(value)
+	if err != nil {
+		return err
+	}
+	return database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value)
+}
+
+// TryToUseEnrollmentKey - checks first if key can be decremented
+// returns true if it is decremented or isvalid
+func TryToUseEnrollmentKey(k *models.EnrollmentKey) bool {
+	key, err := decrementEnrollmentKey(k.Value)
+	if err != nil {
+		if errors.Is(err, EnrollmentErrors.NoUsesRemaining) {
+			return k.IsValid()
+		}
+	} else {
+		k.UsesRemaining = key.UsesRemaining
+		return true
+	}
+	return false
+}
+
+// Tokenize - tokenizes an enrollment key to be used via registration
+// and attaches it to the Token field on the struct
+func Tokenize(k *models.EnrollmentKey, serverAddr string) error {
+	if len(serverAddr) == 0 || k == nil {
+		return EnrollmentErrors.FailedToTokenize
+	}
+	newToken := models.EnrollmentToken{
+		Server: serverAddr,
+		Value:  k.Value,
+	}
+	data, err := json.Marshal(&newToken)
+	if err != nil {
+		return err
+	}
+	k.Token = b64.StdEncoding.EncodeToString(data)
+	return nil
+}
+
+// DeTokenize - detokenizes a base64 encoded string
+// and finds the associated enrollment key
+func DeTokenize(b64Token string) (*models.EnrollmentKey, error) {
+	if len(b64Token) == 0 {
+		return nil, EnrollmentErrors.FailedToDeTokenize
+	}
+	tokenData, err := b64.StdEncoding.DecodeString(b64Token)
+	if err != nil {
+		return nil, err
+	}
+
+	var newToken models.EnrollmentToken
+	err = json.Unmarshal(tokenData, &newToken)
+	if err != nil {
+		return nil, err
+	}
+	k, err := GetEnrollmentKey(newToken.Value)
+	if err != nil {
+		return nil, err
+	}
+	return k, nil
+}
+
+// == private ==
+
+// decrementEnrollmentKey - decrements the uses on a key if above 0 remaining
+func decrementEnrollmentKey(value string) (*models.EnrollmentKey, error) {
+	k, err := GetEnrollmentKey(value)
+	if err != nil {
+		return nil, err
+	}
+	if k.UsesRemaining == 0 {
+		return nil, EnrollmentErrors.NoUsesRemaining
+	}
+	k.UsesRemaining = k.UsesRemaining - 1
+	if err = upsertEnrollmentKey(k); err != nil {
+		return nil, err
+	}
+
+	return k, nil
+}
+
+func upsertEnrollmentKey(k *models.EnrollmentKey) error {
+	if k == nil {
+		return EnrollmentErrors.InvalidKey
+	}
+	data, err := json.Marshal(k)
+	if err != nil {
+		return err
+	}
+	return database.Insert(k.Value, string(data), database.ENROLLMENT_KEYS_TABLE_NAME)
+}
+
+func getUniqueEnrollmentID() (string, error) {
+	currentKeys, err := getEnrollmentKeysMap()
+	if err != nil {
+		return "", err
+	}
+	newID := ncutils.MakeRandomString(models.EnrollmentKeyLength)
+	for _, ok := currentKeys[newID]; ok; {
+		newID = ncutils.MakeRandomString(models.EnrollmentKeyLength)
+	}
+	return newID, nil
+}
+
+func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
+	records, err := database.FetchRecords(database.ENROLLMENT_KEYS_TABLE_NAME)
+	if err != nil {
+		if !database.IsEmptyRecord(err) {
+			return nil, err
+		}
+	}
+	if records == nil {
+		records = make(map[string]string)
+	}
+	currentKeys := make(map[string]*models.EnrollmentKey, 0)
+	if len(records) > 0 {
+		for k := range records {
+			var currentKey models.EnrollmentKey
+			if err = json.Unmarshal([]byte(records[k]), &currentKey); err != nil {
+				continue
+			}
+			currentKeys[k] = &currentKey
+		}
+	}
+	return currentKeys, nil
+}

+ 206 - 0
logic/enrollmentkey_test.go

@@ -0,0 +1,206 @@
+package logic
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCreateEnrollmentKey(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	t.Run("Can_Not_Create_Key", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, false)
+		assert.Nil(t, newKey)
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.InvalidCreate)
+	})
+	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
+		assert.Nil(t, err)
+		assert.Equal(t, 1, newKey.UsesRemaining)
+		assert.True(t, newKey.IsValid())
+	})
+	t.Run("Can_Create_Key_Time", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, false)
+		assert.Nil(t, err)
+		assert.True(t, newKey.IsValid())
+	})
+	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, true)
+		assert.Nil(t, err)
+		assert.True(t, newKey.IsValid())
+	})
+	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+		assert.Nil(t, err)
+		assert.True(t, newKey.IsValid())
+		assert.True(t, len(newKey.Networks) == 2)
+	})
+	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, true)
+		assert.Nil(t, err)
+		assert.True(t, newKey.IsValid())
+		assert.True(t, len(newKey.Tags) == 2)
+	})
+
+	t.Run("Can_Get_List_of_Keys", func(t *testing.T) {
+		keys, err := GetAllEnrollmentKeys()
+		assert.Nil(t, err)
+		assert.True(t, len(keys) > 0)
+		for i := range keys {
+			assert.Equal(t, len(keys[i].Value), models.EnrollmentKeyLength)
+		}
+	})
+	removeAllEnrollments()
+}
+
+func TestDelete_EnrollmentKey(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	t.Run("Can_Delete_Key", func(t *testing.T) {
+		assert.True(t, newKey.IsValid())
+		err := DeleteEnrollmentKey(newKey.Value)
+		assert.Nil(t, err)
+		oldKey, err := GetEnrollmentKey(newKey.Value)
+		assert.Nil(t, oldKey)
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
+	})
+	t.Run("Can_Not_Delete_Invalid_Key", func(t *testing.T) {
+		err := DeleteEnrollmentKey("notakey")
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
+	})
+	removeAllEnrollments()
+}
+
+func TestDecrement_EnrollmentKey(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
+	t.Run("Check_initial_uses", func(t *testing.T) {
+		assert.True(t, newKey.IsValid())
+		assert.Equal(t, newKey.UsesRemaining, 1)
+	})
+	t.Run("Check can decrement", func(t *testing.T) {
+		assert.Equal(t, newKey.UsesRemaining, 1)
+		k, err := decrementEnrollmentKey(newKey.Value)
+		assert.Nil(t, err)
+		newKey = k
+	})
+	t.Run("Check can not decrement", func(t *testing.T) {
+		assert.Equal(t, newKey.UsesRemaining, 0)
+		_, err := decrementEnrollmentKey(newKey.Value)
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.NoUsesRemaining)
+	})
+
+	removeAllEnrollments()
+}
+
+func TestUsability_EnrollmentKey(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true)
+	t.Run("Check if valid use key can be used", func(t *testing.T) {
+		assert.Equal(t, key1.UsesRemaining, 1)
+		ok := TryToUseEnrollmentKey(key1)
+		assert.True(t, ok)
+		assert.Equal(t, 0, key1.UsesRemaining)
+	})
+
+	t.Run("Check if valid time key can be used", func(t *testing.T) {
+		assert.True(t, !key2.Expiration.IsZero())
+		ok := TryToUseEnrollmentKey(key2)
+		assert.True(t, ok)
+	})
+
+	t.Run("Check if valid unlimited key can be used", func(t *testing.T) {
+		assert.True(t, key3.Unlimited)
+		ok := TryToUseEnrollmentKey(key3)
+		assert.True(t, ok)
+	})
+
+	t.Run("check invalid key can not be used", func(t *testing.T) {
+		ok := TryToUseEnrollmentKey(key1)
+		assert.False(t, ok)
+	})
+}
+
+func removeAllEnrollments() {
+	database.DeleteAllRecords(database.ENROLLMENT_KEYS_TABLE_NAME)
+}
+
+//Test that cheks if it can tokenize
+//Test that cheks if it can't tokenize
+
+func TestTokenize_EnrollmentKeys(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
+	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
+	const serverAddr = "api.myserver.com"
+	t.Run("Can_Not_Tokenize_Nil_Key", func(t *testing.T) {
+		err := Tokenize(nil, "ServerAddress")
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.FailedToTokenize)
+	})
+	t.Run("Can_Not_Tokenize_Empty_Server_Address", func(t *testing.T) {
+		err := Tokenize(newKey, "")
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.FailedToTokenize)
+	})
+
+	t.Run("Can_Tokenize", func(t *testing.T) {
+		err := Tokenize(newKey, serverAddr)
+		assert.Nil(t, err)
+		assert.True(t, len(newKey.Token) > 0)
+	})
+
+	t.Run("Is_Correct_B64_Token", func(t *testing.T) {
+		newKey.Value = defaultValue
+		err := Tokenize(newKey, serverAddr)
+		assert.Nil(t, err)
+		assert.Equal(t, newKey.Token, b64value)
+	})
+	removeAllEnrollments()
+}
+
+func TestDeTokenize_EnrollmentKeys(t *testing.T) {
+	database.InitializeDatabase()
+	defer database.CloseDB()
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true)
+	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
+	const serverAddr = "api.myserver.com"
+
+	t.Run("Can_Not_DeTokenize", func(t *testing.T) {
+		value, err := DeTokenize("")
+		assert.Nil(t, value)
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.FailedToDeTokenize)
+	})
+	t.Run("Can_Not_Find_Key", func(t *testing.T) {
+		value, err := DeTokenize(b64Value)
+		assert.Nil(t, value)
+		assert.NotNil(t, err)
+		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
+	})
+	t.Run("Can_DeTokenize", func(t *testing.T) {
+		err := Tokenize(newKey, serverAddr)
+		assert.Nil(t, err)
+		output, err := DeTokenize(newKey.Token)
+		assert.Nil(t, err)
+		assert.NotNil(t, output)
+		assert.Equal(t, newKey.Value, output.Value)
+	})
+
+	removeAllEnrollments()
+}

+ 25 - 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,17 @@ 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
 	}
 	client.ClientID = newclientid
 	client.Enabled = enabled
-	CreateExtClient(client)
+	SetClientACLs(client, newACLs)
+	if err = CreateExtClient(client); err != nil {
+		return client, err
+	}
 	return client, err
 }
 

+ 20 - 10
logic/host_test.go

@@ -2,12 +2,13 @@ package logic
 
 import (
 	"context"
+	"fmt"
 	"net"
+	"os"
 	"testing"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/matryer/is"
 )
@@ -15,21 +16,16 @@ import (
 func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	CreateAdmin(&models.User{
-		UserName: "admin",
-		Password: "password",
-		IsAdmin:  true,
-		Networks: []string{},
-		Groups:   []string{},
-	})
 	peerUpdate := make(chan *models.Node)
 	go ManageZombies(context.Background(), peerUpdate)
 	go func() {
-		for update := range peerUpdate {
+		for y := range peerUpdate {
+			fmt.Printf("Pointless %v\n", y)
 			//do nothing
-			logger.Log(3, "received node update", update.Action)
 		}
 	}()
+
+	os.Exit(m.Run())
 }
 
 func TestCheckPorts(t *testing.T) {
@@ -45,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)
 	})
@@ -56,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)
 	})
@@ -63,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)
 	})
@@ -70,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)
 	})
@@ -77,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)
 	})

+ 56 - 0
logic/hostactions/hostactions.go

@@ -0,0 +1,56 @@
+package hostactions
+
+import (
+	"encoding/json"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+)
+
+// AddAction - adds a host action to a host's list to be retrieved from broker update
+func AddAction(hu models.HostUpdate) {
+	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
+func GetAction(id string) *models.HostUpdate {
+	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
+	}
+	if len(currentList) > 0 {
+		hu := currentList[0]
+		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
+}

+ 39 - 11
logic/hosts.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
@@ -90,12 +91,21 @@ func CreateHost(h *models.Host) error {
 	if (err != nil && !database.IsEmptyRecord(err)) || (err == nil) {
 		return ErrHostExists
 	}
-	//encrypt that password so we never see it
+	// encrypt that password so we never see it
 	hash, err := bcrypt.GenerateFromPassword([]byte(h.HostPass), 5)
 	if err != nil {
 		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)
 }
@@ -208,7 +218,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 {
@@ -236,7 +245,12 @@ func AssociateNodeToHost(n *models.Node, h *models.Host) error {
 	if err != nil {
 		return err
 	}
-	h.Nodes = append(h.Nodes, n.ID.String())
+	currentHost, err := GetHost(h.ID.String())
+	if err != nil {
+		return err
+	}
+	h.HostPass = currentHost.HostPass
+	h.Nodes = append(currentHost.Nodes, n.ID.String())
 	return UpsertHost(h)
 }
 
@@ -357,13 +371,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
 		}
@@ -375,12 +389,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
+		}
 	}
 }
 
@@ -390,9 +410,17 @@ func HostExists(h *models.Host) bool {
 	return (err != nil && !database.IsEmptyRecord(err)) || (err == nil)
 }
 
-func updatePort(p *int) {
-	*p++
-	if *p > maxPort {
-		*p = minPort
+// GetHostByNodeID - returns a host if found to have a node's ID, else nil
+func GetHostByNodeID(id string) *models.Host {
+	hosts, err := GetAllHosts()
+	if err != nil {
+		return nil
+	}
+	for i := range hosts {
+		h := hosts[i]
+		if StringSliceContains(h.Nodes, id) {
+			return &h
+		}
 	}
+	return nil
 }

+ 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
 		}

+ 19 - 34
logic/nodes.go

@@ -1,7 +1,6 @@
 package logic
 
 import (
-	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -33,17 +32,24 @@ 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
 }
 
 // UpdateNode - takes a node and updates another node with it's values
@@ -84,8 +90,9 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 
 // DeleteNode - marks node for deletion (and adds to zombie list) if called by UI or deletes node if called by node
 func DeleteNode(node *models.Node, purge bool) error {
+	alreadyDeleted := node.PendingDelete || node.Action == models.NODE_DELETE
 	node.Action = models.NODE_DELETE
-	if !purge {
+	if !purge && !alreadyDeleted {
 		newnode := *node
 		newnode.PendingDelete = true
 		if err := UpdateNode(node, &newnode); err != nil {
@@ -94,8 +101,15 @@ func DeleteNode(node *models.Node, purge bool) error {
 		newZombie <- node.ID
 		return nil
 	}
+	if alreadyDeleted {
+		logger.Log(1, "forcibly deleting node", node.ID.String())
+	}
 	host, err := GetHost(node.HostID.String())
 	if err != nil {
+		logger.Log(1, "no host found for node", node.ID.String(), "deleting..")
+		if delErr := deleteNodeByID(node); delErr != nil {
+			logger.Log(0, "failed to delete node", node.ID.String(), delErr.Error())
+		}
 		return err
 	}
 	if err := DissasociateNodeFromHost(node, host); err != nil {
@@ -421,35 +435,6 @@ func updateProNodeACLS(node *models.Node) error {
 	return nil
 }
 
-func PurgePendingNodes(ctx context.Context) {
-	ticker := time.NewTicker(NodePurgeCheckTime)
-	defer ticker.Stop()
-	for {
-		select {
-		case <-ctx.Done():
-			return
-		case <-ticker.C:
-			nodes, err := GetAllNodes()
-			if err != nil {
-				logger.Log(0, "PurgePendingNodes failed to retrieve nodes", err.Error())
-				continue
-			}
-			for _, node := range nodes {
-				if node.PendingDelete {
-					modified := node.LastModified
-					if time.Since(modified) > NodePurgeTime {
-						if err := DeleteNode(&node, true); err != nil {
-							logger.Log(0, "failed to purge node", node.ID.String(), err.Error())
-						} else {
-							logger.Log(0, "purged node ", node.ID.String())
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
 // createNode - creates a node in database
 func createNode(node *models.Node) error {
 	host, err := GetHost(node.HostID.String())

+ 282 - 205
logic/peers.go

@@ -1,9 +1,9 @@
 package logic
 
 import (
+	"context"
 	"errors"
 	"fmt"
-	"log"
 	"net"
 	"net/netip"
 
@@ -16,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,
 	}
@@ -34,14 +41,13 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 		} else {
 			logger.Log(0, "couldn't find relay host for:  ", host.ID.String())
 		}
-
 	}
 	if host.IsRelay {
 		relayedHosts := GetRelayedHosts(host)
 		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 {
@@ -65,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
@@ -74,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
@@ -84,6 +96,7 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 				currPeerConf = models.PeerConf{
 					Proxy:            peerHost.ProxyEnabled,
 					PublicListenPort: int32(GetPeerListenPort(peerHost)),
+					ProxyListenPort:  GetProxyListenPort(peerHost),
 				}
 			}
 
@@ -117,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{
@@ -133,18 +159,17 @@ 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{},
 	}
-	var deletedNodes = []models.Node{} // used to track deleted nodes
-	if deletedNode != nil {
-		deletedNodes = append(deletedNodes, *deletedNode)
-	}
-	logger.Log(1, "peer update for host ", host.ID.String())
+
+	logger.Log(1, "peer update for host", host.ID.String())
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {
+		nodeID := nodeID
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
 			continue
@@ -152,230 +177,246 @@ 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 {
-			log.Println("no network nodes")
-			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 == node.ID {
-				logger.Log(2, "peer update, skipping self")
-				//skip yourself
-				continue
-			}
-			if peer.Action == models.NODE_DELETE || peer.PendingDelete {
-				deletedNodes = append(deletedNodes, peer) // track deleted node for peer update
-				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
-			}
-
-			if !peer.Connected {
-				logger.Log(2, "peer update, skipping unconnected node", peer.ID.String())
-				//skip unconnected nodes
-				continue
-			}
-			if !nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) {
-				log.Println("peer update, skipping node for acl")
-				//skip if not permitted by acl
-				continue
-			}
-			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)...)
-			}
-			peerConfig.AllowedIPs = allowedips
-			if node.IsIngressGateway || node.IsEgressGateway {
+				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 {
-					_, 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,
+						}
 
-	// run through delete nodes
-	if len(deletedNodes) > 0 {
-		for i := range deletedNodes {
-			delNode := deletedNodes[i]
-			delHost, err := GetHost(delNode.HostID.String())
-			if err != nil {
-				continue
+						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 _, ok := hostPeerUpdate.HostPeerIDs[delHost.PublicKey.String()]; !ok {
-				var peerConfig = wgtypes.PeerConfig{}
-				peerConfig.PublicKey = delHost.PublicKey
-				peerConfig.Endpoint = &net.UDPAddr{
-					IP:   delHost.EndpointIP,
-					Port: GetPeerListenPort(delHost),
+			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,
 				}
-				peerConfig.Remove = true
-				peerConfig.AllowedIPs = []net.IPNet{delNode.Address, delNode.Address6}
-				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
 			}
 		}
 	}
+	// == post peer calculations ==
+	// indicate removal if no allowed IPs were calculated
+	for i := range hostPeerUpdate.Peers {
+		peer := hostPeerUpdate.Peers[i]
+		if len(peer.AllowedIPs) == 0 {
+			peer.Remove = true
+		}
+		hostPeerUpdate.Peers[i] = peer
+	}
+
+	for i := range hostPeerUpdate.NodePeers {
+		peer := hostPeerUpdate.NodePeers[i]
+		if len(peer.AllowedIPs) == 0 {
+			peer.Remove = true
+		}
+		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
 }
@@ -393,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
@@ -405,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())
@@ -636,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"
@@ -84,7 +85,7 @@ func fetchTelemetryData() (telemetryData, error) {
 	data.Users = getDBLength(database.USERS_TABLE_NAME)
 	data.Networks = getDBLength(database.NETWORKS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
-	//data.Servers = GetServerCount()
+	data.Servers = getServerCount()
 	nodes, err := GetAllNodes()
 	if err == nil {
 		data.Nodes = len(nodes)
@@ -93,6 +94,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()

+ 25 - 38
main.go

@@ -27,7 +27,7 @@ import (
 	stunserver "github.com/gravitl/netmaker/stun-server"
 )
 
-var version = "v0.18.2"
+var version = "v0.18.4"
 
 // Start DB Connection and start API Request Handler
 func main() {
@@ -36,12 +36,16 @@ func main() {
 	setupConfig(*absoluteConfigPath)
 	servercfg.SetVersion(version)
 	fmt.Println(models.RetrieveLogo()) // print the logo
-	// fmt.Println(models.ProLogo())
-	initialize() // initial db and acls; gen cert if required
+	initialize()                       // initial db and acls
 	setGarbageCollection()
 	setVerbosity()
 	defer database.CloseDB()
-	startControllers() // start the api endpoint and mq
+	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
+	defer stop()
+	var waitGroup sync.WaitGroup
+	startControllers(&waitGroup, ctx) // start the api endpoint and mq and stun
+	<-ctx.Done()
+	waitGroup.Wait()
 }
 
 func setupConfig(absoluteConfigPath string) {
@@ -110,8 +114,7 @@ func initialize() { // Client Mode Prereq Check
 	}
 }
 
-func startControllers() {
-	var waitnetwork sync.WaitGroup
+func startControllers(wg *sync.WaitGroup, ctx context.Context) {
 	if servercfg.IsDNSMode() {
 		err := logic.SetDNS()
 		if err != nil {
@@ -127,13 +130,13 @@ func startControllers() {
 				logger.FatalLog("Unable to Set host. Exiting...", err.Error())
 			}
 		}
-		waitnetwork.Add(1)
-		go controller.HandleRESTRequests(&waitnetwork)
+		wg.Add(1)
+		go controller.HandleRESTRequests(wg, ctx)
 	}
 	//Run MessageQueue
 	if servercfg.IsMessageQueueBackend() {
-		waitnetwork.Add(1)
-		go runMessageQueue(&waitnetwork)
+		wg.Add(1)
+		go runMessageQueue(wg, ctx)
 	}
 
 	if !servercfg.IsRestBackend() && !servercfg.IsMessageQueueBackend() {
@@ -141,34 +144,22 @@ func startControllers() {
 	}
 
 	// starts the stun server
-	waitnetwork.Add(1)
-	go stunserver.Start(&waitnetwork)
-	if servercfg.IsProxyEnabled() {
-
-		waitnetwork.Add(1)
-		go func() {
-			defer waitnetwork.Done()
-			_, cancel := context.WithCancel(context.Background())
-			waitnetwork.Add(1)
-
-			//go nmproxy.Start(ctx, logic.ProxyMgmChan, servercfg.GetAPIHost())
-			quit := make(chan os.Signal, 1)
-			signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
-			<-quit
-			cancel()
-		}()
-	}
-
-	waitnetwork.Wait()
+	wg.Add(1)
+	go stunserver.Start(wg, ctx)
 }
 
 // Should we be using a context vice a waitgroup????????????
-func runMessageQueue(wg *sync.WaitGroup) {
+func runMessageQueue(wg *sync.WaitGroup, ctx context.Context) {
 	defer wg.Done()
-	brokerHost, secure := servercfg.GetMessageQueueEndpoint()
-	logger.Log(0, "connecting to mq broker at", brokerHost, "with TLS?", fmt.Sprintf("%v", secure))
+	brokerHost, _ := servercfg.GetMessageQueueEndpoint()
+	logger.Log(0, "connecting to mq broker at", brokerHost)
 	mq.SetupMQTT()
-	ctx, cancel := context.WithCancel(context.Background())
+	if mq.IsConnected() {
+		logger.Log(0, "connected to MQ Broker")
+	} else {
+		logger.FatalLog("error connecting to MQ Broker")
+	}
+	defer mq.CloseClient()
 	go mq.Keepalive(ctx)
 	go func() {
 		peerUpdate := make(chan *models.Node)
@@ -179,11 +170,7 @@ func runMessageQueue(wg *sync.WaitGroup) {
 			}
 		}
 	}()
-	go logic.PurgePendingNodes(ctx)
-	quit := make(chan os.Signal, 1)
-	signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
-	<-quit
-	cancel()
+	<-ctx.Done()
 	logger.Log(0, "Message Queue shutting down")
 }
 

+ 0 - 3
models/api_node.go

@@ -32,7 +32,6 @@ type ApiNode struct {
 	RelayAddrs              []string `json:"relayaddrs"`
 	FailoverNode            string   `json:"failovernode"`
 	DNSOn                   bool     `json:"dnson"`
-	IsLocal                 bool     `json:"islocal"`
 	Server                  string   `json:"server"`
 	InternetGateway         string   `json:"internetgateway"`
 	Connected               bool     `json:"connected"`
@@ -51,7 +50,6 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.Connected = a.Connected
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
-	convertedNode.IsLocal = a.IsLocal
 	convertedNode.IsRelay = a.IsRelay
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.PendingDelete = a.PendingDelete
@@ -150,7 +148,6 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 		apiNode.FailoverNode = ""
 	}
 	apiNode.DNSOn = nm.DNSOn
-	apiNode.IsLocal = nm.IsLocal
 	apiNode.Server = nm.Server
 	apiNode.InternetGateway = nm.InternetGateway.String()
 	if isEmptyAddr(apiNode.InternetGateway) {

+ 65 - 0
models/enrollment_key.go

@@ -0,0 +1,65 @@
+package models
+
+import (
+	"time"
+)
+
+// EnrollmentToken - the tokenized version of an enrollmentkey;
+// to be used for host registration
+type EnrollmentToken struct {
+	Server string `json:"server"`
+	Value  string `json:"value"`
+}
+
+// EnrollmentKeyLength - the length of an enrollment key - 62^16 unique possibilities
+const EnrollmentKeyLength = 32
+
+// EnrollmentKey - the key used to register hosts and join them to specific networks
+type EnrollmentKey struct {
+	Expiration    time.Time `json:"expiration"`
+	UsesRemaining int       `json:"uses_remaining"`
+	Value         string    `json:"value"`
+	Networks      []string  `json:"networks"`
+	Unlimited     bool      `json:"unlimited"`
+	Tags          []string  `json:"tags"`
+	Token         string    `json:"token,omitempty"` // B64 value of EnrollmentToken
+}
+
+// APIEnrollmentKey - used to create enrollment keys via API
+type APIEnrollmentKey struct {
+	Expiration    int64    `json:"expiration"`
+	UsesRemaining int      `json:"uses_remaining"`
+	Networks      []string `json:"networks"`
+	Unlimited     bool     `json:"unlimited"`
+	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 {
+		return false
+	}
+	if k.UsesRemaining > 0 {
+		return true
+	}
+	if !k.Expiration.IsZero() && time.Now().Before(k.Expiration) {
+		return true
+	}
+
+	return k.Unlimited
+}
+
+// EnrollmentKey.Validate - validate's an EnrollmentKey
+// should be used during creation
+func (k *EnrollmentKey) Validate() bool {
+	return k.Networks != nil &&
+		k.Tags != nil &&
+		len(k.Value) == EnrollmentKeyLength &&
+		k.IsValid()
+}

+ 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"`
 }

+ 22 - 2
models/host.go

@@ -7,6 +7,21 @@ import (
 	"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",
+}
+
 // WIREGUARD_INTERFACE name of wireguard interface
 const WIREGUARD_INTERFACE = "netmaker"
 
@@ -29,7 +44,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,9 +52,10 @@ 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"`
@@ -74,6 +90,10 @@ const (
 	DeleteHost = "DELETE_HOST"
 	// JoinHostToNetwork - constant for host network join action
 	JoinHostToNetwork = "JOIN_HOST_TO_NETWORK"
+	// Acknowledgement - ACK response for hosts
+	Acknowledgement = "ACK"
+	// RequestAck - request an ACK
+	RequestAck = "REQ_ACK"
 )
 
 // 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 - 4
models/network.go

@@ -21,7 +21,6 @@ type Network struct {
 	DefaultKeepalive    int32                 `json:"defaultkeepalive" bson:"defaultkeepalive" validate:"omitempty,max=1000"`
 	AccessKeys          []AccessKey           `json:"accesskeys" bson:"accesskeys"`
 	AllowManualSignUp   string                `json:"allowmanualsignup" bson:"allowmanualsignup" validate:"checkyesorno"`
-	IsLocal             string                `json:"islocal" bson:"islocal" validate:"checkyesorno"`
 	IsIPv4              string                `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
 	IsIPv6              string                `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`
 	DefaultUDPHolePunch string                `json:"defaultudpholepunch" bson:"defaultudpholepunch" validate:"checkyesorno"`
@@ -51,9 +50,6 @@ func (network *Network) SetDefaults() {
 	if network.DefaultUDPHolePunch == "" {
 		network.DefaultUDPHolePunch = "no"
 	}
-	if network.IsLocal == "" {
-		network.IsLocal = "no"
-	}
 	if network.DefaultInterface == "" {
 		if len(network.NetID) < 13 {
 			network.DefaultInterface = "nm-" + network.NetID

+ 0 - 14
models/node.go

@@ -68,7 +68,6 @@ type CommonNode struct {
 	Address6            net.IPNet     `json:"address6" yaml:"address6"`
 	Action              string        `json:"action" yaml:"action"`
 	LocalAddress        net.IPNet     `json:"localaddress" yaml:"localaddress"`
-	IsLocal             bool          `json:"islocal" yaml:"islocal"`
 	IsEgressGateway     bool          `json:"isegressgateway" yaml:"isegressgateway"`
 	EgressGatewayRanges []string      `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
 	IsIngressGateway    bool          `json:"isingressgateway" yaml:"isingressgateway"`
@@ -145,7 +144,6 @@ type LegacyNode struct {
 	DNSOn           string      `json:"dnson" bson:"dnson" yaml:"dnson" validate:"checkyesorno"`
 	IsServer        string      `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
 	Action          string      `json:"action" bson:"action" yaml:"action"`
-	IsLocal         string      `json:"islocal" bson:"islocal" yaml:"islocal" validate:"checkyesorno"`
 	IPForwarding    string      `json:"ipforwarding" bson:"ipforwarding" yaml:"ipforwarding" validate:"checkyesorno"`
 	OS              string      `json:"os" bson:"os" yaml:"os"`
 	MTU             int32       `json:"mtu" bson:"mtu" yaml:"mtu"`
@@ -295,13 +293,6 @@ func (node *LegacyNode) SetIPForwardingDefault() {
 	}
 }
 
-// Node.SetIsLocalDefault - set is local default
-func (node *LegacyNode) SetIsLocalDefault() {
-	if node.IsLocal == "" {
-		node.IsLocal = "no"
-	}
-}
-
 // Node.SetDNSOnDefault - sets dns on default
 func (node *LegacyNode) SetDNSOnDefault() {
 	if node.DNSOn == "" {
@@ -408,9 +399,6 @@ func (newNode *Node) Fill(currentNode *Node) { // TODO add new field for nftable
 	if newNode.DNSOn != currentNode.DNSOn {
 		newNode.DNSOn = currentNode.DNSOn
 	}
-	if newNode.IsLocal != currentNode.IsLocal {
-		newNode.IsLocal = currentNode.IsLocal
-	}
 	if newNode.Action == "" {
 		newNode.Action = currentNode.Action
 	}
@@ -526,7 +514,6 @@ func (ln *LegacyNode) ConvertToNewNode() (*Host, *Node) {
 		}
 	}
 	node.Action = ln.Action
-	node.IsLocal = parseBool(ln.IsLocal)
 	node.IsEgressGateway = parseBool(ln.IsEgressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
 	node.DNSOn = parseBool(ln.DNSOn)
@@ -574,7 +561,6 @@ func (n *Node) Legacy(h *Host, s *ServerConfig, net *Network) *LegacyNode {
 	l.UDPHolePunch = formatBool(true)
 	l.DNSOn = formatBool(n.DNSOn)
 	l.Action = n.Action
-	l.IsLocal = formatBool(n.IsLocal)
 	l.IPForwarding = formatBool(h.IPForwarding)
 	l.OS = h.OS
 	l.MTU = int32(h.MTU)

+ 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"`

+ 28 - 17
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
@@ -223,20 +228,20 @@ type NodeJoinResponse struct {
 
 // ServerConfig - struct for dealing with the server information for a netclient
 type ServerConfig struct {
-	CoreDNSAddr string `yaml:"corednsaddr"`
-	API         string `yaml:"api"`
-	APIPort     string `yaml:"apiport"`
-	DNSMode     string `yaml:"dnsmode"`
-	Version     string `yaml:"version"`
-	MQPort      string `yaml:"mqport"`
-	MQUserName  string `yaml:"mq_username"`
-	MQPassword  string `yaml:"mq_password"`
-	Server      string `yaml:"server"`
-	Broker      string `yaml:"broker"`
-	Is_EE       bool   `yaml:"isee"`
-	StunPort    int    `yaml:"stun_port"`
-	StunHost    string `yaml:"stun_host"`
-	TrafficKey  []byte `yaml:"traffickey"`
+	CoreDNSAddr string       `yaml:"corednsaddr"`
+	API         string       `yaml:"api"`
+	APIPort     string       `yaml:"apiport"`
+	DNSMode     string       `yaml:"dnsmode"`
+	Version     string       `yaml:"version"`
+	MQPort      string       `yaml:"mqport"`
+	MQUserName  string       `yaml:"mq_username"`
+	MQPassword  string       `yaml:"mq_password"`
+	Server      string       `yaml:"server"`
+	Broker      string       `yaml:"broker"`
+	Is_EE       bool         `yaml:"isee"`
+	StunPort    int          `yaml:"stun_port"`
+	StunList    []StunServer `yaml:"stun_list"`
+	TrafficKey  []byte       `yaml:"traffickey"`
 }
 
 // User.NameInCharset - returns if name is in charset below or not
@@ -261,3 +266,9 @@ type JoinData struct {
 	Node Node   `json:"node" yaml:"node"`
 	Key  string `json:"key" yaml:"key"`
 }
+
+// StunServer - struct to hold data required for using stun server
+type StunServer struct {
+	Domain string `json:"domain" yaml:"domain"`
+	Port   int    `json:"port" yaml:"port"`
+}

+ 154 - 0
mq/emqx.go

@@ -0,0 +1,154 @@
+package mq
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+type (
+	emqxUser struct {
+		UserID   string `json:"user_id"`
+		Password string `json:"password"`
+		Admin    bool   `json:"is_superuser"`
+	}
+
+	emqxLogin struct {
+		Username string `json:"username"`
+		Password string `json:"password"`
+	}
+
+	emqxLoginResponse struct {
+		License struct {
+			Edition string `json:"edition"`
+		} `json:"license"`
+		Token   string `json:"token"`
+		Version string `json:"version"`
+	}
+)
+
+func getEmqxAuthToken() (string, error) {
+	payload, err := json.Marshal(&emqxLogin{
+		Username: servercfg.GetMqUserName(),
+		Password: servercfg.GetMqPassword(),
+	})
+	if err != nil {
+		return "", err
+	}
+	resp, err := http.Post(servercfg.GetEmqxRestEndpoint()+"/api/v5/login", "application/json", bytes.NewReader(payload))
+	if err != nil {
+		return "", err
+	}
+	msg, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return "", fmt.Errorf("error during EMQX login %v", string(msg))
+	}
+	var loginResp emqxLoginResponse
+	if err := json.Unmarshal(msg, &loginResp); err != nil {
+		return "", err
+	}
+	return loginResp.Token, nil
+}
+
+// CreateEmqxUser - creates an EMQX user
+func CreateEmqxUser(username, password string, admin bool) error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&emqxUser{
+		UserID:   username,
+		Password: password,
+		Admin:    admin,
+	})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 300 {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error creating EMQX user %v", string(msg))
+	}
+	return nil
+}
+
+// DeleteEmqxUser - deletes an EMQX user
+func DeleteEmqxUser(username string) error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodDelete, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication/password_based:built_in_database/users/"+username, nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode >= 300 {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error deleting EMQX user %v", string(msg))
+	}
+	return nil
+}
+
+// CreateEmqxDefaultAuthenticator - creates a default authenticator based on password and using EMQX's built in database as storage
+func CreateEmqxDefaultAuthenticator() error {
+	token, err := getEmqxAuthToken()
+	if err != nil {
+		return err
+	}
+	payload, err := json.Marshal(&struct {
+		Mechanism  string `json:"mechanism"`
+		Backend    string `json:"backend"`
+		UserIDType string `json:"user_id_type"`
+	}{Mechanism: "password_based", Backend: "built_in_database", UserIDType: "username"})
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authentication", bytes.NewReader(payload))
+	if err != nil {
+		return err
+	}
+	req.Header.Add("content-type", "application/json")
+	req.Header.Add("authorization", "Bearer "+token)
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		msg, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("error creating default EMQX authenticator %v", string(msg))
+	}
+	return nil
+}

+ 257 - 170
mq/handlers.go

@@ -1,14 +1,18 @@
 package mq
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
+	"math"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
@@ -21,22 +25,29 @@ func DefaultHandler(client mqtt.Client, msg mqtt.Message) {
 
 // Ping message Handler -- handles ping topic from client nodes
 func Ping(client mqtt.Client, msg mqtt.Message) {
-	go func() {
-		id, err := getID(msg.Topic())
-		if err != nil {
-			logger.Log(0, "error getting node.ID sent on ping topic ")
-			return
-		}
+	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())
-			record, err := database.FetchRecord(database.NODES_TABLE_NAME, id)
-			if err != nil {
-				logger.Log(3, "error reading database ", err.Error())
-				return
+			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())
+					}
+				}
 			}
-			logger.Log(3, "record from database")
-			logger.Log(3, record)
 			return
 		}
 		decrypted, decryptErr := decryptMsg(&node, msg.Payload())
@@ -66,212 +77,251 @@ func Ping(client mqtt.Client, msg mqtt.Message) {
 			return
 		}
 
-		logger.Log(3, "ping processed for node", node.ID.String())
-		// --TODO --set client version once feature is implemented.
-		//node.SetClientVersion(msg.Payload())
-	}()
+		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) {
-	go func() {
-		id, err := getID(msg.Topic())
-		if err != nil {
-			logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
-			return
+	id, err := getID(msg.Topic())
+	if err != nil {
+		logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
+		return
+	}
+	currentNode, err := logic.GetNodeByID(id)
+	if err != nil {
+		logger.Log(1, "error getting node ", id, err.Error())
+		return
+	}
+	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
+	if decryptErr != nil {
+		logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
+		return
+	}
+	var newNode models.Node
+	if err := json.Unmarshal(decrypted, &newNode); err != nil {
+		logger.Log(1, "error unmarshaling payload ", err.Error())
+		return
+	}
+
+	ifaceDelta := logic.IfaceDelta(&currentNode, &newNode)
+	if servercfg.Is_EE && ifaceDelta {
+		if err = logic.EnterpriseResetAllPeersFailovers(currentNode.ID, currentNode.Network); err != nil {
+			logger.Log(1, "failed to reset failover list during node update", currentNode.ID.String(), currentNode.Network)
 		}
-		currentNode, err := logic.GetNodeByID(id)
-		if err != nil {
-			logger.Log(1, "error getting node ", id, err.Error())
-			return
+	}
+	newNode.SetLastCheckIn()
+	if err := logic.UpdateNode(&currentNode, &newNode); err != nil {
+		logger.Log(1, "error saving node", err.Error())
+		return
+	}
+	if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
+		if err = PublishPeerUpdate(); err != nil {
+			logger.Log(0, "error updating peers when node", currentNode.ID.String(), "informed the server of an interface change", err.Error())
 		}
-		decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
-		if decryptErr != nil {
-			logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
-			return
+	}
+
+	logger.Log(1, "updated node", id, newNode.ID.String())
+}
+
+// UpdateHost  message Handler -- handles host updates from clients
+func UpdateHost(client mqtt.Client, msg mqtt.Message) {
+	id, err := getID(msg.Topic())
+	if err != nil {
+		logger.Log(1, "error getting host.ID sent on ", msg.Topic(), err.Error())
+		return
+	}
+	currentHost, err := logic.GetHost(id)
+	if err != nil {
+		logger.Log(1, "error getting host ", id, err.Error())
+		return
+	}
+	decrypted, decryptErr := decryptMsgWithHost(currentHost, msg.Payload())
+	if decryptErr != nil {
+		logger.Log(1, "failed to decrypt message for host ", id, decryptErr.Error())
+		return
+	}
+	var hostUpdate models.HostUpdate
+	if err := json.Unmarshal(decrypted, &hostUpdate); err != nil {
+		logger.Log(1, "error unmarshaling payload ", err.Error())
+		return
+	}
+	logger.Log(3, fmt.Sprintf("recieved host update: %s\n", hostUpdate.Host.ID.String()))
+	var sendPeerUpdate bool
+	switch hostUpdate.Action {
+	case models.Acknowledgement:
+		hu := hostactions.GetAction(currentHost.ID.String())
+		if hu != nil {
+			if err = HostUpdate(hu); err != nil {
+				logger.Log(0, "failed to send new node to host", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
+				return
+			} else {
+				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
+				}
+			}
 		}
-		var newNode models.Node
-		if err := json.Unmarshal(decrypted, &newNode); err != nil {
-			logger.Log(1, "error unmarshaling payload ", err.Error())
+	case models.UpdateHost:
+		sendPeerUpdate = logic.UpdateHostFromClient(&hostUpdate.Host, currentHost)
+		err := logic.UpsertHost(currentHost)
+		if err != nil {
+			logger.Log(0, "failed to update host: ", currentHost.ID.String(), err.Error())
 			return
 		}
-
-		ifaceDelta := logic.IfaceDelta(&currentNode, &newNode)
-		if servercfg.Is_EE && ifaceDelta {
-			if err = logic.EnterpriseResetAllPeersFailovers(currentNode.ID, currentNode.Network); err != nil {
-				logger.Log(1, "failed to reset failover list during node update", currentNode.ID.String(), currentNode.Network)
+	case models.DeleteHost:
+		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+			// delete EMQX credentials for host
+			if err := DeleteEmqxUser(currentHost.ID.String()); err != nil {
+				logger.Log(0, "failed to remove host credentials from EMQX: ", currentHost.ID.String(), err.Error())
+				return
 			}
 		}
-		newNode.SetLastCheckIn()
-		if err := logic.UpdateNode(&currentNode, &newNode); err != nil {
-			logger.Log(1, "error saving node", err.Error())
+		if err := logic.DisassociateAllNodesFromHost(currentHost.ID.String()); err != nil {
+			logger.Log(0, "failed to delete all nodes of host: ", currentHost.ID.String(), err.Error())
 			return
 		}
-		if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
-			if err = PublishPeerUpdate(); err != nil {
-				logger.Log(0, "error updating peers when node", currentNode.ID.String(), "informed the server of an interface change", err.Error())
-			}
+		if err := logic.RemoveHostByID(currentHost.ID.String()); err != nil {
+			logger.Log(0, "failed to delete host: ", currentHost.ID.String(), err.Error())
+			return
 		}
+		sendPeerUpdate = true
+	}
 
-		logger.Log(1, "updated node", id, newNode.ID.String())
-
-	}()
+	if sendPeerUpdate {
+		err := PublishPeerUpdate()
+		if err != nil {
+			logger.Log(0, "failed to pulish peer update: ", err.Error())
+		}
+	}
+	// if servercfg.Is_EE && ifaceDelta {
+	// 	if err = logic.EnterpriseResetAllPeersFailovers(currentHost.ID.String(), currentHost.Network); err != nil {
+	// 		logger.Log(1, "failed to reset failover list during node update", currentHost.ID.String(), currentHost.Network)
+	// 	}
+	// }
 }
 
-// UpdateHost  message Handler -- handles host updates from clients
-func UpdateHost(client mqtt.Client, msg mqtt.Message) {
-	go func(msg mqtt.Message) {
+// UpdateMetrics  message Handler -- handles updates from client nodes for metrics
+func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
+	if servercfg.Is_EE {
 		id, err := getID(msg.Topic())
 		if err != nil {
-			logger.Log(1, "error getting host.ID sent on ", msg.Topic(), err.Error())
+			logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
 			return
 		}
-		currentHost, err := logic.GetHost(id)
+		currentNode, err := logic.GetNodeByID(id)
 		if err != nil {
-			logger.Log(1, "error getting host ", id, err.Error())
+			logger.Log(1, "error getting node ", id, err.Error())
 			return
 		}
-		decrypted, decryptErr := decryptMsgWithHost(currentHost, msg.Payload())
+		decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 		if decryptErr != nil {
-			logger.Log(1, "failed to decrypt message for host ", id, decryptErr.Error())
+			logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
 			return
 		}
-		var hostUpdate models.HostUpdate
-		if err := json.Unmarshal(decrypted, &hostUpdate); err != nil {
+
+		var newMetrics models.Metrics
+		if err := json.Unmarshal(decrypted, &newMetrics); err != nil {
 			logger.Log(1, "error unmarshaling payload ", err.Error())
 			return
 		}
-		logger.Log(3, fmt.Sprintf("recieved host update: %s\n", hostUpdate.Host.ID.String()))
-		var sendPeerUpdate bool
-		switch hostUpdate.Action {
-		case models.UpdateHost:
-			sendPeerUpdate = logic.UpdateHostFromClient(&hostUpdate.Host, currentHost)
-			err := logic.UpsertHost(currentHost)
-			if err != nil {
-				logger.Log(0, "failed to update host: ", currentHost.ID.String(), err.Error())
-				return
-			}
-		case models.DeleteHost:
-			if err := logic.DisassociateAllNodesFromHost(currentHost.ID.String()); err != nil {
-				logger.Log(0, "failed to delete all nodes of host: ", currentHost.ID.String(), err.Error())
-				return
-			}
-			if err := logic.RemoveHostByID(currentHost.ID.String()); err != nil {
-				logger.Log(0, "failed to delete host: ", currentHost.ID.String(), err.Error())
-				return
-			}
-			sendPeerUpdate = true
+
+		shouldUpdate := updateNodeMetrics(&currentNode, &newMetrics)
+
+		if err = logic.UpdateMetrics(id, &newMetrics); err != nil {
+			logger.Log(1, "faield to update node metrics", id, err.Error())
+			return
 		}
-		if sendPeerUpdate {
-			err := PublishPeerUpdate()
-			if err != nil {
-				logger.Log(0, "failed to pulish peer update: ", err.Error())
+		if servercfg.IsMetricsExporter() {
+			if err := pushMetricsToExporter(newMetrics); err != nil {
+				logger.Log(2, fmt.Sprintf("failed to push node: [%s] metrics to exporter, err: %v",
+					currentNode.ID, err))
 			}
 		}
-		// if servercfg.Is_EE && ifaceDelta {
-		// 	if err = logic.EnterpriseResetAllPeersFailovers(currentHost.ID.String(), currentHost.Network); err != nil {
-		// 		logger.Log(1, "failed to reset failover list during node update", currentHost.ID.String(), currentHost.Network)
-		// 	}
-		// }
-
-	}(msg)
-}
 
-// UpdateMetrics  message Handler -- handles updates from client nodes for metrics
-func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
-	if servercfg.Is_EE {
-		go func() {
-			id, err := getID(msg.Topic())
+		if newMetrics.Connectivity != nil {
+			err := logic.EnterpriseFailoverFunc(&currentNode)
 			if err != nil {
-				logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
-				return
-			}
-			currentNode, err := logic.GetNodeByID(id)
-			if err != nil {
-				logger.Log(1, "error getting node ", id, err.Error())
-				return
-			}
-			decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
-			if decryptErr != nil {
-				logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
-				return
-			}
-
-			var newMetrics models.Metrics
-			if err := json.Unmarshal(decrypted, &newMetrics); err != nil {
-				logger.Log(1, "error unmarshaling payload ", err.Error())
-				return
-			}
-
-			shouldUpdate := updateNodeMetrics(&currentNode, &newMetrics)
-
-			if err = logic.UpdateMetrics(id, &newMetrics); err != nil {
-				logger.Log(1, "faield to update node metrics", id, err.Error())
-				return
-			}
-			if servercfg.IsMetricsExporter() {
-				if err := pushMetricsToExporter(newMetrics); err != nil {
-					logger.Log(2, fmt.Sprintf("failed to push node: [%s] metrics to exporter, err: %v",
-						currentNode.ID, err))
-				}
-			}
-
-			if newMetrics.Connectivity != nil {
-				err := logic.EnterpriseFailoverFunc(&currentNode)
-				if err != nil {
-					logger.Log(0, "failed to failover for node", currentNode.ID.String(), "on network", currentNode.Network, "-", err.Error())
-				}
+				logger.Log(0, "failed to failover for node", currentNode.ID.String(), "on network", currentNode.Network, "-", err.Error())
 			}
+		}
 
-			if shouldUpdate {
-				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 {
-						logger.Log(0, "failed to publish update after failover peer change for node", currentNode.ID.String(), currentNode.Network)
-					}
+		if shouldUpdate {
+			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(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)
 				}
 			}
+		}
 
-			logger.Log(1, "updated node metrics", id)
-		}()
+		logger.Log(1, "updated node metrics", id)
 	}
 }
 
 // ClientPeerUpdate  message handler -- handles updating peers after signal from client nodes
 func ClientPeerUpdate(client mqtt.Client, msg mqtt.Message) {
-	go func() {
-		id, err := getID(msg.Topic())
-		if err != nil {
-			logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
-			return
-		}
-		currentNode, err := logic.GetNodeByID(id)
-		if err != nil {
-			logger.Log(1, "error getting node ", id, err.Error())
-			return
-		}
-		decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
-		if decryptErr != nil {
-			logger.Log(1, "failed to decrypt message during client peer update for node ", id, decryptErr.Error())
+	id, err := getID(msg.Topic())
+	if err != nil {
+		logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
+		return
+	}
+	currentNode, err := logic.GetNodeByID(id)
+	if err != nil {
+		logger.Log(1, "error getting node ", id, err.Error())
+		return
+	}
+	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
+	if decryptErr != nil {
+		logger.Log(1, "failed to decrypt message during client peer update for node ", id, decryptErr.Error())
+		return
+	}
+	switch decrypted[0] {
+	case ncutils.ACK:
+		// do we still need this
+	case ncutils.DONE:
+		if err = PublishPeerUpdate(); err != nil {
+			logger.Log(1, "error publishing peer update for node", currentNode.ID.String(), err.Error())
 			return
 		}
-		switch decrypted[0] {
-		case ncutils.ACK:
-			//do we still need this
-		case ncutils.DONE:
-			updateNodePeers(&currentNode)
-		}
-
-		logger.Log(1, "sent peer updates after signal received from", id)
-	}()
-}
-
-func updateNodePeers(currentNode *models.Node) {
-	if err := PublishPeerUpdate(); err != nil {
-		logger.Log(1, "error publishing peer update ", err.Error())
-		return
 	}
+
+	logger.Log(1, "sent peer updates after signal received from", id)
 }
 
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) bool {
@@ -317,6 +367,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 {
@@ -360,3 +425,25 @@ 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
+}

+ 17 - 0
mq/mq.go

@@ -2,6 +2,7 @@ package mq
 
 import (
 	"context"
+	"log"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -38,6 +39,17 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 
 // SetupMQTT creates a connection to broker and return client
 func SetupMQTT() {
+	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+		time.Sleep(10 * time.Second) // wait for the REST endpoint to be ready
+		// setup authenticator and create admin user
+		if err := CreateEmqxDefaultAuthenticator(); err != nil {
+			logger.Log(0, err.Error())
+		}
+		DeleteEmqxUser(servercfg.GetMqUserName())
+		if err := CreateEmqxUser(servercfg.GetMqUserName(), servercfg.GetMqPassword(), true); err != nil {
+			log.Fatal(err)
+		}
+	}
 	opts := mqtt.NewClientOptions()
 	setMqOptions(servercfg.GetMqUserName(), servercfg.GetMqPassword(), opts)
 	opts.SetOnConnectHandler(func(client mqtt.Client) {
@@ -100,3 +112,8 @@ func Keepalive(ctx context.Context) {
 func IsConnected() bool {
 	return mqclient != nil && mqclient.IsConnected()
 }
+
+// CloseClient - function to close the mq connection from server
+func CloseClient() {
+	mqclient.Disconnect(250)
+}

+ 38 - 12
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,14 +79,17 @@ 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
+	}
 	if host.ProxyEnabled {
-		proxyUpdate, err := logic.GetProxyUpdateForHost(host)
+		proxyUpdate, err := logic.GetProxyUpdateForHost(ctx, host)
 		if err != nil {
 			return err
 		}
@@ -402,7 +429,7 @@ func getCustomDNS(network string) []models.DNSUpdate {
 func sendPeers() {
 
 	hosts, err := logic.GetAllHosts()
-	if err != nil {
+	if err != nil && len(hosts) > 0 {
 		logger.Log(1, "error retrieving networks for keepalive", err.Error())
 	}
 
@@ -419,13 +446,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 {

+ 0 - 9
netclient/global_settings/globalsettings.go

@@ -1,9 +0,0 @@
-package global_settings
-
-// globalsettings - settings that are global in nature.  Avoids circular dependencies between config loading and usage.
-
-// PublicIPServices - the list of user-specified IP services to use to obtain the node's public IP
-var PublicIPServices map[string]string = make(map[string]string)
-
-// User - holds a user string for joins when using basic auth
-var User string

+ 0 - 78
netclient/ncutils/iface.go

@@ -2,62 +2,8 @@ package ncutils
 
 import (
 	"net"
-
-	"github.com/gravitl/netmaker/models"
 )
 
-// IfaceDelta - checks if the new node causes an interface change
-func IfaceDelta(currentNode *models.LegacyNode, newNode *models.LegacyNode) bool {
-	// single comparison statements
-	if newNode.Endpoint != currentNode.Endpoint ||
-		newNode.PublicKey != currentNode.PublicKey ||
-		newNode.Address != currentNode.Address ||
-		newNode.Address6 != currentNode.Address6 ||
-		newNode.IsEgressGateway != currentNode.IsEgressGateway ||
-		newNode.IsIngressGateway != currentNode.IsIngressGateway ||
-		newNode.IsRelay != currentNode.IsRelay ||
-		newNode.ListenPort != currentNode.ListenPort ||
-		newNode.UDPHolePunch != currentNode.UDPHolePunch ||
-		newNode.MTU != currentNode.MTU ||
-		newNode.IsPending != currentNode.IsPending ||
-		newNode.PersistentKeepalive != currentNode.PersistentKeepalive ||
-		newNode.DNSOn != currentNode.DNSOn ||
-		newNode.Connected != currentNode.Connected ||
-		len(newNode.AllowedIPs) != len(currentNode.AllowedIPs) {
-		return true
-	}
-
-	// multi-comparison statements
-	if newNode.IsEgressGateway == "yes" {
-		if len(currentNode.EgressGatewayRanges) != len(newNode.EgressGatewayRanges) {
-			return true
-		}
-		for _, address := range newNode.EgressGatewayRanges {
-			if !StringSliceContains(currentNode.EgressGatewayRanges, address) {
-				return true
-			}
-		}
-	}
-
-	if newNode.IsRelay == "yes" {
-		if len(currentNode.RelayAddrs) != len(newNode.RelayAddrs) {
-			return true
-		}
-		for _, address := range newNode.RelayAddrs {
-			if !StringSliceContains(currentNode.RelayAddrs, address) {
-				return true
-			}
-		}
-	}
-
-	for _, address := range newNode.AllowedIPs {
-		if !StringSliceContains(currentNode.AllowedIPs, address) {
-			return true
-		}
-	}
-	return false
-}
-
 // StringSliceContains - sees if a string slice contains a string element
 func StringSliceContains(slice []string, item string) bool {
 	for _, s := range slice {
@@ -68,30 +14,6 @@ func StringSliceContains(slice []string, item string) bool {
 	return false
 }
 
-// IPNetSliceContains - sees if a string slice contains a string element
-func IPNetSliceContains(slice []net.IPNet, item net.IPNet) bool {
-	for _, s := range slice {
-		if s.String() == item.String() {
-			return true
-		}
-	}
-	return false
-}
-
-// IfaceExists - return true if you can find the iface
-func IfaceExists(ifacename string) bool {
-	localnets, err := net.Interfaces()
-	if err != nil {
-		return false
-	}
-	for _, localnet := range localnets {
-		if ifacename == localnet.Name {
-			return true
-		}
-	}
-	return false
-}
-
 func IpIsPrivate(ipnet net.IP) bool {
 	return ipnet.IsPrivate() || ipnet.IsLoopback()
 }

+ 0 - 596
netclient/ncutils/netclientutils.go

@@ -4,562 +4,13 @@ import (
 	"bytes"
 	"crypto/rand"
 	"encoding/gob"
-	"errors"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"net/http"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"regexp"
-	"runtime"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/c-robinson/iplib"
-
-	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/netclient/global_settings"
-)
-
-var (
-	// Version - version of the netclient
-	Version = "dev"
 )
 
-// MAX_NAME_LENGTH - maximum node name length
-const MAX_NAME_LENGTH = 62
-
-// NO_DB_RECORD - error message result
-const NO_DB_RECORD = "no result found"
-
-// NO_DB_RECORDS - error record result
-const NO_DB_RECORDS = "could not find any records"
-
-// LINUX_APP_DATA_PATH - linux path
-const LINUX_APP_DATA_PATH = "/etc/netclient"
-
-// MAC_APP_DATA_PATH - mac path
-const MAC_APP_DATA_PATH = "/Applications/Netclient"
-
-// WINDOWS_APP_DATA_PATH - windows path
-const WINDOWS_APP_DATA_PATH = "C:\\Program Files (x86)\\Netclient"
-
-// WINDOWS_SVC_NAME - service name
-const WINDOWS_SVC_NAME = "netclient"
-
-// NETCLIENT_DEFAULT_PORT - default port
-const NETCLIENT_DEFAULT_PORT = 51821
-
 // DEFAULT_GC_PERCENT - garbage collection percent
 const DEFAULT_GC_PERCENT = 10
 
-// KEY_SIZE = ideal length for keys
-const KEY_SIZE = 2048
-
-// SetVersion -- set netclient version for use by other packages
-func SetVersion(ver string) {
-	Version = ver
-}
-
-// IsWindows - checks if is windows
-func IsWindows() bool {
-	return runtime.GOOS == "windows"
-}
-
-// IsMac - checks if is a mac
-func IsMac() bool {
-	return runtime.GOOS == "darwin"
-}
-
-// IsLinux - checks if is linux
-func IsLinux() bool {
-	return runtime.GOOS == "linux"
-}
-
-// IsFreeBSD - checks if is freebsd
-func IsFreeBSD() bool {
-	return runtime.GOOS == "freebsd"
-}
-
-// HasWGQuick - checks if WGQuick command is present
-func HasWgQuick() bool {
-	cmd, err := exec.LookPath("wg-quick")
-	return err == nil && cmd != ""
-}
-
-// GetWireGuard - checks if wg is installed
-func GetWireGuard() string {
-	userspace := os.Getenv("WG_QUICK_USERSPACE_IMPLEMENTATION")
-	if userspace != "" && (userspace == "boringtun" || userspace == "wireguard-go") {
-		return userspace
-	}
-	return "wg"
-}
-
-// IsNFTablesPresent - returns true if nftables is present, false otherwise.
-// Does not consider OS, up to the caller to determine if the OS supports nftables/whether this check is valid.
-func IsNFTablesPresent() bool {
-	found := false
-	_, err := exec.LookPath("nft")
-	if err == nil {
-		found = true
-	}
-	return found
-}
-
-// IsIPTablesPresent - returns true if iptables is present, false otherwise
-// Does not consider OS, up to the caller to determine if the OS supports iptables/whether this check is valid.
-func IsIPTablesPresent() bool {
-	found := false
-	_, err := exec.LookPath("iptables")
-	if err == nil {
-		found = true
-	}
-	return found
-}
-
-// IsKernel - checks if running kernel WireGuard
-func IsKernel() bool {
-	// TODO
-	// Replace && true with some config file value
-	// This value should be something like kernelmode, which should be 'on' by default.
-	return IsLinux() && os.Getenv("WG_QUICK_USERSPACE_IMPLEMENTATION") == ""
-}
-
-// IsEmptyRecord - repeat from database
-func IsEmptyRecord(err error) bool {
-	if err == nil {
-		return false
-	}
-	return strings.Contains(err.Error(), NO_DB_RECORD) || strings.Contains(err.Error(), NO_DB_RECORDS)
-}
-
-// GetPublicIP - gets public ip
-func GetPublicIP(api string) (string, error) {
-
-	iplist := []string{"https://ip.client.gravitl.com", "https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
-
-	for network, ipService := range global_settings.PublicIPServices {
-		logger.Log(3, "User provided public IP service defined for network", network, "is", ipService)
-
-		// prepend the user-specified service so it's checked first
-		iplist = append([]string{ipService}, iplist...)
-	}
-	if api != "" {
-		api = "https://" + api + "/api/getip"
-		iplist = append([]string{api}, iplist...)
-	}
-
-	endpoint := ""
-	var err error
-	for _, ipserver := range iplist {
-		client := &http.Client{
-			Timeout: time.Second * 10,
-		}
-		var resp *http.Response
-		resp, err = client.Get(ipserver)
-		if err != nil {
-			continue
-		}
-		if resp.StatusCode == http.StatusOK {
-			var bodyBytes []byte
-			bodyBytes, err = io.ReadAll(resp.Body)
-			if err != nil {
-				if resp.Body != nil {
-					_ = resp.Body.Close()
-				}
-				continue
-			}
-			_ = resp.Body.Close()
-			endpoint = string(bodyBytes)
-			break
-		}
-	}
-	if err == nil && endpoint == "" {
-		err = errors.New("public address not found")
-	}
-	return endpoint, err
-}
-
-// GetMacAddr - get's mac address
-func GetMacAddr() ([]string, error) {
-	ifas, err := net.Interfaces()
-	if err != nil {
-		return nil, err
-	}
-	var as []string
-	for _, ifa := range ifas {
-		a := ifa.HardwareAddr.String()
-		if a != "" {
-			as = append(as, a)
-		}
-	}
-	return as, nil
-}
-
-// GetLocalIP - gets local ip of machine
-func GetLocalIP(localrange string) (string, error) {
-	_, localRange, err := net.ParseCIDR(localrange)
-	if err != nil {
-		return "", err
-	}
-	ifaces, err := net.Interfaces()
-	if err != nil {
-		return "", err
-	}
-	var local string
-	found := false
-	for _, i := range ifaces {
-		if i.Flags&net.FlagUp == 0 {
-			continue // interface down
-		}
-		if i.Flags&net.FlagLoopback != 0 {
-			continue // loopback interface
-		}
-		addrs, err := i.Addrs()
-		if err != nil {
-			return "", err
-		}
-		for _, addr := range addrs {
-			var ip net.IP
-			switch v := addr.(type) {
-			case *net.IPNet:
-				if !found {
-					ip = v.IP
-					local = ip.String()
-					found = localRange.Contains(ip)
-				}
-			case *net.IPAddr:
-				if !found {
-					ip = v.IP
-					local = ip.String()
-					found = localRange.Contains(ip)
-				}
-			}
-		}
-	}
-	if !found || local == "" {
-		return "", errors.New("Failed to find local IP in range " + localrange)
-	}
-	return local, nil
-}
-
-// GetNetworkIPMask - Pulls the netmask out of the network
-func GetNetworkIPMask(networkstring string) (string, string, error) {
-	ip, ipnet, err := net.ParseCIDR(networkstring)
-	if err != nil {
-		return "", "", err
-	}
-	ipstring := ip.String()
-	mask := ipnet.Mask
-	maskstring := fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
-	// maskstring := ipnet.Mask.String()
-	return ipstring, maskstring, err
-}
-
-// GetFreePort - gets free port of machine
-func GetFreePort(rangestart int32) (int32, error) {
-	addr := net.UDPAddr{}
-	if rangestart == 0 {
-		rangestart = NETCLIENT_DEFAULT_PORT
-	}
-	for x := rangestart; x <= 65535; x++ {
-		addr.Port = int(x)
-		conn, err := net.ListenUDP("udp", &addr)
-		if err != nil {
-			continue
-		}
-		defer conn.Close()
-		return x, nil
-	}
-	return rangestart, errors.New("no free ports")
-}
-
 // == OS PATH FUNCTIONS ==
 
-// GetHomeDirWindows - gets home directory in windows
-func GetHomeDirWindows() string {
-	if IsWindows() {
-		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
-		if home == "" {
-			home = os.Getenv("USERPROFILE")
-		}
-		return home
-	}
-	return os.Getenv("HOME")
-}
-
-// GetNetclientPath - gets netclient path locally
-func GetNetclientPath() string {
-	if IsWindows() {
-		return WINDOWS_APP_DATA_PATH
-	} else if IsMac() {
-		return MAC_APP_DATA_PATH
-	} else {
-		return LINUX_APP_DATA_PATH
-	}
-}
-
-// GetSeparator - gets the separator for OS
-func GetSeparator() string {
-	if IsWindows() {
-		return "\\"
-	} else {
-		return "/"
-	}
-}
-
-// GetFileWithRetry - retry getting file X number of times before failing
-func GetFileWithRetry(path string, retryCount int) ([]byte, error) {
-	var data []byte
-	var err error
-	for count := 0; count < retryCount; count++ {
-		data, err = os.ReadFile(path)
-		if err == nil {
-			return data, err
-		} else {
-			logger.Log(1, "failed to retrieve file ", path, ", retrying...")
-			time.Sleep(time.Second >> 2)
-		}
-	}
-	return data, err
-}
-
-// GetNetclientServerPath - gets netclient server path
-func GetNetclientServerPath(server string) string {
-	if IsWindows() {
-		return WINDOWS_APP_DATA_PATH + "\\" + server + "\\"
-	} else if IsMac() {
-		return MAC_APP_DATA_PATH + "/" + server + "/"
-	} else {
-		return LINUX_APP_DATA_PATH + "/" + server
-	}
-}
-
-// GetNetclientPathSpecific - gets specific netclient config path
-func GetNetclientPathSpecific() string {
-	if IsWindows() {
-		return WINDOWS_APP_DATA_PATH + "\\"
-	} else if IsMac() {
-		return MAC_APP_DATA_PATH + "/config/"
-	} else {
-		return LINUX_APP_DATA_PATH + "/config/"
-	}
-}
-
-func CheckIPAddress(ip string) error {
-	if net.ParseIP(ip) == nil {
-		return fmt.Errorf("ip address %s is invalid", ip)
-	}
-	return nil
-}
-
-// GetNewIface - Gets the name of the real interface created on Mac
-func GetNewIface(dir string) (string, error) {
-	files, _ := os.ReadDir(dir)
-	var newestFile string
-	var newestTime int64 = 0
-	var err error
-	for _, f := range files {
-		fi, err := os.Stat(dir + f.Name())
-		if err != nil {
-			return "", err
-		}
-		currTime := fi.ModTime().Unix()
-		if currTime > newestTime && strings.Contains(f.Name(), ".sock") {
-			newestTime = currTime
-			newestFile = f.Name()
-		}
-	}
-	resultArr := strings.Split(newestFile, ".")
-	if resultArr[0] == "" {
-		err = errors.New("sock file does not exist")
-	}
-	return resultArr[0], err
-}
-
-// GetFileAsString - returns the string contents of a given file
-func GetFileAsString(path string) (string, error) {
-	content, err := os.ReadFile(path)
-	if err != nil {
-		return "", err
-	}
-	return string(content), err
-}
-
-// GetNetclientPathSpecific - gets specific netclient config path
-func GetWGPathSpecific() string {
-	if IsWindows() {
-		return WINDOWS_APP_DATA_PATH + "\\"
-	} else {
-		return "/etc/wireguard/"
-	}
-}
-
-// Copy - copies a src file to dest
-func Copy(src, dst string) error {
-	sourceFileStat, err := os.Stat(src)
-	if err != nil {
-		return err
-	}
-
-	if !sourceFileStat.Mode().IsRegular() {
-		return errors.New(src + " is not a regular file")
-	}
-
-	source, err := os.Open(src)
-	if err != nil {
-		return err
-	}
-	defer source.Close()
-
-	destination, err := os.Create(dst)
-	if err != nil {
-		return err
-	}
-	defer destination.Close()
-	_, err = io.Copy(destination, source)
-	if err != nil {
-		return err
-	}
-	err = os.Chmod(dst, 0755)
-
-	return err
-}
-
-// RunsCmds - runs cmds
-func RunCmds(commands []string, printerr bool) error {
-	var err error
-	for _, command := range commands {
-		// prevent panic
-		if len(strings.Trim(command, " ")) == 0 {
-			continue
-		}
-		args := strings.Fields(command)
-		out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
-		if err != nil && printerr {
-			logger.Log(0, "error running command:", command)
-			logger.Log(0, strings.TrimSuffix(string(out), "\n"))
-		}
-	}
-	return err
-}
-
-// FileExists - checks if file exists locally
-func FileExists(f string) bool {
-	info, err := os.Stat(f)
-	if os.IsNotExist(err) {
-		return false
-	}
-	if err != nil && strings.Contains(err.Error(), "not a directory") {
-		return false
-	}
-	if err != nil {
-		logger.Log(0, "error reading file: "+f+", "+err.Error())
-	}
-	return !info.IsDir()
-}
-
-// GetSystemNetworks - get networks locally
-func GetSystemNetworks() ([]string, error) {
-	var networks []string
-	files, err := filepath.Glob(GetNetclientPathSpecific() + "netconfig-*")
-	if err != nil {
-		return nil, err
-	}
-	for _, file := range files {
-		// don't want files such as *.bak, *.swp
-		if filepath.Ext(file) != "" {
-			continue
-		}
-		file := filepath.Base(file)
-		temp := strings.Split(file, "-")
-		networks = append(networks, strings.Join(temp[1:], "-"))
-	}
-	return networks, nil
-}
-
-// ShortenString - Brings string down to specified length. Stops names from being too long
-func ShortenString(input string, length int) string {
-	output := input
-	if len(input) > length {
-		output = input[0:length]
-	}
-	return output
-}
-
-// DNSFormatString - Formats a string with correct usage for DNS
-func DNSFormatString(input string) string {
-	reg, err := regexp.Compile("[^a-zA-Z0-9-]+")
-	if err != nil {
-		logger.Log(0, "error with regex: "+err.Error())
-		return ""
-	}
-	return reg.ReplaceAllString(input, "")
-}
-
-// GetHostname - Gets hostname of machine
-func GetHostname() string {
-	hostname, err := os.Hostname()
-	if err != nil {
-		return ""
-	}
-	if len(hostname) > MAX_NAME_LENGTH {
-		hostname = hostname[0:MAX_NAME_LENGTH]
-	}
-	return hostname
-}
-
-// CheckUID - Checks to make sure user has root privileges
-func CheckUID() {
-	// start our application
-	out, err := RunCmd("id -u", true)
-
-	if err != nil {
-		log.Fatal(out, err)
-	}
-	id, err := strconv.Atoi(string(out[:len(out)-1]))
-
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if id != 0 {
-		log.Fatal("This program must be run with elevated privileges (sudo). This program installs a SystemD service and configures WireGuard and networking rules. Please re-run with sudo/root.")
-	}
-}
-
-// CheckFirewall - checks if iptables of nft install, if not exit
-func CheckFirewall() {
-	if !IsIPTablesPresent() && !IsNFTablesPresent() {
-		log.Fatal("neither iptables nor nft is installed - please install one or the other and try again")
-	}
-}
-
-// CheckWG - Checks if WireGuard is installed. If not, exit
-func CheckWG() {
-	uspace := GetWireGuard()
-	if !HasWG() {
-		if uspace == "wg" {
-			log.Fatal("WireGuard not installed. Please install WireGuard (wireguard-tools) and try again.")
-		}
-		logger.Log(0, "running with userspace wireguard: ", uspace)
-	} else if uspace != "wg" {
-		logger.Log(0, "running userspace WireGuard with ", uspace)
-	}
-}
-
-// HasWG - returns true if wg command exists
-func HasWG() bool {
-	var _, err = exec.LookPath("wg")
-	return err == nil
-}
-
 // ConvertKeyToBytes - util to convert a key to bytes to use elsewhere
 func ConvertKeyToBytes(key *[32]byte) ([]byte, error) {
 	var buffer bytes.Buffer
@@ -582,16 +33,6 @@ func ConvertBytesToKey(data []byte) (*[32]byte, error) {
 	return result, err
 }
 
-// ServerAddrSliceContains - sees if a string slice contains a string element
-func ServerAddrSliceContains(slice []models.ServerAddr, item models.ServerAddr) bool {
-	for _, s := range slice {
-		if s.Address == item.Address && s.IsLeader == item.IsLeader {
-			return true
-		}
-	}
-	return false
-}
-
 // MakeRandomString - generates a random string of len n
 func MakeRandomString(n int) string {
 	const validChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -604,40 +45,3 @@ func MakeRandomString(n int) string {
 	}
 	return string(result)
 }
-
-func GetIPNetFromString(ip string) (net.IPNet, error) {
-	var ipnet *net.IPNet
-	var err error
-	// parsing as a CIDR first. If valid CIDR, append
-	if _, cidr, err := net.ParseCIDR(ip); err == nil {
-		ipnet = cidr
-	} else { // parsing as an IP second. If valid IP, check if ipv4 or ipv6, then append
-		if iplib.Version(net.ParseIP(ip)) == 4 {
-			ipnet = &net.IPNet{
-				IP:   net.ParseIP(ip),
-				Mask: net.CIDRMask(32, 32),
-			}
-		} else if iplib.Version(net.ParseIP(ip)) == 6 {
-			ipnet = &net.IPNet{
-				IP:   net.ParseIP(ip),
-				Mask: net.CIDRMask(128, 128),
-			}
-		}
-	}
-	if ipnet == nil {
-		err = errors.New(ip + " is not a valid ip or cidr")
-		return net.IPNet{}, err
-	}
-	return *ipnet, err
-}
-
-// ModPort - Change Node Port if UDP Hole Punching or ListenPort is not free
-func ModPort(node *models.LegacyNode) error {
-	var err error
-	if node.UDPHolePunch == "yes" {
-		node.ListenPort = 0
-	} else {
-		node.ListenPort, err = GetFreePort(node.ListenPort)
-	}
-	return err
-}

+ 0 - 39
netclient/ncutils/netclientutils_darwin.go

@@ -1,39 +0,0 @@
-package ncutils
-
-import (
-	"os/exec"
-	"strings"
-
-	"github.com/gravitl/netmaker/logger"
-)
-
-// WHITESPACE_PLACEHOLDER - used with RunCMD - if a path has whitespace, use this to avoid running path as 2 args in RunCMD
-const WHITESPACE_PLACEHOLDER = "+-+-+-+"
-
-// RunCmd - runs a local command
-func RunCmd(command string, printerr bool) (string, error) {
-
-	args := strings.Fields(command)
-	// return whitespace after split
-	for i, arg := range args {
-		args[i] = strings.Replace(arg, WHITESPACE_PLACEHOLDER, " ", -1)
-	}
-	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Wait()
-	out, err := cmd.CombinedOutput()
-	if err != nil && printerr {
-		logger.Log(0, "error running command:", strings.Join(args, " "))
-		logger.Log(0, strings.TrimSuffix(string(out), "\n"))
-	}
-	return string(out), err
-}
-
-// RunCmdFormatted - run a command formatted for MacOS
-func RunCmdFormatted(command string, printerr bool) (string, error) {
-	return "", nil
-}
-
-// GetEmbedded - if files required for MacOS, put here
-func GetEmbedded() error {
-	return nil
-}

+ 0 - 50
netclient/ncutils/netclientutils_freebsd.go

@@ -1,50 +0,0 @@
-package ncutils
-
-import (
-	"context"
-	"os/exec"
-	"strings"
-	"syscall"
-	"time"
-
-	"github.com/gravitl/netmaker/logger"
-)
-
-// RunCmdFormatted - run a command formatted for freebsd
-func RunCmdFormatted(command string, printerr bool) (string, error) {
-
-	args := strings.Fields(command)
-	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Start()
-	cmd.Wait()
-	out, err := cmd.CombinedOutput()
-	if err != nil && printerr {
-		logger.Log(0, "error running command: ", command)
-		logger.Log(0, strings.TrimSuffix(string(out), "\n"))
-	}
-	return string(out), err
-}
-
-// GetEmbedded - if files required for freebsd, put here
-func GetEmbedded() error {
-	return nil
-}
-
-// Runs Commands for FreeBSD
-func RunCmd(command string, printerr bool) (string, error) {
-	args := strings.Fields(command)
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
-	defer cancel()
-	cmd := exec.Command(args[0], args[1:]...)
-	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
-	go func() {
-		<-ctx.Done()
-		_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
-	}()
-	out, err := cmd.CombinedOutput()
-	if err != nil && printerr {
-		logger.Log(0, "error running command:", command)
-		logger.Log(0, strings.TrimSuffix(string(out), "\n"))
-	}
-	return string(out), err
-}

+ 0 - 32
netclient/ncutils/netclientutils_linux.go

@@ -1,32 +0,0 @@
-package ncutils
-
-import (
-	"fmt"
-	"os/exec"
-	"strings"
-
-	"github.com/gravitl/netmaker/logger"
-)
-
-// RunCmd - runs a local command
-func RunCmd(command string, printerr bool) (string, error) {
-	args := strings.Fields(command)
-	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Wait()
-	out, err := cmd.CombinedOutput()
-	if err != nil && printerr {
-		logger.Log(0, fmt.Sprintf("error running command: %s", command))
-		logger.Log(0, strings.TrimSuffix(string(out), "\n"))
-	}
-	return string(out), err
-}
-
-// RunCmdFormatted - does nothing for linux
-func RunCmdFormatted(command string, printerr bool) (string, error) {
-	return "", nil
-}
-
-// GetEmbedded - if files required for linux, put here
-func GetEmbedded() error {
-	return nil
-}

Some files were not shown because too many files changed in this diff