Browse Source

Merge branch 'develop' into remove-stun

Alex Feiszli 2 years ago
parent
commit
e4895ae4a1
58 changed files with 1080 additions and 880 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 54 3
      .github/workflows/branchtest.yml
  3. 25 3
      .github/workflows/deletedroplets.yml
  4. 1 1
      Dockerfile
  5. 1 1
      Dockerfile-quick
  6. 1 1
      README.md
  7. 0 22
      cli/cmd/host/create_relay.go
  8. 22 0
      cli/cmd/node/create_relay.go
  9. 6 6
      cli/cmd/node/delete_relay.go
  10. 1 1
      cli/cmd/node/flags.go
  11. 3 3
      cli/cmd/node/update.go
  12. 9 8
      cli/functions/host.go
  13. 1 1
      compose/docker-compose.netclient.yml
  14. 56 50
      config/config.go
  15. 1 1
      controllers/docs.go
  16. 2 42
      controllers/hosts.go
  17. 1 9
      controllers/limits.go
  18. 33 50
      controllers/node.go
  19. 0 199
      controllers/relay.go
  20. 39 1
      controllers/server.go
  21. 13 9
      controllers/user.go
  22. 123 0
      controllers/user_test.go
  23. 107 0
      ee/ee_controllers/relay.go
  24. 4 18
      ee/initialize.go
  25. 13 19
      ee/license.go
  26. 20 38
      ee/types.go
  27. 6 3
      ee/util.go
  28. 5 5
      go.mod
  29. 10 10
      go.sum
  30. 1 1
      k8s/client/netclient-daemonset.yaml
  31. 1 1
      k8s/client/netclient.yaml
  32. 1 1
      k8s/server/netmaker-ui.yaml
  33. 0 14
      logic/auth.go
  34. 3 0
      logic/gateway.go
  35. 8 1
      logic/hosts.go
  36. 14 44
      logic/nodes.go
  37. 155 104
      logic/peers.go
  38. 89 145
      logic/relay.go
  39. 11 2
      logic/serverconf.go
  40. 3 0
      logic/telemetry.go
  41. 36 0
      logic/timer.go
  42. 24 0
      logic/users.go
  43. 3 3
      logic/wireguard.go
  44. 6 2
      main.go
  45. 2 8
      models/api_host.go
  46. 7 3
      models/api_node.go
  47. 14 12
      models/mqtt.go
  48. 17 8
      models/node.go
  49. 19 3
      models/structs.go
  50. 6 1
      mq/emqx.go
  51. 1 1
      mq/handlers.go
  52. 13 11
      mq/publishers.go
  53. 1 1
      release.md
  54. 1 0
      scripts/netmaker.default.env
  55. 15 8
      scripts/nm-quick.sh
  56. 1 1
      scripts/nm-upgrade-0-17-1-to-0-19-0.sh
  57. 69 0
      servercfg/serverconf.go
  58. 1 1
      swagger.yaml

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

@@ -31,6 +31,7 @@ body:
       label: Version
       label: Version
       description: What version are you running?
       description: What version are you running?
       options:
       options:
+        - v0.20.3
         - v0.20.2
         - v0.20.2
         - v0.20.1
         - v0.20.1
         - v0.20.0
         - v0.20.0

+ 54 - 3
.github/workflows/branchtest.yml

@@ -10,12 +10,13 @@ jobs:
   skip-check:
   skip-check:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     outputs:
     outputs:
-      skip: ${{ steps.check.outputs.skip }}
+      skip: ${{ steps.skip.outputs.skip }}
     steps:
     steps:
       - id: skip
       - id: skip
         uses: fkirc/skip-duplicate-actions@v5
         uses: fkirc/skip-duplicate-actions@v5
         with:
         with:
           concurrent_skipping: 'always'
           concurrent_skipping: 'always'
+  
   getbranch:
   getbranch:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     needs: skip-check
     needs: skip-check
@@ -29,7 +30,7 @@ jobs:
           repository: gravitl/netclient
           repository: gravitl/netclient
           ref: develop
           ref: develop
       - name: check if branch exists
       - name: check if branch exists
-        id: checkbranch
+        id: getbranch 
         run: |
         run: |
           if git show-ref ${{ github.head_ref}}; then
           if git show-ref ${{ github.head_ref}}; then
             echo branch exists
             echo branch exists
@@ -39,12 +40,62 @@ jobs:
             echo "netclientbranch=develop" >> $GITHUB_OUTPUT
             echo "netclientbranch=develop" >> $GITHUB_OUTPUT
           fi
           fi
   
   
+  getserver:
+    runs-on: ubuntu-latest
+    needs: skip-check
+    if: ${{ needs.skip-check.outputs.skip != 'true' }}
+    outputs:
+      netmakerserver: ${{ steps.getserver.outputs.server }}
+    steps:
+      - name: setup ssh
+        run: |
+          mkdir -p ~/.ssh/
+          echo "$SSH_KEY" > ~/.ssh/id_devops
+          chmod 600 ~/.ssh/id_devops
+          cat >>~/.ssh/config <<END
+          Host *.clustercat.com
+            User root
+            IdentityFile ~/.ssh/id_devops
+            StrictHostKeyChecking no
+          END
+        env:
+          SSH_KEY: ${{ secrets.TESTING_SSH_KEY }}
+      - name: getserver
+        id: getserver
+        run: |
+          server=""
+          for arg in "branch1" "branch2" "branch3" "branch4" "branch5"; do
+            echo checking $arg
+            result=$( ssh root@server.${arg}.clustercat.com '~/branchtesting/check.sh') 
+            echo $result
+            if [ "$result" == "pass" ]
+            then
+              server=$arg
+              echo $server >> /tmp/server
+              break
+            fi
+          done
+          echo server is $server
+          if [ "$server" == "" ]
+          then
+            echo server not set
+            exit 1
+          fi
+          echo "netmakerserver=${ server }" >> $GITHUB_OUTPUT
+      - name: save server name
+        uses: actions/upload-artifact@v3
+        with:
+          name: server
+          path: /tmp/ping
+          retention-days: 3
+          
   terraform:
   terraform:
-    needs: getbranch
+    needs: [getbranch, getserver]
     uses: gravitl/devops/.github/workflows/terraform.yml@master
     uses: gravitl/devops/.github/workflows/terraform.yml@master
     with:
     with:
       netmakerbranch: ${{ github.head_ref }}
       netmakerbranch: ${{ github.head_ref }}
       netclientbranch: ${{ needs.getbranch.outputs.netclientbranch }}
       netclientbranch: ${{ needs.getbranch.outputs.netclientbranch }}
+      server: ${{ needs.getserver.outputs.netmakerserver }}
     secrets: inherit
     secrets: inherit
 
 
 
 

+ 25 - 3
.github/workflows/deletedroplets.yml

@@ -16,6 +16,9 @@ jobs:
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn
+      - name: get server name
+        run: |
+          echo "SERVER=$(cat ./server/server) >> $GITHUB_ENV"
       - name: discord success message
       - name: discord success message
         uses: appleboy/discord-action@master
         uses: appleboy/discord-action@master
         with:
         with:
@@ -23,7 +26,7 @@ jobs:
           webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
           webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
           color: "#42f545"
           color: "#42f545"
           username: "GitHub Bot"
           username: "GitHub Bot"
-          message: "${{ github.repository }}: ${{ github.event.workflow_run.name }} was successful: droplets from this workflow (tag ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}) will be deleted in 15 min"
+          message: "${{ github.repository }}: ${{ github.event.workflow_run.name }} on dashboard.${{ env.SERVER }}.clustercat.com was successful: droplets from this workflow (tag ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}) will be deleted in 15 min"
           file: ./results/results.log
           file: ./results/results.log
       - name: delete droplets
       - name: delete droplets
         if: success() || failure()
         if: success() || failure()
@@ -36,6 +39,14 @@ jobs:
         env:
         env:
           DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
           DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
+      - name: mark server as available
+        uses: appleboy/ssh-action@master
+        with:
+          host: server.${{ env.SERVER }}.clustercat.com
+          username: root
+          key: ${{ secrets.TESTING_SSH_KEY }}
+          script: |
+            rm /tmp/branchtest
 
 
   on-failure:
   on-failure:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -46,6 +57,9 @@ jobs:
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn
+      - name: get server name
+        run: |
+          echo "SERVER=$(cat ./server/server) >> $GITHUB_ENV"
       - name: discord failure message
       - name: discord failure message
         uses: appleboy/discord-action@master
         uses: appleboy/discord-action@master
         with:
         with:
@@ -53,7 +67,7 @@ jobs:
           webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
           webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
           color: "#990000"
           color: "#990000"
           username: "GitHub Bot"
           username: "GitHub Bot"
-          message: "${{ github.repository }}: ${{ github.event.workflow_run.name }} failed: droplets from this workflow (tag ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt}}) will be deleted in 5 hours"
+          message: "${{ github.repository }}: ${{ github.event.workflow_run.name }} failed: droplets from this workflow (tag ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt}}) will be deleted in 3 hours"
           file: ./results/results.log
           file: ./results/results.log
       - name: discord error message
       - name: discord error message
         uses: appleboy/discord-action@master
         uses: appleboy/discord-action@master
@@ -67,7 +81,7 @@ jobs:
       - name: delete droplets
       - name: delete droplets
         if: success() || failure()
         if: success() || failure()
         run: |
         run: |
-          sleep 5h
+          sleep 3h
           curl -X GET \
           curl -X GET \
             -H "Content-Type: application/json" \
             -H "Content-Type: application/json" \
             -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
             -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
@@ -75,3 +89,11 @@ jobs:
         env:
         env:
           DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
           DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
           TAG: ${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
+      - name: mark server as available
+        uses: appleboy/ssh-action@master
+        with:
+          host: server.${{ env.SERVER }}.clustercat.com
+          username: root
+          key: ${{ secrets.TESTING_SSH_KEY }}
+          script: |
+            rm /tmp/branchtest

+ 1 - 1
Dockerfile

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

+ 1 - 1
Dockerfile-quick

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

+ 1 - 1
README.md

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

+ 0 - 22
cli/cmd/host/create_relay.go

@@ -1,22 +0,0 @@
-package host
-
-import (
-	"strings"
-
-	"github.com/gravitl/netmaker/cli/functions"
-	"github.com/spf13/cobra"
-)
-
-var hostCreateRelayCmd = &cobra.Command{
-	Use:   "create_relay [HOST ID] [RELAYED HOST IDS (comma separated)]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Turn a Host into a Relay",
-	Long:  `Turn a Host into a Relay`,
-	Run: func(cmd *cobra.Command, args []string) {
-		functions.PrettyPrint(functions.CreateRelay(args[0], strings.Split(args[1], ",")))
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(hostCreateRelayCmd)
-}

+ 22 - 0
cli/cmd/node/create_relay.go

@@ -0,0 +1,22 @@
+package node
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var hostCreateRelayCmd = &cobra.Command{
+	Use:   "create_relay [NETWORK][NODE ID] [RELAYED NODE IDS (comma separated)]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Turn a Node into a Relay",
+	Long:  `Turn a Node into a Relay`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.CreateRelay(args[0], args[1], strings.Split(args[2], ",")))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(hostCreateRelayCmd)
+}

+ 6 - 6
cli/cmd/host/delete_relay.go → cli/cmd/node/delete_relay.go

@@ -1,4 +1,4 @@
-package host
+package node
 
 
 import (
 import (
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/cli/functions"
@@ -6,12 +6,12 @@ import (
 )
 )
 
 
 var hostDeleteRelayCmd = &cobra.Command{
 var hostDeleteRelayCmd = &cobra.Command{
-	Use:   "delete_relay [HOST ID]",
-	Args:  cobra.ExactArgs(1),
-	Short: "Delete Relay role from a host",
-	Long:  `Delete Relay role from a host`,
+	Use:   "delete_relay [NETWORK] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete Relay from a node",
+	Long:  `Delete Relay from a node`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
-		functions.PrettyPrint(functions.DeleteRelay(args[0]))
+		functions.PrettyPrint(functions.DeleteRelay(args[0], args[1]))
 	},
 	},
 }
 }
 
 

+ 1 - 1
cli/cmd/node/flags.go

@@ -12,7 +12,7 @@ var (
 	postUp                 string
 	postUp                 string
 	postDown               string
 	postDown               string
 	keepAlive              int
 	keepAlive              int
-	relayAddrs             string
+	relayedNodes           string
 	egressGatewayRanges    string
 	egressGatewayRanges    string
 	expirationDateTime     int
 	expirationDateTime     int
 	defaultACL             bool
 	defaultACL             bool

+ 3 - 3
cli/cmd/node/update.go

@@ -35,8 +35,8 @@ var nodeUpdateCmd = &cobra.Command{
 			node.Address6 = address6
 			node.Address6 = address6
 			node.LocalAddress = localAddress
 			node.LocalAddress = localAddress
 			node.PersistentKeepalive = int32(keepAlive)
 			node.PersistentKeepalive = int32(keepAlive)
-			if relayAddrs != "" {
-				node.RelayAddrs = strings.Split(relayAddrs, ",")
+			if relayedNodes != "" {
+				node.RelayedNodes = strings.Split(relayedNodes, ",")
 			}
 			}
 			if egressGatewayRanges != "" {
 			if egressGatewayRanges != "" {
 				node.EgressGatewayRanges = strings.Split(egressGatewayRanges, ",")
 				node.EgressGatewayRanges = strings.Split(egressGatewayRanges, ",")
@@ -62,7 +62,7 @@ func init() {
 	nodeUpdateCmd.Flags().StringVar(&postUp, "post_up", "", "Commands to run after node is up `;` separated")
 	nodeUpdateCmd.Flags().StringVar(&postUp, "post_up", "", "Commands to run after node is up `;` separated")
 	nodeUpdateCmd.Flags().StringVar(&postDown, "post_down", "", "Commands to run after node is down `;` separated")
 	nodeUpdateCmd.Flags().StringVar(&postDown, "post_down", "", "Commands to run after node is down `;` separated")
 	nodeUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval in which packets are sent to keep connections open with peers")
 	nodeUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval in which packets are sent to keep connections open with peers")
-	nodeUpdateCmd.Flags().StringVar(&relayAddrs, "relay_addrs", "", "Addresses for relaying connections if node acts as a relay")
+	nodeUpdateCmd.Flags().StringVar(&relayedNodes, "relayed_nodes", "", "relayed nodes if node acts as a relay")
 	nodeUpdateCmd.Flags().StringVar(&egressGatewayRanges, "egress_addrs", "", "Addresses for egressing traffic if node acts as an egress")
 	nodeUpdateCmd.Flags().StringVar(&egressGatewayRanges, "egress_addrs", "", "Addresses for egressing traffic if node acts as an egress")
 	nodeUpdateCmd.Flags().IntVar(&expirationDateTime, "expiry", 0, "UNIX timestamp after which node will lose access to the network")
 	nodeUpdateCmd.Flags().IntVar(&expirationDateTime, "expiry", 0, "UNIX timestamp after which node will lose access to the network")
 	nodeUpdateCmd.Flags().BoolVar(&defaultACL, "acl", false, "Enable default ACL ?")
 	nodeUpdateCmd.Flags().BoolVar(&defaultACL, "acl", false, "Enable default ACL ?")

+ 9 - 8
cli/functions/host.go

@@ -36,17 +36,18 @@ func DeleteHostFromNetwork(hostID, network string) *hostNetworksUpdatePayload {
 	return request[hostNetworksUpdatePayload](http.MethodDelete, "/api/hosts/"+hostID+"/networks/"+network, nil)
 	return request[hostNetworksUpdatePayload](http.MethodDelete, "/api/hosts/"+hostID+"/networks/"+network, nil)
 }
 }
 
 
-// CreateRelay - turn a host into a relay
-func CreateRelay(hostID string, relayedHosts []string) *models.ApiHost {
-	return request[models.ApiHost](http.MethodPost, fmt.Sprintf("/api/hosts/%s/relay", hostID), &models.HostRelayRequest{
-		HostID:       hostID,
-		RelayedHosts: relayedHosts,
+// CreateRelay - add relay to a node
+func CreateRelay(netID, nodeID string, relayedNodes []string) *models.ApiNode {
+	return request[models.ApiNode](http.MethodPost, fmt.Sprintf("/api/nodes/%s/%s/createrelay", netID, nodeID), &models.RelayRequest{
+		NodeID:       nodeID,
+		NetID:        netID,
+		RelayedNodes: relayedNodes,
 	})
 	})
 }
 }
 
 
-// DeleteRelay - remove relay role from a host
-func DeleteRelay(hostID string) *models.ApiHost {
-	return request[models.ApiHost](http.MethodDelete, fmt.Sprintf("/api/hosts/%s/relay", hostID), nil)
+// DeleteRelay - remove relay from a node
+func DeleteRelay(netID, nodeID string) *models.ApiNode {
+	return request[models.ApiNode](http.MethodDelete, fmt.Sprintf("/api/nodes/%s/%s/deleterelay", netID, nodeID), nil)
 }
 }
 
 
 // RefreshKeys - refresh wireguard keys
 // RefreshKeys - refresh wireguard keys

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

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

+ 56 - 50
config/config.go

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

+ 1 - 1
controllers/docs.go

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

+ 2 - 42
controllers/hosts.go

@@ -6,7 +6,6 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"reflect"
 
 
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -26,11 +25,9 @@ func hostHandlers(r *mux.Router) {
 	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(deleteHost))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(deleteHost))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(addHostToNetwork))).Methods(http.MethodPost)
 	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(addHostToNetwork))).Methods(http.MethodPost)
 	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(deleteHostFromNetwork))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(deleteHostFromNetwork))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/hosts/{hostid}/relay", logic.SecurityCheck(false, http.HandlerFunc(createHostRelay))).Methods(http.MethodPost)
-	r.HandleFunc("/api/hosts/{hostid}/relay", logic.SecurityCheck(false, http.HandlerFunc(deleteHostRelay))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/adm/authenticate", authenticateHost).Methods(http.MethodPost)
 	r.HandleFunc("/api/hosts/adm/authenticate", authenticateHost).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/host", authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
-	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 }
 
 
@@ -174,13 +171,6 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
-	// check if relay information is changed
-	updateRelay := false
-	if newHost.IsRelay && len(newHost.RelayedHosts) > 0 {
-		if len(newHost.RelayedHosts) != len(currHost.RelayedHosts) || !reflect.DeepEqual(newHost.RelayedHosts, currHost.RelayedHosts) {
-			updateRelay = true
-		}
-	}
 
 
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	if err = logic.UpsertHost(newHost); err != nil {
 	if err = logic.UpsertHost(newHost); err != nil {
@@ -188,9 +178,6 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	if updateRelay {
-		logic.UpdateHostRelay(currHost.ID.String(), currHost.RelayedHosts, newHost.RelayedHosts)
-	}
 	// publish host update through MQ
 	// publish host update through MQ
 	if err := mq.HostUpdate(&models.HostUpdate{
 	if err := mq.HostUpdate(&models.HostUpdate{
 		Action: models.UpdateHost,
 		Action: models.UpdateHost,
@@ -244,33 +231,6 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	if currHost.IsRelay {
-		if _, _, err := logic.DeleteHostRelay(hostid); err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to dissociate host from relays:", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-	}
-	if currHost.IsRelayed {
-		relayHost, err := logic.GetHost(currHost.RelayedBy)
-		if err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to fetch relay host:", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-		newRelayedHosts := make([]string, 0)
-		for _, relayedHostID := range relayHost.RelayedHosts {
-			if relayedHostID != hostid {
-				newRelayedHosts = append(newRelayedHosts, relayedHostID)
-			}
-		}
-		relayHost.RelayedHosts = newRelayedHosts
-		if err := logic.UpsertHost(relayHost); err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to update host relays:", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-	}
 	if err = logic.RemoveHost(currHost); err != nil {
 	if err = logic.RemoveHost(currHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))

+ 1 - 9
controllers/limits.go

@@ -6,7 +6,6 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 // limit consts
 // limit consts
@@ -23,20 +22,13 @@ func checkFreeTierLimits(limit_choice int, next http.Handler) http.HandlerFunc {
 			Code: http.StatusForbidden, Message: "free tier limits exceeded on networks",
 			Code: http.StatusForbidden, Message: "free tier limits exceeded on networks",
 		}
 		}
 
 
-		if logic.Free_Tier && servercfg.Is_EE { // check that free tier limits not exceeded
+		if logic.Free_Tier { // check that free tier limits not exceeded
 			if limit_choice == networks_l {
 			if limit_choice == networks_l {
 				currentNetworks, err := logic.GetNetworks()
 				currentNetworks, err := logic.GetNetworks()
 				if (err != nil && !database.IsEmptyRecord(err)) || len(currentNetworks) >= logic.Networks_Limit {
 				if (err != nil && !database.IsEmptyRecord(err)) || len(currentNetworks) >= logic.Networks_Limit {
 					logic.ReturnErrorResponse(w, r, errorResponse)
 					logic.ReturnErrorResponse(w, r, errorResponse)
 					return
 					return
 				}
 				}
-			} else if limit_choice == node_l {
-				nodes, err := logic.GetAllNodes()
-				if (err != nil && !database.IsEmptyRecord(err)) || len(nodes) >= logic.Node_Limit {
-					errorResponse.Message = "free tier limits exceeded on nodes"
-					logic.ReturnErrorResponse(w, r, errorResponse)
-					return
-				}
 			} else if limit_choice == users_l {
 			} else if limit_choice == users_l {
 				users, err := logic.GetUsers()
 				users, err := logic.GetUsers()
 				if (err != nil && !database.IsEmptyRecord(err)) || len(users) >= logic.Users_Limit {
 				if (err != nil && !database.IsEmptyRecord(err)) || len(users) >= logic.Users_Limit {

+ 33 - 50
controllers/node.go

@@ -23,18 +23,16 @@ var hostIDHeader = "host-id"
 
 
 func nodeHandlers(r *mux.Router) {
 func nodeHandlers(r *mux.Router) {
 
 
-	r.HandleFunc("/api/nodes", authorize(false, false, "user", http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}", authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(false, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPut)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", authorize(false, true, "user", http.HandlerFunc(createRelay))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", authorize(false, true, "user", http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", authorize(false, true, "user", http.HandlerFunc(createEgressGateway))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", authorize(false, true, "user", http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes", Authorize(false, false, "user", http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}", Authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(false, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPut)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", Authorize(false, true, "user", http.HandlerFunc(createEgressGateway))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", Authorize(false, true, "user", http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(false, http.HandlerFunc(createIngressGateway))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(false, http.HandlerFunc(createIngressGateway))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(false, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(false, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
 }
@@ -154,7 +152,7 @@ func authenticate(response http.ResponseWriter, request *http.Request) {
 // even if it's technically ok
 // even if it's technically ok
 // This is kind of a poor man's RBAC. There's probably a better/smarter way.
 // This is kind of a poor man's RBAC. There's probably a better/smarter way.
 // TODO: Consider better RBAC implementations
 // TODO: Consider better RBAC implementations
-func authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
+func Authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
 		var errorResponse = models.ErrorResponse{
 			Code: http.StatusForbidden, Message: logic.Forbidden_Msg,
 			Code: http.StatusForbidden, Message: logic.Forbidden_Msg,
@@ -633,12 +631,12 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	newNode := newData.ConvertToServerNode(&currentNode)
 	newNode := newData.ConvertToServerNode(&currentNode)
 	relayupdate := false
 	relayupdate := false
-	if currentNode.IsRelay && len(newNode.RelayAddrs) > 0 {
-		if len(newNode.RelayAddrs) != len(currentNode.RelayAddrs) {
+	if servercfg.Is_EE && newNode.IsRelay && len(newNode.RelayedNodes) > 0 {
+		if len(newNode.RelayedNodes) != len(currentNode.RelayedNodes) {
 			relayupdate = true
 			relayupdate = true
 		} else {
 		} else {
-			for i, addr := range newNode.RelayAddrs {
-				if addr != currentNode.RelayAddrs[i] {
+			for i, node := range newNode.RelayedNodes {
+				if node != currentNode.RelayedNodes[i] {
 					relayupdate = true
 					relayupdate = true
 				}
 				}
 			}
 			}
@@ -651,10 +649,6 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	relayedUpdate := false
-	if currentNode.IsRelayed && (currentNode.Address.String() != newNode.Address.String() || currentNode.Address6.String() != newNode.Address6.String()) {
-		relayedUpdate = true
-	}
 	ifaceDelta := logic.IfaceDelta(&currentNode, newNode)
 	ifaceDelta := logic.IfaceDelta(&currentNode, newNode)
 	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 	aclUpdate := currentNode.DefaultACL != newNode.DefaultACL
 	if ifaceDelta && servercfg.Is_EE {
 	if ifaceDelta && servercfg.Is_EE {
@@ -671,16 +665,13 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	if relayupdate {
 	if relayupdate {
-		updatenodes := logic.UpdateRelay(currentNode.Network, currentNode.RelayAddrs, newNode.RelayAddrs)
+		updatenodes := logic.UpdateRelayed(currentNode.ID.String(), currentNode.RelayedNodes, newNode.RelayedNodes)
 		if len(updatenodes) > 0 {
 		if len(updatenodes) > 0 {
 			for _, relayedNode := range updatenodes {
 			for _, relayedNode := range updatenodes {
 				runUpdates(&relayedNode, false)
 				runUpdates(&relayedNode, false)
 			}
 			}
 		}
 		}
 	}
 	}
-	if relayedUpdate {
-		updateRelay(&currentNode, newNode)
-	}
 	if servercfg.IsDNSMode() {
 	if servercfg.IsDNSMode() {
 		logic.SetDNS()
 		logic.SetDNS()
 	}
 	}
@@ -690,8 +681,8 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	json.NewEncoder(w).Encode(apiNode)
 	runUpdates(newNode, ifaceDelta)
 	runUpdates(newNode, ifaceDelta)
-	go func(aclUpdate bool, newNode *models.Node) {
-		if aclUpdate {
+	go func(aclUpdate, relayupdate bool, newNode *models.Node) {
+		if aclUpdate || relayupdate {
 			if err := mq.PublishPeerUpdate(); err != nil {
 			if err := mq.PublishPeerUpdate(); err != nil {
 				logger.Log(0, "error during node ACL update for node", newNode.ID.String())
 				logger.Log(0, "error during node ACL update for node", newNode.ID.String())
 			}
 			}
@@ -699,7 +690,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		if err := mq.PublishReplaceDNS(&currentNode, newNode, host); err != nil {
 		if err := mq.PublishReplaceDNS(&currentNode, newNode, host); err != nil {
 			logger.Log(1, "failed to publish dns update", err.Error())
 			logger.Log(1, "failed to publish dns update", err.Error())
 		}
 		}
-	}(aclUpdate, newNode)
+	}(aclUpdate, relayupdate, newNode)
 }
 }
 
 
 // swagger:route DELETE /api/nodes/{network}/{nodeid} nodes deleteNode
 // swagger:route DELETE /api/nodes/{network}/{nodeid} nodes deleteNode
@@ -743,6 +734,22 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
 		return
 		return
 	}
 	}
+	if node.IsRelayed {
+		// cleanup node from relayednodes on relay node
+		relayNode, err := logic.GetNodeByID(node.RelayedBy)
+		if err == nil {
+			relayedNodes := []string{}
+			for _, relayedNodeID := range relayNode.RelayedNodes {
+				if relayedNodeID == node.ID.String() {
+					continue
+				}
+				relayedNodes = append(relayedNodes, relayedNodeID)
+			}
+			relayNode.RelayedNodes = relayedNodes
+			logic.UpsertNode(&relayNode)
+		}
+
+	}
 	logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
 	logic.ReturnSuccessResponse(w, r, nodeid+" deleted.")
 	logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"])
 	logger.Log(1, r.Header.Get("user"), "Deleted node", nodeid, "from network", params["network"])
 	if !fromNode { // notify node change
 	if !fromNode { // notify node change
@@ -778,30 +785,6 @@ func runUpdates(node *models.Node, ifaceDelta bool) {
 	}()
 	}()
 }
 }
 
 
-func updateRelay(oldnode, newnode *models.Node) {
-	relay := logic.FindRelay(oldnode)
-	newrelay := relay
-	//check if node's address has been updated and if so, update the relayAddrs of the relay node with the updated address of the relayed node
-	if oldnode.Address.String() != newnode.Address.String() {
-		for i, ip := range newrelay.RelayAddrs {
-			if ip == oldnode.Address.IP.String() {
-				newrelay.RelayAddrs = append(newrelay.RelayAddrs[:i], relay.RelayAddrs[i+1:]...)
-				newrelay.RelayAddrs = append(newrelay.RelayAddrs, newnode.Address.IP.String())
-			}
-		}
-	}
-	//check if node's address(v6) has been updated and if so, update the relayAddrs of the relay node with the updated address(v6) of the relayed node
-	if oldnode.Address6.String() != newnode.Address6.String() {
-		for i, ip := range newrelay.RelayAddrs {
-			if ip == oldnode.Address.IP.String() {
-				newrelay.RelayAddrs = append(newrelay.RelayAddrs[:i], newrelay.RelayAddrs[i+1:]...)
-				newrelay.RelayAddrs = append(newrelay.RelayAddrs, newnode.Address6.IP.String())
-			}
-		}
-	}
-	logic.UpdateNode(relay, newrelay)
-}
-
 func doesUserOwnNode(username, network, nodeID string) bool {
 func doesUserOwnNode(username, network, nodeID string) bool {
 	u, err := logic.GetUser(username)
 	u, err := logic.GetUser(username)
 	if err != nil {
 	if err != nil {

+ 0 - 199
controllers/relay.go

@@ -1,199 +0,0 @@
-package controller
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-
-	"github.com/gorilla/mux"
-	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/mq"
-)
-
-// swagger:route POST /api/nodes/{network}/{nodeid}/createrelay nodes createRelay
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
-func createRelay(w http.ResponseWriter, r *http.Request) {
-	var relay models.RelayRequest
-	var params = mux.Vars(r)
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewDecoder(r.Body).Decode(&relay)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	relay.NetID = params["network"]
-	relay.NodeID = params["nodeid"]
-	updatenodes, node, err := logic.CreateRelay(relay)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create relay on node [%s] on network [%s]: %v", relay.NodeID, relay.NetID, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	logger.Log(1, r.Header.Get("user"), "created relay on node", relay.NodeID, "on network", relay.NetID)
-	for _, relayedNode := range updatenodes {
-
-		err = mq.NodeUpdate(&relayedNode)
-		if err != nil {
-			logger.Log(1, "error sending update to relayed node ", relayedNode.ID.String(), "on network", relay.NetID, ": ", err.Error())
-		}
-	}
-
-	apiNode := node.ConvertToAPINode()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-	runUpdates(&node, true)
-}
-
-// swagger:route DELETE /api/nodes/{network}/{nodeid}/deleterelay nodes deleteRelay
-//
-// Remove a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
-func deleteRelay(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	nodeid := params["nodeid"]
-	netid := params["network"]
-	updatenodes, node, err := logic.DeleteRelay(netid, nodeid)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	logger.Log(1, r.Header.Get("user"), "deleted relay server", nodeid, "on network", netid)
-	for _, relayedNode := range updatenodes {
-		err = mq.NodeUpdate(&relayedNode)
-		if err != nil {
-			logger.Log(1, "error sending update to relayed node ", relayedNode.ID.String(), "on network", netid, ": ", err.Error())
-		}
-	}
-	apiNode := node.ConvertToAPINode()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-	runUpdates(&node, true)
-}
-
-// swagger:route POST /api/hosts/{hostid}/relay hosts createHostRelay
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
-func createHostRelay(w http.ResponseWriter, r *http.Request) {
-	var relay models.HostRelayRequest
-	var params = mux.Vars(r)
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewDecoder(r.Body).Decode(&relay)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	relay.HostID = params["hostid"]
-	relayHost, relayedHosts, err := logic.CreateHostRelay(relay)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create relay on host [%s]: %v", relay.HostID, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	if err := mq.HostUpdate(&models.HostUpdate{
-		Action: models.UpdateHost,
-		Host:   *relayHost,
-	}); err != nil {
-		logger.Log(0, "failed to send host update: ", relayHost.ID.String(), err.Error())
-	}
-	logger.Log(1, r.Header.Get("user"), "created relay on host", relay.HostID)
-	go func(relayHostID string) {
-		for _, relayedHost := range relayedHosts {
-			relayedHost.ProxyEnabled = true
-			logic.UpsertHost(&relayedHost)
-			if err := mq.HostUpdate(&models.HostUpdate{
-				Action: models.UpdateHost,
-				Host:   relayedHost,
-			}); err != nil {
-				logger.Log(0, "failed to send host update: ", relayedHost.ID.String(), err.Error())
-			}
-		}
-		if err := mq.PublishPeerUpdate(); err != nil {
-			logger.Log(0, "fail to publish peer update: ", err.Error())
-		}
-
-	}(relay.HostID)
-
-	apiHostData := relayHost.ConvertNMHostToAPI()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiHostData)
-}
-
-// swagger:route DELETE /api/hosts/{hostid}/relay hosts deleteHostRelay
-//
-// Remove a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
-func deleteHostRelay(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	hostid := params["hostid"]
-	relayHost, relayed, err := logic.DeleteHostRelay(hostid)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	logger.Log(1, r.Header.Get("user"), "deleted relay host", hostid)
-	go func() {
-		if err := mq.PublishPeerUpdate(); err != nil {
-			logger.Log(0, "fail to publish peer update: ", err.Error())
-		}
-		if err := mq.HostUpdate(&models.HostUpdate{
-			Action: models.UpdateHost,
-			Host:   *relayHost,
-		}); err != nil {
-			logger.Log(0, "failed to send host update: ", relayHost.Name, err.Error())
-		}
-		for _, relayedHost := range relayed {
-			if err := mq.HostUpdate(&models.HostUpdate{
-				Action: models.UpdateHost,
-				Host:   relayedHost,
-			}); err != nil {
-				logger.Log(0, "failed to send host update: ", relayedHost.Name, err.Error())
-			}
-		}
-	}()
-	apiHostData := relayHost.ConvertNMHostToAPI()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiHostData)
-}

+ 39 - 1
controllers/server.go

@@ -20,8 +20,40 @@ func serverHandlers(r *mux.Router) {
 		resp.Write([]byte("Server is up and running!!"))
 		resp.Write([]byte("Server is up and running!!"))
 	}))
 	}))
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).Methods(http.MethodGet)
-	r.HandleFunc("/api/server/getserverinfo", authorize(true, false, "node", http.HandlerFunc(getServerInfo))).Methods(http.MethodGet)
+	r.HandleFunc("/api/server/getserverinfo", Authorize(true, false, "node", http.HandlerFunc(getServerInfo))).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", http.HandlerFunc(getStatus)).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", http.HandlerFunc(getStatus)).Methods(http.MethodGet)
+	r.HandleFunc("/api/server/usage", Authorize(true, false, "user", http.HandlerFunc(getUsage))).Methods(http.MethodGet)
+}
+func getUsage(w http.ResponseWriter, r *http.Request) {
+	type usage struct {
+		Hosts    int `json:"hosts"`
+		Clients  int `json:"clients"`
+		Networks int `json:"networks"`
+		Users    int `json:"users"`
+	}
+	var serverUsage usage
+	hosts, err := logic.GetAllHosts()
+	if err == nil {
+		serverUsage.Hosts = len(hosts)
+	}
+	clients, err := logic.GetAllExtClients()
+	if err == nil {
+		serverUsage.Clients = len(clients)
+	}
+	users, err := logic.GetUsers()
+	if err == nil {
+		serverUsage.Users = len(users)
+	}
+	networks, err := logic.GetNetworks()
+	if err == nil {
+		serverUsage.Networks = len(networks)
+	}
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(models.SuccessResponse{
+		Code:     http.StatusOK,
+		Response: serverUsage,
+	})
+
 }
 }
 
 
 // swagger:route GET /api/server/status server getStatus
 // swagger:route GET /api/server/status server getStatus
@@ -41,6 +73,12 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 	type status struct {
 	type status struct {
 		DB     bool `json:"db_connected"`
 		DB     bool `json:"db_connected"`
 		Broker bool `json:"broker_connected"`
 		Broker bool `json:"broker_connected"`
+		Usage  struct {
+			Hosts    int `json:"hosts"`
+			Clients  int `json:"clients"`
+			Networks int `json:"networks"`
+			Users    int `json:"users"`
+		} `json:"usage"`
 	}
 	}
 
 
 	currentServerStatus := status{
 	currentServerStatus := status{

+ 13 - 9
controllers/user.go

@@ -19,6 +19,9 @@ var (
 	upgrader = websocket.Upgrader{}
 	upgrader = websocket.Upgrader{}
 )
 )
 
 
+// verifyJWT makes logic.VerifyJWT fakeable/mockable in tests
+var verifyJWT = logic.VerifyJWT
+
 func userHandlers(r *mux.Router) {
 func userHandlers(r *mux.Router) {
 
 
 	r.HandleFunc("/api/users/adm/hasadmin", hasAdmin).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/adm/hasadmin", hasAdmin).Methods(http.MethodGet)
@@ -152,7 +155,7 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 
 
 	var params = mux.Vars(r)
 	var params = mux.Vars(r)
 	usernameFetched := params["username"]
 	usernameFetched := params["username"]
-	user, err := logic.GetUser(usernameFetched)
+	user, err := logic.GetReturnUser(usernameFetched)
 
 
 	if err != nil {
 	if err != nil {
 		logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
 		logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
@@ -230,7 +233,7 @@ func createAdmin(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	logger.Log(1, admin.UserName, "was made a new admin")
 	logger.Log(1, admin.UserName, "was made a new admin")
-	json.NewEncoder(w).Encode(admin)
+	json.NewEncoder(w).Encode(logic.ToReturnUser(admin))
 }
 }
 
 
 // swagger:route POST /api/users/{username} user createUser
 // swagger:route POST /api/users/{username} user createUser
@@ -264,7 +267,7 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	logger.Log(1, user.UserName, "was created")
 	logger.Log(1, user.UserName, "was created")
-	json.NewEncoder(w).Encode(user)
+	json.NewEncoder(w).Encode(logic.ToReturnUser(user))
 }
 }
 
 
 // swagger:route PUT /api/users/networks/{username} user updateUserNetworks
 // swagger:route PUT /api/users/networks/{username} user updateUserNetworks
@@ -314,12 +317,13 @@ func updateUserNetworks(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	logger.Log(1, username, "status was updated")
 	logger.Log(1, username, "status was updated")
 	// re-read and return the new user struct
 	// re-read and return the new user struct
-	if userChange, err = logic.GetUser(username); err != nil {
+	returnUser, err := logic.GetReturnUser(username)
+	if err != nil {
 		logger.Log(0, username, "failed to fetch user: ", err.Error())
 		logger.Log(0, username, "failed to fetch user: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	json.NewEncoder(w).Encode(userChange)
+	json.NewEncoder(w).Encode(returnUser)
 }
 }
 
 
 // swagger:route PUT /api/users/{username} user updateUser
 // swagger:route PUT /api/users/{username} user updateUser
@@ -337,7 +341,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
 	var params = mux.Vars(r)
 	// start here
 	// start here
-	jwtUser, _, isadmin, err := logic.VerifyJWT(r.Header.Get("Authorization"))
+	jwtUser, _, isadmin, err := verifyJWT(r.Header.Get("Authorization"))
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "verifyJWT error", err.Error())
 		logger.Log(0, "verifyJWT error", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -385,7 +389,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	logger.Log(1, username, "was updated")
 	logger.Log(1, username, "was updated")
-	json.NewEncoder(w).Encode(user)
+	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 }
 
 
 // swagger:route PUT /api/users/{username}/adm user updateUserAdm
 // swagger:route PUT /api/users/{username}/adm user updateUserAdm
@@ -409,7 +413,7 @@ func updateUserAdm(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	if auth.IsOauthUser(user) != nil {
+	if auth.IsOauthUser(user) == nil {
 		err := fmt.Errorf("cannot update user info for oauth user %s", username)
 		err := fmt.Errorf("cannot update user info for oauth user %s", username)
 		logger.Log(0, err.Error())
 		logger.Log(0, err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
@@ -436,7 +440,7 @@ func updateUserAdm(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	logger.Log(1, username, "was updated (admin)")
 	logger.Log(1, username, "was updated (admin)")
-	json.NewEncoder(w).Encode(user)
+	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 }
 
 
 // swagger:route DELETE /api/users/{username} user deleteUser
 // swagger:route DELETE /api/users/{username} user deleteUser

+ 123 - 0
controllers/user_test.go

@@ -1,6 +1,12 @@
 package controller
 package controller
 
 
 import (
 import (
+	"bytes"
+	"github.com/go-jose/go-jose/v3/json"
+	"github.com/gorilla/mux"
+	"io"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 	"testing"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
@@ -19,6 +25,123 @@ func deleteAllUsers(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestGetUserNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	user := models.User{UserName: "freddie", Password: "password"}
+	haveOnlyOneUser(t, user)
+
+	// prepare request
+	rec, req := prepareUserRequest(t, models.User{}, user.UserName)
+
+	// test response
+	getUser(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user.UserName)
+}
+
+func TestCreateAdminNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	deleteAllUsers(t)
+
+	// prepare request
+	user := models.User{UserName: "jonathan", Password: "password"}
+	rec, req := prepareUserRequest(t, user, "")
+
+	// test response
+	createAdmin(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user.UserName)
+}
+
+func TestCreateUserNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	deleteAllUsers(t)
+
+	// prepare request
+	user := models.User{UserName: "jonathan", Password: "password"}
+	rec, req := prepareUserRequest(t, user, "")
+
+	// test response
+	createUser(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user.UserName)
+}
+
+func TestUpdateUserNetworksNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	user1 := models.User{UserName: "joestar", Password: "jonathan"}
+	haveOnlyOneUser(t, user1)
+
+	// prepare request
+	user2 := models.User{UserName: "joestar", Password: "joseph"}
+	rec, req := prepareUserRequest(t, user2, user1.UserName)
+
+	// test response
+	updateUserNetworks(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user1.UserName)
+}
+
+func TestUpdateUserNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	user1 := models.User{UserName: "dio", Password: "brando"}
+	haveOnlyOneUser(t, user1)
+
+	// prepare request
+	user2 := models.User{UserName: "giorno", Password: "giovanna"}
+	rec, req := prepareUserRequest(t, user2, user1.UserName)
+
+	// mock the jwt verification
+	oldVerify := verifyJWT
+	verifyJWT = func(bearerToken string) (username string, networks []string, isadmin bool, err error) {
+		return user1.UserName, user1.Networks, user1.IsAdmin, nil
+	}
+	defer func() { verifyJWT = oldVerify }()
+
+	// test response
+	updateUser(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user2.UserName)
+}
+
+func TestUpdateUserAdmNoHashedPassword(t *testing.T) {
+	// prepare existing user base
+	user1 := models.User{UserName: "dio", Password: "brando", IsAdmin: true}
+	haveOnlyOneUser(t, user1)
+
+	// prepare request
+	user2 := models.User{UserName: "giorno", Password: "giovanna"}
+	rec, req := prepareUserRequest(t, user2, user1.UserName)
+
+	// test response
+	updateUserAdm(rec, req)
+	assertUserNameButNoPassword(t, rec.Body, user2.UserName)
+}
+
+func prepareUserRequest(t *testing.T, userForBody models.User, userNameForParam string) (*httptest.ResponseRecorder, *http.Request) {
+	bits, err := json.Marshal(userForBody)
+	assert.Nil(t, err)
+	body := bytes.NewReader(bits)
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest("ANY", "https://example.com", body) // only the body matters here
+	req = mux.SetURLVars(req, map[string]string{"username": userNameForParam})
+	return rec, req
+}
+
+func haveOnlyOneUser(t *testing.T, user models.User) {
+	deleteAllUsers(t)
+	var err error
+	if user.IsAdmin {
+		err = logic.CreateAdmin(&user)
+	} else {
+		err = logic.CreateUser(&user)
+	}
+	assert.Nil(t, err)
+}
+
+func assertUserNameButNoPassword(t *testing.T, r io.Reader, userName string) {
+	var resp models.User
+	err := json.NewDecoder(r).Decode(&resp)
+	assert.Nil(t, err)
+	assert.Equal(t, userName, resp.UserName)
+	assert.Empty(t, resp.Password)
+}
+
 func TestHasAdmin(t *testing.T) {
 func TestHasAdmin(t *testing.T) {
 	// delete all current users
 	// delete all current users
 	users, _ := logic.GetUsers()
 	users, _ := logic.GetUsers()

+ 107 - 0
ee/ee_controllers/relay.go

@@ -0,0 +1,107 @@
+package ee_controllers
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/gorilla/mux"
+	controller "github.com/gravitl/netmaker/controllers"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+)
+
+// RelayHandlers - handle EE Relays
+func RelayHandlers(r *mux.Router) {
+
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", controller.Authorize(false, true, "user", http.HandlerFunc(createRelay))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", controller.Authorize(false, true, "user", http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
+}
+
+// swagger:route POST /api/nodes/{network}/{nodeid}/createrelay nodes createRelay
+//
+// Create a relay.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: nodeResponse
+func createRelay(w http.ResponseWriter, r *http.Request) {
+	var relayRequest models.RelayRequest
+	var params = mux.Vars(r)
+	w.Header().Set("Content-Type", "application/json")
+	err := json.NewDecoder(r.Body).Decode(&relayRequest)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	relayRequest.NetID = params["network"]
+	relayRequest.NodeID = params["nodeid"]
+	_, relayNode, err := logic.CreateRelay(relayRequest)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to create relay on node [%s] on network [%s]: %v", relayRequest.NodeID, relayRequest.NetID, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	go mq.PublishPeerUpdate()
+	logger.Log(1, r.Header.Get("user"), "created relay on node", relayRequest.NodeID, "on network", relayRequest.NetID)
+	apiNode := relayNode.ConvertToAPINode()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+}
+
+// swagger:route DELETE /api/nodes/{network}/{nodeid}/deleterelay nodes deleteRelay
+//
+// Remove a relay.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: nodeResponse
+func deleteRelay(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	updateNodes, node, err := logic.DeleteRelay(netid, nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	logger.Log(1, r.Header.Get("user"), "deleted relay server", nodeid, "on network", netid)
+	go func() {
+		for _, relayedNode := range updateNodes {
+			err = mq.NodeUpdate(&relayedNode)
+			if err != nil {
+				logger.Log(1, "relayed node update ", relayedNode.ID.String(), "on network", relayedNode.Network, ": ", err.Error())
+
+			}
+			h, err := logic.GetHost(relayedNode.HostID.String())
+			if err == nil {
+				if h.OS == models.OS_Types.IoT {
+					node.IsRelay = true // for iot update to recognise that it has to delete relay peer
+					if err = mq.PublishSingleHostPeerUpdate(context.Background(), h, &node, nil); err != nil {
+						logger.Log(1, "failed to publish peer update to host", h.ID.String(), ": ", err.Error())
+					}
+				}
+			}
+		}
+		mq.PublishPeerUpdate()
+	}()
+	logger.Log(1, r.Header.Get("user"), "deleted relay on node", node.ID.String(), "on network", node.Network)
+	apiNode := node.ConvertToAPINode()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+}

+ 4 - 18
ee/initialize.go

@@ -16,23 +16,20 @@ import (
 // InitEE - Initialize EE Logic
 // InitEE - Initialize EE Logic
 func InitEE() {
 func InitEE() {
 	setIsEnterprise()
 	setIsEnterprise()
+	servercfg.Is_EE = true
 	models.SetLogo(retrieveEELogo())
 	models.SetLogo(retrieveEELogo())
 	controller.HttpHandlers = append(
 	controller.HttpHandlers = append(
 		controller.HttpHandlers,
 		controller.HttpHandlers,
 		ee_controllers.MetricHandlers,
 		ee_controllers.MetricHandlers,
 		ee_controllers.NetworkUsersHandlers,
 		ee_controllers.NetworkUsersHandlers,
 		ee_controllers.UserGroupsHandlers,
 		ee_controllers.UserGroupsHandlers,
+		ee_controllers.RelayHandlers,
 	)
 	)
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 		// == License Handling ==
 		// == License Handling ==
 		ValidateLicense()
 		ValidateLicense()
-		if Limits.FreeTier {
-			logger.Log(0, "proceeding with Free Tier license")
-			logic.SetFreeTierForTelemetry(true)
-		} else {
-			logger.Log(0, "proceeding with Paid Tier license")
-			logic.SetFreeTierForTelemetry(false)
-		}
+		logger.Log(0, "proceeding with Paid Tier license")
+		logic.SetFreeTierForTelemetry(false)
 		// == End License Handling ==
 		// == End License Handling ==
 		AddLicenseHooks()
 		AddLicenseHooks()
 		resetFailover()
 		resetFailover()
@@ -45,17 +42,6 @@ func InitEE() {
 	logic.AllowClientNodeAccess = eelogic.RemoveDeniedNodeFromClient
 	logic.AllowClientNodeAccess = eelogic.RemoveDeniedNodeFromClient
 }
 }
 
 
-func setControllerLimits() {
-	logic.Node_Limit = Limits.Nodes
-	logic.Users_Limit = Limits.Users
-	logic.Clients_Limit = Limits.Clients
-	logic.Free_Tier = Limits.FreeTier
-	servercfg.Is_EE = true
-	if logic.Free_Tier {
-		logic.Networks_Limit = 3
-	}
-}
-
 func resetFailover() {
 func resetFailover() {
 	nets, err := logic.GetNetworks()
 	nets, err := logic.GetNetworks()
 	if err == nil {
 	if err == nil {

+ 13 - 19
ee/license.go

@@ -9,12 +9,13 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"math"
 	"net/http"
 	"net/http"
+	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/nacl/box"
@@ -31,8 +32,14 @@ type apiServerConf struct {
 
 
 // AddLicenseHooks - adds the validation and cache clear hooks
 // AddLicenseHooks - adds the validation and cache clear hooks
 func AddLicenseHooks() {
 func AddLicenseHooks() {
-	logic.AddHook(ValidateLicense)
-	logic.AddHook(ClearLicenseCache)
+	logic.HookManagerCh <- models.HookDetails{
+		Hook:     ValidateLicense,
+		Interval: time.Hour,
+	}
+	logic.HookManagerCh <- models.HookDetails{
+		Hook:     ClearLicenseCache,
+		Interval: time.Hour,
+	}
 }
 }
 
 
 // ValidateLicense - the initial license check for netmaker server
 // ValidateLicense - the initial license check for netmaker server
@@ -58,8 +65,8 @@ func ValidateLicense() error {
 	}
 	}
 
 
 	licenseSecret := LicenseSecret{
 	licenseSecret := LicenseSecret{
-		UserID: netmakerAccountID,
-		Limits: getCurrentServerLimit(),
+		AssociatedID: netmakerAccountID,
+		Limits:       getCurrentServerLimit(),
 	}
 	}
 
 
 	secretData, err := json.Marshal(&licenseSecret)
 	secretData, err := json.Marshal(&licenseSecret)
@@ -92,17 +99,6 @@ func ValidateLicense() error {
 		logger.FatalLog0(errValidation.Error())
 		logger.FatalLog0(errValidation.Error())
 	}
 	}
 
 
-	Limits.Networks = math.MaxInt
-	Limits.FreeTier = license.FreeTier == "yes"
-	Limits.Clients = license.LimitClients
-	Limits.Nodes = license.LimitNodes
-	Limits.Servers = license.LimitServers
-	Limits.Users = license.LimitUsers
-	if Limits.FreeTier {
-		Limits.Networks = 3
-	}
-	setControllerLimits()
-
 	logger.Log(0, "License validation succeeded!")
 	logger.Log(0, "License validation succeeded!")
 	return nil
 	return nil
 }
 }
@@ -167,6 +163,7 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, erro
 	}
 	}
 
 
 	msg := ValidateLicenseRequest{
 	msg := ValidateLicenseRequest{
+		LicenseKey:     servercfg.GetLicenseKey(),
 		NmServerPubKey: base64encode(publicKeyBytes),
 		NmServerPubKey: base64encode(publicKeyBytes),
 		EncryptedPart:  base64encode(encryptedData),
 		EncryptedPart:  base64encode(encryptedData),
 	}
 	}
@@ -180,9 +177,6 @@ func validateLicenseKey(encryptedData []byte, publicKey *[32]byte) ([]byte, erro
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	reqParams := req.URL.Query()
-	reqParams.Add("licensevalue", servercfg.GetLicenseKey())
-	req.URL.RawQuery = reqParams.Encode()
 	req.Header.Add("Content-Type", "application/json")
 	req.Header.Add("Content-Type", "application/json")
 	req.Header.Add("Accept", "application/json")
 	req.Header.Add("Accept", "application/json")
 	client := &http.Client{}
 	client := &http.Client{}

+ 20 - 38
ee/types.go

@@ -3,7 +3,7 @@ package ee
 import "fmt"
 import "fmt"
 
 
 const (
 const (
-	api_endpoint               = "https://api.controller.netmaker.io/api/v1/license/validate"
+	api_endpoint               = "https://api.accounts.netmaker.io/api/v1/license/validate"
 	license_cache_key          = "license_response_cache"
 	license_cache_key          = "license_response_cache"
 	license_validation_err_msg = "invalid license"
 	license_validation_err_msg = "invalid license"
 	server_id_key              = "nm-server-id"
 	server_id_key              = "nm-server-id"
@@ -11,38 +11,17 @@ const (
 
 
 var errValidation = fmt.Errorf(license_validation_err_msg)
 var errValidation = fmt.Errorf(license_validation_err_msg)
 
 
-// Limits - limits to be referenced throughout server
-var Limits = GlobalLimits{
-	Servers:  0,
-	Users:    0,
-	Nodes:    0,
-	Clients:  0,
-	Networks: 0,
-	FreeTier: false,
-}
-
-// GlobalLimits - struct for holding global limits on this netmaker server in memory
-type GlobalLimits struct {
-	Servers  int
-	Users    int
-	Nodes    int
-	Clients  int
-	FreeTier bool
-	Networks int
-}
-
 // LicenseKey - the license key struct representation with associated data
 // LicenseKey - the license key struct representation with associated data
 type LicenseKey struct {
 type LicenseKey struct {
-	LicenseValue   string `json:"license_value"` // actual (public) key and the unique value for the key
-	Expiration     int64  `json:"expiration"`
-	LimitServers   int    `json:"limit_servers"`
-	LimitUsers     int    `json:"limit_users"`
-	LimitNodes     int    `json:"limit_nodes"`
-	LimitClients   int    `json:"limit_clients"`
-	Metadata       string `json:"metadata"`
-	SubscriptionID string `json:"subscription_id"` // for a paid subscription (non-free-tier license)
-	FreeTier       string `json:"free_tier"`       // yes if free tier
-	IsActive       string `json:"is_active"`       // yes if active
+	LicenseValue  string `json:"license_value"` // actual (public) key and the unique value for the key
+	Expiration    int64  `json:"expiration"`
+	LimitServers  int    `json:"limit_servers"`
+	LimitUsers    int    `json:"limit_users"`
+	LimitHosts    int    `json:"limit_hosts"`
+	LimitNetworks int    `json:"limit_networks"`
+	LimitClients  int    `json:"limit_clients"`
+	Metadata      string `json:"metadata"`
+	IsActive      bool   `json:"is_active"` // yes if active
 }
 }
 
 
 // ValidatedLicense - the validated license struct
 // ValidatedLicense - the validated license struct
@@ -53,28 +32,31 @@ type ValidatedLicense struct {
 
 
 // LicenseSecret - the encrypted struct for sending user-id
 // LicenseSecret - the encrypted struct for sending user-id
 type LicenseSecret struct {
 type LicenseSecret struct {
-	UserID string        `json:"user_id" binding:"required"` // UUID for user foreign key to User table
-	Limits LicenseLimits `json:"limits" binding:"required"`
+	AssociatedID string        `json:"associated_id" binding:"required"` // UUID for user foreign key to User table
+	Limits       LicenseLimits `json:"limits" binding:"required"`
 }
 }
 
 
 // LicenseLimits - struct license limits
 // LicenseLimits - struct license limits
 type LicenseLimits struct {
 type LicenseLimits struct {
-	Servers int `json:"servers" binding:"required"`
-	Users   int `json:"users" binding:"required"`
-	Nodes   int `json:"nodes" binding:"required"`
-	Clients int `json:"clients" binding:"required"`
+	Servers  int `json:"servers"`
+	Users    int `json:"users"`
+	Hosts    int `json:"hosts"`
+	Clients  int `json:"clients"`
+	Networks int `json:"networks"`
 }
 }
 
 
 // LicenseLimits.SetDefaults - sets the default values for limits
 // LicenseLimits.SetDefaults - sets the default values for limits
 func (l *LicenseLimits) SetDefaults() {
 func (l *LicenseLimits) SetDefaults() {
 	l.Clients = 0
 	l.Clients = 0
 	l.Servers = 1
 	l.Servers = 1
-	l.Nodes = 0
+	l.Hosts = 0
 	l.Users = 1
 	l.Users = 1
+	l.Networks = 0
 }
 }
 
 
 // ValidateLicenseRequest - used for request to validate license endpoint
 // ValidateLicenseRequest - used for request to validate license endpoint
 type ValidateLicenseRequest struct {
 type ValidateLicenseRequest struct {
+	LicenseKey     string `json:"license_key" binding:"required"`
 	NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license)
 	NmServerPubKey string `json:"nm_server_pub_key" binding:"required"` // Netmaker server public key used to send data back to Netmaker for the Netmaker server to decrypt (eg output from validating license)
 	EncryptedPart  string `json:"secret" binding:"required"`
 	EncryptedPart  string `json:"secret" binding:"required"`
 }
 }

+ 6 - 3
ee/util.go

@@ -30,12 +30,11 @@ func base64decode(input string) []byte {
 
 
 	return bytes
 	return bytes
 }
 }
-
 func getCurrentServerLimit() (limits LicenseLimits) {
 func getCurrentServerLimit() (limits LicenseLimits) {
 	limits.SetDefaults()
 	limits.SetDefaults()
-	nodes, err := logic.GetAllNodes()
+	hosts, err := logic.GetAllHosts()
 	if err == nil {
 	if err == nil {
-		limits.Nodes = len(nodes)
+		limits.Hosts = len(hosts)
 	}
 	}
 	clients, err := logic.GetAllExtClients()
 	clients, err := logic.GetAllExtClients()
 	if err == nil {
 	if err == nil {
@@ -45,5 +44,9 @@ func getCurrentServerLimit() (limits LicenseLimits) {
 	if err == nil {
 	if err == nil {
 		limits.Users = len(users)
 		limits.Users = len(users)
 	}
 	}
+	networks, err := logic.GetNetworks()
+	if err == nil {
+		limits.Networks = len(networks)
+	}
 	return
 	return
 }
 }

+ 5 - 5
go.mod

@@ -15,11 +15,11 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.4
 	github.com/stretchr/testify v1.8.4
 	github.com/txn2/txeh v1.4.0
 	github.com/txn2/txeh v1.4.0
-	golang.org/x/crypto v0.9.0
-	golang.org/x/net v0.10.0 // indirect
-	golang.org/x/oauth2 v0.8.0
-	golang.org/x/sys v0.8.0 // indirect
-	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/crypto v0.10.0
+	golang.org/x/net v0.11.0 // indirect
+	golang.org/x/oauth2 v0.9.0
+	golang.org/x/sys v0.9.0 // indirect
+	golang.org/x/text v0.10.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220324164955-056925b7df31
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220324164955-056925b7df31
 	google.golang.org/protobuf v1.28.1 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect

+ 10 - 10
go.sum

@@ -121,8 +121,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 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/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -133,10 +133,10 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
-golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
+golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -152,8 +152,8 @@ golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -161,8 +161,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

+ 0 - 14
logic/auth.go

@@ -42,20 +42,6 @@ func HasAdmin() (bool, error) {
 	return false, err
 	return false, err
 }
 }
 
 
-// GetReturnUser - gets a user
-func GetReturnUser(username string) (models.ReturnUser, error) {
-
-	var user models.ReturnUser
-	record, err := database.FetchRecord(database.USERS_TABLE_NAME, username)
-	if err != nil {
-		return user, err
-	}
-	if err = json.Unmarshal([]byte(record), &user); err != nil {
-		return models.ReturnUser{}, err
-	}
-	return user, err
-}
-
 // GetUsers - gets users
 // GetUsers - gets users
 func GetUsers() ([]models.ReturnUser, error) {
 func GetUsers() ([]models.ReturnUser, error) {
 
 

+ 3 - 0
logic/gateway.go

@@ -102,6 +102,9 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if err != nil {
 	if err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
 	}
 	}
+	if node.IsRelayed {
+		return models.Node{}, errors.New("ingress cannot be created on a relayed node")
+	}
 	host, err := GetHost(node.HostID.String())
 	host, err := GetHost(node.HostID.String())
 	if err != nil {
 	if err != nil {
 		return models.Node{}, err
 		return models.Node{}, err

+ 8 - 1
logic/hosts.go

@@ -93,7 +93,14 @@ func GetHost(hostid string) (*models.Host, error) {
 
 
 // CreateHost - creates a host if not exist
 // CreateHost - creates a host if not exist
 func CreateHost(h *models.Host) error {
 func CreateHost(h *models.Host) error {
-	_, err := GetHost(h.ID.String())
+	hosts, err := GetAllHosts()
+	if err != nil && !database.IsEmptyRecord(err) {
+		return err
+	}
+	if len(hosts) >= Hosts_Limit {
+		return errors.New("free tier limits exceeded on hosts")
+	}
+	_, err = GetHost(h.ID.String())
 	if (err != nil && !database.IsEmptyRecord(err)) || (err == nil) {
 	if (err != nil && !database.IsEmptyRecord(err)) || (err == nil) {
 		return ErrHostExists
 		return ErrHostExists
 	}
 	}

+ 14 - 44
logic/nodes.go

@@ -75,6 +75,16 @@ func UpdateNodeCheckin(node *models.Node) error {
 	return database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	return database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 }
 }
 
 
+// UpsertNode - updates node in the DB
+func UpsertNode(newNode *models.Node) error {
+	newNode.SetLastModified()
+	data, err := json.Marshal(newNode)
+	if err != nil {
+		return err
+	}
+	return database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
+}
+
 // UpdateNode - takes a node and updates another node with it's values
 // UpdateNode - takes a node and updates another node with it's values
 func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 	if newNode.Address.IP.String() != currentNode.Address.IP.String() {
 	if newNode.Address.IP.String() != currentNode.Address.IP.String() {
@@ -85,7 +95,7 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 		}
 		}
 	}
 	}
 	nodeACLDelta := currentNode.DefaultACL != newNode.DefaultACL
 	nodeACLDelta := currentNode.DefaultACL != newNode.DefaultACL
-	newNode.Fill(currentNode)
+	newNode.Fill(currentNode, servercfg.Is_EE)
 
 
 	// check for un-settable server values
 	// check for un-settable server values
 	if err := ValidateNode(newNode, true); err != nil {
 	if err := ValidateNode(newNode, true); err != nil {
@@ -338,34 +348,6 @@ func GetDeletedNodeByMacAddress(network string, macaddress string) (models.Node,
 	return node, nil
 	return node, nil
 }
 }
 
 
-// GetNodeRelay - gets the relay node of a given network
-func GetNodeRelay(network string, relayedNodeAddr string) (models.Node, error) {
-	collection, err := database.FetchRecords(database.NODES_TABLE_NAME)
-	var relay models.Node
-	if err != nil {
-		if database.IsEmptyRecord(err) {
-			return relay, nil
-		}
-		logger.Log(2, err.Error())
-		return relay, err
-	}
-	for _, value := range collection {
-		err := json.Unmarshal([]byte(value), &relay)
-		if err != nil {
-			logger.Log(2, err.Error())
-			continue
-		}
-		if relay.IsRelay {
-			for _, addr := range relay.RelayAddrs {
-				if addr == relayedNodeAddr {
-					return relay, nil
-				}
-			}
-		}
-	}
-	return relay, errors.New(RELAY_NODE_ERR + " " + relayedNodeAddr)
-}
-
 func GetNodeByID(uuid string) (models.Node, error) {
 func GetNodeByID(uuid string) (models.Node, error) {
 	var record, err = database.FetchRecord(database.NODES_TABLE_NAME, uuid)
 	var record, err = database.FetchRecord(database.NODES_TABLE_NAME, uuid)
 	if err != nil {
 	if err != nil {
@@ -399,24 +381,12 @@ func GetDeletedNodeByID(uuid string) (models.Node, error) {
 
 
 // FindRelay - returns the node that is the relay for a relayed node
 // FindRelay - returns the node that is the relay for a relayed node
 func FindRelay(node *models.Node) *models.Node {
 func FindRelay(node *models.Node) *models.Node {
-	if !node.IsRelayed {
-		return nil
-	}
-	peers, err := GetNetworkNodes(node.Network)
+	relay, err := GetNodeByID(node.RelayedBy)
 	if err != nil {
 	if err != nil {
+		logger.Log(0, "FindRelay: "+err.Error())
 		return nil
 		return nil
 	}
 	}
-	for _, peer := range peers {
-		if !peer.IsRelay {
-			continue
-		}
-		for _, ip := range peer.RelayAddrs {
-			if ip == node.Address.IP.String() || ip == node.Address6.IP.String() {
-				return &peer
-			}
-		}
-	}
-	return nil
+	return &relay
 }
 }
 
 
 // GetNetworkIngresses - gets the gateways of a network
 // GetNetworkIngresses - gets the gateways of a network

+ 155 - 104
logic/peers.go

@@ -13,6 +13,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slices"
 	"golang.org/x/exp/slices"
+	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 )
 
 
@@ -29,41 +30,6 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 		Action: models.ProxyUpdate,
 		Action: models.ProxyUpdate,
 	}
 	}
 	peerConfMap := make(map[string]models.PeerConf)
 	peerConfMap := make(map[string]models.PeerConf)
-	if host.IsRelayed {
-		relayHost, err := GetHost(host.RelayedBy)
-		if err == nil {
-			relayEndpoint, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayHost.EndpointIP, GetPeerListenPort(relayHost)))
-			if err != nil {
-				logger.Log(1, "failed to resolve relay node endpoint: ", err.Error())
-			}
-			proxyPayload.IsRelayed = true
-			proxyPayload.RelayedTo = relayEndpoint
-		} else {
-			logger.Log(0, "couldn't find relay host for:  ", host.ID.String())
-		}
-	}
-	if host.IsRelay {
-		relayedHosts := GetRelayedHosts(host)
-		relayPeersMap := make(map[string]models.RelayedConf)
-		for _, relayedHost := range relayedHosts {
-			relayedHost := relayedHost
-			payload, err := GetPeerUpdateForHost(ctx, "", &relayedHost, nil, nil)
-			if err == nil {
-				relayedEndpoint, udpErr := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayedHost.EndpointIP, GetPeerListenPort(&relayedHost)))
-				if udpErr == nil {
-					relayPeersMap[relayedHost.PublicKey.String()] = models.RelayedConf{
-						RelayedPeerEndpoint: relayedEndpoint,
-						RelayedPeerPubKey:   relayedHost.PublicKey.String(),
-						Peers:               payload.Peers,
-					}
-				}
-
-			}
-		}
-		proxyPayload.IsRelay = true
-		proxyPayload.RelayedPeerConf = relayPeersMap
-
-	}
 	var ingressStatus bool
 	var ingressStatus bool
 	for _, nodeID := range host.Nodes {
 	for _, nodeID := range host.Nodes {
 
 
@@ -101,18 +67,6 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 				}
 				}
 			}
 			}
 
 
-			if peerHost.IsRelayed && peerHost.RelayedBy != host.ID.String() {
-				relayHost, err := GetHost(peerHost.RelayedBy)
-				if err == nil {
-					relayTo, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayHost.EndpointIP, GetPeerListenPort(relayHost)))
-					if err == nil {
-						currPeerConf.IsRelayed = true
-						currPeerConf.RelayedTo = relayTo
-					}
-
-				}
-			}
-
 			peerConfMap[peerHost.PublicKey.String()] = currPeerConf
 			peerConfMap[peerHost.PublicKey.String()] = currPeerConf
 		}
 		}
 		if node.IsIngressGateway {
 		if node.IsIngressGateway {
@@ -167,7 +121,9 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 		HostNetworkInfo: models.HostInfoMap{},
 		HostNetworkInfo: models.HostInfoMap{},
 	}
 	}
 
 
-	logger.Log(1, "peer update for host", host.ID.String())
+	// endpoint detection always comes from the server
+	hostPeerUpdate.EndpointDetection = servercfg.EndpointDetectionEnabled()
+	slog.Debug("peer update for host", "hostId", host.ID.String())
 	peerIndexMap := make(map[string]int)
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {
 	for _, nodeID := range host.Nodes {
 		nodeID := nodeID
 		nodeID := nodeID
@@ -178,35 +134,25 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 			continue
 		}
 		}
-		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
-		var nodePeerMap map[string]models.PeerRouteInfo
-		if node.IsIngressGateway || node.IsEgressGateway {
-			nodePeerMap = make(map[string]models.PeerRouteInfo)
-		}
-		for _, peer := range currentPeers {
-			select {
-			case <-ctx.Done():
-				logger.Log(2, "cancelled peer update for host", host.Name, host.ID.String())
-				return models.HostPeerUpdate{}, fmt.Errorf("peer update cancelled")
-			default:
-				peer := peer
-				if peer.ID.String() == node.ID.String() {
-					logger.Log(2, "peer update, skipping self")
-					//skip yourself
+		if host.OS == models.OS_Types.IoT {
+			hostPeerUpdate.NodeAddrs = append(hostPeerUpdate.NodeAddrs, node.PrimaryAddressIPNet())
+			if node.IsRelayed {
+				relayNode, err := GetNodeByID(node.RelayedBy)
+				if err != nil {
 					continue
 					continue
 				}
 				}
-				var peerConfig wgtypes.PeerConfig
-				peerHost, err := GetHost(peer.HostID.String())
+				relayHost, err := GetHost(relayNode.HostID.String())
 				if err != nil {
 				if err != nil {
-					logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
-					return models.HostPeerUpdate{}, err
+					continue
+				}
+				relayPeer := wgtypes.PeerConfig{
+					PublicKey:                   relayHost.PublicKey,
+					PersistentKeepaliveInterval: &relayNode.PersistentKeepalive,
+					ReplaceAllowedIPs:           true,
+					AllowedIPs:                  GetAllowedIPs(&node, &relayNode, nil),
 				}
 				}
-
-				peerConfig.PublicKey = peerHost.PublicKey
-				peerConfig.PersistentKeepaliveInterval = &peer.PersistentKeepalive
-				peerConfig.ReplaceAllowedIPs = true
 				uselocal := false
 				uselocal := false
-				if host.EndpointIP.String() == peerHost.EndpointIP.String() {
+				if host.EndpointIP.String() == relayHost.EndpointIP.String() {
 					// peer is on same network
 					// peer is on same network
 					// set to localaddress
 					// set to localaddress
 					uselocal = true
 					uselocal = true
@@ -214,39 +160,63 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 						// use public endpint
 						// use public endpint
 						uselocal = false
 						uselocal = false
 					}
 					}
-					if node.LocalAddress.String() == peer.LocalAddress.String() {
+					if node.LocalAddress.String() == relayNode.LocalAddress.String() {
 						uselocal = false
 						uselocal = false
 					}
 					}
 				}
 				}
-				peerConfig.Endpoint = &net.UDPAddr{
-					IP:   peerHost.EndpointIP,
-					Port: getPeerWgListenPort(peerHost),
+				relayPeer.Endpoint = &net.UDPAddr{
+					IP:   relayHost.EndpointIP,
+					Port: getPeerWgListenPort(relayHost),
 				}
 				}
 
 
 				if uselocal {
 				if uselocal {
-					peerConfig.Endpoint.IP = peer.LocalAddress.IP
-					peerConfig.Endpoint.Port = peerHost.ListenPort
+					relayPeer.Endpoint.IP = relayNode.LocalAddress.IP
+					relayPeer.Endpoint.Port = relayHost.ListenPort
 				}
 				}
-				allowedips := GetAllowedIPs(&node, &peer, nil)
-				if peer.IsIngressGateway {
-					for _, entry := range peer.IngressGatewayRange {
-						_, cidr, err := net.ParseCIDR(string(entry))
-						if err == nil {
-							allowedips = append(allowedips, *cidr)
-						}
-					}
+
+				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, relayPeer)
+			} else if deletedNode != nil && deletedNode.IsRelay {
+				relayHost, err := GetHost(deletedNode.HostID.String())
+				if err != nil {
+					continue
 				}
 				}
-				if peer.IsEgressGateway {
-					allowedips = append(allowedips, getEgressIPs(&node, &peer)...)
+				relayPeer := wgtypes.PeerConfig{
+					PublicKey: relayHost.PublicKey,
+					Remove:    true,
 				}
 				}
-				if peer.Action != models.NODE_DELETE &&
-					!peer.PendingDelete &&
-					peer.Connected &&
-					nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
-					(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
-					peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
+				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, relayPeer)
+			}
+			continue
+		}
+
+		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
+		var nodePeerMap map[string]models.PeerRouteInfo
+		if node.IsIngressGateway || node.IsEgressGateway {
+			nodePeerMap = make(map[string]models.PeerRouteInfo)
+		}
+		for _, peer := range currentPeers {
+			select {
+			case <-ctx.Done():
+				logger.Log(2, "cancelled peer update for host", host.Name, host.ID.String())
+				return models.HostPeerUpdate{}, fmt.Errorf("peer update cancelled")
+			default:
+				peer := peer
+				if peer.ID.String() == node.ID.String() {
+					logger.Log(2, "peer update, skipping self")
+					//skip yourself
+					continue
 				}
 				}
 
 
+				peerHost, err := GetHost(peer.HostID.String())
+				if err != nil {
+					logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+					return models.HostPeerUpdate{}, err
+				}
+				peerConfig := wgtypes.PeerConfig{
+					PublicKey:                   peerHost.PublicKey,
+					PersistentKeepaliveInterval: &peer.PersistentKeepalive,
+					ReplaceAllowedIPs:           true,
+				}
 				if node.IsIngressGateway || node.IsEgressGateway {
 				if node.IsIngressGateway || node.IsEgressGateway {
 					if peer.IsIngressGateway {
 					if peer.IsIngressGateway {
 						_, extPeerIDAndAddrs, err := getExtPeers(&peer)
 						_, extPeerIDAndAddrs, err := getExtPeers(&peer)
@@ -279,6 +249,47 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 						ID:      peer.ID.String(),
 						ID:      peer.ID.String(),
 					}
 					}
 				}
 				}
+				if (node.IsRelayed && node.RelayedBy != peer.ID.String()) || (peer.IsRelayed && peer.RelayedBy != node.ID.String()) {
+					// if node is relayed and peer is not the relay, set remove to true
+					if _, ok := hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()]; ok {
+						continue
+					}
+					peerConfig.Remove = true
+					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
+					peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
+					continue
+				}
+
+				uselocal := false
+				if host.EndpointIP.String() == peerHost.EndpointIP.String() {
+					// peer is on same network
+					// set to localaddress
+					uselocal = true
+					if node.LocalAddress.IP == nil {
+						// use public endpint
+						uselocal = false
+					}
+					if node.LocalAddress.String() == peer.LocalAddress.String() {
+						uselocal = false
+					}
+				}
+				peerConfig.Endpoint = &net.UDPAddr{
+					IP:   peerHost.EndpointIP,
+					Port: getPeerWgListenPort(peerHost),
+				}
+
+				if uselocal {
+					peerConfig.Endpoint.IP = peer.LocalAddress.IP
+					peerConfig.Endpoint.Port = peerHost.ListenPort
+				}
+				allowedips := GetAllowedIPs(&node, &peer, nil)
+				if peer.Action != models.NODE_DELETE &&
+					!peer.PendingDelete &&
+					peer.Connected &&
+					nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+					(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
+					peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
+				}
 
 
 				peerProxyPort := GetProxyListenPort(peerHost)
 				peerProxyPort := GetProxyListenPort(peerHost)
 				var nodePeer wgtypes.PeerConfig
 				var nodePeer wgtypes.PeerConfig
@@ -300,8 +311,9 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 					nodePeer = peerConfig
 					nodePeer = peerConfig
 				} else {
 				} else {
 					peerAllowedIPs := hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs
 					peerAllowedIPs := hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs
-					peerAllowedIPs = append(peerAllowedIPs, allowedips...)
+					peerAllowedIPs = append(peerAllowedIPs, peerConfig.AllowedIPs...)
 					hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs = peerAllowedIPs
 					hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs = peerAllowedIPs
+					hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].Remove = false
 					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
 					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
 						ID:              peer.ID.String(),
 						ID:              peer.ID.String(),
 						Address:         peer.PrimaryAddress(),
 						Address:         peer.PrimaryAddress(),
@@ -624,14 +636,15 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet
 			}
 			}
 		}
 		}
 	}
 	}
+	if node.IsRelayed && node.RelayedBy == peer.ID.String() {
+		allowedips = append(allowedips, getAllowedIpsForRelayed(node, peer)...)
+
+	}
 	return allowedips
 	return allowedips
 }
 }
 
 
-func getEgressIPs(node, peer *models.Node) []net.IPNet {
-	host, err := GetHost(node.HostID.String())
-	if err != nil {
-		logger.Log(0, "error retrieving host for node", node.ID.String(), err.Error())
-	}
+func getEgressIPs(peer *models.Node) []net.IPNet {
+
 	peerHost, err := GetHost(peer.HostID.String())
 	peerHost, err := GetHost(peer.HostID.String())
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "error retrieving host for peer", peer.ID.String(), err.Error())
 		logger.Log(0, "error retrieving host for peer", peer.ID.String(), err.Error())
@@ -651,12 +664,12 @@ func getEgressIPs(node, peer *models.Node) []net.IPNet {
 		}
 		}
 		// getting the public ip of node
 		// getting the public ip of node
 		if ipnet.Contains(peerHost.EndpointIP) && !internetGateway { // ensuring egress gateway range does not contain endpoint of node
 		if ipnet.Contains(peerHost.EndpointIP) && !internetGateway { // ensuring egress gateway range does not contain endpoint of node
-			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", host.EndpointIP.String(), ", omitting")
+			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", peerHost.EndpointIP.String(), ", omitting")
 			continue // skip adding egress range if overlaps with node's ip
 			continue // skip adding egress range if overlaps with node's ip
 		}
 		}
 		// TODO: Could put in a lot of great logic to avoid conflicts / bad routes
 		// TODO: Could put in a lot of great logic to avoid conflicts / bad routes
-		if ipnet.Contains(node.LocalAddress.IP) && !internetGateway { // ensuring egress gateway range does not contain public ip of node
-			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", node.LocalAddress.String(), ", omitting")
+		if ipnet.Contains(peer.LocalAddress.IP) && !internetGateway { // ensuring egress gateway range does not contain public ip of node
+			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", peer.LocalAddress.String(), ", omitting")
 			continue // skip adding egress range if overlaps with node's local ip
 			continue // skip adding egress range if overlaps with node's local ip
 		}
 		}
 		if err != nil {
 		if err != nil {
@@ -687,12 +700,50 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 	// handle egress gateway peers
 	// handle egress gateway peers
 	if peer.IsEgressGateway {
 	if peer.IsEgressGateway {
 		//hasGateway = true
 		//hasGateway = true
-		egressIPs := getEgressIPs(node, peer)
+		egressIPs := getEgressIPs(peer)
 		allowedips = append(allowedips, egressIPs...)
 		allowedips = append(allowedips, egressIPs...)
 	}
 	}
+	if peer.IsRelay {
+		for _, relayedNodeID := range peer.RelayedNodes {
+			if node.ID.String() == relayedNodeID {
+				continue
+			}
+			relayedNode, err := GetNodeByID(relayedNodeID)
+			if err != nil {
+				continue
+			}
+			allowed := getRelayedAddresses(relayedNodeID)
+			if relayedNode.IsEgressGateway {
+				allowed = append(allowed, getEgressIPs(&relayedNode)...)
+			}
+			allowedips = append(allowedips, allowed...)
+		}
+	}
 	return allowedips
 	return allowedips
 }
 }
 
 
+// getAllowedIpsForRelayed - returns the peerConfig for a node relayed by relay
+func getAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNet) {
+	if relayed.RelayedBy != relay.ID.String() {
+		logger.Log(0, "RelayedByRelay called with invalid parameters")
+		return
+	}
+	peers, err := GetNetworkNodes(relay.Network)
+	if err != nil {
+		logger.Log(0, "error getting network clients", err.Error())
+		return
+	}
+	for _, peer := range peers {
+		if peer.ID == relayed.ID || peer.ID == relay.ID {
+			continue
+		}
+		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
+			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
+		}
+	}
+	return
+}
+
 func getCIDRMaskFromAddr(addr string) net.IPMask {
 func getCIDRMaskFromAddr(addr string) net.IPMask {
 	cidr := net.CIDRMask(32, 32)
 	cidr := net.CIDRMask(32, 32)
 	ipAddr, err := netip.ParseAddr(addr)
 	ipAddr, err := netip.ParseAddr(addr)

+ 89 - 145
logic/relay.go

@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"time"
+	"net"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -31,8 +31,7 @@ func CreateRelay(relay models.RelayRequest) ([]models.Node, models.Node, error)
 		return returnnodes, models.Node{}, err
 		return returnnodes, models.Node{}, err
 	}
 	}
 	node.IsRelay = true
 	node.IsRelay = true
-	node.RelayAddrs = relay.RelayAddrs
-
+	node.RelayedNodes = relay.RelayedNodes
 	node.SetLastModified()
 	node.SetLastModified()
 	nodeData, err := json.Marshal(&node)
 	nodeData, err := json.Marshal(&node)
 	if err != nil {
 	if err != nil {
@@ -41,144 +40,98 @@ func CreateRelay(relay models.RelayRequest) ([]models.Node, models.Node, error)
 	if err = database.Insert(node.ID.String(), string(nodeData), database.NODES_TABLE_NAME); err != nil {
 	if err = database.Insert(node.ID.String(), string(nodeData), database.NODES_TABLE_NAME); err != nil {
 		return returnnodes, models.Node{}, err
 		return returnnodes, models.Node{}, err
 	}
 	}
-	returnnodes, err = SetRelayedNodes(true, node.Network, node.RelayAddrs)
-	if err != nil {
-		return returnnodes, node, err
-	}
-	return returnnodes, node, nil
-}
-
-// CreateHostRelay - creates a host relay
-func CreateHostRelay(relay models.HostRelayRequest) (relayHost *models.Host, relayedHosts []models.Host, err error) {
-
-	relayHost, err = GetHost(relay.HostID)
-	if err != nil {
-		return
-	}
-	err = validateHostRelay(relay)
-	if err != nil {
-		return
-	}
-	relayHost.IsRelay = true
-	relayHost.ProxyEnabled = true
-	relayHost.RelayedHosts = relay.RelayedHosts
-	err = UpsertHost(relayHost)
-	if err != nil {
-		return
-	}
-	relayedHosts = SetRelayedHosts(true, relay.HostID, relay.RelayedHosts)
-	return
-}
-
-// SetRelayedHosts - updates the relayed hosts status
-func SetRelayedHosts(setRelayed bool, relayHostID string, relayedHostIDs []string) []models.Host {
-	var relayedHosts []models.Host
-	for _, relayedHostID := range relayedHostIDs {
-		host, err := GetHost(relayedHostID)
-		if err == nil {
-			if setRelayed {
-				host.IsRelayed = true
-				host.RelayedBy = relayHostID
-				host.ProxyEnabled = true
-			} else {
-				host.IsRelayed = false
-				host.RelayedBy = ""
-			}
-			err = UpsertHost(host)
-			if err == nil {
-				relayedHosts = append(relayedHosts, *host)
-			}
+	returnnodes = SetRelayedNodes(true, relay.NodeID, relay.RelayedNodes)
+	for _, relayedNode := range returnnodes {
+		data, err := json.Marshal(&relayedNode)
+		if err != nil {
+			logger.Log(0, "marshalling relayed node", err.Error())
+			continue
+		}
+		if err := database.Insert(relayedNode.ID.String(), string(data), database.NODES_TABLE_NAME); err != nil {
+			logger.Log(0, "inserting relayed node", err.Error())
+			continue
 		}
 		}
 	}
 	}
-	return relayedHosts
+	return returnnodes, node, nil
 }
 }
 
 
-// SetRelayedNodes- set relayed nodes
-func SetRelayedNodes(setRelayed bool, networkName string, addrs []string) ([]models.Node, error) {
+// SetRelayedNodes- sets and saves node as relayed
+func SetRelayedNodes(setRelayed bool, relay string, relayed []string) []models.Node {
 	var returnnodes []models.Node
 	var returnnodes []models.Node
-	networkNodes, err := GetNetworkNodes(networkName)
-	if err != nil {
-		return returnnodes, err
-	}
-	for _, node := range networkNodes {
-		for _, addr := range addrs {
-			if addr == node.Address.IP.String() || addr == node.Address6.IP.String() {
-				if setRelayed {
-					node.IsRelayed = true
-				} else {
-					node.IsRelayed = false
-				}
-				data, err := json.Marshal(&node)
-				if err != nil {
-					return returnnodes, err
-				}
-				database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
-				returnnodes = append(returnnodes, node)
-			}
+	for _, id := range relayed {
+		node, err := GetNodeByID(id)
+		if err != nil {
+			logger.Log(0, "setRelayedNodes.GetNodebyID", err.Error())
+			continue
 		}
 		}
-	}
-	return returnnodes, nil
-}
-func GetRelayedNodes(relayNode *models.Node) ([]models.Node, error) {
-	var returnnodes []models.Node
-	networkNodes, err := GetNetworkNodes(relayNode.Network)
-	if err != nil {
-		return returnnodes, err
-	}
-	for _, node := range networkNodes {
-		for _, addr := range relayNode.RelayAddrs {
-			if addr == node.Address.IP.String() || addr == node.Address6.IP.String() {
-				returnnodes = append(returnnodes, node)
-			}
+		node.IsRelayed = setRelayed
+		if node.IsRelayed {
+			node.RelayedBy = relay
+		} else {
+			node.RelayedBy = ""
 		}
 		}
-	}
-	return returnnodes, nil
-}
-
-// GetRelayedHosts - gets the relayed hosts of a relay host
-func GetRelayedHosts(relayHost *models.Host) []models.Host {
-	relayedHosts := []models.Host{}
-
-	for _, hostID := range relayHost.RelayedHosts {
-		relayedHost, err := GetHost(hostID)
-		if err == nil {
-			relayedHosts = append(relayedHosts, *relayedHost)
+		node.SetLastModified()
+		data, err := json.Marshal(&node)
+		if err != nil {
+			logger.Log(0, "setRelayedNodes.Marshal", err.Error())
+			continue
 		}
 		}
+		if err := database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME); err != nil {
+			logger.Log(0, "setRelayedNodes.Insert", err.Error())
+			continue
+		}
+		returnnodes = append(returnnodes, node)
 	}
 	}
-	return relayedHosts
+	return returnnodes
 }
 }
 
 
+//func GetRelayedNodes(relayNode *models.Node) (models.Node, error) {
+//	var returnnodes []models.Node
+//	networkNodes, err := GetNetworkNodes(relayNode.Network)
+//	if err != nil {
+//		return returnnodes, err
+//	}
+//	for _, node := range networkNodes {
+//		for _, addr := range relayNode.RelayAddrs {
+//			if addr == node.Address.IP.String() || addr == node.Address6.IP.String() {
+//				returnnodes = append(returnnodes, node)
+//			}
+//		}
+//	}
+//	return returnnodes, nil
+//}
+
 // ValidateRelay - checks if relay is valid
 // ValidateRelay - checks if relay is valid
 func ValidateRelay(relay models.RelayRequest) error {
 func ValidateRelay(relay models.RelayRequest) error {
 	var err error
 	var err error
 	//isIp := functions.IsIpCIDR(gateway.RangeString)
 	//isIp := functions.IsIpCIDR(gateway.RangeString)
-	empty := len(relay.RelayAddrs) == 0
+	empty := len(relay.RelayedNodes) == 0
 	if empty {
 	if empty {
-		err = errors.New("IP Ranges Cannot Be Empty")
-	}
-	return err
-}
-
-func validateHostRelay(relay models.HostRelayRequest) error {
-	if len(relay.RelayedHosts) == 0 {
-		return errors.New("relayed hosts are empty")
+		return errors.New("IP Ranges Cannot Be Empty")
 	}
 	}
-	return nil
-}
-
-// UpdateRelay - updates a relay
-func UpdateRelay(network string, oldAddrs []string, newAddrs []string) []models.Node {
-	var returnnodes []models.Node
-	time.Sleep(time.Second / 4)
-	_, err := SetRelayedNodes(false, network, oldAddrs)
+	node, err := GetNodeByID(relay.NodeID)
 	if err != nil {
 	if err != nil {
-		logger.Log(1, err.Error())
+		return err
 	}
 	}
-	returnnodes, err = SetRelayedNodes(true, network, newAddrs)
-	if err != nil {
-		logger.Log(1, err.Error())
+	if node.IsRelay {
+		return errors.New("node is already acting as a relay")
 	}
 	}
-	return returnnodes
+	for _, relayedNodeID := range relay.RelayedNodes {
+		relayedNode, err := GetNodeByID(relayedNodeID)
+		if err != nil {
+			return err
+		}
+		if relayedNode.IsIngressGateway {
+			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
+		}
+	}
+	return err
+}
+
+// UpdateRelayed - updates relay nodes
+func UpdateRelayed(relay string, oldNodes []string, newNodes []string) []models.Node {
+	_ = SetRelayedNodes(false, relay, oldNodes)
+	return SetRelayedNodes(true, relay, newNodes)
 }
 }
 
 
 // DeleteRelay - deletes a relay
 // DeleteRelay - deletes a relay
@@ -188,15 +141,10 @@ func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
 	if err != nil {
 	if err != nil {
 		return returnnodes, models.Node{}, err
 		return returnnodes, models.Node{}, err
 	}
 	}
-	returnnodes, err = SetRelayedNodes(false, node.Network, node.RelayAddrs)
-	if err != nil {
-		return returnnodes, node, err
-	}
-
+	returnnodes = SetRelayedNodes(false, nodeid, node.RelayedNodes)
 	node.IsRelay = false
 	node.IsRelay = false
-	node.RelayAddrs = []string{}
+	node.RelayedNodes = []string{}
 	node.SetLastModified()
 	node.SetLastModified()
-
 	data, err := json.Marshal(&node)
 	data, err := json.Marshal(&node)
 	if err != nil {
 	if err != nil {
 		return returnnodes, models.Node{}, err
 		return returnnodes, models.Node{}, err
@@ -207,24 +155,20 @@ func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
 	return returnnodes, node, nil
 	return returnnodes, node, nil
 }
 }
 
 
-// DeleteHostRelay - removes host as relay
-func DeleteHostRelay(relayHostID string) (relayHost *models.Host, relayedHosts []models.Host, err error) {
-	relayHost, err = GetHost(relayHostID)
+func getRelayedAddresses(id string) []net.IPNet {
+	addrs := []net.IPNet{}
+	node, err := GetNodeByID(id)
 	if err != nil {
 	if err != nil {
-		return
+		logger.Log(0, "getRelayedAddresses: "+err.Error())
+		return addrs
 	}
 	}
-	relayedHosts = SetRelayedHosts(false, relayHostID, relayHost.RelayedHosts)
-	relayHost.IsRelay = false
-	relayHost.RelayedHosts = []string{}
-	err = UpsertHost(relayHost)
-	if err != nil {
-		return
+	if node.Address.IP != nil {
+		node.Address.Mask = net.CIDRMask(32, 32)
+		addrs = append(addrs, node.Address)
 	}
 	}
-	return
-}
-
-// UpdateHostRelay - updates the relay host with new relayed hosts
-func UpdateHostRelay(relayHostID string, oldRelayedHosts, newRelayedHosts []string) {
-	_ = SetRelayedHosts(false, relayHostID, oldRelayedHosts)
-	_ = SetRelayedHosts(true, relayHostID, newRelayedHosts)
+	if node.Address6.IP != nil {
+		node.Address.Mask = net.CIDRMask(128, 128)
+		addrs = append(addrs, node.Address6)
+	}
+	return addrs
 }
 }

+ 11 - 2
logic/serverconf.go

@@ -4,17 +4,18 @@ import (
 	"encoding/json"
 	"encoding/json"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 var (
 var (
-	// Node_Limit - dummy var for community
-	Node_Limit = 1000000000
 	// Networks_Limit - dummy var for community
 	// Networks_Limit - dummy var for community
 	Networks_Limit = 1000000000
 	Networks_Limit = 1000000000
 	// Users_Limit - dummy var for community
 	// Users_Limit - dummy var for community
 	Users_Limit = 1000000000
 	Users_Limit = 1000000000
 	// Clients_Limit - dummy var for community
 	// Clients_Limit - dummy var for community
 	Clients_Limit = 1000000000
 	Clients_Limit = 1000000000
+	// Hosts_Limit - dummy var for community
+	Hosts_Limit = 1000000000
 	// Free_Tier - specifies if free tier
 	// Free_Tier - specifies if free tier
 	Free_Tier = false
 	Free_Tier = false
 )
 )
@@ -85,3 +86,11 @@ func StoreJWTSecret(privateKey string) error {
 	}
 	}
 	return database.Insert("nm-jwt-secret", string(data), database.SERVERCONF_TABLE_NAME)
 	return database.Insert("nm-jwt-secret", string(data), database.SERVERCONF_TABLE_NAME)
 }
 }
+
+func SetFreeTierLimits() {
+	Free_Tier = true
+	Users_Limit = servercfg.GetUserLimit()
+	Clients_Limit = servercfg.GetClientLimit()
+	Networks_Limit = servercfg.GetNetworkLimit()
+	Hosts_Limit = servercfg.GetHostLimit()
+}

+ 3 - 0
logic/telemetry.go

@@ -60,6 +60,7 @@ func sendTelemetry() error {
 		Event:      "daily checkin",
 		Event:      "daily checkin",
 		Properties: posthog.NewProperties().
 		Properties: posthog.NewProperties().
 			Set("nodes", d.Nodes).
 			Set("nodes", d.Nodes).
+			Set("hosts", d.Hosts).
 			Set("servers", d.Servers).
 			Set("servers", d.Servers).
 			Set("non-server nodes", d.Count.NonServer).
 			Set("non-server nodes", d.Count.NonServer).
 			Set("extclients", d.ExtClients).
 			Set("extclients", d.ExtClients).
@@ -84,6 +85,7 @@ func fetchTelemetryData() (telemetryData, error) {
 	data.ExtClients = getDBLength(database.EXT_CLIENT_TABLE_NAME)
 	data.ExtClients = getDBLength(database.EXT_CLIENT_TABLE_NAME)
 	data.Users = getDBLength(database.USERS_TABLE_NAME)
 	data.Users = getDBLength(database.USERS_TABLE_NAME)
 	data.Networks = getDBLength(database.NETWORKS_TABLE_NAME)
 	data.Networks = getDBLength(database.NETWORKS_TABLE_NAME)
+	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
 	data.Version = servercfg.GetVersion()
 	data.Servers = getServerCount()
 	data.Servers = getServerCount()
 	nodes, err := GetAllNodes()
 	nodes, err := GetAllNodes()
@@ -167,6 +169,7 @@ func getDBLength(dbname string) int {
 // telemetryData - What data to send to posthog
 // telemetryData - What data to send to posthog
 type telemetryData struct {
 type telemetryData struct {
 	Nodes      int
 	Nodes      int
+	Hosts      int
 	ExtClients int
 	ExtClients int
 	Users      int
 	Users      int
 	Count      clientCount
 	Count      clientCount

+ 36 - 0
logic/timer.go

@@ -1,10 +1,13 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
+	"sync"
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
 )
 )
 
 
 // == Constants ==
 // == Constants ==
@@ -12,6 +15,9 @@ import (
 // How long to wait before sending telemetry to server (24 hours)
 // How long to wait before sending telemetry to server (24 hours)
 const timer_hours_between_runs = 24
 const timer_hours_between_runs = 24
 
 
+// HookManagerCh - channel to add any new hooks
+var HookManagerCh = make(chan models.HookDetails, 2)
+
 // == Public ==
 // == Public ==
 
 
 // TimerCheckpoint - Checks if 24 hours has passed since telemetry was last sent. If so, sends telemetry data to posthog
 // TimerCheckpoint - Checks if 24 hours has passed since telemetry was last sent. If so, sends telemetry data to posthog
@@ -40,6 +46,36 @@ func AddHook(ifaceToAdd interface{}) {
 	timeHooks = append(timeHooks, ifaceToAdd)
 	timeHooks = append(timeHooks, ifaceToAdd)
 }
 }
 
 
+// StartHookManager - listens on `HookManagerCh` to run any hook
+func StartHookManager(ctx context.Context, wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		select {
+		case <-ctx.Done():
+			logger.Log(0, "## Stopping Hook Manager")
+			return
+		case newhook := <-HookManagerCh:
+			wg.Add(1)
+			go addHookWithInterval(ctx, wg, newhook.Hook, newhook.Interval)
+		}
+	}
+}
+
+func addHookWithInterval(ctx context.Context, wg *sync.WaitGroup, hook func() error, interval time.Duration) {
+	defer wg.Done()
+	ticker := time.NewTicker(interval)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-ticker.C:
+			hook()
+		}
+	}
+
+}
+
 // == private ==
 // == private ==
 
 
 // timeHooks - functions to run once a day, functions must take no parameters
 // timeHooks - functions to run once a day, functions must take no parameters

+ 24 - 0
logic/users.go

@@ -26,6 +26,30 @@ func GetUser(username string) (*models.User, error) {
 	return &user, err
 	return &user, err
 }
 }
 
 
+// GetReturnUser - gets a user
+func GetReturnUser(username string) (models.ReturnUser, error) {
+
+	var user models.ReturnUser
+	record, err := database.FetchRecord(database.USERS_TABLE_NAME, username)
+	if err != nil {
+		return user, err
+	}
+	if err = json.Unmarshal([]byte(record), &user); err != nil {
+		return models.ReturnUser{}, err
+	}
+	return user, err
+}
+
+// ToReturnUser - gets a user as a return user
+func ToReturnUser(user models.User) models.ReturnUser {
+	return models.ReturnUser{
+		UserName: user.UserName,
+		Networks: user.Networks,
+		IsAdmin:  user.IsAdmin,
+		Groups:   user.Groups,
+	}
+}
+
 // GetGroupUsers - gets users in a group
 // GetGroupUsers - gets users in a group
 func GetGroupUsers(group string) ([]models.ReturnUser, error) {
 func GetGroupUsers(group string) ([]models.ReturnUser, error) {
 	var returnUsers []models.ReturnUser
 	var returnUsers []models.ReturnUser

+ 3 - 3
logic/wireguard.go

@@ -29,11 +29,11 @@ func IfaceDelta(currentNode *models.Node, newNode *models.Node) bool {
 		}
 		}
 	}
 	}
 	if newNode.IsRelay {
 	if newNode.IsRelay {
-		if len(currentNode.RelayAddrs) != len(newNode.RelayAddrs) {
+		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 			return true
 			return true
 		}
 		}
-		for _, address := range newNode.RelayAddrs {
-			if !StringSliceContains(currentNode.RelayAddrs, address) {
+		for _, node := range newNode.RelayedNodes {
+			if !StringSliceContains(currentNode.RelayedNodes, node) {
 				return true
 				return true
 			}
 			}
 		}
 		}

+ 6 - 2
main.go

@@ -29,7 +29,7 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v0.20.2"
+var version = "v0.20.3"
 
 
 // Start DB Connection and start API Request Handler
 // Start DB Connection and start API Request Handler
 func main() {
 func main() {
@@ -41,6 +41,9 @@ func main() {
 	initialize()                       // initial db and acls
 	initialize()                       // initial db and acls
 	setGarbageCollection()
 	setGarbageCollection()
 	setVerbosity()
 	setVerbosity()
+	if servercfg.DeployedByOperator() && !servercfg.Is_EE {
+		logic.SetFreeTierLimits()
+	}
 	defer database.CloseDB()
 	defer database.CloseDB()
 	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
 	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
 	defer stop()
 	defer stop()
@@ -88,7 +91,6 @@ func initialize() { // Client Mode Prereq Check
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "Timer error occurred: ", err.Error())
 		logger.Log(1, "Timer error occurred: ", err.Error())
 	}
 	}
-
 	logic.EnterpriseCheck()
 	logic.EnterpriseCheck()
 
 
 	var authProvider = auth.InitializeAuthProvider()
 	var authProvider = auth.InitializeAuthProvider()
@@ -146,6 +148,8 @@ func startControllers(wg *sync.WaitGroup, ctx context.Context) {
 		logger.Log(0, "No Server Mode selected, so nothing is being served! Set Rest mode (REST_BACKEND) or MessageQueue (MESSAGEQUEUE_BACKEND) to 'true'.")
 		logger.Log(0, "No Server Mode selected, so nothing is being served! Set Rest mode (REST_BACKEND) or MessageQueue (MESSAGEQUEUE_BACKEND) to 'true'.")
 	}
 	}
 
 
+	wg.Add(1)
+	go logic.StartHookManager(ctx, wg)
 }
 }
 
 
 // Should we be using a context vice a waitgroup????????????
 // Should we be using a context vice a waitgroup????????????

+ 2 - 8
models/api_host.go

@@ -34,6 +34,7 @@ type ApiHost struct {
 	RelayedBy          string   `json:"relayed_by" bson:"relayed_by" yaml:"relayed_by"`
 	RelayedBy          string   `json:"relayed_by" bson:"relayed_by" yaml:"relayed_by"`
 	IsRelay            bool     `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
 	IsRelay            bool     `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
 	RelayedHosts       []string `json:"relay_hosts" bson:"relay_hosts" yaml:"relay_hosts"`
 	RelayedHosts       []string `json:"relay_hosts" bson:"relay_hosts" yaml:"relay_hosts"`
+	NatType            string   `json:"nat_type" yaml:"nat_type"`
 }
 }
 
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -67,10 +68,7 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.Verbosity = h.Verbosity
 	a.Verbosity = h.Verbosity
 	a.Version = h.Version
 	a.Version = h.Version
 	a.IsDefault = h.IsDefault
 	a.IsDefault = h.IsDefault
-	a.IsRelay = h.IsRelay
-	a.RelayedHosts = h.RelayedHosts
-	a.IsRelayed = h.IsRelayed
-	a.RelayedBy = h.RelayedBy
+	a.NatType = h.NatType
 	return &a
 	return &a
 }
 }
 
 
@@ -108,10 +106,6 @@ func (a *ApiHost) ConvertAPIHostToNMHost(currentHost *Host) *Host {
 	h.Nodes = currentHost.Nodes
 	h.Nodes = currentHost.Nodes
 	h.TrafficKeyPublic = currentHost.TrafficKeyPublic
 	h.TrafficKeyPublic = currentHost.TrafficKeyPublic
 	h.OS = currentHost.OS
 	h.OS = currentHost.OS
-	h.RelayedBy = a.RelayedBy
-	h.RelayedHosts = a.RelayedHosts
-	h.IsRelay = a.IsRelay
-	h.IsRelayed = a.IsRelayed
 	h.ProxyEnabled = a.ProxyEnabled
 	h.ProxyEnabled = a.ProxyEnabled
 	h.IsDefault = a.IsDefault
 	h.IsDefault = a.IsDefault
 	h.NatType = currentHost.NatType
 	h.NatType = currentHost.NatType

+ 7 - 3
models/api_node.go

@@ -25,11 +25,12 @@ type ApiNode struct {
 	NetworkRange6           string   `json:"networkrange6"`
 	NetworkRange6           string   `json:"networkrange6"`
 	IsRelayed               bool     `json:"isrelayed"`
 	IsRelayed               bool     `json:"isrelayed"`
 	IsRelay                 bool     `json:"isrelay"`
 	IsRelay                 bool     `json:"isrelay"`
+	RelayedBy               string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
+	RelayedNodes            []string `json:"relaynodes" yaml:"relayedNodes"`
 	IsEgressGateway         bool     `json:"isegressgateway"`
 	IsEgressGateway         bool     `json:"isegressgateway"`
 	IsIngressGateway        bool     `json:"isingressgateway"`
 	IsIngressGateway        bool     `json:"isingressgateway"`
 	EgressGatewayRanges     []string `json:"egressgatewayranges"`
 	EgressGatewayRanges     []string `json:"egressgatewayranges"`
 	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
 	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
-	RelayAddrs              []string `json:"relayaddrs"`
 	FailoverNode            string   `json:"failovernode"`
 	FailoverNode            string   `json:"failovernode"`
 	DNSOn                   bool     `json:"dnson"`
 	DNSOn                   bool     `json:"dnson"`
 	IngressDns              string   `json:"ingressdns"`
 	IngressDns              string   `json:"ingressdns"`
@@ -53,6 +54,8 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	convertedNode.IsRelay = a.IsRelay
 	convertedNode.IsRelay = a.IsRelay
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.IsRelayed = a.IsRelayed
+	convertedNode.RelayedBy = a.RelayedBy
+	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.PendingDelete = a.PendingDelete
 	convertedNode.PendingDelete = a.PendingDelete
 	convertedNode.Failover = a.Failover
 	convertedNode.Failover = a.Failover
 	convertedNode.IsEgressGateway = a.IsEgressGateway
 	convertedNode.IsEgressGateway = a.IsEgressGateway
@@ -66,7 +69,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
 	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
 	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
 	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
 	convertedNode.PersistentKeepalive = time.Second * time.Duration(a.PersistentKeepalive)
 	convertedNode.PersistentKeepalive = time.Second * time.Duration(a.PersistentKeepalive)
-	convertedNode.RelayAddrs = a.RelayAddrs
+	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.DefaultACL = a.DefaultACL
 	convertedNode.OwnerID = currentNode.OwnerID
 	convertedNode.OwnerID = currentNode.OwnerID
 	_, networkRange, err := net.ParseCIDR(a.NetworkRange)
 	_, networkRange, err := net.ParseCIDR(a.NetworkRange)
@@ -140,11 +143,12 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	}
 	}
 	apiNode.IsRelayed = nm.IsRelayed
 	apiNode.IsRelayed = nm.IsRelayed
 	apiNode.IsRelay = nm.IsRelay
 	apiNode.IsRelay = nm.IsRelay
+	apiNode.RelayedBy = nm.RelayedBy
+	apiNode.RelayedNodes = nm.RelayedNodes
 	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
 	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
-	apiNode.RelayAddrs = nm.RelayAddrs
 	apiNode.FailoverNode = nm.FailoverNode.String()
 	apiNode.FailoverNode = nm.FailoverNode.String()
 	if isUUIDSet(apiNode.FailoverNode) {
 	if isUUIDSet(apiNode.FailoverNode) {
 		apiNode.FailoverNode = ""
 		apiNode.FailoverNode = ""

+ 14 - 12
models/mqtt.go

@@ -8,18 +8,20 @@ import (
 
 
 // HostPeerUpdate - struct for host peer updates
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 type HostPeerUpdate struct {
-	Host            Host                 `json:"host" bson:"host" yaml:"host"`
-	Server          string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers           []wgtypes.PeerConfig
-	HostPeerIDs     HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
-	ProxyUpdate     ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
-	EgressInfo      map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
-	IngressInfo     IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
-	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
+	Host              Host                 `json:"host" bson:"host" yaml:"host"`
+	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
+	Server            string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers             []wgtypes.PeerConfig
+	HostPeerIDs       HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
+	ProxyUpdate       ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
+	EgressInfo        map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
+	IngressInfo       IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
+	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	EndpointDetection bool                  `json:"endpointdetection" yaml:"endpointdetection"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
 }
 }
 
 
 // IngressInfo - struct for ingress info
 // IngressInfo - struct for ingress info

+ 17 - 8
models/node.go

@@ -69,6 +69,10 @@ type CommonNode struct {
 	IsEgressGateway     bool          `json:"isegressgateway" yaml:"isegressgateway"`
 	IsEgressGateway     bool          `json:"isegressgateway" yaml:"isegressgateway"`
 	EgressGatewayRanges []string      `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
 	EgressGatewayRanges []string      `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
 	IsIngressGateway    bool          `json:"isingressgateway" yaml:"isingressgateway"`
 	IsIngressGateway    bool          `json:"isingressgateway" yaml:"isingressgateway"`
+	IsRelayed           bool          `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
+	RelayedBy           string        `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
+	IsRelay             bool          `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
+	RelayedNodes        []string      `json:"relaynodes" yaml:"relayedNodes"`
 	IngressDNS          string        `json:"ingressdns" yaml:"ingressdns"`
 	IngressDNS          string        `json:"ingressdns" yaml:"ingressdns"`
 	DNSOn               bool          `json:"dnson" yaml:"dnson"`
 	DNSOn               bool          `json:"dnson" yaml:"dnson"`
 	PersistentKeepalive time.Duration `json:"persistentkeepalive" yaml:"persistentkeepalive"`
 	PersistentKeepalive time.Duration `json:"persistentkeepalive" yaml:"persistentkeepalive"`
@@ -86,9 +90,6 @@ type Node struct {
 	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
 	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
 	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
 	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
 	IngressGatewayRange6    string               `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"`
 	IngressGatewayRange6    string               `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"`
-	IsRelayed               bool                 `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
-	IsRelay                 bool                 `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
-	RelayAddrs              []string             `json:"relayaddrs" bson:"relayaddrs" yaml:"relayaddrs"`
 	// == PRO ==
 	// == PRO ==
 	DefaultACL   string    `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	DefaultACL   string    `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	OwnerID      string    `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"`
 	OwnerID      string    `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"`
@@ -180,6 +181,14 @@ func isLess(ipA string, ipB string) bool {
 	return bytes.Compare(ipNetA, ipNetB) < 0
 	return bytes.Compare(ipNetA, ipNetB) < 0
 }
 }
 
 
+// Node.PrimaryAddress - return ipv4 address if present, else return ipv6
+func (node *Node) PrimaryAddressIPNet() net.IPNet {
+	if node.Address.IP != nil {
+		return node.Address
+	}
+	return node.Address6
+}
+
 // Node.PrimaryAddress - return ipv4 address if present, else return ipv6
 // Node.PrimaryAddress - return ipv4 address if present, else return ipv6
 func (node *Node) PrimaryAddress() string {
 func (node *Node) PrimaryAddress() string {
 	if node.Address.IP != nil {
 	if node.Address.IP != nil {
@@ -350,7 +359,7 @@ func (node *LegacyNode) SetDefaultFailover() {
 }
 }
 
 
 // Node.Fill - fills other node data into calling node data if not set on calling node
 // Node.Fill - fills other node data into calling node data if not set on calling node
-func (newNode *Node) Fill(currentNode *Node) { // TODO add new field for nftables present
+func (newNode *Node) Fill(currentNode *Node, isEE bool) { // TODO add new field for nftables present
 	newNode.ID = currentNode.ID
 	newNode.ID = currentNode.ID
 	newNode.HostID = currentNode.HostID
 	newNode.HostID = currentNode.HostID
 	// Revisit the logic for boolean values
 	// Revisit the logic for boolean values
@@ -401,13 +410,13 @@ func (newNode *Node) Fill(currentNode *Node) { // TODO add new field for nftable
 	if newNode.Action == "" {
 	if newNode.Action == "" {
 		newNode.Action = currentNode.Action
 		newNode.Action = currentNode.Action
 	}
 	}
-	if newNode.RelayAddrs == nil {
-		newNode.RelayAddrs = currentNode.RelayAddrs
+	if newNode.RelayedNodes == nil {
+		newNode.RelayedNodes = currentNode.RelayedNodes
 	}
 	}
-	if newNode.IsRelay != currentNode.IsRelay {
+	if newNode.IsRelay != currentNode.IsRelay && isEE {
 		newNode.IsRelay = currentNode.IsRelay
 		newNode.IsRelay = currentNode.IsRelay
 	}
 	}
-	if newNode.IsRelayed == currentNode.IsRelayed {
+	if newNode.IsRelayed == currentNode.IsRelayed && isEE {
 		newNode.IsRelayed = currentNode.IsRelayed
 		newNode.IsRelayed = currentNode.IsRelayed
 	}
 	}
 	if newNode.Server == "" {
 	if newNode.Server == "" {

+ 19 - 3
models/structs.go

@@ -2,6 +2,7 @@ package models
 
 
 import (
 import (
 	"strings"
 	"strings"
+	"time"
 
 
 	jwt "github.com/golang-jwt/jwt/v4"
 	jwt "github.com/golang-jwt/jwt/v4"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -153,9 +154,9 @@ type EgressGatewayRequest struct {
 
 
 // RelayRequest - relay request struct
 // RelayRequest - relay request struct
 type RelayRequest struct {
 type RelayRequest struct {
-	NodeID     string   `json:"nodeid" bson:"nodeid"`
-	NetID      string   `json:"netid" bson:"netid"`
-	RelayAddrs []string `json:"relayaddrs" bson:"relayaddrs"`
+	NodeID       string   `json:"nodeid"`
+	NetID        string   `json:"netid"`
+	RelayedNodes []string `json:"relayaddrs"`
 }
 }
 
 
 // HostRelayRequest - struct for host relay creation
 // HostRelayRequest - struct for host relay creation
@@ -274,3 +275,18 @@ type StunServer struct {
 	Domain string `json:"domain" yaml:"domain"`
 	Domain string `json:"domain" yaml:"domain"`
 	Port   int    `json:"port" yaml:"port"`
 	Port   int    `json:"port" yaml:"port"`
 }
 }
+
+// HookDetails - struct to hold hook info
+type HookDetails struct {
+	Hook     func() error
+	Interval time.Duration
+}
+
+// LicenseLimits - struct license limits
+type LicenseLimits struct {
+	Servers  int `json:"servers"`
+	Users    int `json:"users"`
+	Hosts    int `json:"hosts"`
+	Clients  int `json:"clients"`
+	Networks int `json:"networks"`
+}

+ 6 - 1
mq/emqx.go

@@ -6,11 +6,14 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"strings"
 	"sync"
 	"sync"
 
 
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
+const already_exists = "ALREADY_EXISTS"
+
 type (
 type (
 	emqxUser struct {
 	emqxUser struct {
 		UserID   string `json:"user_id"`
 		UserID   string `json:"user_id"`
@@ -99,7 +102,9 @@ func CreateEmqxUser(username, password string, admin bool) error {
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		return fmt.Errorf("error creating EMQX user %v", string(msg))
+		if !strings.Contains(string(msg), already_exists) {
+			return fmt.Errorf("error creating EMQX user %v", string(msg))
+		}
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 1 - 1
mq/handlers.go

@@ -240,7 +240,7 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 				}
 				}
 			}
 			}
 		}
 		}
-		slog.Info("updated node metrics", "id", id)
+		slog.Debug("updated node metrics", "id", id)
 	}
 	}
 }
 }
 
 

+ 13 - 11
mq/publishers.go

@@ -88,18 +88,20 @@ func PublishSingleHostPeerUpdate(ctx context.Context, host *models.Host, deleted
 	if len(peerUpdate.Peers) == 0 { // no peers to send
 	if len(peerUpdate.Peers) == 0 { // no peers to send
 		return nil
 		return nil
 	}
 	}
-	proxyUpdate, err := logic.GetProxyUpdateForHost(ctx, host)
-	if err != nil {
-		return err
-	}
-	proxyUpdate.Server = servercfg.GetServer()
-	if host.ProxyEnabled {
-		proxyUpdate.Action = models.ProxyUpdate
-	} else {
-		proxyUpdate.Action = models.NoProxy
-	}
+	if host.OS != models.OS_Types.IoT {
+		proxyUpdate, err := logic.GetProxyUpdateForHost(ctx, host)
+		if err != nil {
+			return err
+		}
+		proxyUpdate.Server = servercfg.GetServer()
+		if host.ProxyEnabled {
+			proxyUpdate.Action = models.ProxyUpdate
+		} else {
+			proxyUpdate.Action = models.NoProxy
+		}
 
 
-	peerUpdate.ProxyUpdate = proxyUpdate
+		peerUpdate.ProxyUpdate = proxyUpdate
+	}
 
 
 	data, err := json.Marshal(&peerUpdate)
 	data, err := json.Marshal(&peerUpdate)
 	if err != nil {
 	if err != nil {

+ 1 - 1
release.md

@@ -1,5 +1,5 @@
 
 
-# Netmaker v0.20.2
+# Netmaker v0.20.3
 
 
 ## whats new
 ## whats new
 - 
 - 

+ 1 - 0
scripts/netmaker.default.env

@@ -19,6 +19,7 @@ NETMAKER_ACCOUNT_ID=
 LICENSE_KEY=
 LICENSE_KEY=
 SERVER_IMAGE_TAG=
 SERVER_IMAGE_TAG=
 UI_IMAGE_TAG=
 UI_IMAGE_TAG=
+NETCLIENT_ENDPOINT_DETECTION="disabled"
 # used for HA - identifies this server vs other servers
 # used for HA - identifies this server vs other servers
 NODE_ID="netmaker-server-1"
 NODE_ID="netmaker-server-1"
 METRICS_EXPORTER="off"
 METRICS_EXPORTER="off"

+ 15 - 8
scripts/nm-quick.sh

@@ -4,7 +4,7 @@ CONFIG_FILE=netmaker.env
 # location of nm-quick.sh (usually `/root`)
 # location of nm-quick.sh (usually `/root`)
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 SCRIPT_DIR=$(dirname "$(realpath "$0")")
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
-NM_QUICK_VERSION="0.1.0"
+NM_QUICK_VERSION="0.1.1"
 LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\")
 LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\")
 
 
 if [ $(id -u) -ne 0 ]; then
 if [ $(id -u) -ne 0 ]; then
@@ -17,11 +17,12 @@ unset BUILD_TYPE
 unset BUILD_TAG
 unset BUILD_TAG
 unset IMAGE_TAG
 unset IMAGE_TAG
 unset AUTO_BUILD
 unset AUTO_BUILD
+unset NETMAKER_BASE_DOMAIN
 
 
 # usage - displays usage instructions
 # usage - displays usage instructions
 usage() {
 usage() {
 	echo "nm-quick.sh v$NM_QUICK_VERSION"
 	echo "nm-quick.sh v$NM_QUICK_VERSION"
-	echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag] [-a auto]"
+	echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag] [-a auto] [-d domain]"
 	echo "  -e      if specified, will install netmaker EE"
 	echo "  -e      if specified, will install netmaker EE"
 	echo "  -b      type of build; options:"
 	echo "  -b      type of build; options:"
 	echo "          \"version\" - will install a specific version of Netmaker using remote git and dockerhub"
 	echo "          \"version\" - will install a specific version of Netmaker using remote git and dockerhub"
@@ -29,14 +30,16 @@ usage() {
 	echo "          \"branch\": - will install a specific branch using remote git and dockerhub"
 	echo "          \"branch\": - will install a specific branch using remote git and dockerhub"
 	echo "  -t      tag of build; if buildtype=version, tag=version. If builtype=branch or builtype=local, tag=branch"
 	echo "  -t      tag of build; if buildtype=version, tag=version. If builtype=branch or builtype=local, tag=branch"
 	echo "  -a      auto-build; skip prompts and use defaults, if none provided"
 	echo "  -a      auto-build; skip prompts and use defaults, if none provided"
+	echo "  -d      domain; if specified, will use this domain instead of auto-generating one"
 	echo "examples:"
 	echo "examples:"
 	echo "          nm-quick.sh -e -b version -t $LATEST"
 	echo "          nm-quick.sh -e -b version -t $LATEST"
 	echo "          nm-quick.sh -e -b local -t feature_v0.17.2_newfeature"
 	echo "          nm-quick.sh -e -b local -t feature_v0.17.2_newfeature"
 	echo "          nm-quick.sh -e -b branch -t develop"
 	echo "          nm-quick.sh -e -b branch -t develop"
+	echo "          nm-quick.sh -e -b version -t $LATEST -a -d example.com"
 	exit 1
 	exit 1
 }
 }
 
 
-while getopts evab:t: flag; do
+while getopts evab:d:t: flag; do
 	case "${flag}" in
 	case "${flag}" in
 	e)
 	e)
 		INSTALL_TYPE="ee"
 		INSTALL_TYPE="ee"
@@ -60,6 +63,9 @@ while getopts evab:t: flag; do
 	t)
 	t)
 		BUILD_TAG=${OPTARG}
 		BUILD_TAG=${OPTARG}
 		;;
 		;;
+	d)
+		NETMAKER_BASE_DOMAIN=${OPTARG}
+		;;
 	esac
 	esac
 done
 done
 
 
@@ -303,9 +309,9 @@ save_config() { (
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "TURN_USERNAME" "TURN_PASSWORD" "MQ_USERNAME" "MQ_PASSWORD"
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "TURN_USERNAME" "TURN_PASSWORD" "MQ_USERNAME" "MQ_PASSWORD"
 		"INSTALL_TYPE" "NODE_ID" "METRICS_EXPORTER" "PROMETHEUS" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"INSTALL_TYPE" "NODE_ID" "METRICS_EXPORTER" "PROMETHEUS" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "STUN_PORT" "VERBOSITY"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "STUN_PORT" "VERBOSITY"
-		"DEFAULT_PROXY_MODE" "TURN_PORT" "USE_TURN" "DEBUG_MODE" "TURN_API_PORT" "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK"
-		"TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET" "FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER"
-		"EXPORTER_API_PORT")
+		"DEFAULT_PROXY_MODE" "TURN_PORT" "USE_TURN" "DEBUG_MODE" "TURN_API_PORT" "REST_BACKEND"
+		"DISABLE_REMOTE_IP_CHECK" "NETCLIENT_ENDPOINT_DETECTION" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT")
 	for name in "${toCopy[@]}"; do
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 		save_config_item $name "${!name}"
 	done
 	done
@@ -490,8 +496,9 @@ set_install_vars() {
 	if [ "$IP_ADDR" = "" ]; then
 	if [ "$IP_ADDR" = "" ]; then
 		IP_ADDR=$(curl -s ifconfig.me)
 		IP_ADDR=$(curl -s ifconfig.me)
 	fi
 	fi
-
-	NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
+	if [ "$NETMAKER_BASE_DOMAIN" = "" ]; then
+		NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
+	fi
 	SERVER_HOST=$IP_ADDR
 	SERVER_HOST=$IP_ADDR
 	if test -z "$MASTER_KEY"; then
 	if test -z "$MASTER_KEY"; then
 		MASTER_KEY=$(
 		MASTER_KEY=$(

+ 1 - 1
scripts/nm-upgrade-0-17-1-to-0-19-0.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 #!/bin/bash
 
 
-LATEST="v0.20.2"
+LATEST="v0.20.3"
 INSTALL_PATH="/root"
 INSTALL_PATH="/root"
 
 
 trap restore_old_netmaker_instructions
 trap restore_old_netmaker_instructions

+ 69 - 0
servercfg/serverconf.go

@@ -10,6 +10,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/config"
 	"github.com/gravitl/netmaker/config"
+
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 )
 )
 
 
@@ -51,6 +52,11 @@ func GetServerConfig() config.ServerConfig {
 	} else {
 	} else {
 		cfg.NetclientAutoUpdate = "disabled"
 		cfg.NetclientAutoUpdate = "disabled"
 	}
 	}
+	if EndpointDetectionEnabled() {
+		cfg.NetclientEndpointDetection = "enabled"
+	} else {
+		cfg.NetclientEndpointDetection = "disabled"
+	}
 	if IsRestBackend() {
 	if IsRestBackend() {
 		cfg.RestBackend = "on"
 		cfg.RestBackend = "on"
 	}
 	}
@@ -432,6 +438,17 @@ func AutoUpdateEnabled() bool {
 	return true
 	return true
 }
 }
 
 
+// EndpointDetectionEnabled returns a boolean indicating whether netclient endpoint detection is enabled or disabled
+// default is enabled
+func EndpointDetectionEnabled() bool {
+	if os.Getenv("NETCLIENT_ENDPOINT_DETECTION") == "disabled" {
+		return false
+	} else if config.Config.Server.NetclientEndpointDetection == "disabled" {
+		return false
+	}
+	return true
+}
+
 // IsDNSMode - should it run with DNS
 // IsDNSMode - should it run with DNS
 func IsDNSMode() bool {
 func IsDNSMode() bool {
 	isdns := true
 	isdns := true
@@ -725,6 +742,58 @@ func IsProxyEnabled() bool {
 	return enabled
 	return enabled
 }
 }
 
 
+// GetNetworkLimit - fetches free tier limits on users
+func GetUserLimit() int {
+	var userslimit int
+	if os.Getenv("USERS_LIMIT") != "" {
+		userslimit, _ = strconv.Atoi(os.Getenv("USERS_LIMIT"))
+	} else {
+		userslimit = config.Config.Server.UsersLimit
+	}
+	return userslimit
+}
+
+// GetNetworkLimit - fetches free tier limits on networks
+func GetNetworkLimit() int {
+	var networkslimit int
+	if os.Getenv("NETWORKS_LIMIT") != "" {
+		networkslimit, _ = strconv.Atoi(os.Getenv("NETWORKS_LIMIT"))
+	} else {
+		networkslimit = config.Config.Server.NetworksLimit
+	}
+	return networkslimit
+}
+
+// GetClientLimit - fetches free tier limits on ext. clients
+func GetClientLimit() int {
+	var clientsLimit int
+	if os.Getenv("CLIENTS_LIMIT") != "" {
+		clientsLimit, _ = strconv.Atoi(os.Getenv("CLIENTS_LIMIT"))
+	} else {
+		clientsLimit = config.Config.Server.ClientsLimit
+	}
+	return clientsLimit
+}
+
+// GetHostLimit - fetches free tier limits on hosts
+func GetHostLimit() int {
+	var hostsLimit int
+	if os.Getenv("HOSTS_LIMIT") != "" {
+		hostsLimit, _ = strconv.Atoi(os.Getenv("HOSTS_LIMIT"))
+	} else {
+		hostsLimit = config.Config.Server.HostsLimit
+	}
+	return hostsLimit
+}
+
+// DeployedByOperator - returns true if the instance is deployed by netmaker operator
+func DeployedByOperator() bool {
+	if os.Getenv("DEPLOYED_BY_OPERATOR") != "" {
+		return os.Getenv("DEPLOYED_BY_OPERATOR") == "true"
+	}
+	return config.Config.Server.DeployedByOperator
+}
+
 // GetDefaultProxyMode - default proxy mode for a server
 // GetDefaultProxyMode - default proxy mode for a server
 func GetDefaultProxyMode() config.ProxyMode {
 func GetDefaultProxyMode() config.ProxyMode {
 	var (
 	var (

+ 1 - 1
swagger.yaml

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