Browse Source

Merge branch 'develop' into story/GRA-1252

Alex Feiszli 2 years ago
parent
commit
0205d3af62
75 changed files with 1629 additions and 1610 deletions
  1. 1 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. 1 1
      README.md
  15. 0 4
      cli/cmd/network/create.go
  16. 0 1
      cli/cmd/network/flags.go
  17. 0 4
      cli/cmd/network/update.go
  18. 0 2
      cli/samples/network.json
  19. 0 4
      cli/samples/node.json
  20. 83 0
      compose/docker-compose-emqx.yml
  21. 14 19
      compose/docker-compose.ee.yml
  22. 2 4
      compose/docker-compose.reference.yml
  23. 2 4
      compose/docker-compose.yml
  24. 4 4
      config/config.go
  25. 4 4
      config/config_test.go
  26. 5 11
      controllers/controller.go
  27. 1 1
      controllers/docs.go
  28. 238 0
      controllers/enrollmentkeys.go
  29. 4 5
      controllers/ext_client.go
  30. 9 8
      controllers/hosts.go
  31. 19 6
      controllers/node.go
  32. 3 0
      database/database.go
  33. 1 1
      docker/Caddyfile
  34. 2 2
      go.mod
  35. 4 4
      go.sum
  36. 1 1
      k8s/client/netclient-daemonset.yaml
  37. 1 1
      k8s/client/netclient.yaml
  38. 5 7
      k8s/server/netmaker-server.yaml
  39. 1 1
      k8s/server/netmaker-ui.yaml
  40. 0 2
      logic/accesskeys_test.go
  41. 221 0
      logic/enrollmentkey.go
  42. 206 0
      logic/enrollmentkey_test.go
  43. 4 13
      logic/host_test.go
  44. 38 0
      logic/hostactions/hostactions.go
  45. 7 2
      logic/hosts.go
  46. 0 30
      logic/nodes.go
  47. 45 181
      logic/peers.go
  48. 3 3
      logic/zombie.go
  49. 25 38
      main.go
  50. 0 3
      models/api_node.go
  51. 59 0
      models/enrollment_key.go
  52. 4 0
      models/host.go
  53. 0 4
      models/network.go
  54. 0 14
      models/node.go
  55. 154 0
      mq/emqx.go
  56. 23 2
      mq/handlers.go
  57. 17 0
      mq/mq.go
  58. 30 13
      mq/publishers.go
  59. 0 9
      netclient/global_settings/globalsettings.go
  60. 0 78
      netclient/ncutils/iface.go
  61. 0 596
      netclient/ncutils/netclientutils.go
  62. 0 39
      netclient/ncutils/netclientutils_darwin.go
  63. 0 50
      netclient/ncutils/netclientutils_freebsd.go
  64. 0 32
      netclient/ncutils/netclientutils_linux.go
  65. 0 61
      netclient/ncutils/netclientutils_windows.go
  66. 0 97
      netclient/ncutils/peerhelper.go
  67. 0 46
      netclient/ncutils/pid.go
  68. 0 25
      netclient/ncutils/util.go
  69. BIN
      netclient/ncutils/windowsdaemon/winsw.exe
  70. 7 0
      release.md
  71. 3 3
      scripts/nm-quick.sh
  72. 4 4
      scripts/nm-upgrade.sh
  73. 36 43
      servercfg/serverconf.go
  74. 30 29
      stun-server/stun-server.go
  75. 1 7
      swagger.yaml

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

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

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

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

+ 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

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

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

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

@@ -0,0 +1,83 @@
+version: "3.4"
+
+services:
+  netmaker:
+    container_name: netmaker
+    image: gravitl/netmaker:v0.18.2
+    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_DOMAIN: "stun.NETMAKER_BASE_DOMAIN"
+      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"
+    ports:
+      - "3478:3478/udp"
+  netmaker-ui:
+    container_name: netmaker-ui
+    image: gravitl/netmaker-ui:v0.18.2
+    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: {}

+ 14 - 19
compose/docker-compose.ee.yml

@@ -9,7 +9,9 @@ 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"
       SERVER_HOST: "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"
@@ -67,21 +67,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 +111,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"

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

+ 2 - 4
compose/docker-compose.yml

@@ -9,7 +9,7 @@ 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"
       SERVER_HOST: "SERVER_PUBLIC_IP"
@@ -23,9 +23,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:1883"
       STUN_PORT: "3478"      
       VERBOSITY: "1"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"

+ 4 - 4
config/config.go

@@ -36,7 +36,10 @@ type ServerConfig struct {
 	APIConnString         string `yaml:"apiconn"`
 	APIHost               string `yaml:"apihost"`
 	APIPort               string `yaml:"apiport"`
-	MQHOST                string `yaml:"mqhost"`
+	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"`
@@ -59,10 +62,7 @@ type ServerConfig struct {
 	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"`

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

+ 5 - 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,11 @@ var HttpHandlers = []interface{}{
 	ipHandlers,
 	loggerHandlers,
 	hostHandlers,
+	enrollmentKeyHandlers,
 }
 
 // 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 +56,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.1
+//	Version: 0.18.2
 //	Host: netmaker.io
 //
 //	Consumes:

+ 238 - 0
controllers/enrollmentkeys.go

@@ -0,0 +1,238 @@
+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) || newHost.TrafficKeyPublic == nil {
+		err := fmt.Errorf("incompatible netclient")
+		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
+	logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(&server)
+	// 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())
+		}
+	}
+}

+ 4 - 5
controllers/ext_client.go

@@ -389,7 +389,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.PublishExtPeerUpdate(&node)
+		err = mq.PublishPeerUpdate()
 		if err != nil {
 			logger.Log(1, "error setting ext peers on "+nodeid+": "+err.Error())
 		}
@@ -488,7 +488,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	logger.Log(0, r.Header.Get("user"), "updated ext client", newExtClient.ClientID)
 	if changedEnabled { // need to send a peer update to the ingress node as enablement of one of it's clients has changed
 		if ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID); err == nil {
-			if err = mq.PublishExtPeerUpdate(&ingressNode); err != nil {
+			if err = mq.PublishPeerUpdate(); err != nil {
 				logger.Log(1, "error setting ext peers on", ingressNode.ID.String(), ":", err.Error())
 			}
 		}
@@ -567,11 +567,10 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	go func() {
-		err = mq.PublishExtPeerUpdate(&ingressnode)
-		if err != nil {
+		if err := mq.PublishPeerUpdate(); err != nil {
 			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())
 		}
-		if err := mq.PublishDeleteExtClientDNS(&extclient); err != nil {
+		if err = mq.PublishDeleteExtClientDNS(&extclient); err != nil {
 			logger.Log(1, "error publishing dns update for extclient deletion", err.Error())
 		}
 	}()

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

+ 19 - 6
controllers/node.go

@@ -433,7 +433,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)
+	hostPeerUpdate, err := logic.GetPeerUpdateForHost(node.Network, host, nil)
 	if err != nil && !database.IsEmptyRecord(err) {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", host.ID.String(), err))
@@ -564,6 +564,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 +596,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 +622,7 @@ func createNode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	hostPeerUpdate, err := logic.GetPeerUpdateForHost(networkName, &data.Host)
+	hostPeerUpdate, err := logic.GetPeerUpdateForHost(networkName, &data.Host, nil)
 	if err != nil && !database.IsEmptyRecord(err) {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", data.Host.ID.String(), err))
@@ -985,10 +991,17 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	if !fromNode { // notify node change
 		runUpdates(&node, false)
 	}
-	go func() { // notify of peer change
-		if err := mq.PublishPeerUpdate(); err != nil {
+	go func(deletedNode *models.Node, fromNode bool) { // notify of peer change
+		var err error
+		if fromNode {
+			err = mq.PublishDeletedNodePeerUpdate(deletedNode)
+		} else {
+			err = mq.PublishPeerUpdate()
+		}
+		if err != nil {
 			logger.Log(1, "error publishing peer update ", err.Error())
 		}
+
 		host, err := logic.GetHost(node.HostID.String())
 		if err != nil {
 			logger.Log(1, "failed to retrieve host for node", node.ID.String(), err.Error())
@@ -996,7 +1009,7 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 		if err := mq.PublishDNSDelete(&node, host); err != nil {
 			logger.Log(1, "error publishing dns update", err.Error())
 		}
-	}()
+	}(&node, fromNode)
 }
 
 func runUpdates(node *models.Node, ifaceDelta bool) {

+ 3 - 0
database/database.go

@@ -57,6 +57,8 @@ 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"
 
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
@@ -138,6 +140,7 @@ func createTables() {
 	createTable(USER_GROUPS_TABLE_NAME)
 	createTable(CACHE_TABLE_NAME)
 	createTable(HOSTS_TABLE_NAME)
+	createTable(ENROLLMENT_KEYS_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`
 }

+ 2 - 2
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
@@ -17,7 +17,7 @@ require (
 	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/oauth2 v0.5.0
 	golang.org/x/sys v0.5.0 // indirect
 	golang.org/x/text v0.7.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect

+ 4 - 4
go.sum

@@ -35,8 +35,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=
@@ -170,8 +170,8 @@ 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/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.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 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=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.18.1
+        image: gravitl/netclient:v0.18.2
         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.1
+        image: gravitl/netclient:v0.18.2
         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.1
+        image: gravitl/netmaker:v0.18.2
         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.1
+        image: gravitl/netmaker-ui:v0.18.2
         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))
 		}

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

+ 4 - 13
logic/host_test.go

@@ -2,37 +2,28 @@ package logic
 
 import (
 	"context"
+	"fmt"
 	"net"
 	"testing"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/matryer/is"
 )
 
-func TestMain(m *testing.M) {
+func TestCheckPorts(t *testing.T) {
 	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)
 		}
 	}()
-}
 
-func TestCheckPorts(t *testing.T) {
 	h := models.Host{
 		ID:              uuid.New(),
 		EndpointIP:      net.ParseIP("192.168.1.1"),

+ 38 - 0
logic/hostactions/hostactions.go

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

+ 7 - 2
logic/hosts.go

@@ -90,7 +90,7 @@ 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
@@ -236,7 +236,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)
 }
 

+ 0 - 30
logic/nodes.go

@@ -1,7 +1,6 @@
 package logic
 
 import (
-	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -421,35 +420,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())

+ 45 - 181
logic/peers.go

@@ -6,7 +6,6 @@ import (
 	"log"
 	"net"
 	"net/netip"
-	"time"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -17,177 +16,6 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
-// GetPeersforProxy calculates the peers for a proxy
-// TODO ==========================
-// TODO ==========================
-// TODO ==========================
-// TODO ==========================
-// TODO ==========================
-// revisit this logic with new host/node models.
-func GetPeersForProxy(node *models.Node, onlyPeers bool) (models.ProxyManagerPayload, error) {
-	proxyPayload := models.ProxyManagerPayload{}
-	var peers []wgtypes.PeerConfig
-	peerConfMap := make(map[string]models.PeerConf)
-	var err error
-	currentPeers, err := GetNetworkNodes(node.Network)
-	if err != nil {
-		return proxyPayload, err
-	}
-	if !onlyPeers {
-		if node.IsRelayed {
-			relayNode := FindRelay(node)
-			relayHost, err := GetHost(relayNode.HostID.String())
-			if err != nil {
-				return proxyPayload, err
-			}
-			if relayNode != nil {
-				host, err := GetHost(relayNode.HostID.String())
-				if err != nil {
-					logger.Log(0, "error retrieving host for relay node", relayNode.HostID.String(), err.Error())
-				}
-				relayEndpoint, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayHost.EndpointIP, host.ListenPort))
-				if err != nil {
-					logger.Log(1, "failed to resolve relay node endpoint: ", err.Error())
-				}
-				proxyPayload.IsRelayed = true
-				proxyPayload.RelayedTo = relayEndpoint
-			} else {
-				logger.Log(0, "couldn't find relay node for:  ", node.ID.String())
-			}
-
-		}
-		if node.IsRelay {
-			host, err := GetHost(node.HostID.String())
-			if err != nil {
-				logger.Log(0, "error retrieving host for relay node", node.ID.String(), err.Error())
-			}
-			relayedNodes, err := GetRelayedNodes(node)
-			if err != nil {
-				logger.Log(1, "failed to relayed nodes: ", node.ID.String(), err.Error())
-				proxyPayload.IsRelay = false
-			} else {
-				relayPeersMap := make(map[string]models.RelayedConf)
-				for _, relayedNode := range relayedNodes {
-					relayedNode := relayedNode
-					payload, err := GetPeersForProxy(&relayedNode, true)
-					if err == nil {
-						relayedHost, err := GetHost(relayedNode.HostID.String())
-						if err != nil {
-							logger.Log(0, "error retrieving host for relayNode", relayedNode.ID.String(), err.Error())
-						}
-						relayedEndpoint, udpErr := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayedHost.EndpointIP, host.ListenPort))
-						if udpErr == nil {
-							relayPeersMap[host.PublicKey.String()] = models.RelayedConf{
-								RelayedPeerEndpoint: relayedEndpoint,
-								RelayedPeerPubKey:   relayedHost.PublicKey.String(),
-								Peers:               payload.Peers,
-							}
-						}
-
-					}
-				}
-				proxyPayload.IsRelay = true
-				proxyPayload.RelayedPeerConf = relayPeersMap
-			}
-		}
-
-	}
-
-	for _, peer := range currentPeers {
-		if peer.ID == node.ID {
-			//skip yourself
-			continue
-		}
-		host, err := GetHost(peer.HostID.String())
-		if err != nil {
-			continue
-		}
-		proxyStatus := host.ProxyEnabled
-		listenPort := host.ListenPort
-		if proxyStatus {
-			listenPort = host.ProxyListenPort
-			if listenPort == 0 {
-				listenPort = models.NmProxyPort
-			}
-		} else if listenPort == 0 {
-			listenPort = host.ListenPort
-
-		}
-
-		endpoint, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host.EndpointIP, listenPort))
-		if err != nil {
-			logger.Log(1, "failed to resolve udp addr for node: ", peer.ID.String(), host.EndpointIP.String(), err.Error())
-			continue
-		}
-		allowedips := GetAllowedIPs(node, &peer, nil)
-		var keepalive time.Duration
-		if node.PersistentKeepalive != 0 {
-			// set_keepalive
-			keepalive = node.PersistentKeepalive
-		}
-		peers = append(peers, wgtypes.PeerConfig{
-			PublicKey:                   host.PublicKey,
-			Endpoint:                    endpoint,
-			AllowedIPs:                  allowedips,
-			PersistentKeepaliveInterval: &keepalive,
-			ReplaceAllowedIPs:           true,
-		})
-		peerConfMap[host.PublicKey.String()] = models.PeerConf{
-			Address:          net.ParseIP(peer.PrimaryAddress()),
-			Proxy:            proxyStatus,
-			PublicListenPort: int32(listenPort),
-		}
-
-		if !onlyPeers && peer.IsRelayed {
-			relayNode := FindRelay(&peer)
-			if relayNode != nil {
-				relayHost, err := GetHost(relayNode.HostID.String())
-				if err != nil {
-					logger.Log(0, "error retrieving host for relayNode", relayNode.ID.String(), err.Error())
-					continue
-				}
-				relayTo, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayHost.EndpointIP, relayHost.ListenPort))
-				if err == nil {
-					peerConfMap[host.PublicKey.String()] = models.PeerConf{
-
-						IsRelayed:        true,
-						RelayedTo:        relayTo,
-						Address:          net.ParseIP(peer.PrimaryAddress()),
-						Proxy:            proxyStatus,
-						PublicListenPort: int32(listenPort),
-					}
-				}
-
-			}
-
-		}
-	}
-	if node.IsIngressGateway {
-		var extPeers []wgtypes.PeerConfig
-		extPeers, peerConfMap, err = getExtPeersForProxy(node, peerConfMap)
-		if err == nil {
-			peers = append(peers, extPeers...)
-
-		} else if !database.IsEmptyRecord(err) {
-			logger.Log(1, "error retrieving external clients:", err.Error())
-		}
-	}
-
-	proxyPayload.IsIngress = node.IsIngressGateway
-	addr := node.Address
-	if addr.String() == "" {
-		addr = node.Address6
-	}
-	proxyPayload.Peers = peers
-	proxyPayload.PeerMap = peerConfMap
-	//proxyPayload.Network = node.Network
-	//proxyPayload.InterfaceName = node.Interface
-	//hardcode or read from host ??
-	proxyPayload.InterfaceName = models.WIREGUARD_INTERFACE
-
-	return proxyPayload, nil
-}
-
 // GetProxyUpdateForHost - gets the proxy update for host
 func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error) {
 	proxyPayload := models.ProxyManagerPayload{
@@ -206,14 +34,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)
+			payload, err := GetPeerUpdateForHost("", &relayedHost, nil)
 			if err == nil {
 				relayedEndpoint, udpErr := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayedHost.EndpointIP, GetPeerListenPort(&relayedHost)))
 				if udpErr == nil {
@@ -290,10 +117,12 @@ func GetProxyUpdateForHost(host *models.Host) (models.ProxyManagerPayload, error
 }
 
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
-func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpdate, error) {
+func GetPeerUpdateForHost(network string, host *models.Host, deletedNode *models.Node) (models.HostPeerUpdate, error) {
 	if host == nil {
 		return models.HostPeerUpdate{}, errors.New("host is nil")
 	}
+	// track which nodes are deleted
+	// after peer calculation, if peer not in list, add delete config of peer
 	hostPeerUpdate := models.HostPeerUpdate{
 		Host:          *host,
 		Server:        servercfg.GetServer(),
@@ -308,14 +137,19 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 		Peers:      []wgtypes.PeerConfig{},
 		NodePeers:  []wgtypes.PeerConfig{},
 	}
-	logger.Log(1, "peer update for host ", host.ID.String())
+	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())
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {
+		nodeID := nodeID
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
 			continue
 		}
-		if !node.Connected || node.Action == models.NODE_DELETE || node.PendingDelete {
+		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 		}
 		currentPeers, err := GetNetworkNodes(node.Network)
@@ -328,10 +162,14 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 			nodePeerMap = make(map[string]models.PeerRouteInfo)
 		}
 		for _, peer := range currentPeers {
-			if peer.ID == node.ID {
+			peer := peer
+			if peer.ID.String() == node.ID.String() {
 				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
@@ -341,13 +179,13 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 				return models.HostPeerUpdate{}, err
 			}
 
-			if !peer.Connected || peer.Action == models.NODE_DELETE || peer.PendingDelete {
+			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")
+				logger.Log(2, "peer update, skipping node for acl")
 				//skip if not permitted by acl
 				continue
 			}
@@ -393,6 +231,7 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 					_, 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),
@@ -459,6 +298,7 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 			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),
@@ -470,6 +310,7 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 				}
 				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,
@@ -514,9 +355,32 @@ func GetPeerUpdateForHost(network string, host *models.Host) (models.HostPeerUpd
 		}
 	}
 
+	// 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
+			}
+			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),
+				}
+				peerConfig.Remove = true
+				peerConfig.AllowedIPs = []net.IPNet{delNode.Address, delNode.Address6}
+				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
+			}
+		}
+	}
+
 	return hostPeerUpdate, nil
 }
 
+// GetPeerListenPort - given a host, retrieve it's appropriate listening port
 func GetPeerListenPort(host *models.Host) int {
 	peerPort := host.ListenPort
 	if host.ProxyEnabled {

+ 3 - 3
logic/zombie.go

@@ -10,8 +10,8 @@ import (
 )
 
 const (
-	// ZOMBIE_TIMEOUT - timeout in seconds for checking zombie status
-	ZOMBIE_TIMEOUT = 60
+	// ZOMBIE_TIMEOUT - timeout in hours for checking zombie status
+	ZOMBIE_TIMEOUT = 6
 	// ZOMBIE_DELETE_TIME - timeout in minutes for zombie node deletion
 	ZOMBIE_DELETE_TIME = 10
 )
@@ -86,7 +86,7 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 			zombies = append(zombies, id)
 		case id := <-newHostZombie:
 			hostZombies = append(hostZombies, id)
-		case <-time.After(time.Second * ZOMBIE_TIMEOUT):
+		case <-time.After(time.Hour * ZOMBIE_TIMEOUT): // run this check 4 times a day
 			logger.Log(3, "checking for zombie nodes")
 			if len(zombies) > 0 {
 				for i := len(zombies) - 1; i >= 0; i-- {

+ 25 - 38
main.go

@@ -27,7 +27,7 @@ import (
 	stunserver "github.com/gravitl/netmaker/stun-server"
 )
 
-var version = "v0.18.1"
+var version = "v0.18.2"
 
 // 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) {

+ 59 - 0
models/enrollment_key.go

@@ -0,0 +1,59 @@
+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"`
+}
+
+// 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()
+}

+ 4 - 0
models/host.go

@@ -74,6 +74,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

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

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

+ 23 - 2
mq/handlers.go

@@ -9,6 +9,7 @@ import (
 	"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"
@@ -144,6 +145,19 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 		logger.Log(3, fmt.Sprintf("recieved host update: %s\n", hostUpdate.Host.ID.String()))
 		var sendPeerUpdate bool
 		switch hostUpdate.Action {
+		case models.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(currentHost, nil); err != nil {
+						logger.Log(0, "failed peers publish after join acknowledged", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
+						return
+					}
+				}
+			}
 		case models.UpdateHost:
 			sendPeerUpdate = logic.UpdateHostFromClient(&hostUpdate.Host, currentHost)
 			err := logic.UpsertHost(currentHost)
@@ -152,6 +166,13 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				return
 			}
 		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
+				}
+			}
 			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
@@ -162,6 +183,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 			}
 			sendPeerUpdate = true
 		}
+
 		if sendPeerUpdate {
 			err := PublishPeerUpdate()
 			if err != nil {
@@ -227,11 +249,10 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 				logger.Log(2, "updating peers after node", currentNode.ID.String(), currentNode.Network, "detected connectivity issues")
 				host, err := logic.GetHost(currentNode.HostID.String())
 				if err == nil {
-					if err = PublishSingleHostUpdate(host); 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)
 					}
 				}
-
 			}
 
 			logger.Log(1, "updated node metrics", id)

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

+ 30 - 13
mq/publishers.go

@@ -25,7 +25,7 @@ func PublishPeerUpdate() error {
 	}
 	for _, host := range hosts {
 		host := host
-		err = PublishSingleHostUpdate(&host)
+		err = PublishSingleHostPeerUpdate(&host, nil)
 		if err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
@@ -33,13 +33,37 @@ func PublishPeerUpdate() error {
 	return err
 }
 
-// PublishSingleHostUpdate --- determines and publishes a peer update to one host
-func PublishSingleHostUpdate(host *models.Host) error {
+// PublishDeletedNodePeerUpdate --- determines and publishes a peer update
+// to all the hosts with a deleted node to account for
+func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
+	if !servercfg.IsMessageQueueBackend() {
+		return nil
+	}
+
+	hosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(1, "err getting all hosts", err.Error())
+		return err
+	}
+	for _, host := range hosts {
+		host := host
+		if err = PublishSingleHostPeerUpdate(&host, delNode); err != nil {
+			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+		}
+	}
+	return err
+}
+
+// PublishSingleHostPeerUpdate --- determines and publishes a peer update to one host
+func PublishSingleHostPeerUpdate(host *models.Host, deletedNode *models.Node) error {
 
-	peerUpdate, err := logic.GetPeerUpdateForHost("", host)
+	peerUpdate, err := logic.GetPeerUpdateForHost("", host, deletedNode)
 	if err != nil {
 		return err
 	}
+	if len(peerUpdate.Peers) == 0 { // no peers to send
+		return nil
+	}
 	if host.ProxyEnabled {
 		proxyUpdate, err := logic.GetProxyUpdateForHost(host)
 		if err != nil {
@@ -56,13 +80,6 @@ func PublishSingleHostUpdate(host *models.Host) error {
 	return publish(host, fmt.Sprintf("peers/host/%s/%s", host.ID.String(), servercfg.GetServer()), data)
 }
 
-// PublishExtPeerUpdate --- publishes a peer update to all the peers of a node
-func PublishExtPeerUpdate(node *models.Node) error {
-
-	go PublishPeerUpdate()
-	return nil
-}
-
 // NodeUpdate -- publishes a node update
 func NodeUpdate(node *models.Node) error {
 	host, err := logic.GetHost(node.HostID.String())
@@ -388,7 +405,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())
 	}
 
@@ -410,7 +427,7 @@ func sendPeers() {
 		if force {
 			host := host
 			logger.Log(2, "sending scheduled peer update (5 min)")
-			err = PublishSingleHostUpdate(&host)
+			err = PublishSingleHostPeerUpdate(&host, nil)
 			if err != nil {
 				logger.Log(1, "error publishing peer updates for host: ", host.ID.String(), " Err: ", err.Error())
 			}

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

+ 0 - 61
netclient/ncutils/netclientutils_windows.go

@@ -1,61 +0,0 @@
-package ncutils
-
-import (
-	"embed"
-	"fmt"
-	"os"
-	"os/exec"
-	"strings"
-	"syscall"
-
-	"github.com/gravitl/netmaker/logger"
-)
-
-//go:embed windowsdaemon/winsw.exe
-var winswContent embed.FS
-
-// 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()
-	//cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "/C \"" + command + "\""}
-	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
-}
-
-// RunCmd - runs a local command
-func RunCmdFormatted(command string, printerr bool) (string, error) {
-	var comSpec = os.Getenv("COMSPEC")
-	if comSpec == "" {
-		comSpec = os.Getenv("SystemRoot") + "\\System32\\cmd.exe"
-	}
-	cmd := exec.Command(comSpec)
-	cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "/C \"" + command + "\""}
-	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 - Gets the Windows daemon creator
-func GetEmbedded() error {
-	data, err := winswContent.ReadFile("windowsdaemon/winsw.exe")
-	if err != nil {
-		return err
-	}
-	fileName := fmt.Sprintf("%swinsw.exe", GetNetclientPathSpecific())
-	err = os.WriteFile(fileName, data, 0700)
-	if err != nil {
-		logger.Log(0, "could not mount winsw.exe")
-		return err
-	}
-	return nil
-}

+ 0 - 97
netclient/ncutils/peerhelper.go

@@ -1,97 +0,0 @@
-package ncutils
-
-import (
-	"net"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/gravitl/netmaker/logger"
-	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
-)
-
-// GetPeers - gets the peers from a given WireGuard interface
-func GetPeers(iface string) ([]wgtypes.Peer, error) {
-
-	var peers []wgtypes.Peer
-	output, err := RunCmd("wg show "+iface+" dump", true)
-	if err != nil {
-		return peers, err
-	}
-	for i, line := range strings.Split(strings.TrimSuffix(output, "\n"), "\n") {
-		if i == 0 {
-			continue
-		}
-		var allowedIPs []net.IPNet
-		fields := strings.Fields(line)
-		if len(fields) < 4 {
-			logger.Log(0, "error parsing peer: "+line)
-			continue
-		}
-		pubkeystring := fields[0]
-		endpointstring := fields[2]
-		allowedipstring := fields[3]
-		var pkeepalivestring string
-		if len(fields) > 7 {
-			pkeepalivestring = fields[7]
-		}
-		// AllowedIPs = private IP + defined networks
-
-		pubkey, err := wgtypes.ParseKey(pubkeystring)
-		if err != nil {
-			logger.Log(0, "error parsing peer key "+pubkeystring)
-			continue
-		}
-		ipstrings := strings.Split(allowedipstring, ",")
-		for _, ipstring := range ipstrings {
-			var netip net.IP
-			if netip = net.ParseIP(strings.Split(ipstring, "/")[0]); netip != nil {
-				allowedIPs = append(
-					allowedIPs,
-					net.IPNet{
-						IP:   netip,
-						Mask: netip.DefaultMask(),
-					},
-				)
-			}
-		}
-		if len(allowedIPs) == 0 {
-			logger.Log(0, "error parsing peer "+pubkeystring+", no allowedips found")
-			continue
-		}
-		var endpointarr []string
-		var endpointip net.IP
-		if endpointarr = strings.Split(endpointstring, ":"); len(endpointarr) != 2 {
-			logger.Log(0, "error parsing peer "+pubkeystring+", could not parse endpoint: "+endpointstring)
-			continue
-		}
-		if endpointip = net.ParseIP(endpointarr[0]); endpointip == nil {
-			logger.Log(0, "error parsing peer "+pubkeystring+", could not parse endpoint: "+endpointarr[0])
-			continue
-		}
-		var port int
-		if port, err = strconv.Atoi(endpointarr[1]); err != nil {
-			logger.Log(0, "error parsing peer "+pubkeystring+", could not parse port: "+err.Error())
-			continue
-		}
-		var endpoint = net.UDPAddr{
-			IP:   endpointip,
-			Port: port,
-		}
-		var dur time.Duration
-		if pkeepalivestring != "" {
-			if dur, err = time.ParseDuration(pkeepalivestring + "s"); err != nil {
-				logger.Log(0, "error parsing peer "+pubkeystring+", could not parse keepalive: "+err.Error())
-			}
-		}
-
-		peers = append(peers, wgtypes.Peer{
-			PublicKey:                   pubkey,
-			Endpoint:                    &endpoint,
-			AllowedIPs:                  allowedIPs,
-			PersistentKeepaliveInterval: dur,
-		})
-	}
-
-	return peers, err
-}

+ 0 - 46
netclient/ncutils/pid.go

@@ -1,46 +0,0 @@
-package ncutils
-
-import (
-	"fmt"
-	"os"
-	"strconv"
-)
-
-// PIDFILE - path/name of pid file
-const PIDFILE = "/var/run/netclient.pid"
-
-// WindowsPIDError - error returned from pid function on windows
-type WindowsPIDError struct{}
-
-// Error generates error for windows os
-func (*WindowsPIDError) Error() string {
-	return "pid tracking not supported on windows"
-}
-
-// SavePID - saves the pid of running program to disk
-func SavePID() error {
-	if IsWindows() {
-		return nil
-	}
-	pid := os.Getpid()
-	if err := os.WriteFile(PIDFILE, []byte(fmt.Sprintf("%d", pid)), 0644); err != nil {
-		return fmt.Errorf("could not write to pid file %w", err)
-	}
-	return nil
-}
-
-// ReadPID - reads a previously saved pid from disk
-func ReadPID() (int, error) {
-	if IsWindows() {
-		return 0, nil
-	}
-	bytes, err := os.ReadFile(PIDFILE)
-	if err != nil {
-		return 0, fmt.Errorf("could not read pid file %w", err)
-	}
-	pid, err := strconv.Atoi(string(bytes))
-	if err != nil {
-		return 0, fmt.Errorf("pid file contents invalid %w", err)
-	}
-	return pid, nil
-}

+ 0 - 25
netclient/ncutils/util.go

@@ -1,29 +1,4 @@
 package ncutils
 
-import (
-	"fmt"
-	"time"
-
-	"github.com/gravitl/netmaker/logger"
-)
-
 // CheckInInterval - the interval for check-in time in units/minute
 const CheckInInterval = 1
-
-// BackOff - back off any function while there is an error
-func BackOff(isExponential bool, maxTime int, f interface{}) (interface{}, error) {
-	// maxTime seconds
-	startTime := time.Now()
-	sleepTime := time.Second
-	for time.Now().Before(startTime.Add(time.Second * time.Duration(maxTime))) {
-		if result, err := f.(func() (interface{}, error))(); err == nil {
-			return result, nil
-		}
-		time.Sleep(sleepTime)
-		if isExponential {
-			sleepTime = sleepTime << 1
-		}
-		logger.Log(1, "retrying...")
-	}
-	return nil, fmt.Errorf("could not find result")
-}

BIN
netclient/ncutils/windowsdaemon/winsw.exe


+ 7 - 0
release.md

@@ -0,0 +1,7 @@
+# Netmaker v0.18.1
+
+## whats new
+
+## whats fixed
+
+## known issues

+ 3 - 3
scripts/nm-quick.sh

@@ -1,12 +1,12 @@
 #!/bin/bash
 
+LATEST="v0.18.2"
+
 if [ $(id -u) -ne 0 ]; then
    echo "This script must be run as root"
    exit 1
 fi
 
-LATEST="v0.18.1"
-
 unset INSTALL_TYPE
 unset BUILD_TYPE
 unset BUILD_TAG
@@ -24,7 +24,7 @@ usage () {
     echo "  -t      tag of build; if buildtype=version, tag=version. If builtype=branch or builtype=local, tag=branch"
     echo "  -a      auto-build; skip prompts and use defaults, if none provided"
     echo "examples:"
-	echo "          nm-quick.sh -e -b version -t v0.18.1"
+	echo "          nm-quick.sh -e -b version -t v0.18.2"
 	echo "          nm-quick.sh -e -b local -t feature_v0.17.2_newfeature"	
 	echo "          nm-quick.sh -e -b branch -t develop"
     exit 1

+ 4 - 4
scripts/nm-upgrade.sh

@@ -202,7 +202,7 @@ collect_server_settings() {
 
   STUN_NAME="stun.$SERVER_NAME"
   echo "-----------------------------------------------------"
-  echo "Netmaker v0.18.1 requires a new DNS entry for $STUN_NAME."
+  echo "Netmaker v0.18.2 requires a new DNS entry for $STUN_NAME."
   echo "Please confirm this is added to your DNS provider before continuing"
   echo "(note: this is not required if using an nip.io address)"
   echo "-----------------------------------------------------"
@@ -260,7 +260,7 @@ set_compose() {
   sed -i "s/v0.17.1/testing/g" /root/docker-compose.yml
 
   # RELEASE_REPLACE - Use this once release is ready
-  #sed -i "s/v0.17.1/v0.18.1/g" /root/docker-compose.yml
+  #sed -i "s/v0.17.1/v0.18.2/g" /root/docker-compose.yml
   yq ".services.netmaker.environment.SERVER_NAME = \"$SERVER_NAME\"" -i /root/docker-compose.yml
   yq ".services.netmaker.environment += {\"BROKER_NAME\": \"$BROKER_NAME\"}" -i /root/docker-compose.yml  
   yq ".services.netmaker.environment += {\"STUN_NAME\": \"$STUN_NAME\"}" -i /root/docker-compose.yml  
@@ -431,7 +431,7 @@ join_networks() {
 
           # create an egress if necessary
           if [[ $HAS_EGRESS == "yes" ]]; then
-            echo "Egress is currently unimplemented. Wait for 0.18.1"
+            echo "Egress is currently unimplemented. Wait for 0.18.2"
           fi
 
           echo "HAS INGRESS: $HAS_INGRESS"
@@ -462,7 +462,7 @@ join_networks() {
 cat << "EOF"
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
-The Netmaker Upgrade Script: Upgrading to v0.18.1 so you don't have to!
+The Netmaker Upgrade Script: Upgrading to v0.18.2 so you don't have to!
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 EOF

+ 36 - 43
servercfg/serverconf.go

@@ -13,6 +13,9 @@ import (
 	"github.com/gravitl/netmaker/models"
 )
 
+// EmqxBrokerType denotes the broker type for EMQX MQTT
+const EmqxBrokerType = "emqx"
+
 var (
 	Version = "dev"
 	Is_EE   = false
@@ -35,7 +38,6 @@ func GetServerConfig() config.ServerConfig {
 	cfg.CoreDNSAddr = GetCoreDNSAddr()
 	cfg.APIHost = GetAPIHost()
 	cfg.APIPort = GetAPIPort()
-	cfg.MQPort = GetMQPort()
 	cfg.MasterKey = "(hidden)"
 	cfg.DNSKey = "(hidden)"
 	cfg.AllowedOrigin = GetAllowedOrigin()
@@ -43,6 +45,8 @@ func GetServerConfig() config.ServerConfig {
 	cfg.NodeID = GetNodeID()
 	cfg.StunHost = GetStunAddr()
 	cfg.StunPort = GetStunPort()
+	cfg.BrokerType = GetBrokerType()
+	cfg.EmqxRestEndpoint = GetEmqxRestEndpoint()
 	if IsRestBackend() {
 		cfg.RestBackend = "on"
 	}
@@ -83,14 +87,13 @@ func GetServerConfig() config.ServerConfig {
 func GetServerInfo() models.ServerConfig {
 	var cfg models.ServerConfig
 	cfg.Server = GetServer()
-	cfg.Broker = GetBroker()
 	cfg.MQUserName = GetMqUserName()
 	cfg.MQPassword = GetMqPassword()
 	cfg.API = GetAPIConnString()
 	cfg.CoreDNSAddr = GetCoreDNSAddr()
 	cfg.APIPort = GetAPIPort()
-	cfg.MQPort = GetMQPort()
 	cfg.DNSMode = "off"
+	cfg.Broker = GetPublicBrokerEndpoint()
 	if IsDNSMode() {
 		cfg.DNSMode = "on"
 	}
@@ -196,32 +199,39 @@ func GetCoreDNSAddr() string {
 	return addr
 }
 
-// GetMQPort - gets the mq port
-func GetMQPort() string {
-	port := "8883" //default
-	if os.Getenv("MQ_PORT") != "" {
-		port = os.Getenv("MQ_PORT")
-	} else if config.Config.Server.MQPort != "" {
-		port = config.Config.Server.MQPort
+// GetPublicBrokerEndpoint - returns the public broker endpoint which shall be used by netclient
+func GetPublicBrokerEndpoint() string {
+	if os.Getenv("BROKER_ENDPOINT") != "" {
+		return os.Getenv("BROKER_ENDPOINT")
+	} else {
+		return config.Config.Server.Broker
 	}
-	return port
 }
 
 // GetMessageQueueEndpoint - gets the message queue endpoint
 func GetMessageQueueEndpoint() (string, bool) {
 	host, _ := GetPublicIP()
-	if os.Getenv("MQ_HOST") != "" {
-		host = os.Getenv("MQ_HOST")
-	} else if config.Config.Server.MQHOST != "" {
-		host = config.Config.Server.MQHOST
-	}
-	secure := strings.Contains(host, "wss") || strings.Contains(host, "ssl")
-	if secure {
-		host = "wss://" + host
+	if os.Getenv("SERVER_BROKER_ENDPOINT") != "" {
+		host = os.Getenv("SERVER_BROKER_ENDPOINT")
+	} else if config.Config.Server.ServerBrokerEndpoint != "" {
+		host = config.Config.Server.ServerBrokerEndpoint
+	} else if os.Getenv("BROKER_ENDPOINT") != "" {
+		host = os.Getenv("BROKER_ENDPOINT")
+	} else if config.Config.Server.Broker != "" {
+		host = config.Config.Server.Broker
+	} else {
+		host += ":1883" // default
+	}
+	return host, strings.Contains(host, "wss") || strings.Contains(host, "ssl") || strings.Contains(host, "mqtts")
+}
+
+// GetBrokerType - returns the type of MQ broker
+func GetBrokerType() string {
+	if os.Getenv("BROKER_TYPE") != "" {
+		return os.Getenv("BROKER_TYPE")
 	} else {
-		host = "ws://" + host
+		return "mosquitto"
 	}
-	return host + ":" + GetMQServerPort(), secure
 }
 
 // GetMasterKey - gets the configured master key of server
@@ -325,17 +335,6 @@ func GetServer() string {
 	return server
 }
 
-// GetBroker - gets the broker name
-func GetBroker() string {
-	server := ""
-	if os.Getenv("BROKER_NAME") != "" {
-		server = os.Getenv("BROKER_NAME")
-	} else if config.Config.Server.Broker != "" {
-		server = config.Config.Server.Broker
-	}
-	return server
-}
-
 func GetVerbosity() int32 {
 	var verbosity = 0
 	var err error
@@ -527,17 +526,6 @@ func GetAzureTenant() string {
 	return azureTenant
 }
 
-// GetMQServerPort - get mq port for server
-func GetMQServerPort() string {
-	port := "1883" //default
-	if os.Getenv("MQ_SERVER_PORT") != "" {
-		port = os.Getenv("MQ_SERVER_PORT")
-	} else if config.Config.Server.MQServerPort != "" {
-		port = config.Config.Server.MQServerPort
-	}
-	return port
-}
-
 // GetMqPassword - fetches the MQ password
 func GetMqPassword() string {
 	password := ""
@@ -560,6 +548,11 @@ func GetMqUserName() string {
 	return password
 }
 
+// GetEmqxRestEndpoint - returns the REST API Endpoint of EMQX
+func GetEmqxRestEndpoint() string {
+	return os.Getenv("EMQX_REST_ENDPOINT")
+}
+
 // IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
 func IsBasicAuthEnabled() bool {
 	var enabled = true //default

+ 30 - 29
stun-server/stun-server.go

@@ -4,11 +4,8 @@ import (
 	"context"
 	"fmt"
 	"net"
-	"os"
-	"os/signal"
 	"strings"
 	"sync"
-	"syscall"
 
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/servercfg"
@@ -23,7 +20,6 @@ import (
 // backwards compatibility with RFC 3489.
 type Server struct {
 	Addr string
-	Ctx  context.Context
 }
 
 var (
@@ -60,48 +56,58 @@ func basicProcess(addr net.Addr, b []byte, req, res *stun.Message) error {
 	)
 }
 
-func (s *Server) serveConn(c net.PacketConn, res, req *stun.Message) error {
+func (s *Server) serveConn(c net.PacketConn, res, req *stun.Message, ctx context.Context) error {
 	if c == nil {
 		return nil
 	}
+	go func(ctx context.Context) {
+		<-ctx.Done()
+		if c != nil {
+			// kill connection on server shutdown
+			c.Close()
+		}
+	}(ctx)
+
 	buf := make([]byte, 1024)
-	n, addr, err := c.ReadFrom(buf)
+	n, addr, err := c.ReadFrom(buf) // this be blocky af
 	if err != nil {
-		logger.Log(1, "ReadFrom: %v", err.Error())
+		if !strings.Contains(err.Error(), "use of closed network connection") {
+			logger.Log(1, "STUN read error:", err.Error())
+		}
 		return nil
 	}
+
 	if _, err = req.Write(buf[:n]); err != nil {
-		logger.Log(1, "Write: %v", err.Error())
+		logger.Log(1, "STUN write error:", err.Error())
 		return err
 	}
 	if err = basicProcess(addr, buf[:n], req, res); err != nil {
 		if err == errNotSTUNMessage {
 			return nil
 		}
-		logger.Log(1, "basicProcess: %v", err.Error())
+		logger.Log(1, "STUN process error:", err.Error())
 		return nil
 	}
 	_, err = c.WriteTo(res.Raw, addr)
 	if err != nil {
-		logger.Log(1, "WriteTo: %v", err.Error())
+		logger.Log(1, "STUN response write error", err.Error())
 	}
 	return err
 }
 
 // Serve reads packets from connections and responds to BINDING requests.
-func (s *Server) serve(c net.PacketConn) error {
+func (s *Server) serve(c net.PacketConn, ctx context.Context) error {
 	var (
 		res = new(stun.Message)
 		req = new(stun.Message)
 	)
 	for {
 		select {
-		case <-s.Ctx.Done():
-			logger.Log(0, "Shutting down stun server...")
-			c.Close()
+		case <-ctx.Done():
+			logger.Log(0, "shut down STUN server")
 			return nil
 		default:
-			if err := s.serveConn(c, res, req); err != nil {
+			if err := s.serveConn(c, res, req, ctx); err != nil {
 				logger.Log(1, "serve: %v", err.Error())
 				continue
 			}
@@ -119,9 +125,8 @@ func listenUDPAndServe(ctx context.Context, serverNet, laddr string) error {
 	}
 	s := &Server{
 		Addr: laddr,
-		Ctx:  ctx,
 	}
-	return s.serve(c)
+	return s.serve(c, ctx)
 }
 
 func normalize(address string) string {
@@ -135,19 +140,15 @@ func normalize(address string) string {
 }
 
 // Start - starts the stun server
-func Start(wg *sync.WaitGroup) {
-	ctx, cancel := context.WithCancel(context.Background())
-	go func(wg *sync.WaitGroup) {
-		defer wg.Done()
-		quit := make(chan os.Signal, 1)
-		signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
-		<-quit
-		cancel()
-	}(wg)
+func Start(wg *sync.WaitGroup, ctx context.Context) {
+	defer wg.Done()
 	normalized := normalize(fmt.Sprintf("0.0.0.0:%d", servercfg.GetStunPort()))
 	logger.Log(0, "netmaker-stun listening on", normalized, "via udp")
-	err := listenUDPAndServe(ctx, "udp", normalized)
-	if err != nil {
-		logger.Log(0, "failed to start stun server: ", err.Error())
+	if err := listenUDPAndServe(ctx, "udp", normalized); err != nil {
+		if strings.Contains(err.Error(), "closed network connection") {
+			logger.Log(0, "shutdown STUN server")
+		} else {
+			logger.Log(0, "server: ", err.Error())
+		}
 	}
 }

+ 1 - 7
swagger.yaml

@@ -273,9 +273,6 @@ definitions:
             isipv6:
                 type: string
                 x-go-name: IsIPv6
-            islocal:
-                type: string
-                x-go-name: IsLocal
             ispointtosite:
                 type: string
                 x-go-name: IsPointToSite
@@ -375,9 +372,6 @@ definitions:
             isk8s:
                 type: string
                 x-go-name: IsK8S
-            islocal:
-                type: string
-                x-go-name: IsLocal
             ispending:
                 type: string
                 x-go-name: IsPending
@@ -710,7 +704,7 @@ info:
 
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
-    version: 0.18.1
+    version: 0.18.2
 paths:
     /api/dns:
         get: