浏览代码

resolve merge conflicts

abhishek9686 8 月之前
父节点
当前提交
843f5a2fbb
共有 100 个文件被更改,包括 4604 次插入1256 次删除
  1. 2 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 26 9
      .github/workflows/branchtest.yml
  3. 40 10
      .github/workflows/deletedroplets.yml
  4. 6 5
      .github/workflows/docker-builder.yml
  5. 1 1
      .github/workflows/docs.yml
  6. 1 1
      .github/workflows/publish-docker.yml
  7. 5 5
      .github/workflows/test.yml
  8. 2 0
      .swaggo
  9. 2 2
      Dockerfile
  10. 1 1
      Dockerfile-quick
  11. 9 7
      README.md
  12. 12 4
      auth/host_session.go
  13. 1 1
      cli/cmd/user/groups.go
  14. 1 1
      cli/cmd/user/roles.go
  15. 2 1
      compose/docker-compose-emqx.yml
  16. 1 1
      compose/docker-compose.netclient.yml
  17. 7 4
      compose/docker-compose.yml
  18. 6 1
      config/config.go
  19. 304 0
      controllers/acls.go
  20. 2 0
      controllers/controller.go
  21. 54 1
      controllers/dns.go
  22. 3 3
      controllers/dns_test.go
  23. 6 3
      controllers/enrollmentkeys.go
  24. 45 4
      controllers/ext_client.go
  25. 9 14
      controllers/hosts.go
  26. 11 3
      controllers/middleware.go
  27. 47 1
      controllers/network.go
  28. 13 0
      controllers/node.go
  29. 4 4
      controllers/regex.go
  30. 38 14
      controllers/server.go
  31. 236 0
      controllers/tags.go
  32. 5 3
      controllers/user.go
  33. 6 0
      database/database.go
  34. 6 2
      docker/Caddyfile
  35. 6 2
      docker/Caddyfile-pro
  36. 1 1
      docker/Dockerfile-go-builder
  37. 21 0
      docker/emqx.conf
  38. 1 1
      docs/Authentication.md
  39. 16 14
      go.mod
  40. 36 32
      go.sum
  41. 1 1
      k8s/client/netclient-daemonset.yaml
  42. 1 1
      k8s/client/netclient.yaml
  43. 1 1
      k8s/server/netmaker-ui.yaml
  44. 1070 0
      logic/acls.go
  45. 3 2
      logic/acls/common.go
  46. 3 0
      logic/acls/nodeacls/modify.go
  47. 6 1
      logic/acls/nodeacls/retrieve.go
  48. 2 1
      logic/auth.go
  49. 1 1
      logic/clients.go
  50. 17 3
      logic/dns.go
  51. 30 5
      logic/enrollmentkey.go
  52. 15 15
      logic/enrollmentkey_test.go
  53. 430 4
      logic/extpeers.go
  54. 16 33
      logic/gateway.go
  55. 19 13
      logic/hosts.go
  56. 3 2
      logic/jwts.go
  57. 24 0
      logic/networks.go
  58. 254 10
      logic/nodes.go
  59. 75 2
      logic/peers.go
  60. 37 0
      logic/proc.go
  61. 1 2
      logic/security.go
  62. 26 0
      logic/status.go
  63. 290 0
      logic/tags.go
  64. 30 0
      logic/user_mgmt.go
  65. 28 1
      logic/util.go
  66. 13 2
      main.go
  67. 4 1
      main_ee.go
  68. 86 62
      migrate/migrate.go
  69. 120 0
      models/acl.go
  70. 44 27
      models/api_node.go
  71. 2 2
      models/dnsEntry.go
  72. 4 1
      models/enrollment_key.go
  73. 33 10
      models/extclient.go
  74. 1 1
      models/host.go
  75. 11 9
      models/metrics.go
  76. 26 4
      models/mqtt.go
  77. 5 229
      models/names.go
  78. 7 2
      models/network.go
  79. 47 14
      models/node.go
  80. 20 4
      models/structs.go
  81. 52 0
      models/tags.go
  82. 21 14
      models/user_mgmt.go
  83. 1 1
      mq/emqx_on_prem.go
  84. 24 2
      mq/migrate.go
  85. 1 1
      mq/mq.go
  86. 69 29
      mq/publishers.go
  87. 61 3
      mq/util.go
  88. 1 1
      netclient/ncutils/netclientutils.go
  89. 28 12
      pro/auth/azure-ad.go
  90. 3 3
      pro/auth/error.go
  91. 71 13
      pro/auth/github.go
  92. 1 1
      pro/auth/google.go
  93. 2 2
      pro/auth/oidc.go
  94. 1 1
      pro/auth/templates.go
  95. 16 0
      pro/controllers/failover.go
  96. 14 0
      pro/controllers/rac.go
  97. 260 22
      pro/controllers/users.go
  98. 6 0
      pro/email/email.go
  99. 42 8
      pro/email/invite.go
  100. 129 521
      pro/email/utils.go

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

@@ -31,6 +31,8 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.30.0
+        - v0.26.0
         - v0.25.0
         - v0.24.3
         - v0.24.2

+ 26 - 9
.github/workflows/branchtest.yml

@@ -2,6 +2,11 @@ name: Deploy and Test Branch
 
 on:
   workflow_dispatch:
+    inputs:
+      branch:
+        description: 'Branch to deploy and test'
+        required: true
+        default: 'develop'
   pull_request:
       types: [opened, synchronize, reopened]
       branches: [develop]
@@ -23,28 +28,40 @@ jobs:
     if: ${{ needs.skip-check.outputs.skip != 'true' }}
     outputs:
       netclientbranch: ${{ steps.getbranch.outputs.netclientbranch }}
+      netmakerbranch: ${{ steps.getbranch.outputs.netmakerbranch }}
     steps:
-      - name: checkout
+      - name: Determine branches
+        id: determine_branches
+        run: |
+          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+            echo "NETMAKER_BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
+            echo "NETCLIENT_BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
+          else
+            echo "NETMAKER_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
+            echo "NETCLIENT_BRANCH=develop" >> $GITHUB_ENV
+          fi
+      - name: Checkout netclient repository
         uses: actions/checkout@v4
         with:
           repository: gravitl/netclient
-          ref: develop
-      - name: check if branch exists
+          fetch-depth: 0
+      - name: Check if netclient branch exists and set output
         id: getbranch 
         run: |
-          if git show-ref ${{ github.head_ref}}; then
-            echo branch exists
-            echo "netclientbranch=${{ github.head_ref }}" >> $GITHUB_OUTPUT
+          if git ls-remote --heads origin ${{ env.NETCLIENT_BRANCH }} | grep -q ${{ env.NETCLIENT_BRANCH }}; then
+            echo "netclient branch ${{ env.NETCLIENT_BRANCH }} exists"
+            echo "netclientbranch=${{ env.NETCLIENT_BRANCH }}" >> $GITHUB_OUTPUT
           else
-            echo branch does not exist
+            echo "netclient branch ${{ env.NETCLIENT_BRANCH }} does not exist, using develop"
             echo "netclientbranch=develop" >> $GITHUB_OUTPUT
           fi
+          echo "netmakerbranch=${{ env.NETMAKER_BRANCH }}" >> $GITHUB_OUTPUT
 
   branchtest:
     uses: gravitl/devops/.github/workflows/testdeploybranch.yml@master
     needs: [getbranch, skip-check]
     with:
       netclientbranch: ${{ needs.getbranch.outputs.netclientbranch }}
-      netmakerbranch: ${{ github.head_ref }}
+      netmakerbranch: ${{ needs.getbranch.outputs.netmakerbranch }}
       tag: ${{ github.run_id }}-${{ github.run_attempt }}
-    secrets: inherit          
+    secrets: inherit

+ 40 - 10
.github/workflows/deletedroplets.yml

@@ -12,7 +12,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     steps:
       - name: get logs
-        uses: dawidd6/action-download-artifact@v6
+        uses: dawidd6/action-download-artifact@v7
         with:
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
@@ -37,13 +37,28 @@ jobs:
       - name: delete droplets
         if: success() || failure()
         run: |
-          sleep 15m
-          curl -X DELETE \
+          sleep 30m
+          response=$(curl -X DELETE \
             -H "Content-Type: application/json" \
             -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG"
+            -w "\n%{http_code}" \
+            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG")
+          
+          status_code=$(echo "$response" | tail -n1)
+          body=$(echo "$response" | sed '$d')
+          
+          echo "Response body: $body"
+          echo "Status code: $status_code"
+          
+          if [ "$status_code" -eq 204 ]; then
+            echo "Droplets deleted successfully"
+          else
+            echo "Failed to delete droplets. Status code: $status_code"
+            exit 1
+          fi
+          sleep 1m
         env:
-          DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
+          DIGITALOCEAN_TOKEN: ${{ secrets.DO_TEST_TOKEN }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
       - name: mark server as available
         if: success() || failure()
@@ -60,7 +75,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     steps:
       - name: get logs
-        uses: dawidd6/action-download-artifact@v6
+        uses: dawidd6/action-download-artifact@v7
         with:
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
@@ -94,13 +109,28 @@ jobs:
       - name: delete droplets
         if: success() || failure()
         run: |
-          sleep 3h
-          curl -X DELETE \
+          sleep 30m
+          response=$(curl -X DELETE \
             -H "Content-Type: application/json" \
             -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG"
+            -w "\n%{http_code}" \
+            "https://api.digitalocean.com/v2/droplets?tag_name=$TAG")
+          
+          status_code=$(echo "$response" | tail -n1)
+          body=$(echo "$response" | sed '$d')
+          
+          echo "Response body: $body"
+          echo "Status code: $status_code"
+          
+          if [ "$status_code" -eq 204 ]; then
+            echo "Droplets deleted successfully"
+          else
+            echo "Failed to delete droplets. Status code: $status_code"
+            exit 1
+          fi
+          sleep 1m
         env:
-          DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
+          DIGITALOCEAN_TOKEN: ${{ secrets.DO_TEST_TOKEN }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
       - name: mark server as available
         if: success() || failure()

+ 6 - 5
.github/workflows/docker-builder.yml

@@ -2,10 +2,11 @@ name: Build go-builder images
 
 on:
   workflow_dispatch:
-  push:
-    branches:
-      - 'develop'
-
+    inputs:
+      docker_tag:
+        description: 'Docker tag to use (default: latest)'
+        required: false
+        default: 'latest'
 jobs:
   go-builder:
     runs-on: ubuntu-latest
@@ -26,4 +27,4 @@ jobs:
         push: true
         platforms: linux/amd64, linux/arm64, linux/arm/v7
         file: ./docker/Dockerfile-go-builder
-        tags: gravitl/go-builder:latest
+        tags: gravitl/go-builder:${{ github.event.inputs.docker_tag || 'latest' }}

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

@@ -36,7 +36,7 @@ jobs:
         run: echo "timestamp=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
 
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@v6
+        uses: peter-evans/create-pull-request@v7
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           commit-message: "Update documentation ${{ steps.timestamp.outputs.timestamp }}"

+ 1 - 1
.github/workflows/publish-docker.yml

@@ -49,7 +49,7 @@ jobs:
           context: .
           platforms: linux/amd64, linux/arm64, linux/arm/v7
           push: true
-          tags: ${{ github.repository }}:${{ env.TAG }}, ${{ github.repository }}:latest
+          tags: ${{ github.repository }}:${{ env.TAG }}
           build-args: | 
             tags=ce
 

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

@@ -15,7 +15,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: Build
         run: |
          env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build main.go
@@ -29,7 +29,7 @@ jobs:
       - name: Setup go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: Build
         run: |
           cd cli
@@ -46,7 +46,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: run tests
         run: |
           go vet ./...
@@ -66,9 +66,9 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.19
+          go-version-file: 'go.mod'
       - name: run static checks
         run: |
           sudo apt update
-          go install honnef.co/go/tools/cmd/staticcheck@v0.4.7
+          go install honnef.co/go/tools/cmd/staticcheck@latest
           { ~/go/bin/staticcheck  -tags=ee ./... ; }

+ 2 - 0
.swaggo

@@ -0,0 +1,2 @@
+// Replace all time.Duration with int64
+replace time.Duration int64

+ 2 - 2
Dockerfile

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

+ 1 - 1
Dockerfile-quick

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

+ 9 - 7
README.md

@@ -16,7 +16,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.25.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.30.0-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
@@ -51,24 +51,26 @@
 
 If you're looking for a managed service, you can get started with just few clicks, visit [netmaker.io](https://account.netmaker.io) to create your netmaker server.  
 
-# Self-Hosted Quick Start  
+# Self-Hosted Open Source Quick Start  
 
-These are the instructions for deploying a Netmaker server on your own cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/install.html).  
+These are the instructions for deploying a Netmaker server on your own cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/docs/server-installation/quick-install#quick-install-script).  
 
 1. Get a cloud VM with Ubuntu 22.04 and a public IP.
 2. Open ports 443, 80, 3479, 8089 and 51821-51830/udp on the VM firewall and in cloud security settings.
 3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP.
-4. Run the script: 
+4. Run the script to setup open source version of Netmaker: 
+
+`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh`
+
+**<pre>To Install Self-Hosted PRO Version - https://docs.netmaker.io/docs/server-installation/netmaker-professional-setup</pre>** 
 
-`sudo wget -qO /root/nm-quick.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick.sh && sudo chmod +x /root/nm-quick.sh && sudo /root/nm-quick.sh`  
 
-This script by default installs PRO version with 14-day trial, check out these instructions for post trial period https://docs.netmaker.io/install.html#after-trial-period-ends. It also gives you the option to use your own domain (recommended) or an auto-generated domain. 
 
 <p float="left" align="middle">
 <img src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/readme.gif" />
 </p>
 
-After installing Netmaker, check out the [Walkthrough](https://itnext.io/getting-started-with-netmaker-a-wireguard-virtual-networking-platform-3d563fbd87f0) and [Getting Started](https://docs.netmaker.io/getting-started.html) guides to learn more about configuring networks. Or, check out some of our other [Tutorials](https://www.netmaker.io/blog) for different use cases, including Kubernetes.
+After installing Netmaker, check out the [Walkthrough](https://itnext.io/getting-started-with-netmaker-a-wireguard-virtual-networking-platform-3d563fbd87f0) and [Getting Started](https://docs.netmaker.io/docs/getting-started) guides to learn more about configuring networks. Or, check out some of our other [Tutorials](https://www.netmaker.io/blog) for different use cases, including Kubernetes.
 
 # Get Support
 

+ 12 - 4
auth/host_session.go

@@ -222,7 +222,7 @@ func SessionHandler(conn *websocket.Conn) {
 		if err = conn.WriteMessage(messageType, reponseData); err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 		}
-		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil)
+		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil, []models.TagID{})
 	case <-timeout: // the read from req.answerCh has timed out
 		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
 		break
@@ -236,7 +236,7 @@ func SessionHandler(conn *websocket.Conn) {
 }
 
 // CheckNetRegAndHostUpdate - run through networks and send a host update
-func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID) {
+func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uuid.UUID, tags []models.TagID) {
 	// publish host update through MQ
 	for i := range networks {
 		network := networks[i]
@@ -246,10 +246,18 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
 				continue
 			}
+			if len(tags) > 0 {
+				newNode.Tags = make(map[models.TagID]struct{})
+				for _, tagI := range tags {
+					newNode.Tags[tagI] = struct{}{}
+				}
+				logic.UpsertNode(newNode)
+			}
+
 			if relayNodeId != uuid.Nil && !newNode.IsRelayed {
 				// check if relay node exists and acting as relay
 				relaynode, err := logic.GetNodeByID(relayNodeId.String())
-				if err == nil && relaynode.IsRelay {
+				if err == nil && relaynode.IsRelay && relaynode.Network == newNode.Network {
 					slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
 					newNode.IsRelayed = true
 					newNode.RelayedBy = relayNodeId.String()
@@ -263,7 +271,7 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 						slog.Error("failed to update node", "nodeid", relayNodeId.String())
 					}
 				} else {
-					slog.Error("failed to relay node. maybe specified relay node is actually not a relay?", "err", err)
+					slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
 				}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)

+ 1 - 1
cli/cmd/user/groups.go

@@ -56,7 +56,7 @@ var userGroupCreateCmd = &cobra.Command{
 	Short: "create user group",
 	Long:  `create user group`,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+		fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api")
 	},
 }
 

+ 1 - 1
cli/cmd/user/roles.go

@@ -58,7 +58,7 @@ var userRoleCreateCmd = &cobra.Command{
 	Short: "create user role",
 	Long:  `create user role`,
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+		fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.netmaker.io/api")
 	},
 }
 

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

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   mq:
     container_name: mq
-    image: emqx/emqx:5.0.9
+    image: emqx/emqx:5.8.2
     env_file: ./netmaker.env
     restart: unless-stopped
     environment:
@@ -20,6 +20,7 @@ services:
       - emqx_data:/opt/emqx/data
       - emqx_etc:/opt/emqx/etc
       - emqx_logs:/opt/emqx/log
+      - ./emqx.conf:/opt/emqx/data/configs/cluster.hocon
 volumes:
   emqx_data: { } # storage for emqx data
   emqx_etc: { }  # storage for emqx etc

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

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

+ 7 - 4
compose/docker-compose.yml

@@ -12,7 +12,7 @@ services:
       - sqldata:/root/data
     environment:
       # config-dependant vars
-      - STUN_LIST=stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302
+      - STUN_SERVERS=stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302
       # The domain/host IP indicating the mq broker address
       - BROKER_ENDPOINT=wss://broker.${NM_DOMAIN} # For EMQX broker use `BROKER_ENDPOINT=wss://broker.${NM_DOMAIN}/mqtt`
       # For EMQX broker (uncomment the two lines below)
@@ -41,7 +41,7 @@ services:
     restart: always
 
   caddy:
-    image: caddy:2.6.2
+    image: caddy:2.8.4
     container_name: caddy
     env_file: ./netmaker.env
     restart: unless-stopped
@@ -52,8 +52,11 @@ services:
       - caddy_data:/data
       - caddy_conf:/config
     ports:
-      - "80:80"
-      - "443:443"
+      - "$SERVER_HOST:80:80/tcp"
+      - "$SERVER_HOST:443:443/tcp"
+    #uncomment to enable IPv6 communication
+     # - "$SERVER_HOST6:80:80/tcp"
+     # - "$SERVER_HOST6:443:443/tcp"
 
   coredns:
     #network_mode: host

+ 6 - 1
config/config.go

@@ -89,7 +89,7 @@ type ServerConfig struct {
 	EgressesLimit              int           `yaml:"egresses_limit"`
 	DeployedByOperator         bool          `yaml:"deployed_by_operator"`
 	Environment                string        `yaml:"environment"`
-	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
+	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
@@ -100,6 +100,11 @@ type ServerConfig struct {
 	SmtpHost                   string        `json:"smtp_host"`
 	SmtpPort                   int           `json:"smtp_port"`
 	MetricInterval             string        `yaml:"metric_interval"`
+	ManageDNS                  bool          `yaml:"manage_dns"`
+	Stun                       bool          `yaml:"stun"`
+	StunServers                string        `yaml:"stun_servers"`
+	DefaultDomain              string        `yaml:"default_domain"`
+	PublicIp                   string        `yaml:"public_ip"`
 }
 
 // SQLConfig - Generic SQL Config

+ 304 - 0
controllers/acls.go

@@ -0,0 +1,304 @@
+package controller
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+)
+
+func aclHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(getAcls))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/acls/policy_types", logic.SecurityCheck(true, http.HandlerFunc(aclPolicyTypes))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(createAcl))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(updateAcl))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(deleteAcl))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/acls/debug", logic.SecurityCheck(true, http.HandlerFunc(aclDebug))).
+		Methods(http.MethodGet)
+}
+
+// @Summary     List Acl Policy types
+// @Router      /api/v1/acls/policy_types [get]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
+	resp := models.AclPolicyTypes{
+		RuleTypes: []models.AclPolicyType{
+			models.DevicePolicy,
+			models.UserPolicy,
+		},
+		SrcGroupTypes: []models.AclGroupType{
+			models.UserAclID,
+			models.UserGroupAclID,
+			models.DeviceAclID,
+		},
+		DstGroupTypes: []models.AclGroupType{
+			models.DeviceAclID,
+			// models.NetmakerIPAclID,
+			// models.NetmakerSubNetRangeAClID,
+		},
+		ProtocolTypes: []models.ProtocolType{
+			{
+				Name: models.Any,
+				AllowedProtocols: []models.Protocol{
+					models.ALL,
+				},
+				PortRange:        "All ports",
+				AllowPortSetting: false,
+			},
+			{
+				Name: models.Http,
+				AllowedProtocols: []models.Protocol{
+					models.TCP,
+				},
+				PortRange: "80",
+			},
+			{
+				Name: models.Https,
+				AllowedProtocols: []models.Protocol{
+					models.TCP,
+				},
+				PortRange: "443",
+			},
+			// {
+			// 	Name: "MySQL",
+			// 	AllowedProtocols: []models.Protocol{
+			// 		models.TCP,
+			// 	},
+			// 	PortRange: "3306",
+			// },
+			// {
+			// 	Name: "DNS TCP",
+			// 	AllowedProtocols: []models.Protocol{
+			// 		models.TCP,
+			// 	},
+			// 	PortRange: "53",
+			// },
+			// {
+			// 	Name: "DNS UDP",
+			// 	AllowedProtocols: []models.Protocol{
+			// 		models.UDP,
+			// 	},
+			// 	PortRange: "53",
+			// },
+			{
+				Name: models.AllTCP,
+				AllowedProtocols: []models.Protocol{
+					models.TCP,
+				},
+				PortRange: "All ports",
+			},
+			{
+				Name: models.AllUDP,
+				AllowedProtocols: []models.Protocol{
+					models.UDP,
+				},
+				PortRange: "All ports",
+			},
+			{
+				Name: models.ICMPService,
+				AllowedProtocols: []models.Protocol{
+					models.ICMP,
+				},
+				PortRange: "",
+			},
+			{
+				Name: models.Custom,
+				AllowedProtocols: []models.Protocol{
+					models.UDP,
+					models.TCP,
+				},
+				PortRange:        "All ports",
+				AllowPortSetting: true,
+			},
+		},
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched acls types")
+}
+
+func aclDebug(w http.ResponseWriter, r *http.Request) {
+	nodeID, _ := url.QueryUnescape(r.URL.Query().Get("node"))
+	peerID, _ := url.QueryUnescape(r.URL.Query().Get("peer"))
+	node, err := logic.GetNodeByID(nodeID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	peer, err := logic.GetNodeByID(peerID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	allowed, _ := logic.IsNodeAllowedToCommunicate(node, peer, true)
+	logic.ReturnSuccessResponseWithJson(w, r, allowed, "fetched all acls in the network ")
+}
+
+// @Summary     List Acls in a network
+// @Router      /api/v1/acls [get]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getAcls(w http.ResponseWriter, r *http.Request) {
+	netID := r.URL.Query().Get("network")
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest"))
+		return
+	}
+	// check if network exists
+	_, err := logic.GetNetwork(netID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	acls, err := logic.ListAclsByNetwork(models.NetworkID(netID))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get all network acl entries: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.SortAclEntrys(acls[:])
+	logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched all acls in the network "+netID)
+}
+
+// @Summary     Create Acl
+// @Router      /api/v1/acls [post]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func createAcl(w http.ResponseWriter, r *http.Request) {
+	var req models.Acl
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.ValidateCreateAclReq(req)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	acl := req
+	acl.ID = uuid.New().String()
+	acl.CreatedBy = user.UserName
+	acl.CreatedAt = time.Now().UTC()
+	acl.Default = false
+	if acl.ServiceType == models.Any {
+		acl.Port = []string{}
+		acl.Proto = models.ALL
+	}
+	// validate create acl policy
+	if !logic.IsAclPolicyValid(acl) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+		return
+	}
+	err = logic.InsertAcl(acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	acl, err = logic.GetAcl(acl.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate(true)
+	logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
+}
+
+// @Summary     Update Acl
+// @Router      /api/v1/acls [put]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateAcl(w http.ResponseWriter, r *http.Request) {
+	var updateAcl models.UpdateAclRequest
+	err := json.NewDecoder(r.Body).Decode(&updateAcl)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	acl, err := logic.GetAcl(updateAcl.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if !logic.IsAclPolicyValid(updateAcl.Acl) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+		return
+	}
+	if updateAcl.Acl.NetworkID != acl.NetworkID {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy, network id mismatch"), "badrequest"))
+		return
+	}
+	if !acl.Default && updateAcl.NewName != "" {
+		//check if policy exists with same name
+		updateAcl.Acl.Name = updateAcl.NewName
+	}
+	err = logic.UpdateAcl(updateAcl.Acl, acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	go mq.PublishPeerUpdate(true)
+	logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
+}
+
+// @Summary     Delete Acl
+// @Router      /api/v1/acls [delete]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteAcl(w http.ResponseWriter, r *http.Request) {
+	aclID, _ := url.QueryUnescape(r.URL.Query().Get("acl_id"))
+	if aclID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("acl id is required"), "badrequest"))
+		return
+	}
+	acl, err := logic.GetAcl(aclID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if acl.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default policy"), "badrequest"))
+		return
+	}
+	err = logic.DeleteAcl(acl)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r,
+			logic.FormatError(errors.New("cannot delete default policy"), "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate(true)
+	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
+}

+ 2 - 0
controllers/controller.go

@@ -34,6 +34,8 @@ var HttpHandlers = []interface{}{
 	loggerHandlers,
 	hostHandlers,
 	enrollmentKeyHandlers,
+	tagHandlers,
+	aclHandlers,
 	legacyHandlers,
 }
 

+ 54 - 1
controllers/dns.go

@@ -5,12 +5,14 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/servercfg"
 )
 
@@ -24,6 +26,8 @@ func dnsHandlers(r *mux.Router) {
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/dns/adm/{network}", logic.SecurityCheck(true, http.HandlerFunc(getDNS))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/adm/{network}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncDNS))).
+		Methods(http.MethodPost)
 	r.HandleFunc("/api/dns/{network}", logic.SecurityCheck(true, http.HandlerFunc(createDNS))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/dns/adm/pushdns", logic.SecurityCheck(true, http.HandlerFunc(pushDNS))).
@@ -147,6 +151,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 
 	var entry models.DNSEntry
 	var params = mux.Vars(r)
+	netID := params["network"]
 
 	_ = json.NewDecoder(r.Body).Decode(&entry)
 	entry.Network = params["network"]
@@ -158,7 +163,11 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-
+	// check if default domain is appended if not append
+	if servercfg.GetDefaultDomain() != "" &&
+		!strings.HasSuffix(entry.Name, servercfg.GetDefaultDomain()) {
+		entry.Name += "." + servercfg.GetDefaultDomain()
+	}
 	entry, err = logic.CreateDNS(entry)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
@@ -176,6 +185,10 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if servercfg.GetManageDNS() {
+		mq.SendDNSSyncByNetwork(netID)
+	}
+
 	logger.Log(1, "new DNS record added:", entry.Name)
 	logger.Log(2, r.Header.Get("user"),
 		fmt.Sprintf("DNS entry is set: %+v", entry))
@@ -197,6 +210,7 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 
 	// get params
 	var params = mux.Vars(r)
+	netID := params["network"]
 	entrytext := params["domain"] + "." + params["network"]
 	err := logic.DeleteDNS(params["domain"], params["network"])
 
@@ -216,6 +230,10 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if servercfg.GetManageDNS() {
+		mq.SendDNSSyncByNetwork(netID)
+	}
+
 	json.NewEncoder(w).Encode(entrytext + " deleted.")
 
 }
@@ -264,3 +282,38 @@ func pushDNS(w http.ResponseWriter, r *http.Request) {
 	logger.Log(1, r.Header.Get("user"), "pushed DNS updates to nameserver")
 	json.NewEncoder(w).Encode("DNS Pushed to CoreDNS")
 }
+
+// @Summary     Sync DNS entries for a given network
+// @Router      /api/dns/adm/{network}/sync [post]
+// @Tags        DNS
+// @Accept      json
+// @Success     200 {string} string "DNS Sync completed successfully"
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func syncDNS(w http.ResponseWriter, r *http.Request) {
+	// Set header
+	w.Header().Set("Content-Type", "application/json")
+	if !servercfg.GetManageDNS() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("manage DNS is set to false"), "badrequest"),
+		)
+		return
+	}
+	var params = mux.Vars(r)
+	netID := params["network"]
+	k, err := logic.GetDNS(netID)
+	if err == nil && len(k) > 0 {
+		err = mq.PushSyncDNS(k)
+	}
+
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("Failed to Sync DNS entries to network %s: %v", netID, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logger.Log(1, r.Header.Get("user"), "DNS Sync complelted successfully")
+	json.NewEncoder(w).Encode("DNS Sync completed successfully")
+}

+ 3 - 3
controllers/dns_test.go

@@ -391,7 +391,7 @@ func TestValidateDNSCreate(t *testing.T) {
 		entry := models.DNSEntry{Address: "10.0.0.2", Network: "skynet"}
 		err := logic.ValidateDNSCreate(entry)
 		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'required' tag")
+		assert.Contains(t, err.Error(), "invalid input")
 	})
 	t.Run("NameTooLong", func(t *testing.T) {
 		name := ""
@@ -414,13 +414,13 @@ func TestValidateDNSCreate(t *testing.T) {
 		entry := models.DNSEntry{Address: "10.10.10.5", Name: "white space", Network: "skynet"}
 		err := logic.ValidateDNSCreate(entry)
 		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'whitespace' tag")
+		assert.Contains(t, err.Error(), "invalid input")
 	})
 	t.Run("AllSpaces", func(t *testing.T) {
 		entry := models.DNSEntry{Address: "10.10.10.5", Name: "     ", Network: "skynet"}
 		err := logic.ValidateDNSCreate(entry)
 		assert.NotNil(t, err)
-		assert.Contains(t, err.Error(), "Field validation for 'Name' failed on the 'whitespace' tag")
+		assert.Contains(t, err.Error(), "invalid input")
 	})
 
 }

+ 6 - 3
controllers/enrollmentkeys.go

@@ -72,7 +72,7 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	keyID := params["keyID"]
-	err := logic.DeleteEnrollmentKey(keyID)
+	err := logic.DeleteEnrollmentKey(keyID, false)
 	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"))
@@ -156,8 +156,10 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		newTime,
 		enrollmentKeyBody.Networks,
 		enrollmentKeyBody.Tags,
+		enrollmentKeyBody.Groups,
 		enrollmentKeyBody.Unlimited,
 		relayId,
+		false,
 	)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
@@ -206,7 +208,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId)
+	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	if err != nil {
 		slog.Error("failed to update enrollment key", "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -307,6 +309,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 		}
+
 		if err = logic.CreateHost(&newHost); err != nil {
 			logger.Log(
 				0,
@@ -355,5 +358,5 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
-	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay)
+	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups)
 }

+ 45 - 4
controllers/ext_client.go

@@ -244,6 +244,9 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if network.DefaultKeepalive != 0 {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 	}
+	if gwnode.IngressPersistentKeepalive != 0 {
+		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
+	}
 
 	gwendpoint := ""
 	if preferredIp == "" {
@@ -284,11 +287,30 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	} else if gwnode.IngressDNS != "" {
 		defaultDNS = "DNS = " + gwnode.IngressDNS
 	}
+	// if servercfg.GetManageDNS() {
+	// 	if gwnode.Address6.IP != nil {
+	// 		if defaultDNS == "" {
+	// 			defaultDNS = "DNS = " + gwnode.Address6.IP.String()
+	// 		} else {
+	// 			defaultDNS = defaultDNS + ", " + gwnode.Address6.IP.String()
+	// 		}
+	// 	}
+	// 	if gwnode.Address.IP != nil {
+	// 		if defaultDNS == "" {
+	// 			defaultDNS = "DNS = " + gwnode.Address.IP.String()
+	// 		} else {
+	// 			defaultDNS = defaultDNS + ", " + gwnode.Address.IP.String()
+	// 		}
+	// 	}
+	// }
 
 	defaultMTU := 1420
 	if host.MTU != 0 {
 		defaultMTU = host.MTU
 	}
+	if gwnode.IngressMTU != 0 {
+		defaultMTU = int(gwnode.IngressMTU)
+	}
 
 	postUp := strings.Builder{}
 	if client.PostUp != "" && params["type"] != "qr" {
@@ -446,13 +468,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	extclient.OwnerID = userName
 	extclient.RemoteAccessClientID = customExtClient.RemoteAccessClientID
 	extclient.IngressGatewayID = nodeid
-
+	extclient.Network = node.Network
+	extclient.Tags = make(map[models.TagID]struct{})
+	// extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
+	// 	models.RemoteAccessTagName))] = struct{}{}
 	// set extclient dns to ingressdns if extclient dns is not explicitly set
 	if (extclient.DNS == "") && (node.IngressDNS != "") {
 		extclient.DNS = node.IngressDNS
 	}
-
-	extclient.Network = node.Network
 	host, err := logic.GetHost(node.HostID.String())
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
@@ -467,6 +490,13 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 	}
+	extclient.Os = customExtClient.Os
+	extclient.DeviceName = customExtClient.DeviceName
+	if customExtClient.IsAlreadyConnectedToInetGw {
+		slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
+	}
+	extclient.PublicEndpoint = customExtClient.PublicEndpoint
+	extclient.Country = customExtClient.Country
 
 	if err = logic.CreateExtClient(&extclient); err != nil {
 		slog.Error(
@@ -531,6 +561,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	var update models.CustomExtClient
 	//var oldExtClient models.ExtClient
 	var sendPeerUpdate bool
+	var replacePeers bool
 	err := json.NewDecoder(r.Body).Decode(&update)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
@@ -588,6 +619,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	if update.Enabled != oldExtClient.Enabled {
 		sendPeerUpdate = true
 	}
+	if update.PublicKey != oldExtClient.PublicKey {
+		//remove old peer entry
+		sendPeerUpdate = true
+		replacePeers = true
+	}
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
 	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil {
 		slog.Error(
@@ -627,6 +663,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		if changedID && servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}
+		if replacePeers {
+			if err := mq.PublishDeletedClientPeerUpdate(&oldExtClient); err != nil {
+				slog.Error("error deleting old ext peers", "error", err.Error())
+			}
+		}
 		if sendPeerUpdate { // need to send a peer update to the ingress node as enablement of one of it's clients has changed
 			ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID)
 			if err == nil {
@@ -735,7 +776,7 @@ func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bo
 	//validate clientid
 	if customExtClient.ClientID != "" {
 		if err := isValid(customExtClient.ClientID, checkID); err != nil {
-			return fmt.Errorf("client validatation: %v", err)
+			return fmt.Errorf("client validation: %v", err)
 		}
 	}
 	//extclient.ClientID = customExtClient.ClientID

+ 9 - 14
controllers/hosts.go

@@ -167,6 +167,8 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+
+	sendPeerUpdate := false
 	for _, nodeID := range host.Nodes {
 		node, err := logic.GetNodeByID(nodeID)
 		if err != nil {
@@ -174,7 +176,13 @@ func pull(w http.ResponseWriter, r *http.Request) {
 			continue
 		}
 		if node.FailedOverBy != uuid.Nil {
-			go logic.ResetFailedOverPeer(&node)
+			logic.ResetFailedOverPeer(&node)
+			sendPeerUpdate = true
+		}
+	}
+	if sendPeerUpdate {
+		if err := mq.PublishPeerUpdate(true); err != nil {
+			logger.Log(0, "fail to publish peer update: ", err.Error())
 		}
 	}
 	allNodes, err := logic.GetAllNodes()
@@ -245,19 +253,6 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 
-	if newHost.Name != currHost.Name {
-		// update any rag role ids
-		for _, nodeID := range newHost.Nodes {
-			node, err := logic.GetNodeByID(nodeID)
-			if err == nil && node.IsIngressGateway {
-				role, err := logic.GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
-				if err == nil {
-					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
-					logic.UpdateRole(role)
-				}
-			}
-		}
-	}
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	if err = logic.UpsertHost(newHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())

+ 11 - 3
controllers/middleware.go

@@ -2,7 +2,6 @@ package controller
 
 import (
 	"net/http"
-	"net/url"
 	"strings"
 
 	"github.com/gorilla/mux"
@@ -28,13 +27,20 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		r.Header.Set("TARGET_RSRC", "")
 		r.Header.Set("RSRC_TYPE", "")
 		r.Header.Set("TARGET_RSRC_ID", "")
+		r.Header.Set("RAC", "")
 		r.Header.Set("NET_ID", params["network"])
+		if r.URL.Query().Get("network") != "" {
+			r.Header.Set("NET_ID", r.URL.Query().Get("network"))
+		}
 		if strings.Contains(route, "hosts") || strings.Contains(route, "nodes") {
 			r.Header.Set("TARGET_RSRC", models.HostRsrc.String())
 		}
 		if strings.Contains(route, "dns") {
 			r.Header.Set("TARGET_RSRC", models.DnsRsrc.String())
 		}
+		if strings.Contains(route, "rac") {
+			r.Header.Set("RAC", "true")
+		}
 		if strings.Contains(route, "users") {
 			r.Header.Set("TARGET_RSRC", models.UserRsrc.String())
 		}
@@ -54,6 +60,9 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		if strings.Contains(route, "acls") {
 			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
 		}
+		if strings.Contains(route, "tags") {
+			r.Header.Set("TARGET_RSRC", models.TagRsrc.String())
+		}
 		if strings.Contains(route, "extclients") {
 			r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String())
 		}
@@ -92,7 +101,7 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		if userID, ok := params["username"]; ok {
 			r.Header.Set("TARGET_RSRC_ID", userID)
 		} else {
-			username, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+			username := r.URL.Query().Get("username")
 			if username != "" {
 				r.Header.Set("TARGET_RSRC_ID", username)
 			}
@@ -102,7 +111,6 @@ func userMiddleWare(handler http.Handler) http.Handler {
 			r.Header.Get("TARGET_RSRC") == models.UserRsrc.String()) {
 			r.Header.Set("IS_GLOBAL_ACCESS", "yes")
 		}
-
 		r.Header.Set("RSRC_TYPE", r.Header.Get("TARGET_RSRC"))
 		handler.ServeHTTP(w, r)
 	})

+ 47 - 1
controllers/network.go

@@ -24,6 +24,8 @@ import (
 func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, http.HandlerFunc(getNetworks))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/networks/stats", logic.SecurityCheck(true, http.HandlerFunc(getNetworksStats))).
+		Methods(http.MethodGet)
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceNetworks, http.HandlerFunc(createNetwork)))).
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(getNetwork))).
@@ -74,6 +76,48 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(allnetworks)
 }
 
+// @Summary     Lists all networks with stats
+// @Router      /api/v1/networks/stats [get]
+// @Tags        Networks
+// @Security    oauth
+// @Produce     json
+// @Success     200 {object} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getNetworksStats(w http.ResponseWriter, r *http.Request) {
+
+	var err error
+	allnetworks, err := logic.GetNetworks()
+	if err != nil && !database.IsEmptyRecord(err) {
+		slog.Error("failed to fetch networks", "error", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	if r.Header.Get("ismaster") != "yes" {
+		username := r.Header.Get("user")
+		user, err := logic.GetUser(username)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		allnetworks = logic.FilterNetworksByRole(allnetworks, *user)
+	}
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	netstats := []models.NetworkStatResp{}
+	logic.SortNetworks(allnetworks[:])
+	for _, network := range allnetworks {
+		netstats = append(netstats, models.NetworkStatResp{
+			Network: network,
+			Hosts:   len(logic.GetNetworkNodesMemory(allNodes, network.NetID)),
+		})
+	}
+	logger.Log(2, r.Header.Get("user"), "fetched networks.")
+	logic.ReturnSuccessResponseWithJson(w, r, netstats, "fetched networks with stats")
+}
+
 // @Summary     Get a network
 // @Router      /api/networks/{networkname} [get]
 // @Tags        Networks
@@ -412,6 +456,7 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	go logic.DeleteNetworkRoles(network)
+	go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network))
 	//delete network from allocated ip map
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
 
@@ -487,7 +532,8 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
-
+	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
+	logic.CreateDefaultTags(models.NetworkID(network.NetID))
 	//add new network to allocated ip map
 	go logic.AddNetworkToAllocatedIpMap(network.NetID)
 

+ 13 - 0
controllers/node.go

@@ -327,6 +327,8 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 		nodes = filteredNodes
 	}
 
+	nodes = logic.AddStaticNodestoList(nodes)
+	nodes = logic.AddStatusToNodes(nodes)
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(2, r.Header.Get("user"), "fetched nodes on network", networkName)
@@ -363,7 +365,10 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 		if !userPlatformRole.FullAccess {
 			nodes = logic.GetFilteredNodesByUserAccess(*user, nodes)
 		}
+
 	}
+	nodes = logic.AddStaticNodestoList(nodes)
+	nodes = logic.AddStatusToNodes(nodes)
 	// return all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
@@ -587,6 +592,7 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 		if err := mq.NodeUpdate(&node); err != nil {
 			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
 		}
+		mq.PublishPeerUpdate(false)
 	}()
 }
 
@@ -631,6 +637,7 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil {
 					slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err)
 				}
+				mq.PublishPeerUpdate(false)
 				if err := mq.NodeUpdate(&node); err != nil {
 					slog.Error(
 						"error publishing node update to node",
@@ -674,6 +681,11 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	err = logic.ValidateNodeIp(&currentNode, &newData)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	if !servercfg.IsPro {
 		newData.AdditionalRagIps = []string{}
 	}
@@ -746,6 +758,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 				logger.Log(0, "error during node ACL update for node", newNode.ID.String())
 			}
 		}
+		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}

+ 4 - 4
controllers/regex.go

@@ -6,10 +6,10 @@ import (
 )
 
 var (
-	errInvalidExtClientPubKey  = errors.New("incorrect ext client public key")
-	errInvalidExtClientID      = errors.New("ext client ID must be alphanumderic and/or dashes and less that 15 chars")
-	errInvalidExtClientExtraIP = errors.New("ext client extra ip must be a valid cidr")
-	errInvalidExtClientDNS     = errors.New("ext client dns must be a valid ip address")
+	errInvalidExtClientPubKey  = errors.New("incorrect client public key")
+	errInvalidExtClientID      = errors.New("node name must be alphanumderic and/or dashes and less that 15 chars")
+	errInvalidExtClientExtraIP = errors.New("client extra ip must be a valid cidr")
+	errInvalidExtClientDNS     = errors.New("client dns must be a valid ip address")
 	errDuplicateExtClientName  = errors.New("duplicate client name")
 )
 

+ 38 - 14
controllers/server.go

@@ -3,6 +3,7 @@ package controller
 import (
 	"encoding/json"
 	"net/http"
+	"os"
 	"strings"
 	"syscall"
 	"time"
@@ -17,6 +18,8 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 )
 
+var cpuProfileLog *os.File
+
 func serverHandlers(r *mux.Router) {
 	// r.HandleFunc("/api/server/addnetwork/{network}", securityCheckServer(true, http.HandlerFunc(addNetwork))).Methods(http.MethodPost)
 	r.HandleFunc(
@@ -43,6 +46,27 @@ func serverHandlers(r *mux.Router) {
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/cpu_profile", logic.SecurityCheck(false, http.HandlerFunc(cpuProfile))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/server/mem_profile", logic.SecurityCheck(false, http.HandlerFunc(memProfile))).
+		Methods(http.MethodPost)
+}
+
+func cpuProfile(w http.ResponseWriter, r *http.Request) {
+	start := r.URL.Query().Get("action") == "start"
+	if start {
+		os.Remove("/root/data/cpu.prof")
+		cpuProfileLog = logic.StartCPUProfiling()
+	} else {
+		if cpuProfileLog != nil {
+			logic.StopCPUProfiling(cpuProfileLog)
+			cpuProfileLog = nil
+		}
+	}
+}
+func memProfile(w http.ResponseWriter, r *http.Request) {
+	os.Remove("/root/data/mem.prof")
+	logic.StartMemProfiling()
 }
 
 func getUsage(w http.ResponseWriter, _ *http.Request) {
@@ -122,26 +146,26 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 	if servercfg.ErrLicenseValidation != nil {
 		licenseErr = servercfg.ErrLicenseValidation.Error()
 	}
-	var trialEndDate time.Time
-	var err error
-	isOnTrial := false
-	if servercfg.IsPro &&
-		(servercfg.GetLicenseKey() == "" || servercfg.GetNetmakerTenantID() == "") {
-		trialEndDate, err = logic.GetTrialEndDate()
-		if err != nil {
-			slog.Error("failed to get trial end date", "error", err)
-		} else {
-			isOnTrial = true
-		}
-	}
+	//var trialEndDate time.Time
+	//var err error
+	// isOnTrial := false
+	// if servercfg.IsPro &&
+	// 	(servercfg.GetLicenseKey() == "" || servercfg.GetNetmakerTenantID() == "") {
+	// 	trialEndDate, err = logic.GetTrialEndDate()
+	// 	if err != nil {
+	// 		slog.Error("failed to get trial end date", "error", err)
+	// 	} else {
+	// 		isOnTrial = true
+	// 	}
+	// }
 	currentServerStatus := status{
 		DB:               database.IsConnected(),
 		Broker:           mq.IsConnected(),
 		IsBrokerConnOpen: mq.IsConnectionOpen(),
 		LicenseError:     licenseErr,
 		IsPro:            servercfg.IsPro,
-		TrialEndDate:     trialEndDate,
-		IsOnTrialLicense: isOnTrial,
+		//TrialEndDate:     trialEndDate,
+		//IsOnTrialLicense: isOnTrial,
 	}
 
 	w.Header().Set("Content-Type", "application/json")

+ 236 - 0
controllers/tags.go

@@ -0,0 +1,236 @@
+package controller
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+)
+
+func tagHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(getTags))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(createTag))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(updateTag))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(deleteTag))).
+		Methods(http.MethodDelete)
+
+}
+
+// @Summary     List Tags in a network
+// @Router      /api/v1/tags [get]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getTags(w http.ResponseWriter, r *http.Request) {
+	netID, _ := url.QueryUnescape(r.URL.Query().Get("network"))
+	if netID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network id param is missing"), "badrequest"))
+		return
+	}
+	// check if network exists
+	_, err := logic.GetNetwork(netID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	tags, err := logic.ListTagsWithNodes(models.NetworkID(netID))
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get all network tag entries: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.SortTagEntrys(tags[:])
+	logic.ReturnSuccessResponseWithJson(w, r, tags, "fetched all tags in the network "+netID)
+}
+
+// @Summary     Create Tag
+// @Router      /api/v1/tags [post]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func createTag(w http.ResponseWriter, r *http.Request) {
+	var req models.CreateTagReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// check if tag network exists
+	_, err = logic.GetNetwork(req.Network.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to get network details for "+req.Network.String()), "badrequest"))
+		return
+	}
+	// check if tag exists
+	tag := models.Tag{
+		ID:        models.TagID(fmt.Sprintf("%s.%s", req.Network, req.TagName)),
+		TagName:   req.TagName,
+		Network:   req.Network,
+		CreatedBy: user.UserName,
+		CreatedAt: time.Now(),
+	}
+	_, err = logic.GetTag(tag.ID)
+	if err == nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("tag with id %s exists already", tag.TagName), "badrequest"))
+		return
+	}
+	// validate name
+	err = logic.CheckIDSyntax(tag.TagName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.InsertTag(tag)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	go func() {
+		for _, node := range req.TaggedNodes {
+			if node.IsStatic {
+				extclient, err := logic.GetExtClient(node.StaticNode.ClientID, node.StaticNode.Network)
+				if err == nil && extclient.RemoteAccessClientID == "" {
+					if extclient.Tags == nil {
+						extclient.Tags = make(map[models.TagID]struct{})
+					}
+					extclient.Tags[tag.ID] = struct{}{}
+					logic.SaveExtClient(&extclient)
+				}
+				continue
+			}
+			node, err := logic.GetNodeByID(node.ID)
+			if err != nil {
+				continue
+			}
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			node.Tags[tag.ID] = struct{}{}
+			logic.UpsertNode(&node)
+		}
+	}()
+	go mq.PublishPeerUpdate(false)
+
+	var res models.TagListRespNodes = models.TagListRespNodes{
+		Tag:         tag,
+		UsedByCnt:   len(req.TaggedNodes),
+		TaggedNodes: req.TaggedNodes,
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, res, "created tag successfully")
+}
+
+// @Summary     Update Tag
+// @Router      /api/v1/tags [put]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateTag(w http.ResponseWriter, r *http.Request) {
+	var updateTag models.UpdateTagReq
+	err := json.NewDecoder(r.Body).Decode(&updateTag)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	tag, err := logic.GetTag(updateTag.ID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	updateTag.NewName = strings.TrimSpace(updateTag.NewName)
+	var newID models.TagID
+	if updateTag.NewName != "" {
+		// validate name
+		err = logic.CheckIDSyntax(updateTag.NewName)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		newID = models.TagID(fmt.Sprintf("%s.%s", tag.Network, updateTag.NewName))
+		tag.ID = newID
+		tag.TagName = updateTag.NewName
+		err = logic.InsertTag(tag)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		// delete old Tag entry
+		logic.DeleteTag(updateTag.ID, false)
+	}
+	go func() {
+		logic.UpdateTag(updateTag, newID)
+		if updateTag.NewName != "" {
+			logic.UpdateDeviceTag(updateTag.ID, newID, tag.Network)
+		}
+		mq.PublishPeerUpdate(false)
+	}()
+
+	var res models.TagListRespNodes = models.TagListRespNodes{
+		Tag:         tag,
+		UsedByCnt:   len(updateTag.TaggedNodes),
+		TaggedNodes: updateTag.TaggedNodes,
+	}
+
+	logic.ReturnSuccessResponseWithJson(w, r, res, "updated tags")
+}
+
+// @Summary     Delete Tag
+// @Router      /api/v1/tags [delete]
+// @Tags        TAG
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteTag(w http.ResponseWriter, r *http.Request) {
+	tagID, _ := url.QueryUnescape(r.URL.Query().Get("tag_id"))
+	if tagID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		return
+	}
+	tag, err := logic.GetTag(models.TagID(tagID))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// check if active policy is using the tag
+	if logic.CheckIfTagAsActivePolicy(tag.ID, tag.Network) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("tag is currently in use by an active policy"), "badrequest"))
+		return
+	}
+	err = logic.DeleteTag(models.TagID(tagID), true)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	go func() {
+		logic.RemoveDeviceTagFromAclPolicies(tag.ID, tag.Network)
+		logic.RemoveTagFromEnrollmentKeys(tag.ID)
+		mq.PublishPeerUpdate(false)
+	}()
+	logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
+}

+ 5 - 3
controllers/user.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"net/url"
 	"reflect"
 
 	"github.com/gorilla/mux"
@@ -240,7 +239,7 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 func getUserV1(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
-	usernameFetched, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+	usernameFetched := r.URL.Query().Get("username")
 	if usernameFetched == "" {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
 		return
@@ -452,6 +451,7 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 	}
 	logic.DeleteUserInvite(user.UserName)
 	logic.DeletePendingUser(user.UserName)
+	go mq.PublishPeerUpdate(false)
 	slog.Info("user was created", "username", user.UserName)
 	json.NewEncoder(w).Encode(logic.ToReturnUser(user))
 }
@@ -591,6 +591,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	go mq.PublishPeerUpdate(false)
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
@@ -693,6 +694,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 				}
 			}
 		}
+		mq.PublishPeerUpdate(false)
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 		}
@@ -720,7 +722,7 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 // @Summary     lists all user roles.
 // @Router      /api/v1/user/roles [get]
 // @Tags        Users
-// @Param       role_id param string true "roleid required to get the role details"
+// @Param       role_id query string true "roleid required to get the role details"
 // @Success     200 {object}  []models.UserRolePermissionTemplate
 // @Failure     500 {object} models.ErrorResponse
 func listRoles(w http.ResponseWriter, r *http.Request) {

+ 6 - 0
database/database.go

@@ -47,6 +47,8 @@ const (
 	GENERATED_TABLE_NAME = "generated"
 	// NODE_ACLS_TABLE_NAME - stores the node ACL rules
 	NODE_ACLS_TABLE_NAME = "nodeacls"
+	// ACLS_TABLE_NAME - table for acls v2
+	ACLS_TABLE_NAME = "acls"
 	// SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins
 	SSO_STATE_CACHE = "ssostatecache"
 	// METRICS_TABLE_NAME - stores network metrics
@@ -67,6 +69,8 @@ const (
 	PENDING_USERS_TABLE_NAME = "pending_users"
 	// USER_INVITES - table for user invites
 	USER_INVITES_TABLE_NAME = "user_invites"
+	// TAG_TABLE_NAME - table for tags
+	TAG_TABLE_NAME = "tags"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -152,6 +156,8 @@ func createTables() {
 	CreateTable(PENDING_USERS_TABLE_NAME)
 	CreateTable(USER_PERMISSIONS_TABLE_NAME)
 	CreateTable(USER_INVITES_TABLE_NAME)
+	CreateTable(TAG_TABLE_NAME)
+	CreateTable(ACLS_TABLE_NAME)
 }
 
 func CreateTable(tableName string) error {

+ 6 - 2
docker/Caddyfile

@@ -25,6 +25,10 @@ https://api.{$NM_DOMAIN} {
 }
 
 # MQ
-wss://broker.{$NM_DOMAIN} {
-	reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083`
+broker.{$NM_DOMAIN} {
+	@ws {
+			header Connection *Upgrade*
+			header Upgrade websocket
+		}
+	reverse_proxy @ws mq:8883   # For EMQX websockets use `reverse_proxy @ws mq:8083`
 }

+ 6 - 2
docker/Caddyfile-pro

@@ -40,6 +40,10 @@ https://api.{$NM_DOMAIN} {
 }
 
 # MQ
-wss://broker.{$NM_DOMAIN} {
-	reverse_proxy ws://mq:8883
+broker.{$NM_DOMAIN} {
+	@ws {
+			header Connection *Upgrade*
+			header Upgrade websocket
+		}
+	reverse_proxy @ws mq:8883
 }

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,4 +1,4 @@
-FROM golang:1.20.13-alpine3.19
+FROM golang:1.23.0-alpine3.20
 ARG version 
 RUN apk add build-base
 WORKDIR /app

+ 21 - 0
docker/emqx.conf

@@ -0,0 +1,21 @@
+authentication = [
+  {
+    backend = "built_in_database"
+    mechanism = "password_based"
+    password_hash_algorithm {
+      name = "sha256",
+      salt_position = "suffix"
+    }
+    user_id_type = "username"
+  }
+]
+authorization {
+  deny_action = ignore
+  no_match = allow
+  sources = [
+    {
+      type = built_in_database
+      enable = true
+    }
+  ]
+}

+ 1 - 1
docs/Authentication.md

@@ -7,4 +7,4 @@ Call the api/users/adm/authenticate endpoint (see documentation below for detail
 
 Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance.
 
-For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html).
+For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.io/).

+ 16 - 14
go.mod

@@ -1,26 +1,28 @@
 module github.com/gravitl/netmaker
 
-go 1.19
+go 1.23
 
 require (
+	github.com/blang/semver v3.5.1+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.22.0
-	github.com/golang-jwt/jwt/v4 v4.5.0
+	github.com/go-playground/validator/v10 v10.23.0
+	github.com/golang-jwt/jwt/v4 v4.5.1
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.22
+	github.com/mattn/go-sqlite3 v1.14.24
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
-	github.com/seancfoley/ipaddress-go v1.6.0
+	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.23.0
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/oauth2 v0.21.0
-	golang.org/x/sys v0.21.0 // indirect
-	golang.org/x/text v0.16.0 // indirect
+	go.uber.org/automaxprocs v1.6.0
+	golang.org/x/crypto v0.30.0
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/oauth2 v0.24.0
+	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -28,7 +30,7 @@ require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0
+	github.com/posthog/posthog-go v1.2.24
 )
 
 require (
@@ -39,6 +41,7 @@ require (
 
 require (
 	github.com/cilium/fake v0.6.1
+	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
@@ -55,7 +58,6 @@ require (
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 )
 
@@ -68,5 +70,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sync v0.10.0 // indirect
 )

+ 36 - 32
go.sum

@@ -2,14 +2,14 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
 cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/cilium/fake v0.6.1 h1:cLkNx1nkF0b0pPW79JaQxaI5oG2/rBzRKpp0YUg1fTA=
 github.com/cilium/fake v0.6.1/go.mod h1:V9lCbbcsnSf3vB6sdOP7Q0bsUUJ/jyHPZxnFAw5nPUc=
 github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
 github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -24,18 +24,21 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
-github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
-github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
+github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -49,6 +52,7 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -61,27 +65,28 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0 h1:Y2hUrkfuM0on62KZOci/VLijlkdF/yeWU262BQgvcjE=
-github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
+github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA=
+github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM=
+github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3EXGI=
 github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU=
-github.com/seancfoley/ipaddress-go v1.6.0 h1:9z7yGmOnV4P2ML/dlR/kCJiv5tp8iHOOetJvxJh/R5w=
-github.com/seancfoley/ipaddress-go v1.6.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/seancfoley/ipaddress-go v1.7.0 h1:vWp3SR3k+HkV3aKiNO2vEe6xbVxS0x/Ixw6hgyP238s=
+github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -90,19 +95,18 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
-github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
-github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
+golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -112,15 +116,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -129,8 +133,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -142,8 +146,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -155,9 +159,9 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

+ 1070 - 0
logic/acls.go

@@ -0,0 +1,1070 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var (
+	aclCacheMutex = &sync.RWMutex{}
+	aclCacheMap   = make(map[string]models.Acl)
+)
+
+func MigrateAclPolicies() {
+	acls := ListAcls()
+	for _, acl := range acls {
+		if acl.Proto.String() == "" {
+			acl.Proto = models.ALL
+			acl.ServiceType = models.Any
+			acl.Port = []string{}
+			UpsertAcl(acl)
+		}
+	}
+
+}
+
+// CreateDefaultAclNetworkPolicies - create default acl network policies
+func CreateDefaultAclNetworkPolicies(netID models.NetworkID) {
+	if netID.String() == "" {
+		return
+	}
+	_, _ = ListAclsByNetwork(netID)
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-nodes")) {
+		defaultDeviceAcl := models.Acl{
+			ID:          fmt.Sprintf("%s.%s", netID, "all-nodes"),
+			Name:        "All Nodes",
+			MetaData:    "This Policy allows all nodes in the network to communicate with each other",
+			Default:     true,
+			NetworkID:   netID,
+			Proto:       models.ALL,
+			ServiceType: models.Any,
+			Port:        []string{},
+			RuleType:    models.DevicePolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				}},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				}},
+			AllowedDirection: models.TrafficDirectionBi,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultDeviceAcl)
+	}
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-users")) {
+		defaultUserAcl := models.Acl{
+			ID:          fmt.Sprintf("%s.%s", netID, "all-users"),
+			Default:     true,
+			Name:        "All Users",
+			MetaData:    "This policy gives access to everything in the network for an user",
+			NetworkID:   netID,
+			Proto:       models.ALL,
+			ServiceType: models.Any,
+			Port:        []string{},
+			RuleType:    models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserAclID,
+					Value: "*",
+				},
+			},
+			Dst: []models.AclPolicyTag{{
+				ID:    models.DeviceAclID,
+				Value: "*",
+			}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultUserAcl)
+	}
+
+	if !IsAclExists(fmt.Sprintf("%s.%s", netID, "all-remote-access-gws")) {
+		defaultUserAcl := models.Acl{
+			ID:          fmt.Sprintf("%s.%s", netID, "all-remote-access-gws"),
+			Default:     true,
+			Name:        "All Remote Access Gateways",
+			NetworkID:   netID,
+			Proto:       models.ALL,
+			ServiceType: models.Any,
+			Port:        []string{},
+			RuleType:    models.DevicePolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+				},
+			},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.DeviceAclID,
+					Value: "*",
+				},
+			},
+			AllowedDirection: models.TrafficDirectionBi,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		InsertAcl(defaultUserAcl)
+	}
+	CreateDefaultUserPolicies(netID)
+}
+
+// DeleteDefaultNetworkPolicies - deletes all default network acl policies
+func DeleteDefaultNetworkPolicies(netId models.NetworkID) {
+	acls, _ := ListAclsByNetwork(netId)
+	for _, acl := range acls {
+		if acl.NetworkID == netId && acl.Default {
+			DeleteAcl(acl)
+		}
+	}
+}
+
+// ValidateCreateAclReq - validates create req for acl
+func ValidateCreateAclReq(req models.Acl) error {
+	// check if acl network exists
+	_, err := GetNetwork(req.NetworkID.String())
+	if err != nil {
+		return errors.New("failed to get network details for " + req.NetworkID.String())
+	}
+	// err = CheckIDSyntax(req.Name)
+	// if err != nil {
+	// 	return err
+	// }
+	return nil
+}
+
+func listAclFromCache() (acls []models.Acl) {
+	aclCacheMutex.RLock()
+	defer aclCacheMutex.RUnlock()
+	for _, acl := range aclCacheMap {
+		acls = append(acls, acl)
+	}
+	return
+}
+
+func storeAclInCache(a models.Acl) {
+	aclCacheMutex.Lock()
+	defer aclCacheMutex.Unlock()
+	aclCacheMap[a.ID] = a
+}
+
+func removeAclFromCache(a models.Acl) {
+	aclCacheMutex.Lock()
+	defer aclCacheMutex.Unlock()
+	delete(aclCacheMap, a.ID)
+}
+
+func getAclFromCache(aID string) (a models.Acl, ok bool) {
+	aclCacheMutex.RLock()
+	defer aclCacheMutex.RUnlock()
+	a, ok = aclCacheMap[aID]
+	return
+}
+
+// InsertAcl - creates acl policy
+func InsertAcl(a models.Acl) error {
+	d, err := json.Marshal(a)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(a.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(a)
+	}
+	return err
+}
+
+// GetAcl - gets acl info by id
+func GetAcl(aID string) (models.Acl, error) {
+	a := models.Acl{}
+	if servercfg.CacheEnabled() {
+		var ok bool
+		a, ok = getAclFromCache(aID)
+		if ok {
+			return a, nil
+		}
+	}
+	d, err := database.FetchRecord(database.ACLS_TABLE_NAME, aID)
+	if err != nil {
+		return a, err
+	}
+	err = json.Unmarshal([]byte(d), &a)
+	if err != nil {
+		return a, err
+	}
+	if servercfg.CacheEnabled() {
+		storeAclInCache(a)
+	}
+	return a, nil
+}
+
+// IsAclExists - checks if acl exists
+func IsAclExists(aclID string) bool {
+	_, err := GetAcl(aclID)
+	return err == nil
+}
+
+// IsAclPolicyValid - validates if acl policy is valid
+func IsAclPolicyValid(acl models.Acl) bool {
+	//check if src and dst are valid
+	if acl.AllowedDirection != models.TrafficDirectionBi &&
+		acl.AllowedDirection != models.TrafficDirectionUni {
+		return false
+	}
+	switch acl.RuleType {
+	case models.UserPolicy:
+		// src list should only contain users
+		for _, srcI := range acl.Src {
+
+			if srcI.ID == "" || srcI.Value == "" {
+				return false
+			}
+			if srcI.Value == "*" {
+				continue
+			}
+			if srcI.ID != models.UserAclID && srcI.ID != models.UserGroupAclID {
+				return false
+			}
+			// check if user group is valid
+			if srcI.ID == models.UserAclID {
+				_, err := GetUser(srcI.Value)
+				if err != nil {
+					return false
+				}
+
+			} else if srcI.ID == models.UserGroupAclID {
+				err := IsGroupValid(models.UserGroupID(srcI.Value))
+				if err != nil {
+					return false
+				}
+				// check if group belongs to this network
+				netGrps := GetUserGroupsInNetwork(acl.NetworkID)
+				if _, ok := netGrps[models.UserGroupID(srcI.Value)]; !ok {
+					return false
+				}
+			}
+
+		}
+		for _, dstI := range acl.Dst {
+
+			if dstI.ID == "" || dstI.Value == "" {
+				return false
+			}
+			if dstI.ID != models.DeviceAclID {
+				return false
+			}
+			if dstI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(dstI.Value))
+			if err != nil {
+				return false
+			}
+		}
+	case models.DevicePolicy:
+		for _, srcI := range acl.Src {
+			if srcI.ID == "" || srcI.Value == "" {
+				return false
+			}
+			if srcI.ID != models.DeviceAclID {
+				return false
+			}
+			if srcI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(srcI.Value))
+			if err != nil {
+				return false
+			}
+		}
+		for _, dstI := range acl.Dst {
+
+			if dstI.ID == "" || dstI.Value == "" {
+				return false
+			}
+			if dstI.ID != models.DeviceAclID {
+				return false
+			}
+			if dstI.Value == "*" {
+				continue
+			}
+			// check if tag is valid
+			_, err := GetTag(models.TagID(dstI.Value))
+			if err != nil {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+// UpdateAcl - updates allowed fields on acls and commits to DB
+func UpdateAcl(newAcl, acl models.Acl) error {
+	if !acl.Default {
+		acl.Name = newAcl.Name
+		acl.Src = newAcl.Src
+		acl.Dst = newAcl.Dst
+		acl.AllowedDirection = newAcl.AllowedDirection
+		acl.Port = newAcl.Port
+		acl.Proto = newAcl.Proto
+		acl.ServiceType = newAcl.ServiceType
+	}
+	if newAcl.ServiceType == models.Any {
+		acl.Port = []string{}
+		acl.Proto = models.ALL
+	}
+	acl.Enabled = newAcl.Enabled
+	d, err := json.Marshal(acl)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(acl)
+	}
+	return err
+}
+
+// UpsertAcl - upserts acl
+func UpsertAcl(acl models.Acl) error {
+	d, err := json.Marshal(acl)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(acl.ID, string(d), database.ACLS_TABLE_NAME)
+	if err == nil && servercfg.CacheEnabled() {
+		storeAclInCache(acl)
+	}
+	return err
+}
+
+// DeleteAcl - deletes acl policy
+func DeleteAcl(a models.Acl) error {
+	err := database.DeleteRecord(database.ACLS_TABLE_NAME, a.ID)
+	if err == nil && servercfg.CacheEnabled() {
+		removeAclFromCache(a)
+	}
+	return err
+}
+
+// GetDefaultPolicy - fetches default policy in the network by ruleType
+func GetDefaultPolicy(netID models.NetworkID, ruleType models.AclPolicyType) (models.Acl, error) {
+	aclID := "all-users"
+	if ruleType == models.DevicePolicy {
+		aclID = "all-nodes"
+	}
+	acl, err := GetAcl(fmt.Sprintf("%s.%s", netID, aclID))
+	if err != nil {
+		return models.Acl{}, errors.New("default rule not found")
+	}
+	if acl.Enabled {
+		return acl, nil
+	}
+	// check if there are any custom all policies
+	srcMap := make(map[string]struct{})
+	dstMap := make(map[string]struct{})
+	defer func() {
+		srcMap = nil
+		dstMap = nil
+	}()
+	policies, _ := ListAclsByNetwork(netID)
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		if policy.RuleType == ruleType {
+			dstMap = convAclTagToValueMap(policy.Dst)
+			srcMap = convAclTagToValueMap(policy.Src)
+			if _, ok := srcMap["*"]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return policy, nil
+				}
+			}
+		}
+
+	}
+
+	return acl, nil
+}
+
+func ListAcls() (acls []models.Acl) {
+	if servercfg.CacheEnabled() && len(aclCacheMap) > 0 {
+		return listAclFromCache()
+	}
+
+	data, err := database.FetchRecords(database.ACLS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Acl{}
+	}
+	for _, dataI := range data {
+		acl := models.Acl{}
+		err := json.Unmarshal([]byte(dataI), &acl)
+		if err != nil {
+			continue
+		}
+		acls = append(acls, acl)
+		if servercfg.CacheEnabled() {
+			storeAclInCache(acl)
+		}
+	}
+	return
+}
+
+// ListUserPolicies - lists all acl policies enforced on an user
+func ListUserPolicies(u models.User) []models.Acl {
+	allAcls := ListAcls()
+	userAcls := []models.Acl{}
+	for _, acl := range allAcls {
+
+		if acl.RuleType == models.UserPolicy {
+			srcMap := convAclTagToValueMap(acl.Src)
+			if _, ok := srcMap[u.UserName]; ok {
+				userAcls = append(userAcls, acl)
+			} else {
+				// check for user groups
+				for gID := range u.UserGroups {
+					if _, ok := srcMap[gID.String()]; ok {
+						userAcls = append(userAcls, acl)
+						break
+					}
+				}
+			}
+
+		}
+	}
+	return userAcls
+}
+
+// listPoliciesOfUser - lists all user acl policies applied to user in an network
+func listPoliciesOfUser(user models.User, netID models.NetworkID) []models.Acl {
+	allAcls := ListAcls()
+	userAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
+			srcMap := convAclTagToValueMap(acl.Src)
+			if _, ok := srcMap[user.UserName]; ok {
+				userAcls = append(userAcls, acl)
+				continue
+			}
+			for netRole := range user.NetworkRoles {
+				if _, ok := srcMap[netRole.String()]; ok {
+					userAcls = append(userAcls, acl)
+					continue
+				}
+			}
+			for userG := range user.UserGroups {
+				if _, ok := srcMap[userG.String()]; ok {
+					userAcls = append(userAcls, acl)
+					continue
+				}
+			}
+
+		}
+	}
+	return userAcls
+}
+
+// listDevicePolicies - lists all device policies in a network
+func listDevicePolicies(netID models.NetworkID) []models.Acl {
+	allAcls := ListAcls()
+	deviceAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.DevicePolicy {
+			deviceAcls = append(deviceAcls, acl)
+		}
+	}
+	return deviceAcls
+}
+
+// listUserPolicies - lists all user policies in a network
+func listUserPolicies(netID models.NetworkID) []models.Acl {
+	allAcls := ListAcls()
+	deviceAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID && acl.RuleType == models.UserPolicy {
+			deviceAcls = append(deviceAcls, acl)
+		}
+	}
+	return deviceAcls
+}
+
+// ListAcls - lists all acl policies
+func ListAclsByNetwork(netID models.NetworkID) ([]models.Acl, error) {
+
+	allAcls := ListAcls()
+	netAcls := []models.Acl{}
+	for _, acl := range allAcls {
+		if acl.NetworkID == netID {
+			netAcls = append(netAcls, acl)
+		}
+	}
+	return netAcls, nil
+}
+
+func convAclTagToValueMap(acltags []models.AclPolicyTag) map[string]struct{} {
+	aclValueMap := make(map[string]struct{})
+	for _, aclTagI := range acltags {
+		aclValueMap[aclTagI.Value] = struct{}{}
+	}
+	return aclValueMap
+}
+
+// IsUserAllowedToCommunicate - check if user is allowed to communicate with peer
+func IsUserAllowedToCommunicate(userName string, peer models.Node) (bool, []models.Acl) {
+	if peer.IsStatic {
+		peer = peer.StaticNode.ConvertToStaticNode()
+	}
+	acl, _ := GetDefaultPolicy(models.NetworkID(peer.Network), models.UserPolicy)
+	if acl.Enabled {
+		return true, []models.Acl{acl}
+	}
+	user, err := GetUser(userName)
+	if err != nil {
+		return false, []models.Acl{}
+	}
+	allowedPolicies := []models.Acl{}
+	policies := listPoliciesOfUser(*user, models.NetworkID(peer.Network))
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		dstMap := convAclTagToValueMap(policy.Dst)
+		if _, ok := dstMap["*"]; ok {
+			allowedPolicies = append(allowedPolicies, policy)
+			continue
+		}
+		for tagID := range peer.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				allowedPolicies = append(allowedPolicies, policy)
+				break
+			}
+		}
+
+	}
+	if len(allowedPolicies) > 0 {
+		return true, allowedPolicies
+	}
+	return false, []models.Acl{}
+}
+
+// IsPeerAllowed - checks if peer needs to be added to the interface
+func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
+	if node.IsStatic {
+		node = node.StaticNode.ConvertToStaticNode()
+	}
+	if peer.IsStatic {
+		peer = peer.StaticNode.ConvertToStaticNode()
+	}
+	if checkDefaultPolicy {
+		// check default policy if all allowed return true
+		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+		if err == nil {
+			if defaultPolicy.Enabled {
+				return true
+			}
+		}
+	}
+	// list device policies
+	policies := listDevicePolicies(models.NetworkID(peer.Network))
+	srcMap := make(map[string]struct{})
+	dstMap := make(map[string]struct{})
+	defer func() {
+		srcMap = nil
+		dstMap = nil
+	}()
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		srcMap = convAclTagToValueMap(policy.Src)
+		dstMap = convAclTagToValueMap(policy.Dst)
+		for tagID := range node.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				if _, ok := srcMap["*"]; ok {
+					return true
+				}
+				for tagID := range peer.Tags {
+					if _, ok := srcMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+			if _, ok := srcMap[tagID.String()]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return true
+				}
+				for tagID := range peer.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+		}
+		for tagID := range peer.Tags {
+			if _, ok := dstMap[tagID.String()]; ok {
+				if _, ok := srcMap["*"]; ok {
+					return true
+				}
+				for tagID := range node.Tags {
+
+					if _, ok := srcMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+			if _, ok := srcMap[tagID.String()]; ok {
+				if _, ok := dstMap["*"]; ok {
+					return true
+				}
+				for tagID := range node.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						return true
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
+// IsNodeAllowedToCommunicate - check node is allowed to communicate with the peer
+func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool) (bool, []models.Acl) {
+	if node.IsStatic {
+		node = node.StaticNode.ConvertToStaticNode()
+	}
+	if peer.IsStatic {
+		peer = peer.StaticNode.ConvertToStaticNode()
+	}
+	if checkDefaultPolicy {
+		// check default policy if all allowed return true
+		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+		if err == nil {
+			if defaultPolicy.Enabled {
+				return true, []models.Acl{defaultPolicy}
+			}
+		}
+	}
+	allowedPolicies := []models.Acl{}
+	// list device policies
+	policies := listDevicePolicies(models.NetworkID(peer.Network))
+	srcMap := make(map[string]struct{})
+	dstMap := make(map[string]struct{})
+	defer func() {
+		srcMap = nil
+		dstMap = nil
+	}()
+	for _, policy := range policies {
+		if !policy.Enabled {
+			continue
+		}
+		srcMap = convAclTagToValueMap(policy.Src)
+		dstMap = convAclTagToValueMap(policy.Dst)
+		for tagID := range node.Tags {
+			allowed := false
+			if _, ok := dstMap[tagID.String()]; policy.AllowedDirection == models.TrafficDirectionBi && ok {
+				if _, ok := srcMap["*"]; ok {
+					allowed = true
+					allowedPolicies = append(allowedPolicies, policy)
+					break
+				}
+				for tagID := range peer.Tags {
+					if _, ok := srcMap[tagID.String()]; ok {
+						allowed = true
+						break
+					}
+				}
+			}
+			if allowed {
+				allowedPolicies = append(allowedPolicies, policy)
+				break
+			}
+			if _, ok := srcMap[tagID.String()]; ok {
+				if _, ok := dstMap["*"]; ok {
+					allowed = true
+					allowedPolicies = append(allowedPolicies, policy)
+					break
+				}
+				for tagID := range peer.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						allowed = true
+						break
+					}
+				}
+			}
+			if allowed {
+				allowedPolicies = append(allowedPolicies, policy)
+				break
+			}
+		}
+		for tagID := range peer.Tags {
+			allowed := false
+			if _, ok := dstMap[tagID.String()]; ok {
+				if _, ok := srcMap["*"]; ok {
+					allowed = true
+					allowedPolicies = append(allowedPolicies, policy)
+					break
+				}
+				for tagID := range node.Tags {
+
+					if _, ok := srcMap[tagID.String()]; ok {
+						allowed = true
+						break
+					}
+				}
+			}
+			if allowed {
+				allowedPolicies = append(allowedPolicies, policy)
+				break
+			}
+
+			if _, ok := srcMap[tagID.String()]; policy.AllowedDirection == models.TrafficDirectionBi && ok {
+				if _, ok := dstMap["*"]; ok {
+					allowed = true
+					allowedPolicies = append(allowedPolicies, policy)
+					break
+				}
+				for tagID := range node.Tags {
+					if _, ok := dstMap[tagID.String()]; ok {
+						allowed = true
+						break
+					}
+				}
+			}
+			if allowed {
+				allowedPolicies = append(allowedPolicies, policy)
+				break
+			}
+		}
+	}
+
+	if len(allowedPolicies) > 0 {
+		return true, allowedPolicies
+	}
+	return false, allowedPolicies
+}
+
+// SortTagEntrys - Sorts slice of Tag entries by their id
+func SortAclEntrys(acls []models.Acl) {
+	sort.Slice(acls, func(i, j int) bool {
+		return acls[i].Name < acls[j].Name
+	})
+}
+
+// UpdateDeviceTag - updates device tag on acl policies
+func UpdateDeviceTag(OldID, newID models.TagID, netID models.NetworkID) {
+	acls := listDevicePolicies(netID)
+	update := false
+	for _, acl := range acls {
+		for i, srcTagI := range acl.Src {
+			if srcTagI.ID == models.DeviceAclID {
+				if OldID.String() == srcTagI.Value {
+					acl.Src[i].Value = newID.String()
+					update = true
+				}
+			}
+		}
+		for i, dstTagI := range acl.Dst {
+			if dstTagI.ID == models.DeviceAclID {
+				if OldID.String() == dstTagI.Value {
+					acl.Dst[i].Value = newID.String()
+					update = true
+				}
+			}
+		}
+		if update {
+			UpsertAcl(acl)
+		}
+	}
+}
+
+func CheckIfTagAsActivePolicy(tagID models.TagID, netID models.NetworkID) bool {
+	acls := listDevicePolicies(netID)
+	for _, acl := range acls {
+		for _, srcTagI := range acl.Src {
+			if srcTagI.ID == models.DeviceAclID {
+				if tagID.String() == srcTagI.Value {
+					return true
+				}
+			}
+		}
+		for _, dstTagI := range acl.Dst {
+			if dstTagI.ID == models.DeviceAclID {
+				if tagID.String() == dstTagI.Value {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
+// RemoveDeviceTagFromAclPolicies - remove device tag from acl policies
+func RemoveDeviceTagFromAclPolicies(tagID models.TagID, netID models.NetworkID) error {
+	acls := listDevicePolicies(netID)
+	update := false
+	for _, acl := range acls {
+		for i, srcTagI := range acl.Src {
+			if srcTagI.ID == models.DeviceAclID {
+				if tagID.String() == srcTagI.Value {
+					acl.Src = append(acl.Src[:i], acl.Src[i+1:]...)
+					update = true
+				}
+			}
+		}
+		for i, dstTagI := range acl.Dst {
+			if dstTagI.ID == models.DeviceAclID {
+				if tagID.String() == dstTagI.Value {
+					acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...)
+					update = true
+				}
+			}
+		}
+		if update {
+			UpsertAcl(acl)
+		}
+	}
+	return nil
+}
+
+func getUserAclRulesForNode(targetnode *models.Node,
+	rules map[string]models.AclRule) map[string]models.AclRule {
+	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(targetnode.Network))
+	userGrpMap := GetUserGrpMap()
+	allowedUsers := make(map[string][]models.Acl)
+	acls := listUserPolicies(models.NetworkID(targetnode.Network))
+	for nodeTag := range targetnode.Tags {
+		for _, acl := range acls {
+			if !acl.Enabled {
+				continue
+			}
+			dstTags := convAclTagToValueMap(acl.Dst)
+			if _, ok := dstTags[nodeTag.String()]; ok {
+				// get all src tags
+				for _, srcAcl := range acl.Src {
+					if srcAcl.ID == models.UserAclID {
+						allowedUsers[srcAcl.Value] = append(allowedUsers[srcAcl.Value], acl)
+					} else if srcAcl.ID == models.UserGroupAclID {
+						// fetch all users in the group
+						if usersMap, ok := userGrpMap[models.UserGroupID(srcAcl.Value)]; ok {
+							for userName := range usersMap {
+								allowedUsers[userName] = append(allowedUsers[userName], acl)
+							}
+						}
+					}
+				}
+
+			}
+		}
+	}
+	for _, userNode := range userNodes {
+		if !userNode.StaticNode.Enabled {
+			continue
+		}
+		acls, ok := allowedUsers[userNode.StaticNode.OwnerID]
+		if !ok {
+			continue
+		}
+		for _, acl := range acls {
+
+			if !acl.Enabled {
+				continue
+			}
+
+			r := models.AclRule{
+				ID:              acl.ID,
+				AllowedProtocol: acl.Proto,
+				AllowedPorts:    acl.Port,
+				Direction:       acl.AllowedDirection,
+				Allowed:         true,
+			}
+			// Get peers in the tags and add allowed rules
+			if userNode.StaticNode.Address != "" {
+				r.IPList = append(r.IPList, userNode.StaticNode.AddressIPNet4())
+			}
+			if userNode.StaticNode.Address6 != "" {
+				r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
+			}
+			if aclRule, ok := rules[acl.ID]; ok {
+				aclRule.IPList = append(aclRule.IPList, r.IPList...)
+				aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
+				rules[acl.ID] = aclRule
+			} else {
+				rules[acl.ID] = r
+			}
+		}
+	}
+	return rules
+}
+
+func GetAclRulesForNode(targetnode *models.Node) (rules map[string]models.AclRule) {
+	defer func() {
+		if !targetnode.IsIngressGateway {
+			rules = getUserAclRulesForNode(targetnode, rules)
+		}
+
+	}()
+	rules = make(map[string]models.AclRule)
+	var taggedNodes map[models.TagID][]models.Node
+	if targetnode.IsIngressGateway {
+		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), false)
+	} else {
+		taggedNodes = GetTagMapWithNodesByNetwork(models.NetworkID(targetnode.Network), true)
+	}
+
+	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
+	targetnode.Tags["*"] = struct{}{}
+	for nodeTag := range targetnode.Tags {
+		for _, acl := range acls {
+			if !acl.Enabled {
+				continue
+			}
+			srcTags := convAclTagToValueMap(acl.Src)
+			dstTags := convAclTagToValueMap(acl.Dst)
+			aclRule := models.AclRule{
+				ID:              acl.ID,
+				AllowedProtocol: acl.Proto,
+				AllowedPorts:    acl.Port,
+				Direction:       acl.AllowedDirection,
+				Allowed:         true,
+			}
+			if acl.AllowedDirection == models.TrafficDirectionBi {
+				var existsInSrcTag bool
+				var existsInDstTag bool
+
+				if _, ok := srcTags[nodeTag.String()]; ok {
+					existsInSrcTag = true
+				}
+				if _, ok := dstTags[nodeTag.String()]; ok {
+					existsInDstTag = true
+				}
+
+				if existsInSrcTag && !existsInDstTag {
+					// get all dst tags
+					for dst := range dstTags {
+						if dst == nodeTag.String() {
+							continue
+						}
+						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(dst)]
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
+						}
+					}
+				}
+				if existsInDstTag && !existsInSrcTag {
+					// get all src tags
+					for src := range srcTags {
+						if src == nodeTag.String() {
+							continue
+						}
+						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(src)]
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
+						}
+					}
+				}
+				if existsInDstTag && existsInSrcTag {
+					nodes := taggedNodes[nodeTag]
+					for _, node := range nodes {
+						if node.ID == targetnode.ID {
+							continue
+						}
+						if node.Address.IP != nil {
+							aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+						}
+						if node.Address6.IP != nil {
+							aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+						}
+						if node.IsStatic && node.StaticNode.Address != "" {
+							aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+						}
+						if node.IsStatic && node.StaticNode.Address6 != "" {
+							aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+						}
+					}
+				}
+			} else {
+				_, all := dstTags["*"]
+				if _, ok := dstTags[nodeTag.String()]; ok || all {
+					// get all src tags
+					for src := range srcTags {
+						if src == nodeTag.String() {
+							continue
+						}
+						// Get peers in the tags and add allowed rules
+						nodes := taggedNodes[models.TagID(src)]
+						for _, node := range nodes {
+							if node.ID == targetnode.ID {
+								continue
+							}
+							if node.Address.IP != nil {
+								aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
+							}
+							if node.Address6.IP != nil {
+								aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
+							}
+							if node.IsStatic && node.StaticNode.Address != "" {
+								aclRule.IPList = append(aclRule.IPList, node.StaticNode.AddressIPNet4())
+							}
+							if node.IsStatic && node.StaticNode.Address6 != "" {
+								aclRule.IP6List = append(aclRule.IP6List, node.StaticNode.AddressIPNet6())
+							}
+						}
+					}
+				}
+			}
+			if len(aclRule.IPList) > 0 || len(aclRule.IP6List) > 0 {
+				rules[acl.ID] = aclRule
+			}
+		}
+	}
+	return rules
+}

+ 3 - 2
logic/acls/common.go

@@ -2,6 +2,7 @@ package acls
 
 import (
 	"encoding/json"
+	"maps"
 	"sync"
 
 	"github.com/gravitl/netmaker/database"
@@ -133,7 +134,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) {
 	defer AclMutex.RUnlock()
 	if servercfg.CacheEnabled() {
 		if aclContainer, ok := fetchAclContainerFromCache(containerID); ok {
-			return aclContainer, nil
+			return maps.Clone(aclContainer), nil
 		}
 	}
 	aclJson, err := fetchACLContainerJson(ContainerID(containerID))
@@ -147,7 +148,7 @@ func fetchACLContainer(containerID ContainerID) (ACLContainer, error) {
 	if servercfg.CacheEnabled() {
 		storeAclContainerInCache(containerID, currentNetworkACL)
 	}
-	return currentNetworkACL, nil
+	return maps.Clone(currentNetworkACL), nil
 }
 
 // fetchACLContainerJson - fetch the current ACL of given container except in json string

+ 3 - 0
logic/acls/nodeacls/modify.go

@@ -25,6 +25,9 @@ func CreateNodeACL(networkID NetworkID, nodeID NodeID, defaultVal byte) (acls.AC
 	acls.AclMutex.Lock()
 	var newNodeACL = make(acls.ACL)
 	for existingNodeID := range currentNetworkACL {
+		if currentNetworkACL[existingNodeID] == nil {
+			currentNetworkACL[existingNodeID] = make(acls.ACL)
+		}
 		currentNetworkACL[existingNodeID][acls.AclID(nodeID)] = defaultVal // set the old nodes to default value for new node
 		newNodeACL[existingNodeID] = defaultVal                            // set the old nodes in new node ACL to default value
 	}

+ 6 - 1
logic/acls/nodeacls/retrieve.go

@@ -3,15 +3,20 @@ package nodeacls
 import (
 	"encoding/json"
 	"fmt"
+	"maps"
 	"sync"
 
 	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 var NodesAllowedACLMutex = &sync.Mutex{}
 
 // AreNodesAllowed - checks if nodes are allowed to communicate in their network ACL
 func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
+	if !servercfg.IsOldAclEnabled() {
+		return true
+	}
 	NodesAllowedACLMutex.Lock()
 	defer NodesAllowedACLMutex.Unlock()
 	var currentNetworkACL, err = FetchAllACLs(networkID)
@@ -67,5 +72,5 @@ func FetchAllACLs(networkID NetworkID) (acls.ACLContainer, error) {
 	if err != nil {
 		return nil, err
 	}
-	return currentNetworkACL, nil
+	return maps.Clone(currentNetworkACL), nil
 }

+ 2 - 1
logic/auth.go

@@ -186,7 +186,7 @@ func CreateUser(user *models.User) error {
 		logger.Log(0, "failed to insert user", err.Error())
 		return err
 	}
-
+	AddGlobalNetRolesToAdmins(*user)
 	return nil
 }
 
@@ -305,6 +305,7 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 	}
 	user.UserGroups = userchange.UserGroups
 	user.NetworkRoles = userchange.NetworkRoles
+	AddGlobalNetRolesToAdmins(*user)
 	err := ValidateUser(user)
 	if err != nil {
 		return &models.User{}, err

+ 1 - 1
logic/clients.go

@@ -32,7 +32,7 @@ var (
 			slog.Error("failed to get network acls", "error", err)
 			return err
 		}
-		networkAcls[acls.AclID(ec.ClientID)] = acls.ACL{}
+		networkAcls[acls.AclID(ec.ClientID)] = make(acls.ACL)
 		for objId := range networkAcls {
 			networkAcls[objId][acls.AclID(ec.ClientID)] = acls.Allowed
 			networkAcls[acls.AclID(ec.ClientID)][objId] = acls.Allowed

+ 17 - 3
logic/dns.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"os"
 	"regexp"
@@ -11,6 +12,7 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 	"github.com/txn2/txeh"
 )
 
@@ -104,7 +106,7 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 	if err != nil {
 		return dns, err
 	}
-
+	defaultDomain := servercfg.GetDefaultDomain()
 	for _, node := range nodes {
 		if node.Network != network {
 			continue
@@ -114,7 +116,11 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 			continue
 		}
 		var entry = models.DNSEntry{}
-		entry.Name = fmt.Sprintf("%s.%s", host.Name, network)
+		if defaultDomain == "" {
+			entry.Name = fmt.Sprintf("%s.%s", host.Name, network)
+		} else {
+			entry.Name = fmt.Sprintf("%s.%s.%s", host.Name, network, defaultDomain)
+		}
 		entry.Network = network
 		if node.Address.IP != nil {
 			entry.Address = node.Address.IP.String()
@@ -224,9 +230,17 @@ func SortDNSEntrys(unsortedDNSEntrys []models.DNSEntry) {
 	})
 }
 
+// IsNetworkNameValid - checks if a netid of a network uses valid characters
+func IsDNSEntryValid(d string) bool {
+	re := regexp.MustCompile(`^[A-Za-z0-9-.]+$`)
+	return re.MatchString(d)
+}
+
 // ValidateDNSCreate - checks if an entry is valid
 func ValidateDNSCreate(entry models.DNSEntry) error {
-
+	if !IsDNSEntryValid(entry.Name) {
+		return errors.New("invalid input. Only uppercase letters (A-Z), lowercase letters (a-z), numbers (0-9), minus sign (-) and dots (.) are allowed")
+	}
 	v := validator.New()
 
 	_ = v.RegisterValidation("whitespace", func(f1 validator.FieldLevel) bool {

+ 30 - 5
logic/enrollmentkey.go

@@ -37,7 +37,7 @@ var (
 )
 
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool, relay uuid.UUID) (*models.EnrollmentKey, error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey bool) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 		return nil, err
@@ -51,6 +51,8 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 		Tags:          []string{},
 		Type:          models.Undefined,
 		Relay:         relay,
+		Groups:        groups,
+		Default:       defaultKey,
 	}
 	if uses > 0 {
 		k.UsesRemaining = uses
@@ -89,7 +91,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 }
 
 // UpdateEnrollmentKey - updates an existing enrollment key's associated relay
-func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey, error) {
+func UpdateEnrollmentKey(keyId string, relayId uuid.UUID, groups []models.TagID) (*models.EnrollmentKey, error) {
 	key, err := GetEnrollmentKey(keyId)
 	if err != nil {
 		return nil, err
@@ -109,7 +111,7 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey
 	}
 
 	key.Relay = relayId
-
+	key.Groups = groups
 	if err = upsertEnrollmentKey(&key); err != nil {
 		return nil, err
 	}
@@ -151,11 +153,14 @@ func deleteEnrollmentkeyFromCache(key string) {
 }
 
 // DeleteEnrollmentKey - delete's a given enrollment key by value
-func DeleteEnrollmentKey(value string) error {
-	_, err := GetEnrollmentKey(value)
+func DeleteEnrollmentKey(value string, force bool) error {
+	key, err := GetEnrollmentKey(value)
 	if err != nil {
 		return err
 	}
+	if key.Default && !force {
+		return errors.New("cannot delete default network key")
+	}
 	err = database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value)
 	if err == nil {
 		if servercfg.CacheEnabled() {
@@ -310,3 +315,23 @@ func getEnrollmentKeysMap() (map[string]models.EnrollmentKey, error) {
 	}
 	return currentKeys, nil
 }
+
+func RemoveTagFromEnrollmentKeys(deletedTagID models.TagID) {
+	keys, _ := GetAllEnrollmentKeys()
+	for _, key := range keys {
+		newTags := []models.TagID{}
+		update := false
+		for _, tagID := range key.Groups {
+			if tagID == deletedTagID {
+				update = true
+				continue
+			}
+			newTags = append(newTags, tagID)
+		}
+		if update {
+			key.Groups = newTags
+			upsertEnrollmentKey(&key)
+		}
+
+	}
+}

+ 15 - 15
logic/enrollmentkey_test.go

@@ -14,35 +14,35 @@ 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, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
 		assert.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.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, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.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, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
 		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, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 		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, uuid.Nil)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
@@ -62,10 +62,10 @@ func TestCreateEnrollmentKey(t *testing.T) {
 func TestDelete_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
-		err := DeleteEnrollmentKey(newKey.Value)
+		err := DeleteEnrollmentKey(newKey.Value, false)
 		assert.Nil(t, err)
 		oldKey, err := GetEnrollmentKey(newKey.Value)
 		assert.Equal(t, oldKey, models.EnrollmentKey{})
@@ -73,7 +73,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
 	})
 	t.Run("Can_Not_Delete_Invalid_Key", func(t *testing.T) {
-		err := DeleteEnrollmentKey("notakey")
+		err := DeleteEnrollmentKey("notakey", false)
 		assert.NotNil(t, err)
 		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
 	})
@@ -83,7 +83,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 func TestDecrement_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -107,9 +107,9 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 func TestUsability_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, false, uuid.Nil)
-	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, false, uuid.Nil)
-	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, true, uuid.Nil)
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -145,7 +145,7 @@ func removeAllEnrollments() {
 func TestTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
@@ -178,7 +178,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, true, uuid.Nil)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 

+ 430 - 4
logic/extpeers.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"reflect"
@@ -9,6 +10,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/goombaio/namegenerator"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls"
@@ -69,7 +71,7 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 			}
 		}
 	}
-	extclients := GetGwExtclients(client.IngressGatewayID, client.Network)
+	extclients, _ := GetNetworkExtClients(client.Network)
 	for _, extclient := range extclients {
 		if extclient.ClientID == client.ClientID {
 			continue
@@ -136,6 +138,12 @@ func DeleteExtClientAndCleanup(extClient models.ExtClient) error {
 	return nil
 }
 
+//TODO - enforce extclient-to-extclient on ingress gw
+/* 1. fetch all non-user static nodes
+a. check against each user node, if allowed add rule
+
+*/
+
 // GetNetworkExtClients - gets the ext clients of given network
 func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 	var extclients []models.ExtClient
@@ -275,13 +283,41 @@ func CreateExtClient(extclient *models.ExtClient) error {
 	}
 
 	if extclient.ClientID == "" {
-		extclient.ClientID = models.GenerateNodeName()
+		extclient.ClientID, err = GenerateNodeName(extclient.Network)
+		if err != nil {
+			return err
+		}
 	}
 
 	extclient.LastModified = time.Now().Unix()
 	return SaveExtClient(extclient)
 }
 
+// GenerateNodeName - generates a random node name
+func GenerateNodeName(network string) (string, error) {
+	seed := time.Now().UTC().UnixNano()
+	nameGenerator := namegenerator.NewNameGenerator(seed)
+	var name string
+	cnt := 0
+	for {
+		if cnt > 10 {
+			return "", errors.New("couldn't generate random name, try again")
+		}
+		cnt += 1
+		name = nameGenerator.Generate()
+		if len(name) > 15 {
+			continue
+		}
+		_, err := GetExtClient(name, network)
+		if err == nil {
+			// config exists with same name
+			continue
+		}
+		break
+	}
+	return name, nil
+}
+
 // SaveExtClient - saves an ext client to database
 func SaveExtClient(extclient *models.ExtClient) error {
 	key, err := GetRecordKey(extclient.ClientID, extclient.Network)
@@ -329,6 +365,7 @@ func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) mode
 	// replace any \r\n with \n in postup and postdown from HTTP request
 	new.PostUp = strings.Replace(update.PostUp, "\r\n", "\n", -1)
 	new.PostDown = strings.Replace(update.PostDown, "\r\n", "\n", -1)
+	new.Tags = update.Tags
 	return new
 }
 
@@ -395,6 +432,293 @@ func ToggleExtClientConnectivity(client *models.ExtClient, enable bool) (models.
 	return newClient, nil
 }
 
+func GetStaticNodeIps(node models.Node) (ips []net.IP) {
+	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+
+	extclients := GetStaticNodesByNetwork(models.NetworkID(node.Network), false)
+	for _, extclient := range extclients {
+		if extclient.IsUserNode && defaultUserPolicy.Enabled {
+			continue
+		}
+		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
+			continue
+		}
+		if extclient.StaticNode.Address != "" {
+			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
+		}
+		if extclient.StaticNode.Address6 != "" {
+			ips = append(ips, extclient.StaticNode.AddressIPNet6().IP)
+		}
+	}
+	return
+}
+
+func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
+	// fetch user access to static clients via policies
+	defer func() {
+		logger.Log(0, fmt.Sprintf("node.ID: %s, Rules: %+v\n", node.ID, rules))
+	}()
+
+	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+	nodes, _ := GetNetworkNodes(node.Network)
+	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
+	//fmt.Printf("=====> NODES: %+v \n\n", nodes)
+	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
+	//fmt.Printf("=====> USER NODES %+v \n\n", userNodes)
+	for _, userNodeI := range userNodes {
+		for _, peer := range nodes {
+			if peer.IsUserNode {
+				continue
+			}
+			if ok, allowedPolicies := IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer); ok {
+				if peer.IsStatic {
+					if userNodeI.StaticNode.Address != "" {
+						if !defaultUserPolicy.Enabled {
+							for _, policy := range allowedPolicies {
+								rules = append(rules, models.FwRule{
+									SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+									DstIP:           peer.StaticNode.AddressIPNet4(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+								rules = append(rules, models.FwRule{
+									SrcIP:           peer.StaticNode.AddressIPNet4(),
+									DstIP:           userNodeI.StaticNode.AddressIPNet4(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+
+					}
+					if userNodeI.StaticNode.Address6 != "" {
+						if !defaultUserPolicy.Enabled {
+							for _, policy := range allowedPolicies {
+								rules = append(rules, models.FwRule{
+									SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+									DstIP:           peer.StaticNode.AddressIPNet6(),
+									Allow:           true,
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+								})
+								rules = append(rules, models.FwRule{
+									SrcIP:           peer.StaticNode.AddressIPNet6(),
+									DstIP:           userNodeI.StaticNode.AddressIPNet6(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+
+							}
+						}
+
+					}
+					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
+						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
+							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+							if err != nil {
+								continue
+							}
+							if ipNet.IP.To4() != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet4(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							} else {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet6(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							}
+
+						}
+
+					}
+				} else {
+
+					if userNodeI.StaticNode.Address != "" {
+						if !defaultUserPolicy.Enabled {
+							for _, policy := range allowedPolicies {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet4(),
+									DstIP: net.IPNet{
+										IP:   peer.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+
+						}
+					}
+
+					if userNodeI.StaticNode.Address6 != "" {
+						if !defaultUserPolicy.Enabled {
+							for _, policy := range allowedPolicies {
+								rules = append(rules, models.FwRule{
+									SrcIP: userNodeI.StaticNode.AddressIPNet6(),
+									DstIP: net.IPNet{
+										IP:   peer.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	if defaultDevicePolicy.Enabled {
+		return
+	}
+	for _, nodeI := range nodes {
+		if !nodeI.IsStatic || nodeI.IsUserNode {
+			continue
+		}
+		for _, peer := range nodes {
+			if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode {
+				continue
+			}
+			if ok, allowedPolicies := IsNodeAllowedToCommunicate(nodeI, peer, true); ok {
+				if peer.IsStatic {
+					if nodeI.StaticNode.Address != "" {
+						for _, policy := range allowedPolicies {
+							rules = append(rules, models.FwRule{
+								SrcIP:           nodeI.StaticNode.AddressIPNet4(),
+								DstIP:           peer.StaticNode.AddressIPNet4(),
+								AllowedProtocol: policy.Proto,
+								AllowedPorts:    policy.Port,
+								Allow:           true,
+							})
+							if policy.AllowedDirection == models.TrafficDirectionBi {
+								rules = append(rules, models.FwRule{
+									SrcIP:           peer.StaticNode.AddressIPNet4(),
+									DstIP:           nodeI.StaticNode.AddressIPNet4(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+
+					}
+					if nodeI.StaticNode.Address6 != "" {
+						for _, policy := range allowedPolicies {
+							rules = append(rules, models.FwRule{
+								SrcIP:           nodeI.StaticNode.AddressIPNet6(),
+								DstIP:           peer.StaticNode.AddressIPNet6(),
+								AllowedProtocol: policy.Proto,
+								AllowedPorts:    policy.Port,
+								Allow:           true,
+							})
+							if policy.AllowedDirection == models.TrafficDirectionBi {
+								rules = append(rules, models.FwRule{
+									SrcIP:           peer.StaticNode.AddressIPNet6(),
+									DstIP:           nodeI.StaticNode.AddressIPNet6(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+					}
+					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
+						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
+							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+							if err != nil {
+								continue
+							}
+							if ipNet.IP.To4() != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: nodeI.StaticNode.AddressIPNet4(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							} else {
+								rules = append(rules, models.FwRule{
+									SrcIP: nodeI.StaticNode.AddressIPNet6(),
+									DstIP: *ipNet,
+									Allow: true,
+								})
+							}
+
+						}
+
+					}
+				} else {
+					if nodeI.StaticNode.Address != "" {
+						for _, policy := range allowedPolicies {
+							rules = append(rules, models.FwRule{
+								SrcIP: nodeI.StaticNode.AddressIPNet4(),
+								DstIP: net.IPNet{
+									IP:   peer.Address.IP,
+									Mask: net.CIDRMask(32, 32),
+								},
+								AllowedProtocol: policy.Proto,
+								AllowedPorts:    policy.Port,
+								Allow:           true,
+							})
+							if policy.AllowedDirection == models.TrafficDirectionBi {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   peer.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									DstIP:           nodeI.StaticNode.AddressIPNet4(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+					}
+					if nodeI.StaticNode.Address6 != "" {
+						for _, policy := range allowedPolicies {
+							rules = append(rules, models.FwRule{
+								SrcIP: nodeI.StaticNode.AddressIPNet6(),
+								DstIP: net.IPNet{
+									IP:   peer.Address6.IP,
+									Mask: net.CIDRMask(128, 128),
+								},
+								AllowedProtocol: policy.Proto,
+								AllowedPorts:    policy.Port,
+								Allow:           true,
+							})
+							if policy.AllowedDirection == models.TrafficDirectionBi {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   peer.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									DstIP:           nodeI.StaticNode.AddressIPNet6(),
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						}
+					}
+				}
+
+			}
+		}
+	}
+	return
+}
+
 func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) {
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
@@ -412,6 +736,16 @@ func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandA
 		if !IsClientNodeAllowed(&extPeer, peer.ID.String()) {
 			continue
 		}
+		if extPeer.RemoteAccessClientID == "" {
+			if ok := IsPeerAllowed(extPeer.ConvertToStaticNode(), *peer, true); !ok {
+				continue
+			}
+		} else {
+			if ok, _ := IsUserAllowedToCommunicate(extPeer.OwnerID, *peer); !ok {
+				continue
+			}
+		}
+
 		pubkey, err := wgtypes.ParseKey(extPeer.PublicKey)
 		if err != nil {
 			logger.Log(1, "error parsing ext pub key:", err.Error())
@@ -483,8 +817,35 @@ func getExtPeerEgressRoute(node models.Node, extPeer models.ExtClient) (egressRo
 	return
 }
 
-func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []models.EgressNetworkRoutes) {
-	extPeers, err := GetNetworkExtClients(network)
+func getExtpeerEgressRanges(node models.Node) (ranges, ranges6 []net.IPNet) {
+	extPeers, err := GetNetworkExtClients(node.Network)
+	if err != nil {
+		return
+	}
+	for _, extPeer := range extPeers {
+		if len(extPeer.ExtraAllowedIPs) == 0 {
+			continue
+		}
+		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
+			continue
+		}
+		for _, allowedRange := range extPeer.ExtraAllowedIPs {
+			_, ipnet, err := net.ParseCIDR(allowedRange)
+			if err == nil {
+				if ipnet.IP.To4() != nil {
+					ranges = append(ranges, *ipnet)
+				} else {
+					ranges6 = append(ranges6, *ipnet)
+				}
+
+			}
+		}
+	}
+	return
+}
+
+func getExtpeersExtraRoutes(node models.Node) (egressRoutes []models.EgressNetworkRoutes) {
+	extPeers, err := GetNetworkExtClients(node.Network)
 	if err != nil {
 		return
 	}
@@ -492,6 +853,9 @@ func getExtpeersExtraRoutes(node models.Node, network string) (egressRoutes []mo
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 		}
+		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
+			continue
+		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...)
 	}
 	return
@@ -528,3 +892,65 @@ func GetExtclientAllowedIPs(client models.ExtClient) (allowedIPs []string) {
 	}
 	return
 }
+
+func GetStaticUserNodesByNetwork(network models.NetworkID) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.Network == network.String() {
+			if extI.RemoteAccessClientID != "" {
+				n := models.Node{
+					IsStatic:   true,
+					StaticNode: extI,
+					IsUserNode: extI.RemoteAccessClientID != "",
+				}
+				staticNode = append(staticNode, n)
+			}
+		}
+	}
+
+	return
+}
+
+func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	SortExtClient(extClients[:])
+	for _, extI := range extClients {
+		if extI.Network == network.String() {
+			if onlyWg && extI.RemoteAccessClientID != "" {
+				continue
+			}
+			n := models.Node{
+				IsStatic:   true,
+				StaticNode: extI,
+				IsUserNode: extI.RemoteAccessClientID != "",
+			}
+			staticNode = append(staticNode, n)
+		}
+	}
+
+	return
+}
+
+func GetStaticNodesByGw(gwNode models.Node) (staticNode []models.Node) {
+	extClients, err := GetAllExtClients()
+	if err != nil {
+		return
+	}
+	for _, extI := range extClients {
+		if extI.IngressGatewayID == gwNode.ID.String() {
+			n := models.Node{
+				IsStatic:   true,
+				StaticNode: extI,
+				IsUserNode: extI.RemoteAccessClientID != "",
+			}
+			staticNode = append(staticNode, n)
+		}
+	}
+	return
+}

+ 16 - 33
logic/gateway.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"errors"
+	"fmt"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -74,7 +75,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 		return models.Node{}, errors.New(host.OS + " is unsupported for egress gateways")
 	}
 	if host.FirewallInUse == models.FIREWALL_NONE {
-		return models.Node{}, errors.New("firewall is not supported for egress gateways. please install iptables or nftables on the device in order to use this feature")
+		return models.Node{}, errors.New("please install iptables or nftables on the device")
 	}
 	for i := len(gateway.Ranges) - 1; i >= 0; i-- {
 		// check if internet gateway IPv4
@@ -149,9 +150,6 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if host.OS != "linux" {
 		return models.Node{}, errors.New("ingress can only be created on linux based node")
 	}
-	if host.FirewallInUse == models.FIREWALL_NONE {
-		return models.Node{}, errors.New("firewall is not supported for ingress gateways")
-	}
 
 	network, err := GetParentNetwork(netid)
 	if err != nil {
@@ -164,6 +162,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
+	node.IngressPersistentKeepalive = 20
+	if ingress.PersistentKeepalive != 0 {
+		node.IngressPersistentKeepalive = ingress.PersistentKeepalive
+	}
+	node.IngressMTU = 1420
+	if ingress.MTU != 0 {
+		node.IngressMTU = ingress.MTU
+	}
 	if servercfg.IsPro {
 		if _, exists := FailOverExists(node.Network); exists {
 			ResetFailedOverPeer(&node)
@@ -174,34 +180,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if node.Metadata == "" {
 		node.Metadata = "This host can be used for remote access"
 	}
+	if node.Tags == nil {
+		node.Tags = make(map[models.TagID]struct{})
+	}
+	node.Tags[models.TagID(fmt.Sprintf("%s.%s", netid, models.RemoteAccessTagName))] = struct{}{}
 	err = UpsertNode(&node)
 	if err != nil {
 		return models.Node{}, err
 	}
-	// create network role for this gateway
-	CreateRole(models.UserRolePermissionTemplate{
-		ID:        models.GetRAGRoleID(node.Network, host.ID.String()),
-		UiName:    models.GetRAGRoleName(node.Network, host.Name),
-		NetworkID: models.NetworkID(node.Network),
-		Default:   true,
-		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
-			models.RemoteAccessGwRsrc: {
-				models.RsrcID(node.ID.String()): models.RsrcPermissionScope{
-					Read:      true,
-					VPNaccess: true,
-				},
-			},
-			models.ExtClientsRsrc: {
-				models.AllExtClientsRsrcID: models.RsrcPermissionScope{
-					Read:     true,
-					Create:   true,
-					Update:   true,
-					Delete:   true,
-					SelfOnly: true,
-				},
-			},
-		},
-	})
 	err = SetNetworkNodesLastModified(netid)
 	return node, err
 }
@@ -249,17 +235,14 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	if !servercfg.IsPro {
 		node.IsInternetGateway = false
 	}
+	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.RemoteAccessTagName)))
 	node.IngressGatewayRange = ""
 	node.Metadata = ""
 	err = UpsertNode(&node)
 	if err != nil {
 		return models.Node{}, removedClients, err
 	}
-	host, err := GetHost(node.HostID.String())
-	if err != nil {
-		return models.Node{}, removedClients, err
-	}
-	go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
+
 	err = SetNetworkNodesLastModified(node.Network)
 	return node, removedClients, err
 }

+ 19 - 13
logic/hosts.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/google/uuid"
 	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/exp/slog"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -254,14 +255,31 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		currHost.WgPublicListenPort = newHost.WgPublicListenPort
 		sendPeerUpdate = true
 	}
+	isEndpointChanged := false
 	if currHost.EndpointIP.String() != newHost.EndpointIP.String() {
 		currHost.EndpointIP = newHost.EndpointIP
 		sendPeerUpdate = true
+		isEndpointChanged = true
 	}
 	if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() {
 		currHost.EndpointIPv6 = newHost.EndpointIPv6
 		sendPeerUpdate = true
+		isEndpointChanged = true
 	}
+
+	if isEndpointChanged {
+		for _, nodeID := range currHost.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err != nil {
+				slog.Error("failed to get node:", "id", node.ID, "error", err)
+				continue
+			}
+			if node.FailedOverBy != uuid.Nil {
+				ResetFailedOverPeer(&node)
+			}
+		}
+	}
+
 	currHost.DaemonInstalled = newHost.DaemonInstalled
 	currHost.Debug = newHost.Debug
 	currHost.Verbosity = newHost.Verbosity
@@ -269,19 +287,7 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 	currHost.IsStaticPort = newHost.IsStaticPort
 	currHost.IsStatic = newHost.IsStatic
 	currHost.MTU = newHost.MTU
-	if newHost.Name != currHost.Name {
-		// update any rag role ids
-		for _, nodeID := range newHost.Nodes {
-			node, err := GetNodeByID(nodeID)
-			if err == nil && node.IsIngressGateway {
-				role, err := GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
-				if err == nil {
-					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
-					UpdateRole(role)
-				}
-			}
-		}
-	}
+
 	currHost.Name = newHost.Name
 	if len(newHost.NatType) > 0 && newHost.NatType != currHost.NatType {
 		currHost.NatType = newHost.NatType

+ 3 - 2
logic/jwts.go

@@ -56,8 +56,9 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
 	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
 	claims := &models.UserClaims{
-		UserName: username,
-		Role:     role,
+		UserName:       username,
+		Role:           role,
+		RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),

+ 24 - 0
logic/networks.go

@@ -8,9 +8,11 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/c-robinson/iplib"
 	validator "github.com/go-playground/validator/v10"
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
@@ -175,6 +177,17 @@ func DeleteNetwork(network string) error {
 	if err != nil {
 		logger.Log(1, "failed to remove the node acls during network delete for network,", network)
 	}
+	// Delete default network enrollment key
+	keys, _ := GetAllEnrollmentKeys()
+	for _, key := range keys {
+		if key.Tags[0] == network {
+			if key.Default {
+				DeleteEnrollmentKey(key.Value, true)
+				break
+			}
+
+		}
+	}
 	nodeCount, err := GetNetworkNonServerNodeCount(network)
 	if nodeCount == 0 || database.IsEmptyRecord(err) {
 		// delete server nodes first then db records
@@ -233,6 +246,17 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		storeNetworkInCache(network.NetID, network)
 	}
 
+	_, _ = CreateEnrollmentKey(
+		0,
+		time.Time{},
+		[]string{network.NetID},
+		[]string{network.NetID},
+		[]models.TagID{},
+		true,
+		uuid.Nil,
+		true,
+	)
+
 	return network, nil
 }
 

+ 254 - 10
logic/nodes.go

@@ -5,7 +5,9 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"maps"
 	"net"
+	"slices"
 	"sort"
 	"sync"
 	"time"
@@ -24,8 +26,10 @@ import (
 )
 
 var (
-	nodeCacheMutex = &sync.RWMutex{}
-	nodesCacheMap  = make(map[string]models.Node)
+	nodeCacheMutex        = &sync.RWMutex{}
+	nodeNetworkCacheMutex = &sync.RWMutex{}
+	nodesCacheMap         = make(map[string]models.Node)
+	nodesNetworkCacheMap  = make(map[string]map[string]models.Node)
 )
 
 func getNodeFromCache(nodeID string) (node models.Node, ok bool) {
@@ -48,12 +52,37 @@ func deleteNodeFromCache(nodeID string) {
 	delete(nodesCacheMap, nodeID)
 	nodeCacheMutex.Unlock()
 }
+func deleteNodeFromNetworkCache(nodeID string, network string) {
+	nodeNetworkCacheMutex.Lock()
+	delete(nodesNetworkCacheMap[network], nodeID)
+	nodeNetworkCacheMutex.Unlock()
+}
+
+func storeNodeInNetworkCache(node models.Node, network string) {
+	nodeNetworkCacheMutex.Lock()
+	if nodesNetworkCacheMap[network] == nil {
+		nodesNetworkCacheMap[network] = make(map[string]models.Node)
+	}
+	nodesNetworkCacheMap[network][node.ID.String()] = node
+	nodeNetworkCacheMutex.Unlock()
+}
 
 func storeNodeInCache(node models.Node) {
 	nodeCacheMutex.Lock()
 	nodesCacheMap[node.ID.String()] = node
 	nodeCacheMutex.Unlock()
 }
+func loadNodesIntoNetworkCache(nMap map[string]models.Node) {
+	nodeNetworkCacheMutex.Lock()
+	for _, v := range nMap {
+		network := v.Network
+		if nodesNetworkCacheMap[network] == nil {
+			nodesNetworkCacheMap[network] = make(map[string]models.Node)
+		}
+		nodesNetworkCacheMap[network][v.ID.String()] = v
+	}
+	nodeNetworkCacheMutex.Unlock()
+}
 
 func loadNodesIntoCache(nMap map[string]models.Node) {
 	nodeCacheMutex.Lock()
@@ -63,6 +92,7 @@ func loadNodesIntoCache(nMap map[string]models.Node) {
 func ClearNodeCache() {
 	nodeCacheMutex.Lock()
 	nodesCacheMap = make(map[string]models.Node)
+	nodesNetworkCacheMap = make(map[string]map[string]models.Node)
 	nodeCacheMutex.Unlock()
 }
 
@@ -77,6 +107,12 @@ const (
 
 // GetNetworkNodes - gets the nodes of a network
 func GetNetworkNodes(network string) ([]models.Node, error) {
+
+	if networkNodes, ok := nodesNetworkCacheMap[network]; ok {
+		nodeNetworkCacheMutex.Lock()
+		defer nodeNetworkCacheMutex.Unlock()
+		return slices.Collect(maps.Values(networkNodes)), nil
+	}
 	allnodes, err := GetAllNodes()
 	if err != nil {
 		return []models.Node{}, err
@@ -99,6 +135,12 @@ func GetHostNodes(host *models.Host) []models.Node {
 
 // GetNetworkNodesMemory - gets all nodes belonging to a network from list in memory
 func GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node {
+
+	if networkNodes, ok := nodesNetworkCacheMap[network]; ok {
+		nodeNetworkCacheMutex.Lock()
+		defer nodeNetworkCacheMutex.Unlock()
+		return slices.Collect(maps.Values(networkNodes))
+	}
 	var nodes = []models.Node{}
 	for i := range allNodes {
 		node := allNodes[i]
@@ -123,6 +165,7 @@ func UpdateNodeCheckin(node *models.Node) error {
 	}
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(*node)
+		storeNodeInNetworkCache(*node, node.Network)
 	}
 	return nil
 }
@@ -140,6 +183,7 @@ func UpsertNode(newNode *models.Node) error {
 	}
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(*newNode)
+		storeNodeInNetworkCache(*newNode, newNode.Network)
 	}
 	return nil
 }
@@ -179,6 +223,17 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 			}
 			if servercfg.CacheEnabled() {
 				storeNodeInCache(*newNode)
+				storeNodeInNetworkCache(*newNode, newNode.Network)
+				if _, ok := allocatedIpMap[newNode.Network]; ok {
+					if newNode.Address.IP != nil && !newNode.Address.IP.Equal(currentNode.Address.IP) {
+						AddIpToAllocatedIpMap(newNode.Network, newNode.Address.IP)
+						RemoveIpFromAllocatedIpMap(currentNode.Network, currentNode.Address.IP.String())
+					}
+					if newNode.Address6.IP != nil && !newNode.Address6.IP.Equal(currentNode.Address6.IP) {
+						AddIpToAllocatedIpMap(newNode.Network, newNode.Address6.IP)
+						RemoveIpFromAllocatedIpMap(currentNode.Network, currentNode.Address6.IP.String())
+					}
+				}
 			}
 			return nil
 		}
@@ -196,10 +251,6 @@ func DeleteNode(node *models.Node, purge bool) error {
 		if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
 			slog.Error("failed to delete ext clients", "nodeid", node.ID.String(), "error", err.Error())
 		}
-		host, err := GetHost(node.HostID.String())
-		if err == nil {
-			go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
-		}
 	}
 	if node.IsRelayed {
 		// cleanup node from relayednodes on relay node
@@ -292,6 +343,7 @@ func DeleteNodeByID(node *models.Node) error {
 	}
 	if servercfg.CacheEnabled() {
 		deleteNodeFromCache(node.ID.String())
+		deleteNodeFromNetworkCache(node.ID.String(), node.Network)
 	}
 	if servercfg.IsDNSMode() {
 		SetDNS()
@@ -354,6 +406,7 @@ func GetAllNodes() ([]models.Node, error) {
 	nodesMap := make(map[string]models.Node)
 	if servercfg.CacheEnabled() {
 		defer loadNodesIntoCache(nodesMap)
+		defer loadNodesIntoNetworkCache(nodesMap)
 	}
 	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)
 	if err != nil {
@@ -378,6 +431,34 @@ func GetAllNodes() ([]models.Node, error) {
 	return nodes, nil
 }
 
+func AddStaticNodestoList(nodes []models.Node) []models.Node {
+	netMap := make(map[string]struct{})
+	for _, node := range nodes {
+		if _, ok := netMap[node.Network]; ok {
+			continue
+		}
+		if node.IsIngressGateway {
+			nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), false)...)
+			netMap[node.Network] = struct{}{}
+		}
+	}
+	return nodes
+}
+
+func AddStatusToNodes(nodes []models.Node) (nodesWithStatus []models.Node) {
+	aclDefaultPolicyStatusMap := make(map[string]bool)
+	for _, node := range nodes {
+		if _, ok := aclDefaultPolicyStatusMap[node.Network]; !ok {
+			// check default policy if all allowed return true
+			defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+			aclDefaultPolicyStatusMap[node.Network] = defaultPolicy.Enabled
+		}
+		GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
+		nodesWithStatus = append(nodesWithStatus, node)
+	}
+	return
+}
+
 // GetNetworkByNode - gets the network model from a node
 func GetNetworkByNode(node *models.Node) (models.Network, error) {
 
@@ -393,7 +474,7 @@ func GetNetworkByNode(node *models.Node) (models.Network, error) {
 }
 
 // SetNodeDefaults - sets the defaults of a node to avoid empty fields
-func SetNodeDefaults(node *models.Node) {
+func SetNodeDefaults(node *models.Node, resetConnected bool) {
 
 	parentNetwork, _ := GetNetworkByNode(node)
 	_, cidr, err := net.ParseCIDR(parentNetwork.AddressRange)
@@ -414,8 +495,14 @@ func SetNodeDefaults(node *models.Node) {
 
 	node.SetLastModified()
 	node.SetLastCheckIn()
-	node.SetDefaultConnected()
+
+	if resetConnected {
+		node.SetDefaultConnected()
+	}
 	node.SetExpirationDateTime()
+	if node.Tags == nil {
+		node.Tags = make(map[models.TagID]struct{})
+	}
 }
 
 // GetRecordKey - get record key
@@ -443,6 +530,7 @@ func GetNodeByID(uuid string) (models.Node, error) {
 	}
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(node)
+		storeNodeInNetworkCache(node, node.Network)
 	}
 	return node, nil
 }
@@ -461,7 +549,7 @@ func GetDeletedNodeByID(uuid string) (models.Node, error) {
 		return models.Node{}, err
 	}
 
-	SetNodeDefaults(&node)
+	SetNodeDefaults(&node, true)
 
 	return node, nil
 }
@@ -531,7 +619,7 @@ func createNode(node *models.Node) error {
 		}
 	}
 
-	SetNodeDefaults(node)
+	SetNodeDefaults(node, true)
 
 	defaultACLVal := acls.Allowed
 	parentNetwork, err := GetNetwork(node.Network)
@@ -596,6 +684,7 @@ func createNode(node *models.Node) error {
 	}
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(*node)
+		storeNodeInNetworkCache(*node, node.Network)
 	}
 	if _, ok := allocatedIpMap[node.Network]; ok {
 		if node.Address.IP != nil {
@@ -647,6 +736,26 @@ func ValidateParams(nodeid, netid string) (models.Node, error) {
 	return node, nil
 }
 
+func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
+
+	if currentNode.Address.IP != nil && currentNode.Address.String() != newNode.Address {
+		newIp, _, _ := net.ParseCIDR(newNode.Address)
+		ipAllocated := allocatedIpMap[currentNode.Network]
+		if _, ok := ipAllocated[newIp.String()]; ok {
+			return errors.New("ip specified is already allocated:  " + newNode.Address)
+		}
+	}
+	if currentNode.Address6.IP != nil && currentNode.Address6.String() != newNode.Address6 {
+		newIp, _, _ := net.ParseCIDR(newNode.Address6)
+		ipAllocated := allocatedIpMap[currentNode.Network]
+		if _, ok := ipAllocated[newIp.String()]; ok {
+			return errors.New("ip specified is already allocated:  " + newNode.Address6)
+		}
+	}
+
+	return nil
+}
+
 func ValidateEgressRange(gateway models.EgressGatewayRequest) error {
 	network, err := GetNetworkSettings(gateway.NetID)
 	if err != nil {
@@ -694,3 +803,138 @@ func GetAllFailOvers() ([]models.Node, error) {
 	}
 	return igs, nil
 }
+
+func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := GetAllNodes()
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		for nodeTagID := range nodeI.Tags {
+			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
+		}
+	}
+	return
+}
+
+func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (tagNodesMap map[models.TagID][]models.Node) {
+	tagNodesMap = make(map[models.TagID][]models.Node)
+	nodes, _ := GetNetworkNodes(netID.String())
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		for nodeTagID := range nodeI.Tags {
+			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
+		}
+	}
+	tagNodesMap["*"] = nodes
+	if !withStaticNodes {
+		return
+	}
+	return AddTagMapWithStaticNodes(netID, tagNodesMap)
+}
+
+func AddTagMapWithStaticNodes(netID models.NetworkID,
+	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
+	extclients, err := GetNetworkExtClients(netID.String())
+	if err != nil {
+		return tagNodesMap
+	}
+	for _, extclient := range extclients {
+		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {
+			continue
+		}
+		for tagID := range extclient.Tags {
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			})
+			tagNodesMap["*"] = append(tagNodesMap["*"], models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			})
+		}
+
+	}
+	return tagNodesMap
+}
+
+func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
+	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
+	extclients, err := GetNetworkExtClients(netID.String())
+	if err != nil {
+		return tagNodesMap
+	}
+	for _, extclient := range extclients {
+		if extclient.Tags == nil {
+			continue
+		}
+		for tagID := range extclient.Tags {
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			})
+		}
+
+	}
+	return tagNodesMap
+}
+
+func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
+	nMap := make(map[string]models.Node)
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return nMap
+	}
+	nodes, _ := GetNetworkNodes(tag.Network.String())
+	for _, nodeI := range nodes {
+		if nodeI.Tags == nil {
+			continue
+		}
+		if _, ok := nodeI.Tags[tagID]; ok {
+			nMap[nodeI.ID.String()] = nodeI
+		}
+	}
+	return AddStaticNodesWithTag(tag, nMap)
+}
+
+func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[string]models.Node {
+	extclients, err := GetNetworkExtClients(tag.Network.String())
+	if err != nil {
+		return nMap
+	}
+	for _, extclient := range extclients {
+		if extclient.RemoteAccessClientID != "" {
+			continue
+		}
+		if _, ok := extclient.Tags[tag.ID]; ok {
+			nMap[extclient.ClientID] = models.Node{
+				IsStatic:   true,
+				StaticNode: extclient,
+			}
+		}
+
+	}
+	return nMap
+}
+
+func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node {
+	nMap := make(map[string]models.Node)
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return nMap
+	}
+	extclients, err := GetNetworkExtClients(tag.Network.String())
+	if err != nil {
+		return nMap
+	}
+	for _, extclient := range extclients {
+		nMap[extclient.ClientID] = models.Node{
+			IsStatic:   true,
+			StaticNode: extclient,
+		}
+	}
+	return nMap
+}

+ 75 - 2
logic/peers.go

@@ -74,7 +74,10 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		ServerVersion: servercfg.GetVersion(),
 		ServerAddrs:   []models.ServerAddr{},
 		FwUpdate: models.FwUpdate{
-			EgressInfo: make(map[string]models.EgressInfo),
+			AllowAll:    true,
+			EgressInfo:  make(map[string]models.EgressInfo),
+			IngressInfo: make(map[string]models.IngressInfo),
+			AclRules:    make(map[string]models.AclRule),
 		},
 		PeerIDs:           make(models.PeerMap, 0),
 		Peers:             []wgtypes.PeerConfig{},
@@ -82,6 +85,24 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		HostNetworkInfo:   models.HostInfoMap{},
 		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
+	defer func() {
+		if !hostPeerUpdate.FwUpdate.AllowAll {
+			aclRule := models.AclRule{
+				ID:              "allowed-network-rules",
+				AllowedProtocol: models.ALL,
+				Direction:       models.TrafficDirectionBi,
+				Allowed:         true,
+			}
+			for _, allowedNet := range hostPeerUpdate.FwUpdate.AllowedNetworks {
+				if allowedNet.IP.To4() != nil {
+					aclRule.IPList = append(aclRule.IPList, allowedNet)
+				} else {
+					aclRule.IP6List = append(aclRule.IP6List, allowedNet)
+				}
+			}
+			hostPeerUpdate.FwUpdate.AclRules["allowed-network-rules"] = aclRule
+		}
+	}()
 
 	slog.Debug("peer update for host", "hostId", host.ID.String())
 	peerIndexMap := make(map[string]int)
@@ -153,6 +174,22 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
 		}
+		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+
+		if defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled {
+			if node.NetworkRange.IP != nil {
+				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange)
+			}
+			if node.NetworkRange6.IP != nil {
+				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange6)
+			}
+
+		} else {
+			hostPeerUpdate.FwUpdate.AllowAll = false
+			hostPeerUpdate.FwUpdate.AclRules = GetAclRulesForNode(&node)
+		}
+
 		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		for _, peer := range currentPeers {
 			peer := peer
@@ -182,7 +219,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				})
 			}
 			if peer.IsIngressGateway {
-				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node, peer.Network)...)
+				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			if servercfg.IsPro {
@@ -226,6 +263,23 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			} else if host.EndpointIPv6 != nil && peerHost.EndpointIPv6 != nil {
 				peerEndpoint = peerHost.EndpointIPv6
 			}
+			if host.EndpointIP == nil && peerEndpoint == nil {
+				if peerHost.EndpointIP != nil {
+					peerEndpoint = peerHost.EndpointIP
+				}
+			}
+			if host.EndpointIPv6 == nil && peerEndpoint == nil {
+				if peerHost.EndpointIPv6 != nil {
+					peerEndpoint = peerHost.EndpointIPv6
+				}
+			}
+			if node.IsRelay && peer.RelayedBy == node.ID.String() && !peer.IsStatic {
+				// don't set endpoint on relayed peer
+				peerEndpoint = nil
+			}
+			if isFailOverPeer && peer.FailedOverBy == node.ID && !peer.IsStatic {
+				peerEndpoint = nil
+			}
 
 			peerConfig.Endpoint = &net.UDPAddr{
 				IP:   peerEndpoint,
@@ -237,10 +291,12 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 			}
 			allowedips := GetAllowedIPs(&node, &peer, nil)
+			allowedToComm := IsPeerAllowed(node, peer, false)
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+				(defaultDevicePolicy.Enabled || allowedToComm) &&
 				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
 				peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
 			}
@@ -287,8 +343,21 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		var extPeerIDAndAddrs []models.IDandAddr
 		var egressRoutes []models.EgressNetworkRoutes
 		if node.IsIngressGateway {
+			hostPeerUpdate.FwUpdate.IsIngressGw = true
 			extPeers, extPeerIDAndAddrs, egressRoutes, err = GetExtPeers(&node, &node)
 			if err == nil {
+				if !defaultDevicePolicy.Enabled || !defaultUserPolicy.Enabled {
+					ingFwUpdate := models.IngressInfo{
+						IngressID:     node.ID.String(),
+						Network:       node.NetworkRange,
+						Network6:      node.NetworkRange6,
+						AllowAll:      defaultDevicePolicy.Enabled && defaultUserPolicy.Default,
+						StaticNodeIps: GetStaticNodeIps(node),
+						Rules:         GetFwRulesOnIngressGateway(node),
+					}
+					ingFwUpdate.EgressRanges, ingFwUpdate.EgressRanges6 = getExtpeerEgressRanges(node)
+					hostPeerUpdate.FwUpdate.IngressInfo[node.ID.String()] = ingFwUpdate
+				}
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, egressRoutes...)
 				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...)
 				for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
@@ -391,6 +460,9 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		}
 	}
 
+	hostPeerUpdate.ManageDNS = servercfg.GetManageDNS()
+	hostPeerUpdate.Stun = servercfg.IsStunEnabled()
+	hostPeerUpdate.StunServers = servercfg.GetStunServers()
 	return hostPeerUpdate, nil
 }
 
@@ -425,6 +497,7 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet
 			logger.Log(2, "could not retrieve ext peers for ", peer.ID.String(), err.Error())
 		}
 		for _, extPeer := range extPeers {
+
 			allowedips = append(allowedips, extPeer.AllowedIPs...)
 		}
 	}

+ 37 - 0
logic/proc.go

@@ -0,0 +1,37 @@
+package logic
+
+import (
+	"os"
+	"runtime"
+	"runtime/pprof"
+
+	"github.com/gravitl/netmaker/logger"
+)
+
+func StartCPUProfiling() *os.File {
+	f, err := os.OpenFile("/root/data/cpu.prof", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
+	if err != nil {
+		logger.Log(0, "could not create CPU profile: ", err.Error())
+	}
+	if err := pprof.StartCPUProfile(f); err != nil {
+		logger.Log(0, "could not start CPU profile: ", err.Error())
+	}
+	return f
+}
+
+func StopCPUProfiling(f *os.File) {
+	pprof.StopCPUProfile()
+	f.Close()
+}
+
+func StartMemProfiling() {
+	f, err := os.OpenFile("/root/data/mem.prof", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
+	if err != nil {
+		logger.Log(0, "could not create Memory profile: ", err.Error())
+	}
+	defer f.Close()
+	runtime.GC() // get up-to-date statistics
+	if err = pprof.WriteHeapProfile(f); err != nil {
+		logger.Log(0, "could not write memory profile: ", err.Error())
+	}
+}

+ 1 - 2
logic/security.go

@@ -2,7 +2,6 @@ package logic
 
 import (
 	"net/http"
-	"net/url"
 	"strings"
 
 	"github.com/gorilla/mux"
@@ -97,7 +96,7 @@ func ContinueIfUserMatch(next http.Handler) http.HandlerFunc {
 		var params = mux.Vars(r)
 		var requestedUser = params["username"]
 		if requestedUser == "" {
-			requestedUser, _ = url.QueryUnescape(r.URL.Query().Get("username"))
+			requestedUser = r.URL.Query().Get("username")
 		}
 		if requestedUser != r.Header.Get("user") {
 			ReturnErrorResponse(w, r, errorResponse)

+ 26 - 0
logic/status.go

@@ -0,0 +1,26 @@
+package logic
+
+import (
+	"time"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+var GetNodeStatus = getNodeStatus
+
+func getNodeStatus(node *models.Node, t bool) {
+	// On CE check only last check-in time
+	if node.IsStatic {
+		if !node.StaticNode.Enabled {
+			node.Status = models.OfflineSt
+			return
+		}
+		node.Status = models.OnlineSt
+		return
+	}
+	if time.Since(node.LastCheckIn) > time.Minute*10 {
+		node.Status = models.OfflineSt
+		return
+	}
+	node.Status = models.OnlineSt
+}

+ 290 - 0
logic/tags.go

@@ -0,0 +1,290 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"sort"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
+)
+
+var tagMutex = &sync.RWMutex{}
+
+// GetTag - fetches tag info
+func GetTag(tagID models.TagID) (models.Tag, error) {
+	data, err := database.FetchRecord(database.TAG_TABLE_NAME, tagID.String())
+	if err != nil {
+		return models.Tag{}, err
+	}
+	tag := models.Tag{}
+	err = json.Unmarshal([]byte(data), &tag)
+	if err != nil {
+		return tag, err
+	}
+	return tag, nil
+}
+
+// InsertTag - creates new tag
+func InsertTag(tag models.Tag) error {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	_, err := database.FetchRecord(database.TAG_TABLE_NAME, tag.ID.String())
+	if err == nil {
+		return fmt.Errorf("tag `%s` exists already", tag.ID)
+	}
+	d, err := json.Marshal(tag)
+	if err != nil {
+		return err
+	}
+	return database.Insert(tag.ID.String(), string(d), database.TAG_TABLE_NAME)
+}
+
+// DeleteTag - delete tag, will also untag hosts
+func DeleteTag(tagID models.TagID, removeFromPolicy bool) error {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	// cleanUp tags on hosts
+	tag, err := GetTag(tagID)
+	if err != nil {
+		return err
+	}
+	nodes, err := GetNetworkNodes(tag.Network.String())
+	if err != nil {
+		return err
+	}
+	for _, nodeI := range nodes {
+		nodeI := nodeI
+		if _, ok := nodeI.Tags[tagID]; ok {
+			delete(nodeI.Tags, tagID)
+			UpsertNode(&nodeI)
+		}
+	}
+	if removeFromPolicy {
+		// remove tag used on acl policy
+		go RemoveDeviceTagFromAclPolicies(tagID, tag.Network)
+	}
+	extclients, _ := GetNetworkExtClients(tag.Network.String())
+	for _, extclient := range extclients {
+		if _, ok := extclient.Tags[tagID]; ok {
+			delete(extclient.Tags, tagID)
+			SaveExtClient(&extclient)
+		}
+	}
+	return database.DeleteRecord(database.TAG_TABLE_NAME, tagID.String())
+}
+
+// ListTagsWithHosts - lists all tags with tagged hosts
+func ListTagsWithNodes(netID models.NetworkID) ([]models.TagListResp, error) {
+	tags, err := ListNetworkTags(netID)
+	if err != nil {
+		return []models.TagListResp{}, err
+	}
+	tagsNodeMap := GetTagMapWithNodesByNetwork(netID, true)
+	resp := []models.TagListResp{}
+	for _, tagI := range tags {
+		tagRespI := models.TagListResp{
+			Tag:         tagI,
+			UsedByCnt:   len(tagsNodeMap[tagI.ID]),
+			TaggedNodes: GetAllNodesAPI(tagsNodeMap[tagI.ID]),
+		}
+		resp = append(resp, tagRespI)
+	}
+	return resp, nil
+}
+
+// ListTags - lists all tags from DB
+func ListTags() ([]models.Tag, error) {
+	tagMutex.RLock()
+	defer tagMutex.RUnlock()
+	data, err := database.FetchRecords(database.TAG_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Tag{}, err
+	}
+	tags := []models.Tag{}
+	for _, dataI := range data {
+		tag := models.Tag{}
+		err := json.Unmarshal([]byte(dataI), &tag)
+		if err != nil {
+			continue
+		}
+		tags = append(tags, tag)
+	}
+	return tags, nil
+}
+
+// ListTags - lists all tags from DB
+func ListNetworkTags(netID models.NetworkID) ([]models.Tag, error) {
+	tagMutex.RLock()
+	defer tagMutex.RUnlock()
+	data, err := database.FetchRecords(database.TAG_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.Tag{}, err
+	}
+	tags := []models.Tag{}
+	for _, dataI := range data {
+		tag := models.Tag{}
+		err := json.Unmarshal([]byte(dataI), &tag)
+		if err != nil {
+			continue
+		}
+		if tag.Network == netID {
+			tags = append(tags, tag)
+		}
+
+	}
+	return tags, nil
+}
+
+// UpdateTag - updates and syncs hosts with tag update
+func UpdateTag(req models.UpdateTagReq, newID models.TagID) {
+	tagMutex.Lock()
+	defer tagMutex.Unlock()
+	var err error
+	tagNodesMap := GetNodesWithTag(req.ID)
+	for _, apiNode := range req.TaggedNodes {
+		node := models.Node{}
+		var nodeID string
+		if apiNode.IsStatic {
+			if apiNode.StaticNode.RemoteAccessClientID != "" {
+				continue
+			}
+			extclient, err := GetExtClient(apiNode.StaticNode.ClientID, apiNode.StaticNode.Network)
+			if err != nil {
+				continue
+			}
+			node.IsStatic = true
+			nodeID = extclient.ClientID
+			node.StaticNode = extclient
+		} else {
+			node, err = GetNodeByID(apiNode.ID)
+			if err != nil {
+				continue
+			}
+			nodeID = node.ID.String()
+		}
+
+		if _, ok := tagNodesMap[nodeID]; !ok {
+			if node.StaticNode.Tags == nil {
+				node.StaticNode.Tags = make(map[models.TagID]struct{})
+			}
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			if newID != "" {
+				if node.IsStatic {
+					node.StaticNode.Tags[newID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[newID] = struct{}{}
+					UpsertNode(&node)
+				}
+
+			} else {
+				if node.IsStatic {
+					node.StaticNode.Tags[req.ID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[req.ID] = struct{}{}
+					UpsertNode(&node)
+				}
+			}
+		} else {
+			if newID != "" {
+				delete(node.Tags, req.ID)
+				delete(node.StaticNode.Tags, req.ID)
+				if node.IsStatic {
+					node.StaticNode.Tags[newID] = struct{}{}
+					SaveExtClient(&node.StaticNode)
+				} else {
+					node.Tags[newID] = struct{}{}
+					UpsertNode(&node)
+				}
+			}
+			delete(tagNodesMap, nodeID)
+		}
+
+	}
+	for _, deletedTaggedNode := range tagNodesMap {
+		delete(deletedTaggedNode.Tags, req.ID)
+		delete(deletedTaggedNode.StaticNode.Tags, req.ID)
+		if deletedTaggedNode.IsStatic {
+			SaveExtClient(&deletedTaggedNode.StaticNode)
+		} else {
+			UpsertNode(&deletedTaggedNode)
+		}
+	}
+	go func(req models.UpdateTagReq) {
+		if newID != "" {
+			tagNodesMap = GetNodesWithTag(req.ID)
+			for _, nodeI := range tagNodesMap {
+				nodeI := nodeI
+				if nodeI.StaticNode.Tags == nil {
+					nodeI.StaticNode.Tags = make(map[models.TagID]struct{})
+				}
+				if nodeI.Tags == nil {
+					nodeI.Tags = make(map[models.TagID]struct{})
+				}
+				delete(nodeI.Tags, req.ID)
+				delete(nodeI.StaticNode.Tags, req.ID)
+				nodeI.Tags[newID] = struct{}{}
+				nodeI.StaticNode.Tags[newID] = struct{}{}
+				if nodeI.IsStatic {
+					SaveExtClient(&nodeI.StaticNode)
+				} else {
+					UpsertNode(&nodeI)
+				}
+			}
+		}
+	}(req)
+
+}
+
+// SortTagEntrys - Sorts slice of Tag entries by their id
+func SortTagEntrys(tags []models.TagListResp) {
+	sort.Slice(tags, func(i, j int) bool {
+		return tags[i].ID < tags[j].ID
+	})
+}
+
+func CheckIDSyntax(id string) error {
+	if id == "" {
+		return errors.New("name is required")
+	}
+	if len(id) < 3 {
+		return errors.New("name should have min 3 characters")
+	}
+	reg, err := regexp.Compile("^[a-zA-Z-]+$")
+	if err != nil {
+		return err
+	}
+	if !reg.MatchString(id) {
+		return errors.New("invalid name. allowed characters are [a-zA-Z-]")
+	}
+	return nil
+}
+
+func CreateDefaultTags(netID models.NetworkID) {
+	// create tag for remote access gws in the network
+	tag := models.Tag{
+		ID:        models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.RemoteAccessTagName)),
+		TagName:   models.RemoteAccessTagName,
+		Network:   netID,
+		CreatedBy: "auto",
+		CreatedAt: time.Now(),
+	}
+	_, err := GetTag(tag.ID)
+	if err == nil {
+		return
+	}
+	err = InsertTag(tag)
+	if err != nil {
+		slog.Error("failed to create remote access gw tag", "error", err.Error())
+		return
+	}
+}

+ 30 - 0
logic/user_mgmt.go

@@ -39,17 +39,28 @@ var FilterNetworksByRole = func(allnetworks []models.Network, user models.User)
 var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error {
 	return nil
 }
+var IsGroupValid = func(groupID models.UserGroupID) error {
+	return nil
+}
 var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
 	return nil
 }
 
+var MigrateUserRoleAndGroups = func(u models.User) {
+
+}
+
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
 
 var InitialiseRoles = userRolesInit
+var IntialiseGroups = func() {}
 var DeleteNetworkRoles = func(netID string) {}
 var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
+var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
+var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
+var AddGlobalNetRolesToAdmins = func(u models.User) {}
 
 // GetRole - fetches role template by id
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
@@ -87,6 +98,25 @@ func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) {
 	return userRoles, nil
 }
 
+func GetUserGrpMap() map[models.UserGroupID]map[string]struct{} {
+	grpUsersMap := make(map[models.UserGroupID]map[string]struct{})
+	users, _ := GetUsersDB()
+	for _, user := range users {
+		for gID := range user.UserGroups {
+			if grpUsers, ok := grpUsersMap[gID]; ok {
+				grpUsers[user.UserName] = struct{}{}
+				grpUsersMap[gID] = grpUsers
+			} else {
+				grpUsersMap[gID] = make(map[string]struct{})
+				grpUsersMap[gID][user.UserName] = struct{}{}
+			}
+		}
+
+	}
+
+	return grpUsersMap
+}
+
 func userRolesInit() {
 	d, _ := json.Marshal(SuperAdminPermissionTemplate)
 	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)

+ 28 - 1
logic/util.go

@@ -6,11 +6,14 @@ import (
 	"encoding/base32"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"net"
 	"os"
 	"strings"
 	"time"
+	"unicode"
 
+	"github.com/blang/semver"
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -148,4 +151,28 @@ func IsSlicesEqual(a, b []string) bool {
 	return true
 }
 
-// == private ==
+// VersionLessThan checks if v1 < v2 semantically
+// dev is the latest version
+func VersionLessThan(v1, v2 string) (bool, error) {
+	if v1 == "dev" {
+		return false, nil
+	}
+	if v2 == "dev" {
+		return true, nil
+	}
+	semVer1 := strings.TrimFunc(v1, func(r rune) bool {
+		return !unicode.IsNumber(r)
+	})
+	semVer2 := strings.TrimFunc(v2, func(r rune) bool {
+		return !unicode.IsNumber(r)
+	})
+	sv1, err := semver.Parse(semVer1)
+	if err != nil {
+		return false, fmt.Errorf("failed to parse semver1 (%s): %w", semVer1, err)
+	}
+	sv2, err := semver.Parse(semVer2)
+	if err != nil {
+		return false, fmt.Errorf("failed to parse semver2 (%s): %w", semVer2, err)
+	}
+	return sv1.LT(sv2), nil
+}

+ 13 - 2
main.go

@@ -27,13 +27,14 @@ import (
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/serverctl"
+	_ "go.uber.org/automaxprocs"
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.25.0"
+var version = "v0.30.0"
 
 //	@title			NetMaker
-//	@version		0.24.3
+//	@version		0.30.0
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.description.markdown
@@ -102,10 +103,20 @@ func initialize() { // Client Mode Prereq Check
 		logger.FatalLog("Error connecting to database: ", err.Error())
 	}
 	logger.Log(0, "database successfully connected")
+
+	//initialize cache
+	_, _ = logic.GetNetworks()
+	_, _ = logic.GetAllNodes()
+	_, _ = logic.GetAllHosts()
+	_, _ = logic.GetAllExtClients()
+	_ = logic.ListAcls()
+	_, _ = logic.GetAllEnrollmentKeys()
+
 	migrate.Run()
 
 	logic.SetJWTSecret()
 	logic.InitialiseRoles()
+	logic.IntialiseGroups()
 	err = serverctl.SetDefaults()
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())

+ 4 - 1
main_ee.go

@@ -3,7 +3,10 @@
 
 package main
 
-import "github.com/gravitl/netmaker/pro"
+import (
+	"github.com/gravitl/netmaker/pro"
+	_ "go.uber.org/automaxprocs"
+)
 
 func init() {
 	pro.InitPro()

+ 86 - 62
migrate/migrate.go

@@ -8,6 +8,7 @@ import (
 
 	"golang.org/x/exp/slog"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -21,11 +22,12 @@ import (
 func Run() {
 	updateEnrollmentKeys()
 	assignSuperAdmin()
+	createDefaultTagsAndPolicies()
+	removeOldUserGrps()
 	syncUsers()
 	updateHosts()
 	updateNodes()
 	updateAcls()
-
 }
 
 func assignSuperAdmin() {
@@ -122,6 +124,49 @@ func updateEnrollmentKeys() {
 		}
 
 	}
+
+	existingKeys, err := logic.GetAllEnrollmentKeys()
+	if err != nil {
+		return
+	}
+	// check if any tags are duplicate
+	existingTags := make(map[string]struct{})
+	for _, existingKey := range existingKeys {
+		for _, t := range existingKey.Tags {
+			existingTags[t] = struct{}{}
+		}
+	}
+	networks, _ := logic.GetNetworks()
+	for _, network := range networks {
+		if _, ok := existingTags[network.NetID]; ok {
+			continue
+		}
+		_, _ = logic.CreateEnrollmentKey(
+			0,
+			time.Time{},
+			[]string{network.NetID},
+			[]string{network.NetID},
+			[]models.TagID{},
+			true,
+			uuid.Nil,
+			true,
+		)
+
+	}
+}
+
+func removeOldUserGrps() {
+	rows, err := database.FetchRecords(database.USER_GROUPS_TABLE_NAME)
+	if err != nil {
+		return
+	}
+	for key, row := range rows {
+		userG := models.UserGroup{}
+		_ = json.Unmarshal([]byte(row), &userG)
+		if userG.ID == "" {
+			database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, key)
+		}
+	}
 }
 
 func updateHosts() {
@@ -152,6 +197,26 @@ func updateNodes() {
 		return
 	}
 	for _, node := range nodes {
+		node := node
+		if node.Tags == nil {
+			node.Tags = make(map[models.TagID]struct{})
+			logic.UpsertNode(&node)
+		}
+		if node.IsIngressGateway {
+			tagID := models.TagID(fmt.Sprintf("%s.%s", node.Network,
+				models.RemoteAccessTagName))
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			if _, ok := node.Tags[tagID]; !ok {
+				node.Tags[tagID] = struct{}{}
+				logic.UpsertNode(&node)
+			}
+			host, err := logic.GetHost(node.HostID.String())
+			if err == nil {
+				go logic.DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
+			}
+		}
 		if node.IsEgressGateway {
 			egressRanges, update := removeInterGw(node.EgressGatewayRanges)
 			if update {
@@ -316,41 +381,8 @@ func syncUsers() {
 	// create default network user roles for existing networks
 	if servercfg.IsPro {
 		networks, _ := logic.GetNetworks()
-		nodes, err := logic.GetAllNodes()
-		if err == nil {
-			for _, netI := range networks {
-				networkNodes := logic.GetNetworkNodesMemory(nodes, netI.NetID)
-				for _, networkNodeI := range networkNodes {
-					if networkNodeI.IsIngressGateway {
-						h, err := logic.GetHost(networkNodeI.HostID.String())
-						if err == nil {
-							logic.CreateRole(models.UserRolePermissionTemplate{
-								ID:        models.GetRAGRoleID(networkNodeI.Network, h.ID.String()),
-								UiName:    models.GetRAGRoleName(networkNodeI.Network, h.Name),
-								NetworkID: models.NetworkID(netI.NetID),
-								NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
-									models.RemoteAccessGwRsrc: {
-										models.RsrcID(networkNodeI.ID.String()): models.RsrcPermissionScope{
-											Read:      true,
-											VPNaccess: true,
-										},
-									},
-									models.ExtClientsRsrc: {
-										models.AllExtClientsRsrcID: models.RsrcPermissionScope{
-											Read:     true,
-											Create:   true,
-											Update:   true,
-											Delete:   true,
-											SelfOnly: true,
-										},
-									},
-								},
-							})
-						}
-
-					}
-				}
-			}
+		for _, netI := range networks {
+			logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(netI.NetID))
 		}
 	}
 
@@ -367,6 +399,8 @@ func syncUsers() {
 				logic.UpsertUser(user)
 			}
 			if user.PlatformRoleID.String() != "" {
+				logic.MigrateUserRoleAndGroups(user)
+				logic.AddGlobalNetRolesToAdmins(user)
 				continue
 			}
 			user.AuthType = models.BasicAuth
@@ -388,32 +422,22 @@ func syncUsers() {
 				user.PlatformRoleID = models.ServiceUser
 			}
 			logic.UpsertUser(user)
-			if len(user.RemoteGwIDs) > 0 {
-				// define user roles for network
-				// assign relevant network role to user
-				for remoteGwID := range user.RemoteGwIDs {
-					gwNode, err := logic.GetNodeByID(remoteGwID)
-					if err != nil {
-						continue
-					}
-					h, err := logic.GetHost(gwNode.HostID.String())
-					if err != nil {
-						continue
-					}
-					r, err := logic.GetRole(models.GetRAGRoleID(gwNode.Network, h.ID.String()))
-					if err != nil {
-						continue
-					}
-					if netRoles, ok := user.NetworkRoles[models.NetworkID(gwNode.Network)]; ok {
-						netRoles[r.ID] = struct{}{}
-					} else {
-						user.NetworkRoles[models.NetworkID(gwNode.Network)] = map[models.UserRoleID]struct{}{
-							r.ID: {},
-						}
-					}
-				}
-				logic.UpsertUser(user)
-			}
+			logic.AddGlobalNetRolesToAdmins(user)
+			logic.MigrateUserRoleAndGroups(user)
 		}
 	}
+
+}
+
+func createDefaultTagsAndPolicies() {
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		return
+	}
+	for _, network := range networks {
+		logic.CreateDefaultTags(models.NetworkID(network.NetID))
+		logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
+
+	}
+	logic.MigrateAclPolicies()
 }

+ 120 - 0
models/acl.go

@@ -0,0 +1,120 @@
+package models
+
+import (
+	"net"
+	"time"
+)
+
+// AllowedTrafficDirection - allowed direction of traffic
+type AllowedTrafficDirection int
+
+const (
+	// TrafficDirectionUni implies traffic is only allowed in one direction (src --> dst)
+	TrafficDirectionUni AllowedTrafficDirection = iota
+	// TrafficDirectionBi implies traffic is allowed both direction (src <--> dst )
+	TrafficDirectionBi
+)
+
+// Protocol - allowed protocol
+type Protocol string
+
+const (
+	ALL  Protocol = "all"
+	UDP  Protocol = "udp"
+	TCP  Protocol = "tcp"
+	ICMP Protocol = "icmp"
+)
+
+type ServiceType string
+
+const (
+	Http        = "HTTP"
+	Https       = "HTTPS"
+	AllTCP      = "All TCP"
+	AllUDP      = "All UDP"
+	ICMPService = "ICMP"
+	Custom      = "Custom"
+	Any         = "Any"
+)
+
+func (p Protocol) String() string {
+	return string(p)
+}
+
+type AclPolicyType string
+
+const (
+	UserPolicy   AclPolicyType = "user-policy"
+	DevicePolicy AclPolicyType = "device-policy"
+)
+
+type AclPolicyTag struct {
+	ID    AclGroupType `json:"id"`
+	Value string       `json:"value"`
+}
+
+type AclGroupType string
+
+const (
+	UserAclID                AclGroupType = "user"
+	UserGroupAclID           AclGroupType = "user-group"
+	DeviceAclID              AclGroupType = "tag"
+	NetmakerIPAclID          AclGroupType = "ip"
+	NetmakerSubNetRangeAClID AclGroupType = "ipset"
+)
+
+func (g AclGroupType) String() string {
+	return string(g)
+}
+
+type UpdateAclRequest struct {
+	Acl
+	NewName string `json:"new_name"`
+}
+
+type AclPolicy struct {
+	TypeID        AclPolicyType
+	PrefixTagUser AclGroupType
+}
+
+type Acl struct {
+	ID               string                  `json:"id"`
+	Default          bool                    `json:"default"`
+	MetaData         string                  `json:"meta_data"`
+	Name             string                  `json:"name"`
+	NetworkID        NetworkID               `json:"network_id"`
+	RuleType         AclPolicyType           `json:"policy_type"`
+	Src              []AclPolicyTag          `json:"src_type"`
+	Dst              []AclPolicyTag          `json:"dst_type"`
+	Proto            Protocol                `json:"protocol"` // tcp, udp, etc.
+	ServiceType      string                  `json:"type"`
+	Port             []string                `json:"ports"`
+	AllowedDirection AllowedTrafficDirection `json:"allowed_traffic_direction"`
+	Enabled          bool                    `json:"enabled"`
+	CreatedBy        string                  `json:"created_by"`
+	CreatedAt        time.Time               `json:"created_at"`
+}
+
+type AclPolicyTypes struct {
+	ProtocolTypes []ProtocolType
+	RuleTypes     []AclPolicyType `json:"policy_types"`
+	SrcGroupTypes []AclGroupType  `json:"src_grp_types"`
+	DstGroupTypes []AclGroupType  `json:"dst_grp_types"`
+}
+
+type ProtocolType struct {
+	Name             string     `json:"name"`
+	AllowedProtocols []Protocol `json:"allowed_protocols"`
+	PortRange        string     `json:"port_range"`
+	AllowPortSetting bool       `json:"allow_port_setting"`
+}
+
+type AclRule struct {
+	ID              string                  `json:"id"`
+	IPList          []net.IPNet             `json:"ip_list"`
+	IP6List         []net.IPNet             `json:"ip6_list"`
+	AllowedProtocol Protocol                `json:"allowed_protocols"` // tcp, udp, etc.
+	AllowedPorts    []string                `json:"allowed_ports"`
+	Direction       AllowedTrafficDirection `json:"direction"` // single or two-way
+	Allowed         bool
+}

+ 44 - 27
models/api_node.go

@@ -10,33 +10,35 @@ import (
 
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 type ApiNode struct {
-	ID                      string   `json:"id,omitempty" validate:"required,min=5,id_unique"`
-	HostID                  string   `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
-	Address                 string   `json:"address" validate:"omitempty,cidrv4"`
-	Address6                string   `json:"address6" validate:"omitempty,cidrv6"`
-	LocalAddress            string   `json:"localaddress" validate:"omitempty,cidr"`
-	AllowedIPs              []string `json:"allowedips"`
-	LastModified            int64    `json:"lastmodified"`
-	ExpirationDateTime      int64    `json:"expdatetime"`
-	LastCheckIn             int64    `json:"lastcheckin"`
-	LastPeerUpdate          int64    `json:"lastpeerupdate"`
-	Network                 string   `json:"network"`
-	NetworkRange            string   `json:"networkrange"`
-	NetworkRange6           string   `json:"networkrange6"`
-	IsRelayed               bool     `json:"isrelayed"`
-	IsRelay                 bool     `json:"isrelay"`
-	RelayedBy               string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
-	RelayedNodes            []string `json:"relaynodes" yaml:"relayedNodes"`
-	IsEgressGateway         bool     `json:"isegressgateway"`
-	IsIngressGateway        bool     `json:"isingressgateway"`
-	EgressGatewayRanges     []string `json:"egressgatewayranges"`
-	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
-	DNSOn                   bool     `json:"dnson"`
-	IngressDns              string   `json:"ingressdns"`
-	Server                  string   `json:"server"`
-	Connected               bool     `json:"connected"`
-	PendingDelete           bool     `json:"pendingdelete"`
-	Metadata                string   `json:"metadata"`
+	ID                         string   `json:"id,omitempty" validate:"required,min=5,id_unique"`
+	HostID                     string   `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
+	Address                    string   `json:"address" validate:"omitempty,cidrv4"`
+	Address6                   string   `json:"address6" validate:"omitempty,cidrv6"`
+	LocalAddress               string   `json:"localaddress" validate:"omitempty,cidr"`
+	AllowedIPs                 []string `json:"allowedips"`
+	LastModified               int64    `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
+	ExpirationDateTime         int64    `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
+	LastCheckIn                int64    `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
+	LastPeerUpdate             int64    `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
+	Network                    string   `json:"network"`
+	NetworkRange               string   `json:"networkrange"`
+	NetworkRange6              string   `json:"networkrange6"`
+	IsRelayed                  bool     `json:"isrelayed"`
+	IsRelay                    bool     `json:"isrelay"`
+	RelayedBy                  string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
+	RelayedNodes               []string `json:"relaynodes" yaml:"relayedNodes"`
+	IsEgressGateway            bool     `json:"isegressgateway"`
+	IsIngressGateway           bool     `json:"isingressgateway"`
+	EgressGatewayRanges        []string `json:"egressgatewayranges"`
+	EgressGatewayNatEnabled    bool     `json:"egressgatewaynatenabled"`
+	DNSOn                      bool     `json:"dnson"`
+	IngressDns                 string   `json:"ingressdns"`
+	IngressPersistentKeepalive int32    `json:"ingresspersistentkeepalive"`
+	IngressMTU                 int32    `json:"ingressmtu"`
+	Server                     string   `json:"server"`
+	Connected                  bool     `json:"connected"`
+	PendingDelete              bool     `json:"pendingdelete"`
+	Metadata                   string   `json:"metadata"`
 	// == PRO ==
 	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	IsFailOver        bool                `json:"is_fail_over"`
@@ -46,6 +48,11 @@ type ApiNode struct {
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
 	AdditionalRagIps  []string            `json:"additional_rag_ips" yaml:"additional_rag_ips"`
+	Tags              map[TagID]struct{}  `json:"tags" yaml:"tags"`
+	IsStatic          bool                `json:"is_static"`
+	IsUserNode        bool                `json:"is_user_node"`
+	StaticNode        ExtClient           `json:"static_node"`
+	Status            NodeStatus          `json:"status"`
 }
 
 // ApiNode.ConvertToServerNode - converts an api node to a server node
@@ -72,6 +79,8 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.IngressDNS = a.IngressDns
+	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
+	convertedNode.IngressMTU = a.IngressMTU
 	convertedNode.IsInternetGateway = a.IsInternetGateway
 	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
 	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
@@ -119,6 +128,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 		}
 		convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp)
 	}
+	convertedNode.Tags = a.Tags
 	return &convertedNode
 }
 
@@ -162,6 +172,8 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
+	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
+	apiNode.IngressMTU = nm.IngressMTU
 	apiNode.Server = nm.Server
 	apiNode.Connected = nm.Connected
 	apiNode.PendingDelete = nm.PendingDelete
@@ -174,9 +186,14 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata
 	apiNode.AdditionalRagIps = []string{}
+	apiNode.Tags = nm.Tags
 	for _, ip := range nm.AdditionalRagIps {
 		apiNode.AdditionalRagIps = append(apiNode.AdditionalRagIps, ip.String())
 	}
+	apiNode.IsStatic = nm.IsStatic
+	apiNode.IsUserNode = nm.IsUserNode
+	apiNode.StaticNode = nm.StaticNode
+	apiNode.Status = nm.Status
 	return &apiNode
 }
 

+ 2 - 2
models/dnsEntry.go

@@ -42,8 +42,8 @@ type DNSUpdate struct {
 
 // DNSEntry - a DNS entry represented as struct
 type DNSEntry struct {
-	Address  string `json:"address" validate:"ip"`
-	Address6 string `json:"address6"`
+	Address  string `json:"address" validate:"omitempty,ip"`
+	Address6 string `json:"address6" validate:"omitempty,ip"`
 	Name     string `json:"name" validate:"required,name_unique,min=1,max=192,whitespace"`
 	Network  string `json:"network" validate:"network_exists"`
 }

+ 4 - 1
models/enrollment_key.go

@@ -52,17 +52,20 @@ type EnrollmentKey struct {
 	Token         string    `json:"token,omitempty"` // B64 value of EnrollmentToken
 	Type          KeyType   `json:"type"`
 	Relay         uuid.UUID `json:"relay"`
+	Groups        []TagID   `json:"groups"`
+	Default       bool      `json:"default"`
 }
 
 // APIEnrollmentKey - used to create enrollment keys via API
 type APIEnrollmentKey struct {
-	Expiration    int64    `json:"expiration"`
+	Expiration    int64    `json:"expiration" swaggertype:"primitive,integer" format:"int64"`
 	UsesRemaining int      `json:"uses_remaining"`
 	Networks      []string `json:"networks"`
 	Unlimited     bool     `json:"unlimited"`
 	Tags          []string `json:"tags" validate:"required,dive,min=3,max=32"`
 	Type          KeyType  `json:"type"`
 	Relay         string   `json:"relay"`
+	Groups        []TagID  `json:"groups"`
 }
 
 // RegisterResponse - the response to a successful enrollment register

+ 33 - 10
models/extclient.go

@@ -13,24 +13,47 @@ type ExtClient struct {
 	AllowedIPs             []string            `json:"allowed_ips"`
 	IngressGatewayID       string              `json:"ingressgatewayid" bson:"ingressgatewayid"`
 	IngressGatewayEndpoint string              `json:"ingressgatewayendpoint" bson:"ingressgatewayendpoint"`
-	LastModified           int64               `json:"lastmodified" bson:"lastmodified"`
+	LastModified           int64               `json:"lastmodified" bson:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
 	Enabled                bool                `json:"enabled" bson:"enabled"`
 	OwnerID                string              `json:"ownerid" bson:"ownerid"`
 	DeniedACLs             map[string]struct{} `json:"deniednodeacls" bson:"acls,omitempty"`
 	RemoteAccessClientID   string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
 	PostUp                 string              `json:"postup" bson:"postup"`
 	PostDown               string              `json:"postdown" bson:"postdown"`
+	Tags                   map[TagID]struct{}  `json:"tags"`
+	Os                     string              `json:"os"`
+	DeviceName             string              `json:"device_name"`
+	PublicEndpoint         string              `json:"public_endpoint"`
+	Country                string              `json:"country"`
 }
 
 // CustomExtClient - struct for CustomExtClient params
 type CustomExtClient struct {
-	ClientID             string              `json:"clientid,omitempty"`
-	PublicKey            string              `json:"publickey,omitempty"`
-	DNS                  string              `json:"dns,omitempty"`
-	ExtraAllowedIPs      []string            `json:"extraallowedips,omitempty"`
-	Enabled              bool                `json:"enabled,omitempty"`
-	DeniedACLs           map[string]struct{} `json:"deniednodeacls" bson:"acls,omitempty"`
-	RemoteAccessClientID string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
-	PostUp               string              `json:"postup" bson:"postup" validate:"max=1024"`
-	PostDown             string              `json:"postdown" bson:"postdown" validate:"max=1024"`
+	ClientID                   string              `json:"clientid,omitempty"`
+	PublicKey                  string              `json:"publickey,omitempty"`
+	DNS                        string              `json:"dns,omitempty"`
+	ExtraAllowedIPs            []string            `json:"extraallowedips,omitempty"`
+	Enabled                    bool                `json:"enabled,omitempty"`
+	DeniedACLs                 map[string]struct{} `json:"deniednodeacls" bson:"acls,omitempty"`
+	RemoteAccessClientID       string              `json:"remote_access_client_id"` // unique ID (MAC address) of RAC machine
+	PostUp                     string              `json:"postup" bson:"postup" validate:"max=1024"`
+	PostDown                   string              `json:"postdown" bson:"postdown" validate:"max=1024"`
+	Tags                       map[TagID]struct{}  `json:"tags"`
+	Os                         string              `json:"os"`
+	DeviceName                 string              `json:"device_name"`
+	IsAlreadyConnectedToInetGw bool                `json:"is_already_connected_to_inet_gw"`
+	PublicEndpoint             string              `json:"public_endpoint"`
+	Country                    string              `json:"country"`
+}
+
+func (ext *ExtClient) ConvertToStaticNode() Node {
+
+	return Node{
+		CommonNode: CommonNode{
+			Network: ext.Network,
+		},
+		Tags:       ext.Tags,
+		IsStatic:   true,
+		StaticNode: *ext,
+	}
 }

+ 1 - 1
models/host.go

@@ -71,7 +71,7 @@ type Host struct {
 	IsDefault           bool             `json:"isdefault"               yaml:"isdefault"`
 	NatType             string           `json:"nat_type,omitempty"      yaml:"nat_type,omitempty"`
 	TurnEndpoint        *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
-	PersistentKeepalive time.Duration    `json:"persistentkeepalive"     yaml:"persistentkeepalive"`
+	PersistentKeepalive time.Duration    `json:"persistentkeepalive" swaggertype:"primitive,integer" format:"int64" yaml:"persistentkeepalive"`
 }
 
 // FormatBool converts a boolean to a [yes|no] string

+ 11 - 9
models/metrics.go

@@ -14,15 +14,17 @@ type Metrics struct {
 
 // Metric - holds a metric for data between nodes
 type Metric struct {
-	NodeName      string        `json:"node_name" bson:"node_name" yaml:"node_name"`
-	Uptime        int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
-	TotalTime     int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
-	Latency       int64         `json:"latency" bson:"latency" yaml:"latency"`
-	TotalReceived int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
-	TotalSent     int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
-	ActualUptime  time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
-	PercentUp     float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
-	Connected     bool          `json:"connected" bson:"connected" yaml:"connected"`
+	NodeName          string        `json:"node_name" bson:"node_name" yaml:"node_name"`
+	Uptime            int64         `json:"uptime" bson:"uptime" yaml:"uptime" swaggertype:"primitive,integer" format:"int64"`
+	TotalTime         int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime" swaggertype:"primitive,integer" format:"int64"`
+	Latency           int64         `json:"latency" bson:"latency" yaml:"latency" swaggertype:"primitive,integer" format:"int64"`
+	TotalReceived     int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived" swaggertype:"primitive,integer" format:"int64"`
+	LastTotalReceived int64         `json:"lasttotalreceived" bson:"lasttotalreceived" yaml:"lasttotalreceived" swaggertype:"primitive,integer" format:"int64"`
+	TotalSent         int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent" swaggertype:"primitive,integer" format:"int64"`
+	LastTotalSent     int64         `json:"lasttotalsent" bson:"lasttotalsent" yaml:"lasttotalsent" swaggertype:"primitive,integer" format:"int64"`
+	ActualUptime      time.Duration `json:"actualuptime" swaggertype:"primitive,integer" format:"int64" bson:"actualuptime" yaml:"actualuptime"`
+	PercentUp         float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
+	Connected         bool          `json:"connected" bson:"connected" yaml:"connected"`
 }
 
 // IDandAddr - struct to hold ID and primary Address

+ 26 - 4
models/mqtt.go

@@ -24,12 +24,29 @@ type HostPeerUpdate struct {
 	FwUpdate          FwUpdate              `json:"fw_update"`
 	ReplacePeers      bool                  `json:"replace_peers"`
 	EndpointDetection bool                  `json:"endpoint_detection"`
+	ManageDNS         bool                  `yaml:"manage_dns"`
+	Stun              bool                  `yaml:"stun"`
+	StunServers       string                `yaml:"stun_servers"`
+}
+
+type FwRule struct {
+	SrcIP           net.IPNet `json:"src_ip"`
+	DstIP           net.IPNet `json:"dst_ip"`
+	AllowedProtocol Protocol  `json:"allowed_protocols"` // tcp, udp, etc.
+	AllowedPorts    []string  `json:"allowed_ports"`
+	Allow           bool      `json:"allow"`
 }
 
 // IngressInfo - struct for ingress info
 type IngressInfo struct {
-	ExtPeers     map[string]ExtClientInfo `json:"ext_peers" yaml:"ext_peers"`
-	EgressRanges []string                 `json:"egress_ranges" yaml:"egress_ranges"`
+	IngressID     string      `json:"ingress_id"`
+	Network       net.IPNet   `json:"network"`
+	Network6      net.IPNet   `json:"network6"`
+	StaticNodeIps []net.IP    `json:"static_node_ips"`
+	Rules         []FwRule    `json:"rules"`
+	AllowAll      bool        `json:"allow_all"`
+	EgressRanges  []net.IPNet `json:"egress_ranges"`
+	EgressRanges6 []net.IPNet `json:"egress_ranges6"`
 }
 
 // EgressInfo - struct for egress info
@@ -77,8 +94,13 @@ type KeyUpdate struct {
 
 // FwUpdate - struct for firewall updates
 type FwUpdate struct {
-	IsEgressGw bool                  `json:"is_egress_gw"`
-	EgressInfo map[string]EgressInfo `json:"egress_info"`
+	AllowAll        bool                   `json:"allow_all"`
+	AllowedNetworks []net.IPNet            `json:"networks"`
+	IsEgressGw      bool                   `json:"is_egress_gw"`
+	IsIngressGw     bool                   `json:"is_ingress_gw"`
+	EgressInfo      map[string]EgressInfo  `json:"egress_info"`
+	IngressInfo     map[string]IngressInfo `json:"ingress_info"`
+	AclRules        map[string]AclRule     `json:"acl_rules"`
 }
 
 // FailOverMeReq - struct for failover req

+ 5 - 229
models/names.go

@@ -1,242 +1,18 @@
 package models
 
 import (
-	"math/rand"
 	"time"
-)
-
-// NAMES - list of names 4-7 chars in length
-var NAMES = []string{
-	"logic",
-	"warrant",
-	"iconic",
-	"threat",
-	"strike",
-	"boy",
-	"vital",
-	"unity",
-	"audio",
-	"schemer",
-	"depth",
-	"gravitl",
-	"mystic",
-	"donkey",
-	"atomic",
-	"turtle",
-	"monkey",
-	"rabbit",
-	"static",
-	"mosaic",
-	"elite",
-	"stonks",
-	"doggy",
-	"python",
-	"mohawk",
-	"arctic",
-	"rival",
-	"vibes",
-	"delay",
-	"bridge",
-	"weeble",
-	"combat",
-	"animal",
-	"wobble",
-	"rubble",
-	"bucket",
-	"proof",
-	"worker",
-	"beetle",
-	"racket",
-	"equal",
-	"panda",
-	"antics",
-	"strong",
-	"forum",
-	"koala",
-	"anchor",
-	"ornery",
-	"indigo",
-	"schism",
-	"dragon",
-	"knight",
-	"bishop",
-	"laser",
-	"rhino",
-	"clutch",
-	"shark",
-	"leader",
-	"young",
-	"robot",
-	"squish",
-	"chimp",
-	"rocket",
-	"space",
-	"queen",
-	"royalty",
-	"flush",
-	"earth",
-	"planet",
-	"heart",
-	"droplet",
-	"dillon",
-	"saturn",
-	"pluto",
-	"school",
-	"alien",
-	"matte",
-	"dingo",
-	"meercat",
-	"cookie",
-	"snack",
-	"goose",
-	"pepper",
-	"melissa",
-	"alex",
-	"elon",
-	"yeet",
-	"meh",
-	"walrus",
-	"avatar",
-	"chicken",
-	"proton",
-	"mohawk",
-	"tattoo",
-	"zebra",
-	"star",
-	"butter",
-	"tango",
-	"homie",
-	"rambo",
-	"cosmo",
-	"bubbles",
-	"hulk",
-	"pluto",
-	"scooby",
-	"thanos",
-	"yoda",
-	"draco",
-	"goofy",
-	"ditto",
-	"puff",
-	"duck",
-	"mouse",
-	"akita",
-	"water",
-	"hound",
-	"baby",
-	"spider",
-	"squid",
-	"roach",
-	"crab",
-	"cougar",
-	"cyborg",
-	"android",
-	"being",
-	"ninja",
-	"unicorn",
-	"zombie",
-	"warrior",
-	"zamboni",
-	"life",
-	"marine",
-	"node",
-	"mother",
-	"father",
-	"tesla",
-}
 
-// SMALL_NAMES - list of small (4 char or less) names
-var SMALL_NAMES = []string{
-	"ace",
-	"odd",
-	"hot",
-	"ill",
-	"root",
-	"sudo",
-	"moon",
-	"beef",
-	"bro",
-	"dank",
-	"red",
-	"gold",
-	"big",
-	"old",
-	"og",
-	"best",
-	"blue",
-	"lil",
-	"mom",
-	"bot",
-	"evil",
-	"good",
-	"holy",
-	"rad",
-	"bad",
-	"sad",
-	"mad",
-	"chad",
-	"pre",
-	"post",
-	"foot",
-	"soft",
-	"hard",
-	"lite",
-	"dark",
-	"true",
-	"toy",
-	"soy",
-	"rude",
-	"nice",
-	"fun",
-	"fat",
-	"pro",
-	"sly",
-	"tan",
-	"pet",
-	"fine",
-	"main",
-	"last",
-	"wide",
-	"free",
-	"open",
-	"poor",
-	"rich",
-	"next",
-	"real",
-	"long",
-	"huge",
-	"wild",
-	"sick",
-	"weak",
-	"firm",
-	"pink",
-	"okay",
-	"dull",
-	"loud",
-	"lazy",
-	"dumb",
-	"tidy",
-	"idle",
-	"bony",
-	"cute",
-	"oily",
-	"lame",
-	"mega",
-	"limp",
-	"wavy",
-	"edgy",
-	"nosy",
-	"zany",
-	"base",
-	"cold",
-}
+	"github.com/goombaio/namegenerator"
+)
 
 var logoString = retrieveLogo()
 
 // GenerateNodeName - generates a random node name
 func GenerateNodeName() string {
-	rand.Seed(time.Now().UnixNano())
-	return SMALL_NAMES[rand.Intn(len(SMALL_NAMES))] + "-" + NAMES[seededRand.Intn(len(NAMES))]
+	seed := time.Now().UTC().UnixNano()
+	nameGenerator := namegenerator.NewNameGenerator(seed)
+	return nameGenerator.Generate()
 }
 
 // RetrieveLogo - retrieves the ascii art logo for Netmaker

+ 7 - 2
models/network.go

@@ -11,8 +11,8 @@ type Network struct {
 	AddressRange        string `json:"addressrange" bson:"addressrange" validate:"omitempty,cidrv4"`
 	AddressRange6       string `json:"addressrange6" bson:"addressrange6" validate:"omitempty,cidrv6"`
 	NetID               string `json:"netid" bson:"netid" validate:"required,min=1,max=32,netid_valid"`
-	NodesLastModified   int64  `json:"nodeslastmodified" bson:"nodeslastmodified"`
-	NetworkLastModified int64  `json:"networklastmodified" bson:"networklastmodified"`
+	NodesLastModified   int64  `json:"nodeslastmodified" bson:"nodeslastmodified" swaggertype:"primitive,integer" format:"int64"`
+	NetworkLastModified int64  `json:"networklastmodified" bson:"networklastmodified" swaggertype:"primitive,integer" format:"int64"`
 	DefaultInterface    string `json:"defaultinterface" bson:"defaultinterface" validate:"min=1,max=35"`
 	DefaultListenPort   int32  `json:"defaultlistenport,omitempty" bson:"defaultlistenport,omitempty" validate:"omitempty,min=1024,max=65535"`
 	NodeLimit           int32  `json:"nodelimit" bson:"nodelimit"`
@@ -97,3 +97,8 @@ func (network *Network) GetNetworkNetworkCIDR6() *net.IPNet {
 	_, netCidr, _ := net.ParseCIDR(network.AddressRange6)
 	return netCidr
 }
+
+type NetworkStatResp struct {
+	Network
+	Hosts int `json:"hosts"`
+}

+ 47 - 14
models/node.go

@@ -11,6 +11,19 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
+type NodeStatus string
+
+const (
+	OnlineSt  NodeStatus = "online"
+	OfflineSt NodeStatus = "offline"
+	WarningSt NodeStatus = "warning"
+	ErrorSt   NodeStatus = "error"
+	UnKnown   NodeStatus = "unknown"
+)
+
+// LastCheckInThreshold - if node's checkin more than this threshold,then node is declared as offline
+const LastCheckInThreshold = time.Minute * 10
+
 const (
 	// NODE_SERVER_NAME - the default server name
 	NODE_SERVER_NAME = "netmaker"
@@ -77,16 +90,18 @@ type CommonNode struct {
 // Node - a model of a network node
 type Node struct {
 	CommonNode
-	PendingDelete           bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
-	LastModified            time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
-	LastCheckIn             time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
-	LastPeerUpdate          time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
-	ExpirationDateTime      time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
-	EgressGatewayNatEnabled bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
-	IngressGatewayRange     string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
-	IngressGatewayRange6    string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
-	Metadata                string               `json:"metadata"`
+	PendingDelete              bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
+	LastModified               time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
+	LastCheckIn                time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
+	LastPeerUpdate             time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
+	ExpirationDateTime         time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
+	EgressGatewayNatEnabled    bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
+	EgressGatewayRequest       EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
+	IngressGatewayRange        string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
+	IngressGatewayRange6       string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
+	IngressPersistentKeepalive int32                `json:"ingresspersistentkeepalive"     bson:"ingresspersistentkeepalive"     yaml:"ingresspersistentkeepalive"`
+	IngressMTU                 int32                `json:"ingressmtu"     bson:"ingressmtu"     yaml:"ingressmtu"`
+	Metadata                   string               `json:"metadata"`
 	// == PRO ==
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
@@ -97,6 +112,11 @@ type Node struct {
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id"                                     yaml:"internetgw_node_id"`
 	AdditionalRagIps  []net.IP            `json:"additional_rag_ips"                                     yaml:"additional_rag_ips"                                     swaggertype:"array,number"`
+	Tags              map[TagID]struct{}  `json:"tags" yaml:"tags"`
+	IsStatic          bool                `json:"is_static"`
+	IsUserNode        bool                `json:"is_user_node"`
+	StaticNode        ExtClient           `json:"static_node"`
+	Status            NodeStatus          `json:"node_status"`
 }
 
 // LegacyNode - legacy struct for node model
@@ -117,10 +137,10 @@ type LegacyNode struct {
 	IsHub                   string               `json:"ishub"                   bson:"ishub"                   yaml:"ishub"                   validate:"checkyesorno"`
 	AccessKey               string               `json:"accesskey"               bson:"accesskey"               yaml:"accesskey"`
 	Interface               string               `json:"interface"               bson:"interface"               yaml:"interface"`
-	LastModified            int64                `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
-	ExpirationDateTime      int64                `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
-	LastPeerUpdate          int64                `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
-	LastCheckIn             int64                `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
+	LastModified            int64                `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
+	ExpirationDateTime      int64                `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
+	LastPeerUpdate          int64                `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
+	LastCheckIn             int64                `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
 	MacAddress              string               `json:"macaddress"              bson:"macaddress"              yaml:"macaddress"`
 	Password                string               `json:"password"                bson:"password"                yaml:"password"                validate:"required,min=6"`
 	Network                 string               `json:"network"                 bson:"network"                 yaml:"network"                 validate:"network_exists"`
@@ -195,6 +215,19 @@ func (node *Node) PrimaryAddress() string {
 	return node.Address6.IP.String()
 }
 
+func (node *Node) AddressIPNet4() net.IPNet {
+	return net.IPNet{
+		IP:   node.Address.IP,
+		Mask: net.CIDRMask(32, 32),
+	}
+}
+func (node *Node) AddressIPNet6() net.IPNet {
+	return net.IPNet{
+		IP:   node.Address6.IP,
+		Mask: net.CIDRMask(128, 128),
+	}
+}
+
 // ExtClient.PrimaryAddress - returns ipv4 IPNet format
 func (extPeer *ExtClient) AddressIPNet4() net.IPNet {
 	return net.IPNet{

+ 20 - 4
models/structs.go

@@ -45,6 +45,16 @@ type UserRemoteGws struct {
 	NetworkAddresses  []string  `json:"network_addresses"`
 }
 
+// UserRAGs - struct for user access gws
+type UserRAGs struct {
+	GwID              string `json:"remote_access_gw_id"`
+	GWName            string `json:"gw_name"`
+	Network           string `json:"network"`
+	Connected         bool   `json:"connected"`
+	IsInternetGateway bool   `json:"is_internet_gateway"`
+	Metadata          string `json:"metadata"`
+}
+
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
 type UserRemoteGwsReq struct {
 	RemoteAccessClientID string `json:"remote_access_clientid"`
@@ -163,9 +173,11 @@ type HostRelayRequest struct {
 
 // IngressRequest - ingress request struct
 type IngressRequest struct {
-	ExtclientDNS      string `json:"extclientdns"`
-	IsInternetGateway bool   `json:"is_internet_gw"`
-	Metadata          string `json:"metadata"`
+	ExtclientDNS        string `json:"extclientdns"`
+	IsInternetGateway   bool   `json:"is_internet_gw"`
+	Metadata            string `json:"metadata"`
+	PersistentKeepalive int32  `json:"persistentkeepalive"`
+	MTU                 int32  `json:"mtu"`
 }
 
 // InetNodeReq - exit node request struct
@@ -184,7 +196,7 @@ type ServerUpdateData struct {
 // also contains assymetrical encryption pub/priv keys for any server traffic
 type Telemetry struct {
 	UUID           string `json:"uuid" bson:"uuid"`
-	LastSend       int64  `json:"lastsend" bson:"lastsend"`
+	LastSend       int64  `json:"lastsend" bson:"lastsend" swaggertype:"primitive,integer" format:"int64"`
 	TrafficKeyPriv []byte `json:"traffickeypriv" bson:"traffickeypriv"`
 	TrafficKeyPub  []byte `json:"traffickeypub" bson:"traffickeypub"`
 }
@@ -254,6 +266,10 @@ type ServerConfig struct {
 	IsPro          bool   `yaml:"isee" json:"Is_EE"`
 	TrafficKey     []byte `yaml:"traffickey"`
 	MetricInterval string `yaml:"metric_interval"`
+	ManageDNS      bool   `yaml:"manage_dns"`
+	Stun           bool   `yaml:"stun"`
+	StunServers    string `yaml:"stun_servers"`
+	DefaultDomain  string `yaml:"default_domain"`
 }
 
 // User.NameInCharset - returns if name is in charset below or not

+ 52 - 0
models/tags.go

@@ -0,0 +1,52 @@
+package models
+
+import (
+	"fmt"
+	"time"
+)
+
+type TagID string
+
+const (
+	RemoteAccessTagName = "remote-access-gws"
+)
+
+func (id TagID) String() string {
+	return string(id)
+}
+
+func (t Tag) GetIDFromName() string {
+	return fmt.Sprintf("%s.%s", t.Network, t.TagName)
+}
+
+type Tag struct {
+	ID        TagID     `json:"id"`
+	TagName   string    `json:"tag_name"`
+	Network   NetworkID `json:"network"`
+	CreatedBy string    `json:"created_by"`
+	CreatedAt time.Time `json:"created_at"`
+}
+
+type CreateTagReq struct {
+	TagName     string    `json:"tag_name"`
+	Network     NetworkID `json:"network"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type TagListResp struct {
+	Tag
+	UsedByCnt   int       `json:"used_by_count"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type TagListRespNodes struct {
+	Tag
+	UsedByCnt   int       `json:"used_by_count"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}
+
+type UpdateTagReq struct {
+	Tag
+	NewName     string    `json:"new_name"`
+	TaggedNodes []ApiNode `json:"tagged_nodes"`
+}

+ 21 - 14
models/user_mgmt.go

@@ -62,6 +62,7 @@ const (
 	EnrollmentKeysRsrc RsrcType = "enrollment_key"
 	UserRsrc           RsrcType = "users"
 	AclRsrc            RsrcType = "acl"
+	TagRsrc            RsrcType = "tag"
 	DnsRsrc            RsrcType = "dns"
 	FailOverRsrc       RsrcType = "fail_over"
 	MetricRsrc         RsrcType = "metrics"
@@ -79,7 +80,8 @@ const (
 	AllUserRsrcID           RsrcID = "all_user"
 	AllDnsRsrcID            RsrcID = "all_dns"
 	AllFailOverRsrcID       RsrcID = "all_fail_over"
-	AllAclsRsrcID           RsrcID = "all_acls"
+	AllAclsRsrcID           RsrcID = "all_acl"
+	AllTagsRsrcID           RsrcID = "all_tag"
 )
 
 // Pre-Defined User Roles
@@ -116,8 +118,9 @@ type RsrcPermissionScope struct {
 
 type UserRolePermissionTemplate struct {
 	ID                  UserRoleID                                  `json:"id"`
-	UiName              string                                      `json:"ui_name"`
+	Name                string                                      `json:"name"`
 	Default             bool                                        `json:"default"`
+	MetaData            string                                      `json:"meta_data"`
 	DenyDashboardAccess bool                                        `json:"deny_dashboard_access"`
 	FullAccess          bool                                        `json:"full_access"`
 	NetworkID           NetworkID                                   `json:"network_id"`
@@ -132,22 +135,25 @@ type CreateGroupReq struct {
 
 type UserGroup struct {
 	ID           UserGroupID                           `json:"id"`
+	Default      bool                                  `json:"default"`
+	Name         string                                `json:"name"`
 	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
 	MetaData     string                                `json:"meta_data"`
 }
 
 // User struct - struct for Users
 type User struct {
-	UserName       string                                `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"`
-	Password       string                                `json:"password" bson:"password" validate:"required,min=5"`
-	IsAdmin        bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
-	IsSuperAdmin   bool                                  `json:"issuperadmin"`           // deprecated
-	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"`          // deprecated
-	AuthType       AuthType                              `json:"auth_type"`
-	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
-	PlatformRoleID UserRoleID                            `json:"platform_role_id"`
-	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
-	LastLoginTime  time.Time                             `json:"last_login_time"`
+	UserName                   string                                `json:"username" bson:"username" validate:"min=3,in_charset|email"`
+	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	Password                   string                                `json:"password" bson:"password" validate:"required,min=5"`
+	IsAdmin                    bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
+	IsSuperAdmin               bool                                  `json:"issuperadmin"`           // deprecated
+	RemoteGwIDs                map[string]struct{}                   `json:"remote_gw_ids"`          // deprecated
+	AuthType                   AuthType                              `json:"auth_type"`
+	UserGroups                 map[UserGroupID]struct{}              `json:"user_group_ids"`
+	PlatformRoleID             UserRoleID                            `json:"platform_role_id"`
+	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	LastLoginTime              time.Time                             `json:"last_login_time"`
 }
 
 type ReturnUserWithRolesAndGroups struct {
@@ -176,8 +182,9 @@ type UserAuthParams struct {
 
 // UserClaims - user claims struct
 type UserClaims struct {
-	Role     UserRoleID
-	UserName string
+	Role           UserRoleID
+	UserName       string
+	RacAutoDisable bool
 	jwt.RegisteredClaims
 }
 

+ 1 - 1
mq/emqx_on_prem.go

@@ -261,7 +261,7 @@ func (e *EmqxOnPrem) CreateDefaultAllowRule() error {
 	if err != nil {
 		return err
 	}
-	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/all", bytes.NewReader(payload))
+	req, err := http.NewRequest(http.MethodPost, servercfg.GetEmqxRestEndpoint()+"/api/v5/authorization/sources/built_in_database/rules/all", bytes.NewReader(payload))
 	if err != nil {
 		return err
 	}

+ 24 - 2
mq/migrate.go

@@ -88,10 +88,32 @@ func SendPullSYN() error {
 			Host:   host,
 		}
 		msg, _ := json.Marshal(hostUpdate)
-		encrypted, encryptErr := encryptMsg(&host, msg)
-		if encryptErr != nil {
+		var encrypted []byte
+		var encryptErr error
+		vlt, err := logic.VersionLessThan(host.Version, "v0.30.0")
+		if err != nil {
+			slog.Warn("error checking version less than", "warn", err)
 			continue
 		}
+		if vlt {
+			encrypted, encryptErr = encryptMsg(&host, msg)
+			if encryptErr != nil {
+				slog.Warn("error encrypt with encryptMsg", "warn", encryptErr)
+				continue
+			}
+		} else {
+			zipped, err := compressPayload(msg)
+			if err != nil {
+				slog.Warn("error compressing message", "warn", err)
+				continue
+			}
+			encrypted, encryptErr = encryptAESGCM(host.TrafficKeyPublic[0:32], zipped)
+			if encryptErr != nil {
+				slog.Warn("error encrypt with encryptMsg", "warn", encryptErr)
+				continue
+			}
+		}
+
 		logger.Log(0, "sending pull syn to", host.Name)
 		mqclient.Publish(fmt.Sprintf("host/update/%s/%s", hostUpdate.Host.ID.String(), servercfg.GetServer()), 0, true, encrypted)
 	}

+ 1 - 1
mq/mq.go

@@ -35,7 +35,7 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 	opts.SetConnectRetry(true)
 	opts.SetCleanSession(true)
 	opts.SetConnectRetryInterval(time.Second * 1)
-	opts.SetKeepAlive(time.Second * 10)
+	opts.SetKeepAlive(time.Second * 15)
 	opts.SetOrderMatters(false)
 	opts.SetWriteTimeout(time.Minute)
 }

+ 69 - 29
mq/publishers.go

@@ -7,6 +7,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -14,15 +15,17 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var batchSize = servercfg.GetPeerUpdateBatchSize()
-var batchUpdate = servercfg.GetBatchPeerUpdate()
-
 // PublishPeerUpdate --- determines and publishes a peer update to all the hosts
 func PublishPeerUpdate(replacePeers bool) error {
+
 	if !servercfg.IsMessageQueueBackend() {
 		return nil
 	}
 
+	if servercfg.GetManageDNS() {
+		sendDNSSync()
+	}
+
 	hosts, err := logic.GetAllHosts()
 	if err != nil {
 		logger.Log(1, "err getting all hosts", err.Error())
@@ -33,35 +36,20 @@ func PublishPeerUpdate(replacePeers bool) error {
 		return err
 	}
 
-	//if batch peer update disabled
-	if !batchUpdate {
-		for _, host := range hosts {
-			host := host
-			go func(host models.Host) {
-				if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers, nil); err != nil {
-					logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+	for _, host := range hosts {
+		host := host
+		time.Sleep(5 * time.Millisecond)
+		go func(host models.Host) {
+			if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers, nil); err != nil {
+				id := host.Name
+				if host.ID != uuid.Nil {
+					id = host.ID.String()
 				}
-			}(host)
-		}
-		return nil
+				slog.Error("failed to publish peer update to host", id, ": ", err)
+			}
+		}(host)
 	}
 
-	//if batch peer update enabled
-	batchHost := BatchItems(hosts, batchSize)
-	var wg sync.WaitGroup
-	for _, v := range batchHost {
-		hostLen := len(v)
-		wg.Add(hostLen)
-		for i := 0; i < hostLen; i++ {
-			host := hosts[i]
-			go func(host models.Host) {
-				if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers, &wg); err != nil {
-					logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
-				}
-			}(host)
-		}
-		wg.Wait()
-	}
 	return nil
 }
 
@@ -249,3 +237,55 @@ func sendPeers() {
 		}
 	}
 }
+
+func SendDNSSyncByNetwork(network string) error {
+
+	k, err := logic.GetDNS(network)
+	if err == nil && len(k) > 0 {
+		err = PushSyncDNS(k)
+		if err != nil {
+			slog.Warn("error publishing dns entry data for network ", network, err.Error())
+		}
+	}
+
+	return err
+}
+
+func sendDNSSync() error {
+
+	networks, err := logic.GetNetworks()
+	if err == nil && len(networks) > 0 {
+		for _, v := range networks {
+			k, err := logic.GetDNS(v.NetID)
+			if err == nil && len(k) > 0 {
+				err = PushSyncDNS(k)
+				if err != nil {
+					slog.Warn("error publishing dns entry data for network ", v.NetID, err.Error())
+				}
+			}
+		}
+		return nil
+	}
+	return err
+}
+
+func PushSyncDNS(dnsEntries []models.DNSEntry) error {
+	logger.Log(2, "----> Pushing Sync DNS")
+	data, err := json.Marshal(dnsEntries)
+	if err != nil {
+		return errors.New("failed to marshal DNS entries: " + err.Error())
+	}
+	if mqclient == nil || !mqclient.IsConnectionOpen() {
+		return errors.New("cannot publish ... mqclient not connected")
+	}
+	if token := mqclient.Publish(fmt.Sprintf("host/dns/sync/%s", dnsEntries[0].Network), 0, true, data); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil {
+		var err error
+		if token.Error() == nil {
+			err = errors.New("connection timeout")
+		} else {
+			err = token.Error()
+		}
+		return err
+	}
+	return nil
+}

+ 61 - 3
mq/util.go

@@ -1,8 +1,14 @@
 package mq
 
 import (
+	"bytes"
+	"compress/gzip"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
 	"errors"
 	"fmt"
+	"io"
 	"math"
 	"strings"
 	"time"
@@ -66,6 +72,39 @@ func BatchItems[T any](items []T, batchSize int) [][]T {
 	return batches
 }
 
+func compressPayload(data []byte) ([]byte, error) {
+	var buf bytes.Buffer
+	zw := gzip.NewWriter(&buf)
+	if _, err := zw.Write(data); err != nil {
+		return nil, err
+	}
+	zw.Close()
+	return buf.Bytes(), nil
+}
+func encryptAESGCM(key, plaintext []byte) ([]byte, error) {
+	// Create AES block cipher
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create GCM (Galois/Counter Mode) cipher
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create a random nonce
+	nonce := make([]byte, aesGCM.NonceSize())
+	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+		return nil, err
+	}
+
+	// Encrypt the data
+	ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
+	return ciphertext, nil
+}
+
 func encryptMsg(host *models.Host, msg []byte) ([]byte, error) {
 	if host.OS == models.OS_Types.IoT {
 		return msg, nil
@@ -96,10 +135,29 @@ func encryptMsg(host *models.Host, msg []byte) ([]byte, error) {
 
 func publish(host *models.Host, dest string, msg []byte) error {
 
-	encrypted, encryptErr := encryptMsg(host, msg)
-	if encryptErr != nil {
-		return encryptErr
+	var encrypted []byte
+	var encryptErr error
+	vlt, err := logic.VersionLessThan(host.Version, "v0.30.0")
+	if err != nil {
+		slog.Warn("error checking version less than", "error", err)
+		return err
 	}
+	if vlt {
+		encrypted, encryptErr = encryptMsg(host, msg)
+		if encryptErr != nil {
+			return encryptErr
+		}
+	} else {
+		zipped, err := compressPayload(msg)
+		if err != nil {
+			return err
+		}
+		encrypted, encryptErr = encryptAESGCM(host.TrafficKeyPublic[0:32], zipped)
+		if encryptErr != nil {
+			return encryptErr
+		}
+	}
+
 	if mqclient == nil || !mqclient.IsConnectionOpen() {
 		return errors.New("cannot publish ... mqclient not connected")
 	}

+ 1 - 1
netclient/ncutils/netclientutils.go

@@ -6,7 +6,7 @@ import (
 )
 
 // DEFAULT_GC_PERCENT - garbage collection percent
-const DEFAULT_GC_PERCENT = 10
+const DEFAULT_GC_PERCENT = 100
 
 // == OS PATH FUNCTIONS ==
 

+ 28 - 12
pro/auth/azure-ad.go

@@ -3,6 +3,7 @@ package auth
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -60,7 +61,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getAzureUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
-		if strings.Contains(err.Error(), "invalid oauth state") {
+		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
 			handleOauthNotValid(w)
 			return
 		}
@@ -74,12 +75,23 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		inviteExists = true
 	}
 	// check if user approval is already pending
-	if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
-
-	_, err = logic.GetUser(content.UserPrincipalName)
+	// if user exists with provider ID, convert them into email ID
+	user, err := logic.GetUser(content.UserPrincipalName)
+	if err == nil {
+		_, err := logic.GetUser(content.Email)
+		if err != nil {
+			user.UserName = content.Email
+			user.ExternalIdentityProviderID = content.UserPrincipalName
+			database.DeleteRecord(database.USERS_TABLE_NAME, content.UserPrincipalName)
+			d, _ := json.Marshal(user)
+			database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME)
+		}
+	}
+	_, err = logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			if inviteExists {
@@ -89,20 +101,20 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 				}
-				user.UserName = content.UserPrincipalName // override username with azure id
+				user.ExternalIdentityProviderID = content.UserPrincipalName
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					return
 				}
 				logic.DeleteUserInvite(content.Email)
-				logic.DeletePendingUser(content.UserPrincipalName)
+				logic.DeletePendingUser(content.Email)
 			} else {
-				if !isEmailAllowed(content.UserPrincipalName) {
+				if !isEmailAllowed(content.Email) {
 					handleOauthUserNotAllowedToSignUp(w)
 					return
 				}
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.UserPrincipalName,
+					UserName: content.Email,
 				})
 				if err != nil {
 					handleSomethingWentWrong(w)
@@ -116,7 +128,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	user, err := logic.GetUser(content.UserPrincipalName)
+	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		handleOauthUserNotFound(w)
 		return
@@ -136,7 +148,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	}
 	// send a netmaker jwt token
 	var authRequest = models.UserAuthParams{
-		UserName: content.UserPrincipalName,
+		UserName: content.Email,
 		Password: newPass,
 	}
 
@@ -146,8 +158,8 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	logger.Log(1, "completed azure OAuth sigin in for", content.UserPrincipalName)
-	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.UserPrincipalName, http.StatusPermanentRedirect)
+	logger.Log(1, "completed azure OAuth sigin in for", content.Email)
+	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }
 
 func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
@@ -187,6 +199,10 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	if userInfo.Email == "" {
 		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
 	}
+	if userInfo.Email == "" {
+		err = errors.New("failed to fetch user email from SSO state")
+		return userInfo, err
+	}
 	return userInfo, nil
 }
 

+ 3 - 3
pro/auth/error.go

@@ -18,7 +18,7 @@ var htmlBaseTemplate = `<!DOCTYPE html>
 	<script type="text/javascript">
 	function redirect()
     {
-    	window.location.href="` + servercfg.GetFrontendURL() + `";
+    	window.location.href="` + fmt.Sprintf("https://dashboard.%s/login", servercfg.GetNmBaseDomain()) + `";
     }
 	</script>
 	<style>
@@ -93,12 +93,12 @@ var htmlBaseTemplate = `<!DOCTYPE html>
 </html>`
 
 var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `<h2>Your Netmaker server does not have OAuth configured.</h2>
-<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
+<p>Please visit the docs <a href="https://docs.netmaker.io/docs/server-installation/integrating-oauth" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
 
 var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
 
 var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account does not have access to the dashboard. Please contact your administrator for more information about your account.</h2>
-<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>`)
+<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/docs/remote-access-client-rac#downloadinstallation" target="_blank" rel="noopener">our Remote Access Client.</a></p>`)
 
 var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
 

+ 71 - 13
pro/auth/github.go

@@ -3,6 +3,7 @@ package auth
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -60,7 +61,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGithubUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
-		if strings.Contains(err.Error(), "invalid oauth state") {
+		if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") {
 			handleOauthNotValid(w)
 			return
 		}
@@ -74,11 +75,25 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		inviteExists = true
 	}
 	// check if user approval is already pending
-	if !inviteExists && logic.IsPendingUser(content.Login) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
-	_, err = logic.GetUser(content.Login)
+	// if user exists with provider ID, convert them into email ID
+	user, err := logic.GetUser(content.Login)
+	if err == nil {
+		// checks if user exists with email
+		_, err := logic.GetUser(content.Email)
+		if err != nil {
+			user.UserName = content.Email
+			user.ExternalIdentityProviderID = content.Login
+			database.DeleteRecord(database.USERS_TABLE_NAME, content.Login)
+			d, _ := json.Marshal(user)
+			database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME)
+		}
+
+	}
+	_, err = logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			if inviteExists {
@@ -88,20 +103,20 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 				}
-				user.UserName = content.Login // overrides email with github id
+				user.ExternalIdentityProviderID = content.Login
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					return
 				}
 				logic.DeleteUserInvite(content.Email)
-				logic.DeletePendingUser(content.Login)
+				logic.DeletePendingUser(content.Email)
 			} else {
-				if !isEmailAllowed(content.Login) {
+				if !isEmailAllowed(content.Email) {
 					handleOauthUserNotAllowedToSignUp(w)
 					return
 				}
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Login,
+					UserName: content.Email,
 				})
 				if err != nil {
 					handleSomethingWentWrong(w)
@@ -115,7 +130,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	user, err := logic.GetUser(content.Login)
+	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		handleOauthUserNotFound(w)
 		return
@@ -135,7 +150,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	}
 	// send a netmaker jwt token
 	var authRequest = models.UserAuthParams{
-		UserName: content.Login,
+		UserName: content.Email,
 		Password: newPass,
 	}
 
@@ -145,11 +160,11 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	logger.Log(1, "completed github OAuth sigin in for", content.Login)
-	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Login, http.StatusPermanentRedirect)
+	logger.Log(1, "completed github OAuth sigin in for", content.Email)
+	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }
 
-func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
+func getGithubUserInfo(state, code string) (*OAuthUser, error) {
 	oauth_state_string, isValid := logic.IsStateValid(state)
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth state")
@@ -187,7 +202,16 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
 	}
 	userInfo.AccessToken = string(data)
 	if userInfo.Email == "" {
-		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
+		// if user's email is not made public, get the info from the github emails api
+		logger.Log(2, "fetching user email from github api")
+		userInfo.Email, err = getGithubEmailsInfo(token.AccessToken)
+		if err != nil {
+			logger.Log(0, "failed to fetch user's email from github: ", err.Error())
+		}
+	}
+	if userInfo.Email == "" {
+		err = errors.New("failed to fetch user email from SSO state")
+		return userInfo, err
 	}
 	return userInfo, nil
 }
@@ -195,3 +219,37 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
 func verifyGithubUser(token *oauth2.Token) bool {
 	return token.Valid()
 }
+
+func getGithubEmailsInfo(accessToken string) (string, error) {
+
+	var httpClient = &http.Client{}
+	var httpReq, reqErr = http.NewRequest("GET", "https://api.github.com/user/emails", nil)
+	if reqErr != nil {
+		return "", fmt.Errorf("failed to create request to GitHub")
+	}
+	httpReq.Header.Add("Accept", "application/vnd.github.v3+json")
+	httpReq.Header.Set("Authorization", "token "+accessToken)
+	response, err := httpClient.Do(httpReq)
+	if err != nil {
+		return "", fmt.Errorf("failed getting user info: %s", err.Error())
+	}
+	defer response.Body.Close()
+	contents, err := io.ReadAll(response.Body)
+	if err != nil {
+		return "", fmt.Errorf("failed reading response body: %s", err.Error())
+	}
+
+	emailsInfo := []interface{}{}
+	err = json.Unmarshal(contents, &emailsInfo)
+	if err != nil {
+		return "", err
+	}
+	for _, info := range emailsInfo {
+		emailInfoMap := info.(map[string]interface{})
+		if emailInfoMap["primary"].(bool) {
+			return emailInfoMap["email"].(string), nil
+		}
+
+	}
+	return "", errors.New("email not found")
+}

+ 1 - 1
pro/auth/google.go

@@ -90,7 +90,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 				}
-
+				user.ExternalIdentityProviderID = content.Email
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					return

+ 2 - 2
pro/auth/oidc.go

@@ -80,10 +80,9 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-
 	var inviteExists bool
 	// check if invite exists for User
-	in, err := logic.GetUserInvite(content.Login)
+	in, err := logic.GetUserInvite(content.Email)
 	if err == nil {
 		inviteExists = true
 	}
@@ -102,6 +101,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 				}
+				user.ExternalIdentityProviderID = content.Email
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					return

+ 1 - 1
pro/auth/templates.go

@@ -118,7 +118,7 @@ var ssoErrCallbackTemplate = template.Must(
 		<h4>Error reason: {.Verb}</h4>
 		<em>Your Netmaker server may not have SSO configured properly.</em>
 		<em>
-			Please visit the <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">docs</a> for more information.
+			Please visit the <a href="https://docs.netmaker.io/docs/server-installation/integrating-oauth" target="_blank" rel="noopener">docs</a> for more information.
 		</em>
 		<p>
 			If you feel this is a mistake, please contact your network administrator.

+ 16 - 0
pro/controllers/failover.go

@@ -203,6 +203,14 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	if peerNode.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as failover"), "badrequest"),
+		)
+		return
+	}
 	if node.IsFailOver {
 		logic.ReturnErrorResponse(
 			w,
@@ -211,6 +219,14 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		return
 	}
+	if peerNode.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as failover"), "badrequest"),
+		)
+		return
+	}
 	if node.IsRelayed && node.RelayedBy == peerNode.ID.String() {
 		logic.ReturnErrorResponse(
 			w,

+ 14 - 0
pro/controllers/rac.go

@@ -0,0 +1,14 @@
+package controllers
+
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logic"
+)
+
+func RacHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/rac/networks", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworks))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/rac/network/{network}/access_points", logic.SecurityCheck(false, http.HandlerFunc(getUserRemoteAccessNetworkGateways))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/rac/access_point/{access_point_id}/config", logic.SecurityCheck(false, http.HandlerFunc(getRemoteAccessGatewayConf))).Methods(http.MethodGet)
+}

+ 260 - 22
pro/controllers/users.go

@@ -71,8 +71,8 @@ func UserHandlers(r *mux.Router) {
 //	Responses:
 //		200: ReturnSuccessResponse
 func userInviteSignUp(w http.ResponseWriter, r *http.Request) {
-	email, _ := url.QueryUnescape(r.URL.Query().Get("email"))
-	code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code"))
+	email := r.URL.Query().Get("email")
+	code := r.URL.Query().Get("invite_code")
 	in, err := logic.GetUserInvite(email)
 	if err != nil {
 		logger.Log(0, "failed to fetch users: ", err.Error())
@@ -133,8 +133,8 @@ func userInviteSignUp(w http.ResponseWriter, r *http.Request) {
 //	Responses:
 //		200: ReturnSuccessResponse
 func userInviteVerify(w http.ResponseWriter, r *http.Request) {
-	email, _ := url.QueryUnescape(r.URL.Query().Get("email"))
-	code, _ := url.QueryUnescape(r.URL.Query().Get("invite_code"))
+	email := r.URL.Query().Get("email")
+	code := r.URL.Query().Get("invite_code")
 	err := logic.ValidateAndApproveUserInvite(email, code)
 	if err != nil {
 		logger.Log(0, "failed to fetch users: ", err.Error())
@@ -206,6 +206,10 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 	}
 	for _, inviteeEmail := range inviteReq.UserEmails {
 		// check if user with email exists, then ignore
+		if !email.IsValid(inviteeEmail) {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid email "+inviteeEmail), "badrequest"))
+			return
+		}
 		_, err := logic.GetUser(inviteeEmail)
 		if err == nil {
 			// user exists already, so ignore
@@ -228,6 +232,14 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 			slog.Error("failed to parse to invite url", "error", err)
 			return
 		}
+		if servercfg.DeployedByOperator() {
+			u, err = url.Parse(fmt.Sprintf("%s/invite?tenant_id=%s&email=%s&invite_code=%s",
+				proLogic.GetAccountsUIHost(), url.QueryEscape(servercfg.GetNetmakerTenantID()), url.QueryEscape(invite.Email), url.QueryEscape(invite.InviteCode)))
+			if err != nil {
+				slog.Error("failed to parse to invite url", "error", err)
+				return
+			}
+		}
 		invite.InviteURL = u.String()
 		err = logic.InsertUserInvite(invite)
 		if err != nil {
@@ -238,8 +250,9 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 			// Set E-Mail body. You can set plain text or html with text/html
 
 			e := email.UserInvitedMail{
-				BodyBuilder: &email.EmailBodyBuilderWithH1HeadlineAndImage{},
-				InviteURL:   invite.InviteURL,
+				BodyBuilder:    &email.EmailBodyBuilderWithH1HeadlineAndImage{},
+				InviteURL:      invite.InviteURL,
+				PlatformRoleID: invite.PlatformRoleID,
 			}
 			n := email.Notification{
 				RecipientMail: invite.Email,
@@ -250,7 +263,7 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
 			}
 		}(invite)
 	}
-
+	logic.ReturnSuccessResponse(w, r, "triggered user invites")
 }
 
 // swagger:route GET /api/v1/users/invites user listUserInvites
@@ -286,7 +299,7 @@ func listUserInvites(w http.ResponseWriter, r *http.Request) {
 //			Responses:
 //				200: ReturnSuccessResponse
 func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
-	email, _ := url.QueryUnescape(r.URL.Query().Get("invitee_email"))
+	email := r.URL.Query().Get("invitee_email")
 	err := logic.DeleteUserInvite(email)
 	if err != nil {
 		logger.Log(0, "failed to delete user invite: ", email, err.Error())
@@ -352,7 +365,7 @@ func listUserGroups(w http.ResponseWriter, r *http.Request) {
 //				200: userBodyResponse
 func getUserGroup(w http.ResponseWriter, r *http.Request) {
 
-	gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id"))
+	gid := r.URL.Query().Get("group_id")
 	if gid == "" {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest"))
 		return
@@ -438,6 +451,10 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	if currUserG.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update default user group"), "badrequest"))
+		return
+	}
 	err = proLogic.ValidateUpdateGroupReq(userGroup)
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -448,6 +465,7 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+
 	// reset configs for service user
 	go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
 	logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
@@ -468,19 +486,23 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
 // @Summary     Delete user group.
 // @Router      /api/v1/user/group [delete]
 // @Tags        Users
-// @Param       group_id param string true "group id required to delete the role"
+// @Param       group_id query string true "group id required to delete the role"
 // @Success     200 {string} string
 // @Failure     500 {object} models.ErrorResponse
 func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 
-	gid, _ := url.QueryUnescape(r.URL.Query().Get("group_id"))
+	gid := r.URL.Query().Get("group_id")
 	if gid == "" {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("group id is required"), "badrequest"))
 		return
 	}
 	userG, err := proLogic.GetUserGroup(models.UserGroupID(gid))
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to fetch group details"), "badrequest"))
+		return
+	}
+	if userG.Default {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot delete default user group"), "badrequest"))
 		return
 	}
 	err = proLogic.DeleteUserGroup(models.UserGroupID(gid))
@@ -495,11 +517,11 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
 // @Summary     lists all user roles.
 // @Router      /api/v1/user/roles [get]
 // @Tags        Users
-// @Param       role_id param string true "roleid required to get the role details"
+// @Param       role_id query string true "roleid required to get the role details"
 // @Success     200 {object}  []models.UserRolePermissionTemplate
 // @Failure     500 {object} models.ErrorResponse
 func ListRoles(w http.ResponseWriter, r *http.Request) {
-	platform, _ := url.QueryUnescape(r.URL.Query().Get("platform"))
+	platform := r.URL.Query().Get("platform")
 	var roles []models.UserRolePermissionTemplate
 	var err error
 	if platform == "true" {
@@ -521,11 +543,11 @@ func ListRoles(w http.ResponseWriter, r *http.Request) {
 // @Summary     Get user role permission template.
 // @Router      /api/v1/user/role [get]
 // @Tags        Users
-// @Param       role_id param string true "roleid required to get the role details"
+// @Param       role_id query string true "roleid required to get the role details"
 // @Success     200 {object} models.UserRolePermissionTemplate
 // @Failure     500 {object} models.ErrorResponse
 func getRole(w http.ResponseWriter, r *http.Request) {
-	rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id"))
+	rid := r.URL.Query().Get("role_id")
 	if rid == "" {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
 		return
@@ -544,7 +566,7 @@ func getRole(w http.ResponseWriter, r *http.Request) {
 // @Summary     Create user role permission template.
 // @Router      /api/v1/user/role [post]
 // @Tags        Users
-// @Param       body models.UserRolePermissionTemplate true "user role template"
+// @Param       body body models.UserRolePermissionTemplate true "user role template"
 // @Success     200 {object}  models.UserRolePermissionTemplate
 // @Failure     500 {object} models.ErrorResponse
 func createRole(w http.ResponseWriter, r *http.Request) {
@@ -574,8 +596,8 @@ func createRole(w http.ResponseWriter, r *http.Request) {
 // @Summary     Update user role permission template.
 // @Router      /api/v1/user/role [put]
 // @Tags        Users
-// @Param       body models.UserRolePermissionTemplate true "user role template"
-// @Success     200 {object} userBodyResponse
+// @Param       body body models.UserRolePermissionTemplate true "user role template"
+// @Success     200 {object} models.UserRolePermissionTemplate
 // @Failure     500 {object} models.ErrorResponse
 func updateRole(w http.ResponseWriter, r *http.Request) {
 	var userRole models.UserRolePermissionTemplate
@@ -610,12 +632,12 @@ func updateRole(w http.ResponseWriter, r *http.Request) {
 // @Summary     Delete user role permission template.
 // @Router      /api/v1/user/role [delete]
 // @Tags        Users
-// @Param       role_id param string true "roleid required to delete the role"
+// @Param       role_id query string true "roleid required to delete the role"
 // @Success     200 {string} string
 // @Failure     500 {object} models.ErrorResponse
 func deleteRole(w http.ResponseWriter, r *http.Request) {
 
-	rid, _ := url.QueryUnescape(r.URL.Query().Get("role_id"))
+	rid := r.URL.Query().Get("role_id")
 	if rid == "" {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("role is required"), "badrequest"))
 		return
@@ -803,6 +825,221 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @Tags        Users
+// @Param       username path string true "Username to fetch all the gateways with access"
+// @Success     200 {object} map[string][]models.UserRemoteGws
+// @Failure     500 {object} models.ErrorResponse
+func getUserRemoteAccessNetworks(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, username, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
+		return
+	}
+	userGws := make(map[string][]models.UserRemoteGws)
+	networks := []models.Network{}
+	networkMap := make(map[string]struct{})
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	for _, node := range userGwNodes {
+		network, err := logic.GetNetwork(node.Network)
+		if err != nil {
+			slog.Error("failed to get node network", "error", err)
+			continue
+		}
+		if _, ok := networkMap[network.NetID]; ok {
+			continue
+		}
+		networkMap[network.NetID] = struct{}{}
+		networks = append(networks, network)
+	}
+
+	slog.Debug("returned user gws", "user", username, "gws", userGws)
+	logic.ReturnSuccessResponseWithJson(w, r, networks, "fetched user accessible networks")
+}
+
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @Tags        Users
+// @Param       username path string true "Username to fetch all the gateways with access"
+// @Success     200 {object} map[string][]models.UserRemoteGws
+// @Failure     500 {object} models.ErrorResponse
+func getUserRemoteAccessNetworkGateways(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, username, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
+		return
+	}
+	network := params["network"]
+	if network == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params network"), "badrequest"))
+		return
+	}
+	userGws := []models.UserRAGs{}
+
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	for _, node := range userGwNodes {
+		if node.Network != network {
+			continue
+		}
+
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			continue
+		}
+
+		userGws = append(userGws, models.UserRAGs{
+			GwID:              node.ID.String(),
+			GWName:            host.Name,
+			Network:           node.Network,
+			IsInternetGateway: node.IsInternetGateway,
+			Metadata:          node.Metadata,
+		})
+
+	}
+
+	slog.Debug("returned user gws", "user", username, "gws", userGws)
+	logic.ReturnSuccessResponseWithJson(w, r, userGws, "fetched user accessible gateways in network "+network)
+}
+
+// @Summary     Get Users Remote Access Gw Networks.
+// @Router      /api/users/{username}/remote_access_gw [get]
+// @Tags        Users
+// @Param       username path string true "Username to fetch all the gateways with access"
+// @Success     200 {object} map[string][]models.UserRemoteGws
+// @Failure     500 {object} models.ErrorResponse
+func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := r.Header.Get("user")
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, username, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch user %s, error: %v", username, err), "badrequest"))
+		return
+	}
+	remoteGwID := params["access_point_id"]
+	if remoteGwID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("required params access_point_id"), "badrequest"))
+		return
+	}
+	var req models.UserRemoteGwsReq
+	err = json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		slog.Error("error decoding request body: ", "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	userGwNodes := proLogic.GetUserRAGNodes(*user)
+	if _, ok := userGwNodes[remoteGwID]; !ok {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access denied"), "forbidden"))
+		return
+	}
+	node, err := logic.GetNodeByID(remoteGwID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw node %s, error: %v", remoteGwID, err), "badrequest"))
+		return
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to fetch gw host %s, error: %v", remoteGwID, err), "badrequest"))
+		return
+	}
+	network, err := logic.GetNetwork(node.Network)
+	if err != nil {
+		slog.Error("failed to get node network", "error", err)
+	}
+	var userConf models.ExtClient
+	allextClients, err := logic.GetAllExtClients()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, extClient := range allextClients {
+		if extClient.Network != network.NetID || extClient.IngressGatewayID != node.ID.String() {
+			continue
+		}
+		if extClient.RemoteAccessClientID == req.RemoteAccessClientID && extClient.OwnerID == username {
+			userConf = extClient
+			userConf.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
+		}
+	}
+	if userConf.ClientID == "" {
+		// create a new conf
+		userConf.OwnerID = user.UserName
+		userConf.RemoteAccessClientID = req.RemoteAccessClientID
+		userConf.IngressGatewayID = node.ID.String()
+
+		// set extclient dns to ingressdns if extclient dns is not explicitly set
+		if (userConf.DNS == "") && (node.IngressDNS != "") {
+			userConf.DNS = node.IngressDNS
+		}
+
+		userConf.Network = node.Network
+		host, err := logic.GetHost(node.HostID.String())
+		if err != nil {
+			logger.Log(0, r.Header.Get("user"),
+				fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", node.ID, err))
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		listenPort := logic.GetPeerListenPort(host)
+		if host.EndpointIP.To4() == nil {
+			userConf.IngressGatewayEndpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), listenPort)
+		} else {
+			userConf.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
+		}
+		userConf.Enabled = true
+		parentNetwork, err := logic.GetNetwork(node.Network)
+		if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
+			userConf.Enabled = parentNetwork.DefaultACL == "yes"
+		}
+		userConf.Tags = make(map[models.TagID]struct{})
+		// userConf.Tags[models.TagID(fmt.Sprintf("%s.%s", userConf.Network,
+		// 	models.RemoteAccessTagName))] = struct{}{}
+		if err = logic.CreateExtClient(&userConf); err != nil {
+			slog.Error(
+				"failed to create extclient",
+				"user",
+				r.Header.Get("user"),
+				"network",
+				node.Network,
+				"error",
+				err,
+			)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+	userGw := models.UserRemoteGws{
+		GwID:              node.ID.String(),
+		GWName:            host.Name,
+		Network:           node.Network,
+		GwClient:          userConf,
+		Connected:         true,
+		IsInternetGateway: node.IsInternetGateway,
+		GwPeerPublicKey:   host.PublicKey.String(),
+		GwListenPort:      logic.GetPeerListenPort(host),
+		Metadata:          node.Metadata,
+		AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
+		NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+	}
+
+	slog.Debug("returned user gw config", "user", user.UserName, "gws", userGw)
+	logic.ReturnSuccessResponseWithJson(w, r, userGw, "fetched user config to gw "+remoteGwID)
+}
+
 // @Summary     Get Users Remote Access Gw.
 // @Router      /api/users/{username}/remote_access_gw [get]
 // @Tags        Users
@@ -863,6 +1100,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			network, err := logic.GetNetwork(node.Network)
 			if err != nil {
 				slog.Error("failed to get node network", "error", err)
+				continue
 			}
 
 			gws := userGws[node.Network]

+ 6 - 0
pro/email/email.go

@@ -2,6 +2,7 @@ package email
 
 import (
 	"context"
+	"regexp"
 
 	"github.com/gravitl/netmaker/servercfg"
 )
@@ -52,3 +53,8 @@ type Notification struct {
 func GetClient() (e EmailSender) {
 	return client
 }
+
+func IsValid(email string) bool {
+	emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
+	return emailRegex.MatchString(email)
+}

+ 42 - 8
pro/email/invite.go

@@ -2,26 +2,60 @@ package email
 
 import (
 	"fmt"
+	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 // UserInvitedMail - mail for users that are invited to a tenant
 type UserInvitedMail struct {
-	BodyBuilder EmailBodyBuilder
-	InviteURL   string
+	BodyBuilder    EmailBodyBuilder
+	InviteURL      string
+	PlatformRoleID string
 }
 
 // GetSubject - gets the subject of the email
 func (UserInvitedMail) GetSubject(info Notification) string {
-	return "Netmaker: Pending Invitation"
+	return "Connect to Your Secure Network Using Netmaker"
 }
 
 // GetBody - gets the body of the email
 func (invite UserInvitedMail) GetBody(info Notification) string {
+	downloadLink := "https://www.netmaker.io/download"
+	supportEmail := "[email protected]"
 
-	return invite.BodyBuilder.
-		WithHeadline("Join Netmaker from this invite!").
-		WithParagraph("Hello from Netmaker,").
-		WithParagraph("You have been invited to join Netmaker.").
-		WithParagraph(fmt.Sprintf("Join Using This Invite Link <a href=\"%s\">Netmaker</a>", invite.InviteURL)).
+	dashboardURL := fmt.Sprintf("https://dashboard.%s", servercfg.GetNmBaseDomain())
+	if servercfg.DeployedByOperator() {
+		dashboardURL = fmt.Sprintf("%s/dashboard?tenant_id=%s", proLogic.GetAccountsUIHost(), servercfg.GetNetmakerTenantID())
+	}
+
+	content := invite.BodyBuilder.
+		WithParagraph("Hi,").
+		WithParagraph("You've been invited to access a secure network via Netmaker's Remote Access Client (RAC). Follow these simple steps to get connected:").
+		WithHtml("<ol>").
+		WithHtml(fmt.Sprintf("<li>Click <a href=\"%s\">here</a> to accept your invitation and setup your account.</li>", invite.InviteURL)).
+		WithHtml("<br>").
+		WithHtml(fmt.Sprintf("<li><a href=\"%s\">Download the Remote Access Client (RAC)</a>.</li>", downloadLink))
+
+	if invite.PlatformRoleID == models.AdminRole.String() || invite.PlatformRoleID == models.PlatformUser.String() {
+		content = content.
+			WithHtml("<br>").
+			WithHtml(fmt.Sprintf("<li>Access the <a href=\"%s\">Netmaker Dashboard</a> - use it to manage your network settings and view network status.</li>", dashboardURL))
+	}
+
+	connectionID := servercfg.GetNetmakerTenantID()
+	if !servercfg.DeployedByOperator() {
+		connectionID = fmt.Sprintf("api.%s", servercfg.GetNmBaseDomain())
+	}
+
+	return content.
+		WithHtml("</ol>").
+		WithParagraph("Important Information:").
+		WithHtml("<ul>").
+		WithHtml(fmt.Sprintf("<li>When connecting through RAC, please enter your server connection ID: %s.</li>", connectionID)).
+		WithHtml("</ul>").
+		WithParagraph(fmt.Sprintf("If you have any questions or need assistance, please contact our support team at <a href=\"mailto:%s\">%s</a>.", supportEmail, supportEmail)).
+		WithParagraph("Best Regards,").
+		WithParagraph("The Netmaker Team").
 		Build()
 }

+ 129 - 521
pro/email/utils.go

@@ -4,27 +4,20 @@ import "strings"
 
 // mail related images hosted on github
 var (
-	nLogoTeal        = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/N_Teal.png"
 	netmakerLogoTeal = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-logo-2.png"
-	netmakerMeshLogo = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-mesh.png"
-	linkedinIcon     = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/linkedin2x.png"
-	discordIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/discord-logo-png-7617.png"
-	githubIcon       = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/Octocat.png"
-	mailIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-mail-24.png"
-	addressIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-address-16.png"
-	linkIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-hyperlink-64.png"
 )
 
 type EmailBodyBuilder interface {
 	WithHeadline(text string) EmailBodyBuilder
 	WithParagraph(text string) EmailBodyBuilder
+	WithHtml(text string) EmailBodyBuilder
 	WithSignature() EmailBodyBuilder
 	Build() string
 }
 
 type EmailBodyBuilderWithH1HeadlineAndImage struct {
 	headline     string
-	paragraphs   []string
+	bodyContent  []string
 	hasSignature bool
 }
 
@@ -34,7 +27,12 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) Email
 }
 
 func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder {
-	b.paragraphs = append(b.paragraphs, text)
+	b.bodyContent = append(b.bodyContent, styledParagraph(text))
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHtml(text string) EmailBodyBuilder {
+	b.bodyContent = append(b.bodyContent, text)
 	return b
 }
 
@@ -44,524 +42,134 @@ func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilde
 }
 
 func (b *EmailBodyBuilderWithH1HeadlineAndImage) Build() string {
-	// map paragraphs to styled paragraphs
-	styledParagraphsSlice := make([]string, len(b.paragraphs))
-	for i, paragraph := range b.paragraphs {
-		styledParagraphsSlice[i] = styledParagraph(paragraph)
-	}
-	// join styled paragraphs
-	styledParagraphsString := strings.Join(styledParagraphsSlice, "")
+	bodyContent := strings.Join(b.bodyContent, "")
 
-	signature := ""
-	if b.hasSignature {
-		signature = styledSignature()
-	}
+	// TODO: Edit design to add signature.
+	//signature := ""
+	//if b.hasSignature {
+	//	signature = styledSignature()
+	//}
 
 	return `
-		<!DOCTYPE html>
-		<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="en">
-		<head>
-		    <title></title>
-		    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-		    <meta name="viewport" content="width=device-width,initial-scale=1">
-		    <!--[if mso]>
-		    <xml>
-		        <o:OfficeDocumentSettings>
-		            <o:PixelsPerInch>96</o:PixelsPerInch>
-		            <o:AllowPNG/>
-		        </o:OfficeDocumentSettings>
-		    </xml>
-		    <![endif]-->
-		    <style>
-		        *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}@media (max-width:720px){.desktop_hide table.icons-inner{display:inline-block!important}.icons-inner{text-align:center}.icons-inner td{margin:0 auto}.image_block img.big,.row-content{width:100%!important}.mobile_hide{display:none}.stack .column{width:100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width:0;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}}
-		    </style>
-		</head>
-		<body style="background-color:transparent;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
-		<table class="nl-container" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:transparent">
-		    <tbody>
-		    <tr>
-		        <td>
-		            <table class="row row-1" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0"
-		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="padding-left:15px;padding-right:15px;width:100%;padding-top:5px">
-		                                                <div class="alignment" align="left" style="line-height:10px"><a href="https://www.netmaker.io/" target="_blank" style="outline:none" tabindex="-1"><img class="big" src="` + netmakerLogoTeal + `"
-		                                                                                                                                                                                                        style="display:block;height:auto;border:0;width:333px;max-width:100%" width="333" alt="Netmaker" title="Netmaker"></a></div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                    <table class="divider_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="padding-bottom:10px;padding-left:5px;padding-right:5px;padding-top:10px">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0" cellspacing="0"
-		                                                           role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                                <td class="column column-2" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="empty_block block-2" width="100%" border="0"
-		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="padding-right:0;padding-bottom:5px;padding-left:0;padding-top:5px">
-		                                                <div></div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack" align="center"
-		                               border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;padding-left:10px;padding-right:10px;vertical-align:top;padding-top:10px;padding-bottom:10px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="divider_block block-1" width="100%" border="0"
-		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table
-		                    class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="50%"
-		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="divider_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="padding-bottom:20px;padding-left:20px;padding-right:20px;padding-top:25px">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0"
-		                                                           cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                    <table class="heading_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad"
-		                                                style="padding-bottom:15px;padding-left:10px;padding-right:10px;padding-top:10px;text-align:center;width:100%">
-		                                                <h1 style="margin:0;color:#2b2d2d;direction:ltr;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:28px;font-weight:400;letter-spacing:normal;line-height:120%;text-align:left;margin-top:0;margin-bottom:0"><strong>` + b.headline + `</strong></h1>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                                <td class="column column-2" width="50%"
-		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="width:100%;padding-right:0;padding-left:0;padding-top:5px;padding-bottom:5px">
-		                                                <div class="alignment" align="center" style="line-height:10px"><img
-		                                                        src="` + netmakerMeshLogo + `" style="display:block;height:auto;border:0;width:350px;max-width:100%" width="350" alt="Netmaker Mesh"></div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-4" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
-		                   style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-5" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
-		                               style="mso-table-lspace:0;mso-table-rspace:0;background-color:#0098a5;color:#000;border-top:2px solid transparent;border-right:2px solid transparent;border-left:2px solid transparent;border-bottom:2px solid transparent;border-radius:0;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%"
-		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;border-bottom:0 solid #000;border-left:0 solid #000;border-right:0 solid #000;border-top:0 solid #000;vertical-align:top;padding-top:25px;padding-bottom:25px">
-		                                    <table class="text_block block-3" width="100%" border="0"
-		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
-		                                        <tr>
-		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
-		                                                <div style="font-family:Verdana,sans-serif">
-		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#393d47;line-height:1.5;font-family:Verdana,Geneva,sans-serif">
-		
-		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:18px">&nbsp;</p>
-		                                                        ` + styledParagraphsString + `
-		                                                        <p style="margin:0;mso-line-height-alt:18px">&nbsp;</p>
-		                                                    </div>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-6" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table
-		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="divider_block block-1" width="100%" border="0"
-		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table
-		                    class="row row-7" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#f7fafe">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%"
-		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:25px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad">
-		                                                <div class="alignment" align="center">
-		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"
-		                                                           style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                        <tr>
-		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-8" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#090660">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table class="row-content stack"
-		                               align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="text_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0"
-		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
-		                                        <tr>
-		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
-		                                                <div style="font-family:sans-serif">
-		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#6f7077;line-height:1.5;font-family:Arial,Helvetica Neue,Helvetica,sans-serif">
-		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:33px">
-		                                                            <span style="color:#ffffff;font-size:22px;">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Get In Touch With Us</span>
-		                                                        </p>
-		                                                    </div>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                    <table class="social_block block-2" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad">
-		                                                <div class="alignment" style="text-align:center">
-		                                                    <table class="social-table"
-		                                                           width="114.49624060150376px" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block">
-		                                                        <tr>
-		                                                            <td style="padding:0 2px 0 2px"><a href="https://www.linkedin.com/company/netmaker-inc/" target="_blank"><img src="` + linkedinIcon + `" width="32" height="32" alt="Linkedin" title="linkedin" style="display:block;height:auto;border:0"></a></td>
-		                                                            <td
-		                                                                    style="padding:0 2px 0 2px"><a href="https://discord.gg/zRb9Vfhk8A" target="_blank"><img src="` + discordIcon + `" width="32" height="32" alt="Discord" title="Discord" style="display:block;height:auto;border:0"></a></td>
-		                                                            <td style="padding:0 2px 0 2px"><a href="https://github.com/gravitl/netmaker" target="_blank"><img
-		                                                                    src="` + githubIcon + `" width="38.49624060150376" height="32" alt="Github" title="Github" style="display:block;height:auto;border:0"></a></td>
-		                                                        </tr>
-		                                                    </table>
-		                                                </div>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		            <table class="row row-9" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                <tbody>
-		                <tr>
-		                    <td>
-		                        <table
-		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
-		                            <tbody>
-		                            <tr>
-		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
-		                                    <table class="icons_block block-1" width="100%" border="0"
-		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                        <tr>
-		                                            <td class="pad" style="vertical-align:middle;padding-bottom:5px;padding-top:5px;text-align:center;color:#9d9d9d;font-family:inherit;font-size:15px">
-		                                                <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
-		                                                    <tr>
-		                                                        <td class="alignment" style="vertical-align:middle;text-align:center">
-		                                                            <!--[if vml]>
-		                                                            <table align="left" cellpadding="0" cellspacing="0" role="presentation" style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
-		                                                            <![endif]--><!--[if !vml]><!-->
-		                                                            <table class="icons-inner" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0" cellpadding="0" cellspacing="0" role="presentation">
-		                                                                <!--<![endif]-->
-		                                                            </table>
-		                                                        </td></tr>
-		                                                </table>
-		                                            </td>
-		                                        </tr>
-		                                    </table>
-		                                </td>
-		                            </tr>
-		                            </tbody>
-		                        </table>
-		                    </td>
-		                </tr>
-		                </tbody>
-		            </table>
-		        </td>
-		    </tr>
-		    </tbody>
-		</table>
-		<!-- End -->
-		</body>
-		` + signature + `
-		</html>`
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <title>Simple Transactional Email</title>
+    <style media="all" type="text/css">
+@media all {
+  .btn-primary table td:hover {
+    background-color: #ec0867 !important;
+  }
+
+  .btn-primary a:hover {
+    background-color: #ec0867 !important;
+    border-color: #ec0867 !important;
+  }
 }
+@media only screen and (max-width: 640px) {
+  .main p,
+.main td,
+.main span {
+    font-size: 16px !important;
+  }
 
-func styledSignature() string {
-	return `
-	<footer style="display:block">
-	<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-	<tbody>
-	   <tr>
-		  <td>
-			 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-				<tbody>
-				   <tr>
-					  <td style="vertical-align: top;">
-						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-							<tbody>
-							   <tr>
-								  <td class="sc-TOsTZ kjYrri" style="text-align: center;"><img src="` + nLogoTeal + `" role="presentation" width="130" class="sc-cHGsZl bHiaRe" style="max-width: 130px; display: block;"></td>
-							   </tr>
-							   <tr>
-								  <td height="30"></td>
-							   </tr>
-							   <tr>
-								  <td style="text-align: center;">
-									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; display: inline-block;">
-										<tbody>
-										   <tr style="text-align: center;">
-											  <td><a href="https://www.linkedin.com/company/netmaker-inc/" color="#6a78d1" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px; background-color: rgb(106, 120, 209);"><img src="` + linkedinIcon + `" alt="Linkedin" color="#6a78d1" height="24" class="sc-bRBYWo ccSRck" style="background-color: rgb(106, 120, 209); max-width: 135px; display: block;"></a></td>
-											  <td width="5">
-												 <div></div>
-											  </td>
-										 
-                                    <td><a href="https://discord.gg/zRb9Vfhk8A" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + discordIcon + `" alt="Discord" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
-                                    <td width="5">
-                                    <div></div>
-                                    </td>
-                              
-                                    <td><a href="https://github.com/gravitl/netmaker" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + githubIcon + `" alt="Github" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
-                                    <td width="5">
-                                    <div></div>
-                                    </td>
-                                 </tr>
-										</tbody>
-									 </table>
-								  </td>
-							   </tr>
-							</tbody>
-						 </table>
-					  </td>
-					  <td width="46">
-						 <div></div>
-					  </td>
-					  <td style="padding: 0px; vertical-align: middle;">
-						 <h3 color="#000000" class="sc-fBuWsC eeihxG" style="margin: 0px; font-size: 18px; color: rgb(0, 0, 0);"><span>Alex</span><span>&nbsp;</span><span>Feiszli</span></h3>
-						 <p color="#000000" font-size="medium" class="sc-fMiknA bxZCMx" style="margin: 0px; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Co-Founder &amp; CEO</span></p>
-						 <p color="#000000" font-size="medium" class="sc-dVhcbM fghLuF" style="margin: 0px; font-weight: 500; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Netmaker</span></p>
-						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; width: 100%;">
-							<tbody>
-							   <tr>
-								  <td height="30"></td>
-							   </tr>
-							   <tr>
-								  <td color="#545af2" direction="horizontal" height="1" class="sc-jhAzac hmXDXQ" style="width: 100%; border-bottom: 1px solid rgb(84, 90, 242); border-left: none; display: block;"></td>
-							   </tr>
-							   <tr>
-								  <td height="30"></td>
-							   </tr>
-							</tbody>
-						 </table>
-						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-							<tbody>
-							   <tr height="25" style="vertical-align: middle;">
-								  <td width="30" style="vertical-align: middle;">
-									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-										<tbody>
-										   <tr>
-											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block"><img src="` + mailIcon + `" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
-										   </tr>
-										</tbody>
-									 </table>
-								  </td>
-								  <td style="padding: 0px;"><a href="mailto:[email protected]" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>[email protected]</span></a></td>
-							   </tr>
-							   <tr height="25" style="vertical-align: middle;">
-								  <td width="30" style="vertical-align: middle;">
-									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-										<tbody>
-										   <tr>
-											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + linkIcon + `" color="#545af2" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
-										   </tr>
-										</tbody>
-									 </table>
-								  </td>
-								  <td style="padding: 0px;"><a href="https://www.netmaker.io/" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>https://www.netmaker.io/</span></a></td>
-							   </tr>
-							   <tr height="25" style="vertical-align: middle;">
-								  <td width="30" style="vertical-align: middle;">
-									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-										<tbody>
-										   <tr>
-											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + addressIcon + `"  width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
-										   </tr>
-										</tbody>
-									 </table>
-								  </td>
-								  <td style="padding: 0px;"><span color="#000000" class="sc-csuQGl CQhxV" style="font-size: 12px; color: rgb(0, 0, 0);"><span>1465 Sand Hill Rd.Suite 2014, Candler, NC 28715</span></span></td>
-							   </tr>
-							</tbody>
-						 </table>
-						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
-							<tbody>
-							   <tr>
-								  <td height="30"></td>
-							   </tr>
-							</tbody>
-						 </table>
-					  </td>
-				   </tr>
-				</tbody>
-			 </table>
-		  </td>
-	   </tr>
-	</tbody>
- </table>
-</footer>`
+  .wrapper {
+    padding: 8px !important;
+  }
+
+  .content {
+    padding: 0 !important;
+  }
+
+  .container {
+    padding: 0 !important;
+    padding-top: 8px !important;
+    width: 100% !important;
+  }
+
+  .main {
+    border-left-width: 0 !important;
+    border-radius: 0 !important;
+    border-right-width: 0 !important;
+  }
+
+  .btn table {
+    max-width: 100% !important;
+    width: 100% !important;
+  }
+
+  .btn a {
+    font-size: 16px !important;
+    max-width: 100% !important;
+    width: 100% !important;
+  }
 }
+@media all {
+  .ExternalClass {
+    width: 100%;
+  }
 
-func styledParagraph(text string) string {
-	return `<p style="margin:0;mso-line-height-alt:22.5px">
-	<span style="color:#ffffff;font-size:15px;">` + text + `</span>
-	</p>`
+  .ExternalClass,
+.ExternalClass p,
+.ExternalClass span,
+.ExternalClass font,
+.ExternalClass td,
+.ExternalClass div {
+    line-height: 100%;
+  }
+
+  .apple-link a {
+    color: inherit !important;
+    font-family: inherit !important;
+    font-size: inherit !important;
+    font-weight: inherit !important;
+    line-height: inherit !important;
+    text-decoration: none !important;
+  }
+
+  #MessageViewBody a {
+    color: inherit;
+    text-decoration: none;
+    font-size: inherit;
+    font-family: inherit;
+    font-weight: inherit;
+    line-height: inherit;
+  }
 }
+</style>
+  </head>
+  <body style="font-family: Helvetica, sans-serif; -webkit-font-smoothing: antialiased; font-size: 16px; line-height: 1.3; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f4f5f6; margin: 0; padding: 0;">
+    <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f4f5f6; width: 100%;" width="100%" bgcolor="#f4f5f6">
+      <tr>
+        <td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top">&nbsp;</td>
+        <td class="container" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; max-width: 600px; padding: 24px 0px 24px 0px; width: 600px; margin: 0 auto;" width="600" valign="top">
+          <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 0;">
+
+            <!-- START CENTERED WHITE CONTAINER -->
+            <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border: 1px solid #eaebed; border-radius: 16px; width: 100%;" width="100%">
+
+              <!-- START MAIN CONTENT AREA -->
+              <tr>
+                <td class="wrapper" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; box-sizing: border-box; padding: 24px;" valign="top">
+                  <img src="` + netmakerLogoTeal + `" alt="Netmaker Logo" width="200" height="100" border="0" style="border:0; outline:none; text-decoration:none; display:block; margin-left: auto;">
+                  ` + bodyContent + `
+                </td>
+              </tr>
 
-func GetMailSignature() string {
-	return styledSignature()
+              <!-- END MAIN CONTENT AREA -->
+              </table>
+
+<!-- END CENTERED WHITE CONTAINER --></div>
+        </td>
+        <td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top">&nbsp;</td>
+      </tr>
+    </table>
+  </body>
+</html>`
+}
+
+func styledParagraph(text string) string {
+	return `<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">` + text + `</p>`
 }

部分文件因为文件数量过多而无法显示