Browse Source

fix merge conflicts

Abhishek Kondur 2 years ago
parent
commit
963730dfc7
63 changed files with 1203 additions and 940 deletions
  1. 3 1
      .dockerignore
  2. 3 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  3. 1 1
      .github/workflows/branchtest.yml
  4. 3 21
      .github/workflows/deletedroplets.yml
  5. 1 1
      Dockerfile
  6. 1 1
      Dockerfile-quick
  7. 3 4
      README.md
  8. 18 2
      cli/cmd/ext_client/create.go
  9. 8 27
      cli/cmd/ext_client/update.go
  10. 0 2
      cli/cmd/network/create.go
  11. 0 1
      cli/cmd/network/flags.go
  12. 3 9
      cli/functions/ext_client.go
  13. 1 0
      cli/functions/http_client.go
  14. 4 73
      compose/docker-compose-emqx.yml
  15. 22 123
      compose/docker-compose.ee.yml
  16. 1 1
      compose/docker-compose.netclient.yml
  17. 0 122
      compose/docker-compose.reference.yml
  18. 54 55
      compose/docker-compose.yml
  19. 51 50
      config/config.go
  20. 1 1
      controllers/docs.go
  21. 19 6
      controllers/enrollmentkeys.go
  22. 56 29
      controllers/ext_client.go
  23. 32 1
      controllers/hosts.go
  24. 49 4
      controllers/network.go
  25. 5 8
      controllers/node.go
  26. 15 3
      controllers/regex.go
  27. 51 0
      controllers/regex_test.go
  28. 1 1
      controllers/server.go
  29. 21 4
      controllers/user.go
  30. 9 9
      docker/Caddyfile
  31. 11 11
      docker/Caddyfile-EE
  32. 1 2
      functions/helpers_test.go
  33. 11 11
      go.mod
  34. 23 54
      go.sum
  35. 1 1
      k8s/client/netclient-daemonset.yaml
  36. 1 1
      k8s/client/netclient.yaml
  37. 1 1
      k8s/server/netmaker-server.yaml
  38. 1 1
      k8s/server/netmaker-ui.yaml
  39. 15 0
      logic/enrollmentkey.go
  40. 74 0
      logic/enrollmentkey_test.go
  41. 22 25
      logic/extpeers.go
  42. 6 2
      logic/gateway.go
  43. 5 0
      logic/hosts.go
  44. 12 0
      logic/nodes.go
  45. 17 1
      logic/peers.go
  46. 14 6
      logic/security.go
  47. 1 0
      logic/users.go
  48. 22 1
      main.go
  49. 28 22
      models/api_host.go
  50. 3 0
      models/api_node.go
  51. 11 1
      models/extclient.go
  52. 54 37
      models/host.go
  53. 0 1
      models/network.go
  54. 1 0
      models/node.go
  55. 7 6
      models/structs.go
  56. 59 51
      mq/handlers.go
  57. 7 3
      release.md
  58. 83 0
      scripts/netmaker.default.env
  59. 18 17
      scripts/nm-certs.sh
  60. 240 123
      scripts/nm-quick.sh
  61. 1 1
      scripts/nm-upgrade-0-17-1-to-0-19-0.sh
  62. 16 0
      servercfg/serverconf.go
  63. 1 1
      swagger.yaml

+ 3 - 1
.dockerignore

@@ -1,2 +1,4 @@
 config/dnsconfig/
 config/dnsconfig/
-data/
+data/
+/.git
+/*.tar

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

@@ -31,6 +31,9 @@ body:
       label: Version
       label: Version
       description: What version are you running?
       description: What version are you running?
       options:
       options:
+        - v0.20.2
+        - v0.20.1
+        - v0.20.0
         - v0.19.0
         - v0.19.0
         - v0.18.7
         - v0.18.7
         - v0.18.6
         - v0.18.6

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

@@ -21,7 +21,7 @@ jobs:
     needs: skip-check
     needs: skip-check
     if: ${{ needs.skip-check.outputs.skip != 'true' }}
     if: ${{ needs.skip-check.outputs.skip != 'true' }}
     outputs:
     outputs:
-      netclientbranch: ${{ steps.checkbranch.outputs.netclientbranch }}
+      netclientbranch: ${{ steps.getbranch.outputs.netclientbranch }}
     steps:
     steps:
       - name: checkout
       - name: checkout
         uses: actions/checkout@v3
         uses: actions/checkout@v3

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

@@ -23,17 +23,8 @@ 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"
+          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"
           file: ./results/results.log
           file: ./results/results.log
-      - name: discord server message
-        uses: appleboy/discord-action@master
-        with:
-          webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
-          webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
-          color: "#42f545"
-          username: "GitHub Bot"
-          message: "droplets from this workflow (tag ${{ github.event.workflow_run.id }}-{{ $github.event.workflow_run.run_number }}) will be deleted in 15 min"
-          file: ./server/serverinfo.txt
       - name: delete droplets
       - name: delete droplets
         if: success() || failure()
         if: success() || failure()
         run: |
         run: |
@@ -62,17 +53,8 @@ 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"
+          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"
           file: ./results/results.log
           file: ./results/results.log
-      - name: discord server message
-        uses: appleboy/discord-action@master
-        with:
-          webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
-          webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
-          color: "#990000"
-          username: "GitHub Bot"
-          message: "droplets from this workflow (tag ${{ github.event.workflow_run.id }}-{{ $github.event.workflow_run.run_number }}) will be deleted in 6 hours"
-          file: ./server/serverinfo.txt
       - name: discord error message
       - name: discord error message
         uses: appleboy/discord-action@master
         uses: appleboy/discord-action@master
         with:
         with:
@@ -85,7 +67,7 @@ jobs:
       - name: delete droplets
       - name: delete droplets
         if: success() || failure()
         if: success() || failure()
         run: |
         run: |
-          sleep 6h
+          sleep 5h
           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" \

+ 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.17.3
+FROM alpine:3.18.0
 
 
 # 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.17.3
+FROM alpine:3.18.0
 ARG version 
 ARG version 
 WORKDIR /app
 WORKDIR /app
 COPY ./netmaker /root/netmaker
 COPY ./netmaker /root/netmaker

+ 3 - 4
README.md

@@ -6,18 +6,17 @@
 </p>
 </p>
 
 
 <p align="center">
 <p align="center">
-<a href="https://runacap.com/ross-index/q1-2022/" target="_blank" rel="noopener">
-    <img src="https://runacap.com/wp-content/uploads/2022/06/ROSS_badge_white_Q1_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2022 | Runa Capital"  width="15%"/>
+<a href="https://runacap.com/ross-index/annual-2022/" target="_blank" rel="noopener">
+    <img src="https://runacap.com/wp-content/uploads/2023/02/Annual_ROSS_badge_white_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups | Runa Capital" width="17%" />
 </a>  
 </a>  
 <a href="https://www.ycombinator.com/companies/netmaker/" target="_blank" rel="noopener">
 <a href="https://www.ycombinator.com/companies/netmaker/" target="_blank" rel="noopener">
     <img src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/y-combinator.png" alt="Y-Combinator" width="16%" />
     <img src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/y-combinator.png" alt="Y-Combinator" width="16%" />
 </a>  
 </a>  
-
 </p>
 </p>
 
 
 <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.19.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.20.2-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" />

+ 18 - 2
cli/cmd/ext_client/create.go

@@ -4,10 +4,16 @@ import (
 	"fmt"
 	"fmt"
 
 
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-var extClientID string
+var (
+	extClientID string
+	publicKey   string
+	dns         string
+	allowedips  []string
+)
 
 
 var extClientCreateCmd = &cobra.Command{
 var extClientCreateCmd = &cobra.Command{
 	Use:   "create [NETWORK NAME] [NODE ID]",
 	Use:   "create [NETWORK NAME] [NODE ID]",
@@ -15,12 +21,22 @@ var extClientCreateCmd = &cobra.Command{
 	Short: "Create an External Client",
 	Short: "Create an External Client",
 	Long:  `Create an External Client`,
 	Long:  `Create an External Client`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
-		functions.CreateExtClient(args[0], args[1], extClientID)
+		extClient := models.CustomExtClient{
+			ClientID:        extClientID,
+			PublicKey:       publicKey,
+			DNS:             dns,
+			ExtraAllowedIPs: allowedips,
+		}
+
+		functions.CreateExtClient(args[0], args[1], extClient)
 		fmt.Println("Success")
 		fmt.Println("Success")
 	},
 	},
 }
 }
 
 
 func init() {
 func init() {
 	extClientCreateCmd.Flags().StringVar(&extClientID, "id", "", "ID of the external client")
 	extClientCreateCmd.Flags().StringVar(&extClientID, "id", "", "ID of the external client")
+	extClientCreateCmd.Flags().StringVar(&publicKey, "public_key", "", "updated public key of the external client")
+	extClientCreateCmd.Flags().StringVar(&dns, "dns", "", "updated DNS of the external client")
+	extClientCreateCmd.Flags().StringSliceVar(&allowedips, "allowedips", []string{}, "updated extra allowed IPs of the external client")
 	rootCmd.AddCommand(extClientCreateCmd)
 	rootCmd.AddCommand(extClientCreateCmd)
 }
 }

+ 8 - 27
cli/cmd/ext_client/update.go

@@ -11,15 +11,7 @@ import (
 )
 )
 
 
 var (
 var (
-	extClientUpdateFile    string
-	description            string
-	privateKey             string
-	publicKey              string
-	address                string
-	address6               string
-	ingressGatewayID       string
-	ingressGatewayEndpoint string
-	ownerID                string
+	extClientUpdateFile string
 )
 )
 
 
 var extClientUpdateCmd = &cobra.Command{
 var extClientUpdateCmd = &cobra.Command{
@@ -31,7 +23,7 @@ var extClientUpdateCmd = &cobra.Command{
 		var (
 		var (
 			network   = args[0]
 			network   = args[0]
 			clientID  = args[1]
 			clientID  = args[1]
-			extClient = &models.ExtClient{}
+			extClient = &models.CustomExtClient{}
 		)
 		)
 		if extClientUpdateFile != "" {
 		if extClientUpdateFile != "" {
 			content, err := os.ReadFile(extClientUpdateFile)
 			content, err := os.ReadFile(extClientUpdateFile)
@@ -42,30 +34,19 @@ var extClientUpdateCmd = &cobra.Command{
 				log.Fatal(err)
 				log.Fatal(err)
 			}
 			}
 		} else {
 		} else {
-			extClient.ClientID = clientID
-			extClient.Description = description
-			extClient.PrivateKey = privateKey
+			extClient.ClientID = extClientID
 			extClient.PublicKey = publicKey
 			extClient.PublicKey = publicKey
-			extClient.Network = network
-			extClient.Address = address
-			extClient.Address6 = address6
-			extClient.IngressGatewayID = ingressGatewayID
-			extClient.IngressGatewayEndpoint = ingressGatewayEndpoint
-			extClient.OwnerID = ownerID
+			extClient.DNS = dns
 		}
 		}
 		functions.PrettyPrint(functions.UpdateExtClient(network, clientID, extClient))
 		functions.PrettyPrint(functions.UpdateExtClient(network, clientID, extClient))
 	},
 	},
 }
 }
 
 
 func init() {
 func init() {
+	extClientUpdateCmd.Flags().StringVar(&extClientID, "id", "", "updated ID of the external client")
 	extClientUpdateCmd.Flags().StringVar(&extClientUpdateFile, "file", "", "Filepath of updated external client definition in JSON")
 	extClientUpdateCmd.Flags().StringVar(&extClientUpdateFile, "file", "", "Filepath of updated external client definition in JSON")
-	extClientUpdateCmd.Flags().StringVar(&description, "desc", "", "Description of the external client")
-	extClientUpdateCmd.Flags().StringVar(&privateKey, "private_key", "", "Filepath of updated external client definition in JSON")
-	extClientUpdateCmd.Flags().StringVar(&publicKey, "public_key", "", "Filepath of updated external client definition in JSON")
-	extClientUpdateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the external client")
-	extClientUpdateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the external client")
-	extClientUpdateCmd.Flags().StringVar(&ingressGatewayID, "ingress_gateway_id", "", "ID of the ingress gateway")
-	extClientUpdateCmd.Flags().StringVar(&ingressGatewayEndpoint, "ingress_gateway_endpoint", "", "Endpoint of the ingress gateway")
-	extClientUpdateCmd.Flags().StringVar(&ownerID, "owner_id", "", "External Client owner's ID")
+	extClientUpdateCmd.Flags().StringVar(&publicKey, "public_key", "", "updated public key of the external client")
+	extClientUpdateCmd.Flags().StringVar(&dns, "dns", "", "updated DNS of the external client")
+	extClientUpdateCmd.Flags().StringSliceVar(&allowedips, "allowedips", []string{}, "updated extra allowed IPs of the external client")
 	rootCmd.AddCommand(extClientUpdateCmd)
 	rootCmd.AddCommand(extClientUpdateCmd)
 }
 }

+ 0 - 2
cli/cmd/network/create.go

@@ -45,7 +45,6 @@ var networkCreateCmd = &cobra.Command{
 			if allowManualSignUp {
 			if allowManualSignUp {
 				network.AllowManualSignUp = "yes"
 				network.AllowManualSignUp = "yes"
 			}
 			}
-			network.DefaultExtClientDNS = defaultExtClientDNS
 			network.DefaultMTU = int32(defaultMTU)
 			network.DefaultMTU = int32(defaultMTU)
 		}
 		}
 		functions.PrettyPrint(functions.CreateNetwork(network))
 		functions.PrettyPrint(functions.CreateNetwork(network))
@@ -61,7 +60,6 @@ func init() {
 	networkCreateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
 	networkCreateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
 	networkCreateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
 	networkCreateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
 	networkCreateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
 	networkCreateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
-	networkCreateCmd.Flags().StringVar(&defaultExtClientDNS, "ext_client_dns", "", "IPv4 address of DNS server to be used by external clients")
 	networkCreateCmd.Flags().IntVar(&defaultListenPort, "listen_port", 51821, "Default wireguard port each node will attempt to use")
 	networkCreateCmd.Flags().IntVar(&defaultListenPort, "listen_port", 51821, "Default wireguard port each node will attempt to use")
 	networkCreateCmd.Flags().IntVar(&nodeLimit, "node_limit", 999999999, "Maximum number of nodes that can be associated with this network")
 	networkCreateCmd.Flags().IntVar(&nodeLimit, "node_limit", 999999999, "Maximum number of nodes that can be associated with this network")
 	networkCreateCmd.Flags().IntVar(&defaultKeepalive, "keep_alive", 20, "Keep Alive in seconds")
 	networkCreateCmd.Flags().IntVar(&defaultKeepalive, "keep_alive", 20, "Keep Alive in seconds")

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

@@ -12,6 +12,5 @@ var (
 	nodeLimit                 int
 	nodeLimit                 int
 	defaultKeepalive          int
 	defaultKeepalive          int
 	allowManualSignUp         bool
 	allowManualSignUp         bool
-	defaultExtClientDNS       string
 	defaultMTU                int
 	defaultMTU                int
 )
 )

+ 3 - 9
cli/functions/ext_client.go

@@ -28,14 +28,8 @@ func GetExtClientConfig(networkName, clientID string) string {
 }
 }
 
 
 // CreateExtClient - create an external client
 // CreateExtClient - create an external client
-func CreateExtClient(networkName, nodeID, extClientID string) {
-	if extClientID != "" {
-		request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), &models.CustomExtClient{
-			ClientID: extClientID,
-		})
-	} else {
-		request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), nil)
-	}
+func CreateExtClient(networkName, nodeID string, extClient models.CustomExtClient) {
+	request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), extClient)
 }
 }
 
 
 // DeleteExtClient - delete an external client
 // DeleteExtClient - delete an external client
@@ -44,6 +38,6 @@ func DeleteExtClient(networkName, clientID string) *models.SuccessResponse {
 }
 }
 
 
 // UpdateExtClient - update an external client
 // UpdateExtClient - update an external client
-func UpdateExtClient(networkName, clientID string, payload *models.ExtClient) *models.ExtClient {
+func UpdateExtClient(networkName, clientID string, payload *models.CustomExtClient) *models.ExtClient {
 	return request[models.ExtClient](http.MethodPut, fmt.Sprintf("/api/extclients/%s/%s", networkName, clientID), payload)
 	return request[models.ExtClient](http.MethodPut, fmt.Sprintf("/api/extclients/%s/%s", networkName, clientID), payload)
 }
 }

+ 1 - 0
cli/functions/http_client.go

@@ -148,6 +148,7 @@ retry:
 	if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
 	if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
 		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
 		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
 		retried = true
 		retried = true
+		// TODO add a retry limit, drop goto
 		goto retry
 		goto retry
 	}
 	}
 	resBodyBytes, err := io.ReadAll(res.Body)
 	resBodyBytes, err := io.ReadAll(res.Body)

+ 4 - 73
compose/docker-compose-emqx.yml

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

+ 22 - 123
compose/docker-compose.ee.yml

@@ -1,109 +1,29 @@
 version: "3.4"
 version: "3.4"
 
 
 services:
 services:
-  netmaker:
-    container_name: netmaker
-    image: gravitl/netmaker:REPLACE_SERVER_IMAGE_TAG
-    restart: on-failure
-    volumes:
-      - dnsconfig:/root/config/dnsconfig
-      - sqldata:/root/data
-    environment:
-      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN"
-      SERVER_NAME: "NETMAKER_BASE_DOMAIN"
-      STUN_LIST: "stun.NETMAKER_BASE_DOMAIN:3478,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302"
-      SERVER_HOST: "SERVER_PUBLIC_IP"
-      SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
-      COREDNS_ADDR: "SERVER_PUBLIC_IP"
-      DNS_MODE: "on"
-      SERVER_HTTP_HOST: "api.NETMAKER_BASE_DOMAIN"
-      NETCLIENT_AUTO_UPDATE: "enabled"
-      API_PORT: "8081"
-      MASTER_KEY: "REPLACE_MASTER_KEY"
-      CORS_ALLOWED_ORIGIN: "*"
-      DISPLAY_KEYS: "on"
-      DATABASE: "sqlite"
-      NODE_ID: "netmaker-server-1"
-      SERVER_BROKER_ENDPOINT: "ws://mq:1883"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      STUN_PORT: "3478"
-      VERBOSITY: "1"
-      METRICS_EXPORTER: "on"
-      LICENSE_KEY: "YOUR_LICENSE_KEY"
-      NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID"
-      DEFAULT_PROXY_MODE: "off"
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN"
-      TURN_SERVER_API_HOST: "https://turnapi.NETMAKER_BASE_DOMAIN"
-      TURN_PORT: "3479"
-      TURN_USERNAME: "REPLACE_TURN_USERNAME"
-      TURN_PASSWORD: "REPLACE_TURN_PASSWORD"
-      USE_TURN: "true"
-    ports:
-      - "3478:3478/udp"
-  netmaker-ui:
-    container_name: netmaker-ui
-    image: gravitl/netmaker-ui:REPLACE_UI_IMAGE_TAG
-    depends_on:
-      - netmaker
-    links:
-      - "netmaker:api"
-    restart: always
-    environment:
-      BACKEND_URL: "https://api.NETMAKER_BASE_DOMAIN"
-  caddy:
-    image: caddy:2.6.2
-    container_name: caddy
-    restart: unless-stopped
-    volumes:
-      - /root/Caddyfile:/etc/caddy/Caddyfile
-      - /root/certs:/root/certs
-      - caddy_data:/data
-      - caddy_conf:/config
-    ports:
-      - "80:80"
-      - "443:443"
-  coredns:
-    container_name: coredns
-    image: coredns/coredns
-    command: -conf /root/dnsconfig/Corefile
-    depends_on:
-      - netmaker
-    restart: always
-    volumes:
-      - dnsconfig:/root/dnsconfig
-  mq:
-    container_name: mq
-    image: eclipse-mosquitto:2.0.15-openssl
-    depends_on:
-      - netmaker
-    restart: unless-stopped
-    command: ["/mosquitto/config/wait.sh"]
-    environment:
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
-    volumes:
-      - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf
-      - /root/wait.sh:/mosquitto/config/wait.sh
-      - mosquitto_logs:/mosquitto/log
+
   prometheus:
   prometheus:
     container_name: prometheus
     container_name: prometheus
     image: gravitl/netmaker-prometheus:latest
     image: gravitl/netmaker-prometheus:latest
+    env_file: ./netmaker.env
     environment:
     environment:
-      NETMAKER_METRICS_TARGET: "netmaker-exporter.NETMAKER_BASE_DOMAIN"
-      LICENSE_KEY: "YOUR_LICENSE_KEY"
+      # config-dependant vars
+      - NETMAKER_METRICS_TARGET=netmaker-exporter.${NM_DOMAIN}
     restart: always
     restart: always
     volumes:
     volumes:
       - prometheus_data:/prometheus
       - prometheus_data:/prometheus
     depends_on:
     depends_on:
       - netmaker
       - netmaker
+
   grafana:
   grafana:
     container_name: grafana
     container_name: grafana
     image: gravitl/netmaker-grafana:latest
     image: gravitl/netmaker-grafana:latest
+    env_file: ./netmaker.env
     environment:
     environment:
-      PROMETHEUS_HOST: "prometheus.NETMAKER_BASE_DOMAIN"
-      NETMAKER_METRICS_TARGET: "netmaker-exporter.NETMAKER_BASE_DOMAIN"
-      LICENSE_KEY: "YOUR_LICENSE_KEY"
+      # config-dependant vars
+      # TODO unify with netmaker-exporter
+      - PROMETHEUS_HOST=prometheus.${NM_DOMAIN}
+      - NETMAKER_METRICS_TARGET=netmaker-exporter.${NM_DOMAIN}
     volumes:
     volumes:
       - grafana_data:/var/lib/grafana
       - grafana_data:/var/lib/grafana
     restart: always
     restart: always
@@ -112,43 +32,22 @@ services:
     depends_on:
     depends_on:
       - prometheus
       - prometheus
       - netmaker
       - netmaker
+
   netmaker-exporter:
   netmaker-exporter:
     container_name: netmaker-exporter
     container_name: netmaker-exporter
     image: gravitl/netmaker-exporter:latest
     image: gravitl/netmaker-exporter:latest
+    env_file: ./netmaker.env
+    environment:
+      # config-dependant vars
+      # TODO unify with grafana
+      - PROMETHEUS_HOST=https://prometheus.${NM_DOMAIN}
+      # The domain/host IP indicating the mq broker address
+      - BROKER_ENDPOINT=wss://broker.${NM_DOMAIN}
+      - API_PORT=${EXPORTER_API_PORT}
     restart: always
     restart: always
     depends_on:
     depends_on:
       - netmaker
       - netmaker
-    environment:
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
-      MQ_URL: "ws://mq:1883"
-      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN"
-      PROMETHEUS: "on"
-      VERBOSITY: "1"
-      API_PORT: "8085"
-      LICENSE_KEY: "YOUR_LICENSE_KEY"
-      PROMETHEUS_HOST: https://prometheus.NETMAKER_BASE_DOMAIN
-  turn:
-    container_name: turn
-    image: gravitl/turnserver:v1.0.0
-    network_mode: "host"
-    volumes:
-      - turn_server:/etc/config
-    environment:
-      DEBUG_MODE: "off"
-      VERBOSITY: "1"
-      TURN_PORT: "3479"
-      TURN_API_PORT: "8089"
-      CORS_ALLOWED_ORIGIN: "*"
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN"
-      USERNAME: "REPLACE_TURN_USERNAME"
-      PASSWORD: "REPLACE_TURN_PASSWORD"
+
 volumes:
 volumes:
-  caddy_data: {}
-  caddy_conf: {}
-  sqldata: {}
-  dnsconfig: {}
-  mosquitto_logs: {}
-  prometheus_data: {}
-  grafana_data: {}
-  turn_server: {}
+  prometheus_data: { }
+  grafana_data: { }

+ 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.19.0'
+    image: 'gravitl/netclient:v0.20.2'
     hostname: netmaker-1
     hostname: netmaker-1
     network_mode: host
     network_mode: host
     restart: on-failure
     restart: on-failure

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

@@ -1,122 +0,0 @@
-version: "3.4"
-
-services:
-  netmaker: # The Primary Server for running Netmaker
-    container_name: netmaker
-    image: gravitl/netmaker:REPLACE_SERVER_IMAGE_TAG
-    restart: on-failure
-    volumes: # Volume mounts necessary for sql, coredns, and mqtt
-      - dnsconfig:/root/config/dnsconfig
-      - sqldata:/root/data
-      - shared_certs:/etc/netmaker
-    environment: # Necessary capabilities to set iptables when running in container
-      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN" # The domain/host IP indicating the mq broker address
-      SERVER_NAME: "NETMAKER_BASE_DOMAIN" # The base domain of netmaker
-      SERVER_HOST: "SERVER_PUBLIC_IP" # Set to public IP of machine.
-      SERVER_HTTP_HOST: "api.NETMAKER_BASE_DOMAIN" # Overrides SERVER_HOST if set. Useful for making HTTP available via different interfaces/networks.
-      NETCLIENT_AUTO_UPDATE: "enabled" # Enable auto update of netclient ? ENUM:- enabled,disabled | default: enabled
-      SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
-      COREDNS_ADDR: "SERVER_PUBLIC_IP" # Address of the CoreDNS server. Defaults to SERVER_HOST
-      DNS_MODE: "on" # Enables DNS Mode, meaning all nodes will set hosts file for private dns settings.
-      API_PORT: "8081" # The HTTP API port for Netmaker. Used for API calls / communication from front end. If changed, need to change port of BACKEND_URL for netmaker-ui.
-      REST_BACKEND: "on" # Enables the REST backend (API running on API_PORT at SERVER_HTTP_HOST). Change to "off" to turn off.
-      DISABLE_REMOTE_IP_CHECK: "off" # If turned "on", Server will not set Host based on remote IP check. This is already overridden if SERVER_HOST is set. Turned "off" by default.
-      TELEMETRY: "on" # Whether or not to send telemetry data to help improve Netmaker. Switch to "off" to opt out of sending telemetry.
-      MASTER_KEY: "REPLACE_MASTER_KEY" # The admin master key for accessing the API. Change this in any production installation.
-      CORS_ALLOWED_ORIGIN: "*" # The "allowed origin" for API requests. Change to restrict where API requests can come from with comma-separated URLs. ex:- https://dashboard.netmaker.domain1.com,https://dashboard.netmaker.domain2.com
-      DISPLAY_KEYS: "on" # Show keys permanently in UI (until deleted) as opposed to 1-time display.
-      DATABASE: "sqlite" # Database to use - sqlite, postgres, or rqlite
-      NODE_ID: "netmaker-server-1" # used for HA - identifies this server vs other servers
-      SERVER_BROKER_ENDPOINT: ""ws://mq:1883""  # the address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address. If using "host networking", it will find and detect the IP of the mq container.
-      MQ_USERNAME: "REPLACE_MQ_USERNAME" # the username to set for MQ access
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD" # the password to set for MQ access
-      STUN_PORT: "3478" # the reachable port of STUN on the server
-      VERBOSITY: "1" # logging verbosity level - 1, 2, or 3
-      # this section is for OAuth
-      AUTH_PROVIDER: "" # "<azure-ad|github|google|oidc>"
-      CLIENT_ID: "" # "<client id of your oauth provider>"
-      CLIENT_SECRET: "" # "<client secret of your oauth provider>"
-      FRONTEND_URL: "" # "https://dashboard.<netmaker base domain>"
-      AZURE_TENANT: "" # "<only for azure, you may optionally specify the tenant for the OAuth>"
-      OIDC_ISSUER: "" # https://oidc.yourprovider.com - URL of oidc provider
-      DEFAULT_PROXY_MODE: "off" # if ON, all new clients will enable proxy by default if OFF, all new clients will disable proxy by default, if AUTO, stick with the existing logic for NAT detection
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN" # domain for your turn server
-      TURN_SERVER_API_HOST: "https://turnapi.NETMAKER_BASE_DOMAIN" # domain of the turn api server
-      TURN_PORT: "3479" #  port to access turn server
-      TURN_USERNAME: "REPLACE_TURN_USERNAME"  # the username to set for turn api access
-      TURN_PASSWORD: "REPLACE_TURN_PASSWORD" #  the password to set for turn api access
-      USE_TURN: "true" #config for using turn, accepts either true/false
-    ports:
-      - "3478:3478/udp" # the stun port
-  netmaker-ui:  # The Netmaker UI Component
-    container_name: netmaker-ui
-    image: gravitl/netmaker-ui:REPLACE_UI_IMAGE_TAG
-    depends_on:
-      - netmaker
-    links:
-      - "netmaker:api"
-    restart: always
-    environment:
-      BACKEND_URL: "https://api.NETMAKER_BASE_DOMAIN" # URL where UI will send API requests. Change based on SERVER_HOST, SERVER_HTTP_HOST, and API_PORT
-  caddy: # The reverse proxy that manages traffic for Netmaker
-    image: caddy:2.6.2
-    container_name: caddy
-    restart: unless-stopped
-    volumes:
-      - /root/Caddyfile:/etc/caddy/Caddyfile # Config file for Caddy
-      - /root/certs:/root/certs
-      - caddy_data:/data
-      - caddy_conf:/config
-    ports:
-      - "80:80"
-      - "443:443"
-  coredns: # The DNS Server. CoreDNS can be removed unless doing special advanced use cases
-    container_name: coredns
-    image: coredns/coredns
-    command: -conf /root/dnsconfig/Corefile
-    depends_on:
-      - netmaker
-    restart: always
-    volumes:
-      - dnsconfig:/root/dnsconfig
-  mq: # the mqtt broker for netmaker
-    container_name: mq
-    image: eclipse-mosquitto:2.0.15-openssl
-    depends_on:
-      - netmaker
-    restart: unless-stopped
-    command: ["/mosquitto/config/wait.sh"]
-    environment:
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD" # must be same value as in netmaker env 
-      MQ_USERNAME: "REPLACE_MQ_USERNAME" # must be same value as in netmaker env
-    volumes:
-      - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf
-      - /root/wait.sh:/mosquitto/config/wait.sh
-      - mosquitto_logs:/mosquitto/log
-    ports:
-      - "1883:1883"
-      - "8883:8883"
-  turn:
-    container_name: turn
-    image: gravitl/turnserver:v1.0.0
-    network_mode: "host"
-    volumes:
-      - turn_server:/etc/config
-    environment:
-      DEBUG_MODE: "off"
-      VERBOSITY: "1"
-      TURN_PORT: "3479"
-      TURN_API_PORT: "8089"
-      CORS_ALLOWED_ORIGIN: "*"
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN"
-      USERNAME: "REPLACE_TURN_USERNAME"
-      PASSWORD: "REPLACE_TURN_PASSWORD"
-      USE_TURN: "true"
-volumes:
-  caddy_data: {} # runtime data for caddy
-  caddy_conf: {} # configuration file for Caddy
-  shared_certs: {} # netmaker certs generated for MQ comms - used by nodes/servers
-  sqldata: {} # storage for embedded sqlite
-  dnsconfig: {} # storage for coredns
-  mosquitto_logs: {} # storage for mqtt logs
-  turn_server: {}

+ 54 - 55
compose/docker-compose.yml

@@ -1,71 +1,69 @@
 version: "3.4"
 version: "3.4"
 
 
 services:
 services:
+
   netmaker:
   netmaker:
     container_name: netmaker
     container_name: netmaker
-    image: gravitl/netmaker:REPLACE_SERVER_IMAGE_TAG
-    restart: on-failure
+    image: gravitl/netmaker:$SERVER_IMAGE_TAG
+    env_file: ./netmaker.env
+    restart: always
     volumes:
     volumes:
       - dnsconfig:/root/config/dnsconfig
       - dnsconfig:/root/config/dnsconfig
       - sqldata:/root/data
       - sqldata:/root/data
     environment:
     environment:
-      BROKER_ENDPOINT: "wss://broker.NETMAKER_BASE_DOMAIN"
-      SERVER_NAME: "NETMAKER_BASE_DOMAIN"
-      STUN_LIST: "stun.NETMAKER_BASE_DOMAIN:3478,stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302"
-      SERVER_HOST: "SERVER_PUBLIC_IP"
-      SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443"
-      COREDNS_ADDR: "SERVER_PUBLIC_IP"
-      DNS_MODE: "on"
-      SERVER_HTTP_HOST: "api.NETMAKER_BASE_DOMAIN"
-      NETCLIENT_AUTO_UPDATE: "enabled"
-      API_PORT: "8081"
-      MASTER_KEY: "REPLACE_MASTER_KEY"
-      CORS_ALLOWED_ORIGIN: "*"
-      DISPLAY_KEYS: "on"
-      DATABASE: "sqlite"
-      NODE_ID: "netmaker-server-1"
-      SERVER_BROKER_ENDPOINT: "ws://mq:1883"
-      VERBOSITY: "1"
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
-      STUN_PORT: "3478"
-      DEFAULT_PROXY_MODE: "off"
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN"
-      TURN_SERVER_API_HOST: "https://turnapi.NETMAKER_BASE_DOMAIN"
-      TURN_PORT: "3479"
-      TURN_USERNAME: "REPLACE_TURN_USERNAME"
-      TURN_PASSWORD: "REPLACE_TURN_PASSWORD"
-      USE_TURN: "true"
+      # config-dependant vars
+      - STUN_LIST=stun.${NM_DOMAIN}:${STUN_PORT},stun1.netmaker.io:3478,stun2.netmaker.io:3478,stun1.l.google.com:19302,stun2.l.google.com:19302
+      # The domain/host IP indicating the mq broker address
+      - BROKER_ENDPOINT=wss://broker.${NM_DOMAIN}
+      # The base domain of netmaker
+      - SERVER_NAME=${NM_DOMAIN}
+      - SERVER_API_CONN_STRING=api.${NM_DOMAIN}:443
+      # Address of the CoreDNS server. Defaults to SERVER_HOST
+      - COREDNS_ADDR=${SERVER_HOST}
+      # Overrides SERVER_HOST if set. Useful for making HTTP available via different interfaces/networks.
+      - SERVER_HTTP_HOST=api.${NM_DOMAIN}
+      # domain for your turn server
+      - TURN_SERVER_HOST=turn.${NM_DOMAIN}
+      # domain of the turn api server
+      - TURN_SERVER_API_HOST=https://turnapi.${NM_DOMAIN}
     ports:
     ports:
       - "3478:3478/udp"
       - "3478:3478/udp"
+
   netmaker-ui:
   netmaker-ui:
     container_name: netmaker-ui
     container_name: netmaker-ui
-    image: gravitl/netmaker-ui:REPLACE_UI_IMAGE_TAG
+    image: gravitl/netmaker-ui:$UI_IMAGE_TAG
+    env_file: ./netmaker.env
+    environment:
+      # config-dependant vars
+      # URL where UI will send API requests. Change based on SERVER_HOST, SERVER_HTTP_HOST, and API_PORT
+      BACKEND_URL: "https://api.${NM_DOMAIN}"
     depends_on:
     depends_on:
       - netmaker
       - netmaker
     links:
     links:
       - "netmaker:api"
       - "netmaker:api"
     restart: always
     restart: always
-    environment:
-      BACKEND_URL: "https://api.NETMAKER_BASE_DOMAIN"
+
   caddy:
   caddy:
     image: caddy:2.6.2
     image: caddy:2.6.2
     container_name: caddy
     container_name: caddy
+    env_file: ./netmaker.env
     restart: unless-stopped
     restart: unless-stopped
     extra_hosts:
     extra_hosts:
       - "host.docker.internal:host-gateway"
       - "host.docker.internal:host-gateway"
     volumes:
     volumes:
-      - /root/Caddyfile:/etc/caddy/Caddyfile
-      - /root/certs:/root/certs
+      - ./Caddyfile:/etc/caddy/Caddyfile
+      - ./certs:/root/certs
       - caddy_data:/data
       - caddy_data:/data
       - caddy_conf:/config
       - caddy_conf:/config
     ports:
     ports:
       - "80:80"
       - "80:80"
       - "443:443"
       - "443:443"
+
   coredns:
   coredns:
     container_name: coredns
     container_name: coredns
     image: coredns/coredns
     image: coredns/coredns
     command: -conf /root/dnsconfig/Corefile
     command: -conf /root/dnsconfig/Corefile
+    env_file: ./netmaker.env
     depends_on:
     depends_on:
       - netmaker
       - netmaker
     restart: always
     restart: always
@@ -74,36 +72,37 @@ services:
   mq:
   mq:
     container_name: mq
     container_name: mq
     image: eclipse-mosquitto:2.0.15-openssl
     image: eclipse-mosquitto:2.0.15-openssl
+    env_file: ./netmaker.env
     depends_on:
     depends_on:
       - netmaker
       - netmaker
     restart: unless-stopped
     restart: unless-stopped
-    command: ["/mosquitto/config/wait.sh"]
-    environment:
-      MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
-      MQ_USERNAME: "REPLACE_MQ_USERNAME"
+    command: [ "/mosquitto/config/wait.sh" ]
     volumes:
     volumes:
-      - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf
-      - /root/wait.sh:/mosquitto/config/wait.sh
+      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
+      - ./wait.sh:/mosquitto/config/wait.sh
       - mosquitto_logs:/mosquitto/log
       - mosquitto_logs:/mosquitto/log
+      - mosquitto_data:/mosquitto/data
+
   turn:
   turn:
     container_name: turn
     container_name: turn
     image: gravitl/turnserver:v1.0.0
     image: gravitl/turnserver:v1.0.0
+    env_file: ./netmaker.env
+    environment:
+      # config-dependant vars
+      - USERNAME=${TURN_USERNAME}
+      - PASSWORD=${TURN_PASSWORD}
+      # domain for your turn server
+      - TURN_SERVER_HOST=turn.${NM_DOMAIN}
     network_mode: "host"
     network_mode: "host"
     volumes:
     volumes:
       - turn_server:/etc/config
       - turn_server:/etc/config
-    environment:
-      DEBUG_MODE: "off"
-      VERBOSITY: "1"
-      TURN_PORT: "3479"
-      TURN_API_PORT: "8089"
-      CORS_ALLOWED_ORIGIN: "*"
-      TURN_SERVER_HOST: "turn.NETMAKER_BASE_DOMAIN"
-      USERNAME: "REPLACE_TURN_USERNAME"
-      PASSWORD: "REPLACE_TURN_PASSWORD"
+    restart: always
+
 volumes:
 volumes:
-  caddy_data: {}
-  caddy_conf: {}
-  sqldata: {}
-  dnsconfig: {}
-  mosquitto_logs: {}
-  turn_server: {}
+  caddy_data: { } # runtime data for caddy
+  caddy_conf: { } # configuration file for Caddy
+  sqldata: { }
+  dnsconfig: { } # storage for coredns
+  mosquitto_logs: { } # storage for mqtt logs
+  mosquitto_data: { } # storage for mqtt data
+  turn_server: { }

+ 51 - 50
config/config.go

@@ -32,56 +32,57 @@ 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"`
 }
 }
 
 
 // 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.19.0
+//	Version: 0.20.2
 //	Host: netmaker.io
 //	Host: netmaker.io
 //
 //
 //	Consumes:
 //	Consumes:

+ 19 - 6
controllers/enrollmentkeys.go

@@ -17,7 +17,7 @@ import (
 
 
 func enrollmentKeyHandlers(r *mux.Router) {
 func enrollmentKeyHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(createEnrollmentKey))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(createEnrollmentKey))).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(getEnrollmentKeys))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(false, http.HandlerFunc(getEnrollmentKeys))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(deleteEnrollmentKey))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(deleteEnrollmentKey))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).Methods(http.MethodPost)
 }
 }
@@ -34,24 +34,37 @@ func enrollmentKeyHandlers(r *mux.Router) {
 //			Responses:
 //			Responses:
 //				200: getEnrollmentKeysSlice
 //				200: getEnrollmentKeysSlice
 func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
-	currentKeys, err := logic.GetAllEnrollmentKeys()
+	keys, err := logic.GetAllEnrollmentKeys()
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	for i := range currentKeys {
-		currentKey := currentKeys[i]
-		if err = logic.Tokenize(currentKey, servercfg.GetAPIHost()); err != nil {
+	isMasterAdmin := r.Header.Get("ismaster") == "yes"
+	// regular user flow
+	user, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil && !isMasterAdmin {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// TODO drop double pointer
+	ret := []*models.EnrollmentKey{}
+	for _, key := range keys {
+		if !isMasterAdmin && !logic.UserHasNetworksAccess(key.Networks, user) {
+			continue
+		}
+		if err = logic.Tokenize(key, servercfg.GetAPIHost()); err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
 			logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			return
 			return
 		}
 		}
+		ret = append(ret, key)
 	}
 	}
 	// return JSON/API formatted keys
 	// return JSON/API formatted keys
 	logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
 	logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(currentKeys)
+	json.NewEncoder(w).Encode(ret)
 }
 }
 
 
 // swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey
 // swagger:route DELETE /api/v1/enrollment-keys/{keyID} enrollmentKeys deleteEnrollmentKey

+ 56 - 29
controllers/ext_client.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"net"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
 
 
@@ -230,8 +231,10 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 	defaultDNS := ""
 	defaultDNS := ""
-	if network.DefaultExtClientDNS != "" {
-		defaultDNS = "DNS = " + network.DefaultExtClientDNS
+	if client.DNS != "" {
+		defaultDNS = "DNS = " + client.DNS
+	} else if gwnode.IngressDNS != "" {
+		defaultDNS = "DNS = " + gwnode.IngressDNS
 	}
 	}
 
 
 	defaultMTU := 1420
 	defaultMTU := 1420
@@ -321,20 +324,13 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	var extclient models.ExtClient
 	var extclient models.ExtClient
 	var customExtClient models.CustomExtClient
 	var customExtClient models.CustomExtClient
 
 
-	err := json.NewDecoder(r.Body).Decode(&customExtClient)
-	if err == nil {
-		if customExtClient.ClientID != "" && !validName(customExtClient.ClientID) {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(errInvalidExtClientID, "badrequest"))
-			return
-		}
-		extclient.ClientID = customExtClient.ClientID
-		if len(customExtClient.PublicKey) > 0 {
-			if _, err := wgtypes.ParseKey(customExtClient.PublicKey); err != nil {
-				logic.ReturnErrorResponse(w, r, logic.FormatError(errInvalidExtClientPubKey, "badrequest"))
-				return
-			}
-			extclient.PublicKey = customExtClient.PublicKey
-		}
+	if err := json.NewDecoder(r.Body).Decode(&customExtClient); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if err := validateExtClient(&extclient, &customExtClient); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
 	}
 	}
 
 
 	extclient.Network = networkName
 	extclient.Network = networkName
@@ -392,7 +388,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 				logger.Log(0, "failed to associate client", extclient.ClientID, "to user", userID)
 				logger.Log(0, "failed to associate client", extclient.ClientID, "to user", userID)
 			}
 			}
 			extclient.OwnerID = userID
 			extclient.OwnerID = userID
-			if _, err := logic.UpdateExtClient(extclient.ClientID, extclient.Network, extclient.Enabled, &extclient, extclient.ACLs); err != nil {
+			if err := logic.SaveExtClient(&extclient); err != nil {
 				logger.Log(0, "failed to add owner id", userID, "to client", extclient.ClientID)
 				logger.Log(0, "failed to add owner id", userID, "to client", extclient.ClientID)
 			}
 			}
 		}
 		}
@@ -426,9 +422,9 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 
 
 	var params = mux.Vars(r)
 	var params = mux.Vars(r)
 
 
-	var newExtClient models.ExtClient
+	var update models.CustomExtClient
 	var oldExtClient models.ExtClient
 	var oldExtClient models.ExtClient
-	err := json.NewDecoder(r.Body).Decode(&newExtClient)
+	err := json.NewDecoder(r.Body).Decode(&update)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
 		logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
 			err.Error())
 			err.Error())
@@ -445,8 +441,8 @@ func updateExtClient(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 !validName(newExtClient.ClientID) {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errInvalidExtClientID, "badrequest"))
+	if err := validateExtClient(&oldExtClient, &update); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
 	data, err := database.FetchRecord(database.EXT_CLIENT_TABLE_NAME, key)
 	data, err := database.FetchRecord(database.EXT_CLIENT_TABLE_NAME, key)
@@ -466,7 +462,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 
 
 	// == PRO ==
 	// == PRO ==
 	networkName := params["network"]
 	networkName := params["network"]
-	var changedID = newExtClient.ClientID != oldExtClient.ClientID
+	var changedID = update.ClientID != oldExtClient.ClientID
 	if r.Header.Get("ismaster") != "yes" {
 	if r.Header.Get("ismaster") != "yes" {
 		userID := r.Header.Get("user")
 		userID := r.Header.Get("user")
 		_, doesOwn := doesUserOwnClient(userID, params["clientid"], networkName)
 		_, doesOwn := doesUserOwnClient(userID, params["clientid"], networkName)
@@ -479,17 +475,16 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		if err := pro.DissociateNetworkUserClient(oldExtClient.OwnerID, networkName, oldExtClient.ClientID); err != nil {
 		if err := pro.DissociateNetworkUserClient(oldExtClient.OwnerID, networkName, oldExtClient.ClientID); err != nil {
 			logger.Log(0, "failed to dissociate client", oldExtClient.ClientID, "from user", oldExtClient.OwnerID)
 			logger.Log(0, "failed to dissociate client", oldExtClient.ClientID, "from user", oldExtClient.OwnerID)
 		}
 		}
-		if err := pro.AssociateNetworkUserClient(oldExtClient.OwnerID, networkName, newExtClient.ClientID); err != nil {
-			logger.Log(0, "failed to associate client", newExtClient.ClientID, "to user", oldExtClient.OwnerID)
+		if err := pro.AssociateNetworkUserClient(oldExtClient.OwnerID, networkName, update.ClientID); err != nil {
+			logger.Log(0, "failed to associate client", update.ClientID, "to user", oldExtClient.OwnerID)
 		}
 		}
 	}
 	}
 	// == END PRO ==
 	// == END PRO ==
 
 
-	var changedEnabled = (newExtClient.Enabled != oldExtClient.Enabled) || // indicates there was a change in enablement
-		len(newExtClient.ACLs) != len(oldExtClient.ACLs)
+	var changedEnabled = (update.Enabled != oldExtClient.Enabled) // indicates there was a change in enablement
 	// extra var need as logic.Update changes oldExtClient
 	// extra var need as logic.Update changes oldExtClient
 	currentClient := oldExtClient
 	currentClient := oldExtClient
-	newclient, err := logic.UpdateExtClient(newExtClient.ClientID, params["network"], newExtClient.Enabled, &oldExtClient, newExtClient.ACLs)
+	newclient, err := logic.UpdateExtClient(&oldExtClient, &update)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to update ext client [%s], network [%s]: %v",
 			fmt.Sprintf("failed to update ext client [%s], network [%s]: %v",
@@ -497,7 +492,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	logger.Log(0, r.Header.Get("user"), "updated ext client", newExtClient.ClientID)
+	logger.Log(0, r.Header.Get("user"), "updated ext client", update.ClientID)
 	if changedEnabled { // need to send a peer update to the ingress node as enablement of one of it's clients has changed
 	if changedEnabled { // need to send a peer update to the ingress node as enablement of one of it's clients has changed
 		if ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID); err == nil {
 		if ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID); err == nil {
 			if err = mq.PublishPeerUpdate(); err != nil {
 			if err = mq.PublishPeerUpdate(); err != nil {
@@ -509,7 +504,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newclient)
 	json.NewEncoder(w).Encode(newclient)
 	if changedID {
 	if changedID {
 		go func() {
 		go func() {
-			if err := mq.PublishExtClientDNSUpdate(currentClient, newExtClient, networkName); err != nil {
+			if err := mq.PublishExtClientDNSUpdate(currentClient, *newclient, networkName); err != nil {
 				logger.Log(1, "error pubishing dns update for extcient update", err.Error())
 				logger.Log(1, "error pubishing dns update for extcient update", err.Error())
 			}
 			}
 		}()
 		}()
@@ -648,3 +643,35 @@ func doesUserOwnClient(username, clientID, network string) (bool, bool) {
 
 
 	return false, logic.StringSliceContains(netUser.Clients, clientID)
 	return false, logic.StringSliceContains(netUser.Clients, clientID)
 }
 }
+
+// validateExtClient	Validates the extclient object
+func validateExtClient(extclient *models.ExtClient, customExtClient *models.CustomExtClient) error {
+	//validate clientid
+	if customExtClient.ClientID != "" && !validName(customExtClient.ClientID) {
+		return errInvalidExtClientID
+	}
+	extclient.ClientID = customExtClient.ClientID
+	if len(customExtClient.PublicKey) > 0 {
+		if _, err := wgtypes.ParseKey(customExtClient.PublicKey); err != nil {
+			return errInvalidExtClientPubKey
+		}
+		extclient.PublicKey = customExtClient.PublicKey
+	}
+	//validate extra ips
+	if len(customExtClient.ExtraAllowedIPs) > 0 {
+		for _, ip := range customExtClient.ExtraAllowedIPs {
+			if _, _, err := net.ParseCIDR(ip); err != nil {
+				return errInvalidExtClientExtraIP
+			}
+		}
+		extclient.ExtraAllowedIPs = customExtClient.ExtraAllowedIPs
+	}
+	//validate DNS
+	if customExtClient.DNS != "" {
+		if ip := net.ParseIP(customExtClient.DNS); ip == nil {
+			return errInvalidExtClientDNS
+		}
+		extclient.DNS = customExtClient.DNS
+	}
+	return nil
+}

+ 32 - 1
controllers/hosts.go

@@ -18,7 +18,7 @@ import (
 )
 )
 
 
 func hostHandlers(r *mux.Router) {
 func hostHandlers(r *mux.Router) {
-	r.HandleFunc("/api/hosts", logic.SecurityCheck(true, http.HandlerFunc(getHosts))).Methods(http.MethodGet)
+	r.HandleFunc("/api/hosts", logic.SecurityCheck(false, http.HandlerFunc(getHosts))).Methods(http.MethodGet)
 	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).Methods(http.MethodPut)
@@ -49,9 +49,38 @@ func getHosts(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	//isMasterAdmin := r.Header.Get("ismaster") == "yes"
+	//user, err := logic.GetUser(r.Header.Get("user"))
+	//if err != nil && !isMasterAdmin {
+	//	logger.Log(0, r.Header.Get("user"), "failed to fetch user: ", err.Error())
+	//	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	//	return
+	//}
 	// return JSON/API formatted hosts
 	// return JSON/API formatted hosts
+	//ret := []models.ApiHost{}
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])
 	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
 	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
+	//for _, host := range apiHosts {
+	//	nodes := host.Nodes
+	//	// work on the copy
+	//	host.Nodes = []string{}
+	//	for _, nid := range nodes {
+	//		node, err := logic.GetNodeByID(nid)
+	//		if err != nil {
+	//			logger.Log(0, r.Header.Get("user"), "failed to fetch node: ", err.Error())
+	//			// TODO find the reason for the DB error, skip this node for now
+	//			continue
+	//		}
+	//		if !isMasterAdmin && !logic.UserHasNetworksAccess([]string{node.Network}, user) {
+	//			continue
+	//		}
+	//		host.Nodes = append(host.Nodes, nid)
+	//	}
+	//	// add to the response only if has perms to some nodes / networks
+	//	if len(host.Nodes) > 0 {
+	//		ret = append(ret, host)
+	//	}
+	//}
 	logic.SortApiHosts(apiHosts[:])
 	logic.SortApiHosts(apiHosts[:])
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiHosts)
 	json.NewEncoder(w).Encode(apiHosts)
@@ -98,9 +127,11 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+
 	serverConf.TrafficKey = key
 	serverConf.TrafficKey = key
 	response := models.HostPull{
 	response := models.HostPull{
 		Host:         *host,
 		Host:         *host,
+		Nodes:        logic.GetHostNodes(host),
 		ServerConfig: serverConf,
 		ServerConfig: serverConf,
 		Peers:        hPU.Peers,
 		Peers:        hPU.Peers,
 		PeerIDs:      hPU.PeerIDs,
 		PeerIDs:      hPU.PeerIDs,

+ 49 - 4
controllers/network.go

@@ -8,6 +8,7 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
+	"golang.org/x/exp/slog"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -23,6 +24,7 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(networks_l, http.HandlerFunc(createNetwork)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(networks_l, http.HandlerFunc(createNetwork)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(false, http.HandlerFunc(getNetwork))).Methods(http.MethodGet)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(false, http.HandlerFunc(getNetwork))).Methods(http.MethodGet)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(updateNetwork))).Methods(http.MethodPut)
 	// ACLs
 	// ACLs
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
@@ -40,10 +42,7 @@ func networkHandlers(r *mux.Router) {
 //			Responses:
 //			Responses:
 //				200: getNetworksSliceResponse
 //				200: getNetworksSliceResponse
 func getNetworks(w http.ResponseWriter, r *http.Request) {
 func getNetworks(w http.ResponseWriter, r *http.Request) {
-
-	headerNetworks := r.Header.Get("networks")
-	networksSlice := []string{}
-	marshalErr := json.Unmarshal([]byte(headerNetworks), &networksSlice)
+	networksSlice, marshalErr := getHeaderNetworks(r)
 	if marshalErr != nil {
 	if marshalErr != nil {
 		logger.Log(0, r.Header.Get("user"), "error unmarshalling networks: ",
 		logger.Log(0, r.Header.Get("user"), "error unmarshalling networks: ",
 			marshalErr.Error())
 			marshalErr.Error())
@@ -285,3 +284,49 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
 	json.NewEncoder(w).Encode(network)
 }
 }
+
+// swagger:route PUT /api/networks networks updateNetwork
+//
+// Update pro settings for a network.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: networkBodyResponse
+func updateNetwork(w http.ResponseWriter, r *http.Request) {
+
+	w.Header().Set("Content-Type", "application/json")
+
+	var payload models.Network
+
+	// we decode our body request params
+	err := json.NewDecoder(r.Body).Decode(&payload)
+	if err != nil {
+		slog.Info("error decoding request body", "user", r.Header.Get("user"), "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	netOld1, err := logic.GetNetwork(payload.NetID)
+	if err != nil {
+		slog.Info("error fetching network", "user", r.Header.Get("user"), "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	// partial update
+	netOld2 := netOld1
+	netOld2.ProSettings = payload.ProSettings
+	_, _, _, _, _, err = logic.UpdateNetwork(&netOld1, &netOld2)
+	if err != nil {
+		slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	slog.Info("updated network", "network", payload.NetID, "user", r.Header.Get("user"))
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(payload)
+}

+ 5 - 8
controllers/node.go

@@ -158,7 +158,7 @@ func authenticate(response http.ResponseWriter, request *http.Request) {
 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.StatusUnauthorized, Message: logic.Unauthorized_Msg,
+			Code: http.StatusForbidden, Message: logic.Forbidden_Msg,
 		}
 		}
 
 
 		var params = mux.Vars(r)
 		var params = mux.Vars(r)
@@ -521,13 +521,10 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
 	nodeid := params["nodeid"]
 	nodeid := params["nodeid"]
 	netid := params["network"]
 	netid := params["network"]
-	type failoverData struct {
-		Failover bool `json:"failover"`
-	}
-	var failoverReqBody failoverData
-	json.NewDecoder(r.Body).Decode(&failoverReqBody)
+	var request models.IngressRequest
+	json.NewDecoder(r.Body).Decode(&request)
 
 
-	node, err := logic.CreateIngressGateway(netid, nodeid, failoverReqBody.Failover)
+	node, err := logic.CreateIngressGateway(netid, nodeid, request)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to create ingress gateway on node [%s] on network [%s]: %v",
 			fmt.Sprintf("failed to create ingress gateway on node [%s] on network [%s]: %v",
@@ -536,7 +533,7 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if servercfg.Is_EE && failoverReqBody.Failover {
+	if servercfg.Is_EE && request.Failover {
 		if err = logic.EnterpriseResetFailoverFunc(node.Network); err != nil {
 		if err = logic.EnterpriseResetFailoverFunc(node.Network); err != nil {
 			logger.Log(1, "failed to reset failover list during failover create", node.ID.String(), node.Network)
 			logger.Log(1, "failed to reset failover list during failover create", node.ID.String(), node.Network)
 		}
 		}

+ 15 - 3
controllers/regex.go

@@ -6,11 +6,23 @@ import (
 )
 )
 
 
 var (
 var (
-	errInvalidExtClientPubKey = errors.New("incorrect ext client public key")
-	errInvalidExtClientID     = errors.New("ext client ID must be alphanumderic and/or dashes")
+	errInvalidExtClientPubKey  = errors.New("incorrect ext client public key")
+	errInvalidExtClientID      = errors.New("ext client ID must be alphanumderic and/or dashes and less that 15 chars")
+	errInvalidExtClientExtraIP = errors.New("ext client extra ip must be a valid cidr")
+	errInvalidExtClientDNS     = errors.New("ext client dns must be a valid ip address")
 )
 )
 
 
 // allow only dashes and alphaneumeric for ext client and node names
 // allow only dashes and alphaneumeric for ext client and node names
 func validName(name string) bool {
 func validName(name string) bool {
-	return regexp.MustCompile("^[a-zA-Z0-9-]+$").MatchString(name)
+	reg, err := regexp.Compile("^[a-zA-Z0-9-]+$")
+	if err != nil {
+		return false
+	}
+	if !reg.MatchString(name) {
+		return false
+	}
+	if len(name) > 15 {
+		return false
+	}
+	return true
 }
 }

+ 51 - 0
controllers/regex_test.go

@@ -0,0 +1,51 @@
+package controller
+
+import "testing"
+
+// TestValidName tests the validName function
+func TestValidName(t *testing.T) {
+	type args struct {
+		Name string
+	}
+	tests := []struct {
+		Name string
+		Args args
+		Want bool
+	}{
+		{
+			Name: "validName",
+			Args: args{
+				Name: "TestvalidName",
+			},
+			Want: true,
+		},
+		{
+			Name: "invalidName",
+			Args: args{
+				Name: "Test*Name",
+			},
+			Want: false,
+		},
+		{
+			Name: "nametoolong",
+			Args: args{
+				Name: "TestvalidNameTestvalidName",
+			},
+			Want: false,
+		},
+		{
+			Name: "maxlength",
+			Args: args{
+				Name: "123456789012345",
+			},
+			Want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.Name, func(t *testing.T) {
+			if got := validName(tt.Args.Name); got != tt.Want {
+				t.Errorf("validName() = %v, want %v", got, tt.Want)
+			}
+		})
+	}
+}

+ 1 - 1
controllers/server.go

@@ -56,7 +56,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 func allowUsers(next http.Handler) http.HandlerFunc {
 func allowUsers(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.StatusInternalServerError, Message: logic.Unauthorized_Msg,
+			Code: http.StatusUnauthorized, Message: logic.Unauthorized_Msg,
 		}
 		}
 		bearerToken := r.Header.Get("Authorization")
 		bearerToken := r.Header.Get("Authorization")
 		var tokenSplit = strings.Split(bearerToken, " ")
 		var tokenSplit = strings.Split(bearerToken, " ")

+ 21 - 4
controllers/user.go

@@ -290,16 +290,16 @@ func updateUserNetworks(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	var userchange models.User
+	userChange := &models.User{}
 	// we decode our body request params
 	// we decode our body request params
-	err = json.NewDecoder(r.Body).Decode(&userchange)
+	err = json.NewDecoder(r.Body).Decode(userChange)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username, "error decoding request body: ",
 		logger.Log(0, username, "error decoding request body: ",
 			err.Error())
 			err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	err = logic.UpdateUserNetworks(userchange.Networks, userchange.Groups, userchange.IsAdmin, &models.ReturnUser{
+	err = logic.UpdateUserNetworks(userChange.Networks, userChange.Groups, userChange.IsAdmin, &models.ReturnUser{
 		Groups:   user.Groups,
 		Groups:   user.Groups,
 		IsAdmin:  user.IsAdmin,
 		IsAdmin:  user.IsAdmin,
 		Networks: user.Networks,
 		Networks: user.Networks,
@@ -313,7 +313,13 @@ func updateUserNetworks(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	logger.Log(1, username, "status was updated")
 	logger.Log(1, username, "status was updated")
-	json.NewEncoder(w).Encode(user)
+	// re-read and return the new user struct
+	if userChange, err = logic.GetUser(username); err != nil {
+		logger.Log(0, username, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	json.NewEncoder(w).Encode(userChange)
 }
 }
 
 
 // swagger:route PUT /api/users/{username} user updateUser
 // swagger:route PUT /api/users/{username} user updateUser
@@ -485,3 +491,14 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	// Start handling the session
 	go auth.SessionHandler(conn)
 	go auth.SessionHandler(conn)
 }
 }
+
+// getHeaderNetworks returns a slice of networks parsed form the request header.
+func getHeaderNetworks(r *http.Request) ([]string, error) {
+	headerNetworks := r.Header.Get("networks")
+	networksSlice := []string{}
+	err := json.Unmarshal([]byte(headerNetworks), &networksSlice)
+	if err != nil {
+		return nil, err
+	}
+	return networksSlice, nil
+}

+ 9 - 9
docker/Caddyfile

@@ -1,10 +1,10 @@
 # Dashboard
 # Dashboard
-https://dashboard.NETMAKER_BASE_DOMAIN {
+https://dashboard.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	# Apply basic security headers
 	# Apply basic security headers
 	header {
 	header {
-		# Enable cross origin access to *.NETMAKER_BASE_DOMAIN
-		Access-Control-Allow-Origin *.NETMAKER_BASE_DOMAIN
+		# Enable cross origin access to *.{$NM_DOMAIN}
+		Access-Control-Allow-Origin *.{$NM_DOMAIN}
 		# Enable HTTP Strict Transport Security (HSTS)
 		# Enable HTTP Strict Transport Security (HSTS)
 		Strict-Transport-Security "max-age=31536000;"
 		Strict-Transport-Security "max-age=31536000;"
 		# Enable cross-site filter (XSS) and tell browser to block detected attacks
 		# Enable cross-site filter (XSS) and tell browser to block detected attacks
@@ -21,31 +21,31 @@ https://dashboard.NETMAKER_BASE_DOMAIN {
 }
 }
 
 
 # API
 # API
-https://api.NETMAKER_BASE_DOMAIN {
+https://api.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://netmaker:8081
 	reverse_proxy http://netmaker:8081
 }
 }
 
 
 # STUN
 # STUN
-https://stun.NETMAKER_BASE_DOMAIN {
+https://stun.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy netmaker:3478
 	reverse_proxy netmaker:3478
 }
 }
 
 
 # TURN
 # TURN
-https://turn.NETMAKER_BASE_DOMAIN {
+https://turn.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy host.docker.internal:3479
 	reverse_proxy host.docker.internal:3479
 }
 }
 
 
 # TURN API
 # TURN API
-https://turnapi.NETMAKER_BASE_DOMAIN {
+https://turnapi.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
-    reverse_proxy http://host.docker.internal:8089
+	reverse_proxy http://host.docker.internal:8089
 }
 }
 
 
 # MQ
 # MQ
-wss://broker.NETMAKER_BASE_DOMAIN {
+wss://broker.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083`
 	reverse_proxy ws://mq:8883 # For EMQX websockets use `reverse_proxy ws://mq:8083`
 }
 }

+ 11 - 11
docker/Caddyfile-EE

@@ -1,10 +1,10 @@
 # Dashboard
 # Dashboard
-https://dashboard.NETMAKER_BASE_DOMAIN {
+https://dashboard.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	# Apply basic security headers
 	# Apply basic security headers
 	header {
 	header {
-		# Enable cross origin access to *.NETMAKER_BASE_DOMAIN
-		Access-Control-Allow-Origin *.NETMAKER_BASE_DOMAIN
+		# Enable cross origin access to *.{$NM_DOMAIN}
+		Access-Control-Allow-Origin *.{$NM_DOMAIN}
 		# Enable HTTP Strict Transport Security (HSTS)
 		# Enable HTTP Strict Transport Security (HSTS)
 		Strict-Transport-Security "max-age=31536000;"
 		Strict-Transport-Security "max-age=31536000;"
 		# Enable cross-site filter (XSS) and tell browser to block detected attacks
 		# Enable cross-site filter (XSS) and tell browser to block detected attacks
@@ -21,49 +21,49 @@ https://dashboard.NETMAKER_BASE_DOMAIN {
 }
 }
 
 
 # Netmaker Exporter
 # Netmaker Exporter
-https://netmaker-exporter.NETMAKER_BASE_DOMAIN {
+https://netmaker-exporter.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://netmaker-exporter:8085
 	reverse_proxy http://netmaker-exporter:8085
 }
 }
 
 
 # Prometheus
 # Prometheus
-https://prometheus.NETMAKER_BASE_DOMAIN {
+https://prometheus.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://prometheus:9090
 	reverse_proxy http://prometheus:9090
 }
 }
 
 
 # Grafana
 # Grafana
-https://grafana.NETMAKER_BASE_DOMAIN {
+https://grafana.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://grafana:3000
 	reverse_proxy http://grafana:3000
 }
 }
 
 
 # API
 # API
-https://api.NETMAKER_BASE_DOMAIN {
+https://api.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://netmaker:8081
 	reverse_proxy http://netmaker:8081
 }
 }
 
 
 # STUN
 # STUN
-https://stun.NETMAKER_BASE_DOMAIN {
+https://stun.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy netmaker:3478
 	reverse_proxy netmaker:3478
 }
 }
 
 
 # TURN
 # TURN
-https://turn.NETMAKER_BASE_DOMAIN {
+https://turn.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy host.docker.internal:3479
 	reverse_proxy host.docker.internal:3479
 }
 }
 
 
 # TURN API
 # TURN API
-https://turnapi.NETMAKER_BASE_DOMAIN {
+https://turnapi.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy http://host.docker.internal:8089
 	reverse_proxy http://host.docker.internal:8089
 }
 }
 
 
 # MQ
 # MQ
-wss://broker.NETMAKER_BASE_DOMAIN {
+wss://broker.{$NM_DOMAIN} {
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	tls /root/certs/fullchain.pem /root/certs/privkey.pem
 	reverse_proxy ws://mq:8883
 	reverse_proxy ws://mq:8883
 }
 }

+ 1 - 2
functions/helpers_test.go

@@ -18,8 +18,7 @@ var (
 		NetID: "not-a-network",
 		NetID: "not-a-network",
 	}
 	}
 	testExternalClient = &models.ExtClient{
 	testExternalClient = &models.ExtClient{
-		ClientID:    "testExtClient",
-		Description: "ext client for testing",
+		ClientID: "testExtClient",
 	}
 	}
 )
 )
 
 

+ 11 - 11
go.mod

@@ -4,21 +4,21 @@ go 1.19
 
 
 require (
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.2
 	github.com/eclipse/paho.mqtt.golang v1.4.2
-	github.com/go-playground/validator/v10 v10.13.0
+	github.com/go-playground/validator/v10 v10.14.1
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.3.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/handlers v1.5.1
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/mux v1.8.0
 	github.com/lib/pq v1.10.9
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.16
+	github.com/mattn/go-sqlite3 v1.14.17
 	github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f
 	github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f
 	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.2
+	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.8.0
-	golang.org/x/net v0.9.0 // indirect
-	golang.org/x/oauth2 v0.7.0
-	golang.org/x/sys v0.7.0 // indirect
+	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/text v0.9.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
@@ -33,10 +33,10 @@ require (
 )
 )
 
 
 require (
 require (
-	github.com/coreos/go-oidc/v3 v3.5.0
+	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/errors v0.9.1
-	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
+	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	gortc.io/stun v1.23.0
 	gortc.io/stun v1.23.0
 )
 )
 
 
@@ -50,9 +50,9 @@ require (
 
 
 require (
 require (
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/kr/pretty v0.3.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 )
 )
@@ -67,7 +67,7 @@ require (
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/hashicorp/go-version v1.6.0
 	github.com/hashicorp/go-version v1.6.0
 	github.com/josharian/native v1.0.0 // indirect
 	github.com/josharian/native v1.0.0 // indirect
-	github.com/leodido/go-urn v1.2.3 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mdlayher/genetlink v1.2.0 // indirect
 	github.com/mdlayher/genetlink v1.2.0 // indirect
 	github.com/mdlayher/netlink v1.6.0 // indirect
 	github.com/mdlayher/netlink v1.6.0 // indirect

+ 23 - 54
go.sum

@@ -1,6 +1,5 @@
 cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
 cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
-cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
 cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
 filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
@@ -8,11 +7,10 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/c-robinson/iplib v1.0.6 h1:FfZV9BWNrah3BgLCFl5/nDXe4RbOi/C9n+DeXFOv5CQ=
 github.com/c-robinson/iplib v1.0.6 h1:FfZV9BWNrah3BgLCFl5/nDXe4RbOi/C9n+DeXFOv5CQ=
 github.com/c-robinson/iplib v1.0.6/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/c-robinson/iplib v1.0.6/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
-github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
-github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
+github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
+github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,6 +21,8 @@ github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
 github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
 github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -30,8 +30,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
-github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
+github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
+github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -42,7 +42,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -62,15 +61,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
 github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
 github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
-github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
@@ -79,8 +71,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
-github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
+github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
 github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
 github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
 github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
 github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
 github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
@@ -91,7 +83,6 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
 github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
 github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -101,8 +92,6 @@ github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sA
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f h1:BSnJgAfHzEp7o8PYJ7YfwAVHhqu7BYUTggcn/LGlUWY=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f h1:BSnJgAfHzEp7o8PYJ7YfwAVHhqu7BYUTggcn/LGlUWY=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f/go.mod h1:UW/gxgQwSePTvL1KA8QEHsXeYHP4xkoXgbDdN781p34=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f/go.mod h1:UW/gxgQwSePTvL1KA8QEHsXeYHP4xkoXgbDdN781p34=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -118,47 +107,37 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/txn2/txeh v1.4.0 h1:0tdvpA4HGJrj8X3kmrU6o/JFStI009nKxwDpMK5CnRU=
 github.com/txn2/txeh v1.4.0 h1:0tdvpA4HGJrj8X3kmrU6o/JFStI009nKxwDpMK5CnRU=
 github.com/txn2/txeh v1.4.0/go.mod h1:Mgq0hY184zCrDBLgvkIp+9NYGHoYbJcu4xKqUcx1shc=
 github.com/txn2/txeh v1.4.0/go.mod h1:Mgq0hY184zCrDBLgvkIp+9NYGHoYbJcu4xKqUcx1shc=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-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.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
-golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
-golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+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/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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
-golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
-golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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/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.0.0-20220722155255-886fb9371eb4/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=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -173,26 +152,18 @@ 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.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
-golang.org/x/sys v0.7.0/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/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/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
 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=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 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.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-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=
 golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg=
 golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg=
@@ -206,12 +177,10 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

+ 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.19.0
+        image: gravitl/netclient:v0.20.2
         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.19.0
+        image: gravitl/netclient:v0.20.2
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

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

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

+ 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.19.0
+        image: gravitl/netmaker-ui:v0.20.2
         ports:
         ports:
         - containerPort: 443
         - containerPort: 443
         env:
         env:

+ 15 - 0
logic/enrollmentkey.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"golang.org/x/exp/slices"
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
@@ -68,6 +69,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 }
 }
 
 
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB
+// TODO drop double pointer
 func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
 func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
 	currentKeys, err := getEnrollmentKeysMap()
 	currentKeys, err := getEnrollmentKeysMap()
 	if err != nil {
 	if err != nil {
@@ -222,3 +224,16 @@ func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
 	}
 	}
 	return currentKeys, nil
 	return currentKeys, nil
 }
 }
+
+// UserHasNetworksAccess - checks if a user `u` has access to all `networks`
+func UserHasNetworksAccess(networks []string, u *models.User) bool {
+	if u.IsAdmin {
+		return true
+	}
+	for _, n := range networks {
+		if !slices.Contains(u.Networks, n) {
+			return false
+		}
+	}
+	return true
+}

+ 74 - 0
logic/enrollmentkey_test.go

@@ -204,3 +204,77 @@ func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 
 
 	removeAllEnrollments()
 	removeAllEnrollments()
 }
 }
+
+func TestHasNetworksAccess(t *testing.T) {
+	type Case struct {
+		// network names
+		n []string
+		u models.User
+	}
+	pass := []Case{
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n1", "n2"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{},
+				IsAdmin:  true,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n1", "n2", "n3"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n2"},
+			u: models.User{
+				Networks: []string{"n2"},
+				IsAdmin:  false,
+			},
+		},
+	}
+	deny := []Case{
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n2"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n1", "n2"},
+			u: models.User{
+				Networks: []string{"n3"},
+				IsAdmin:  false,
+			},
+		},
+		{
+			n: []string{"n2"},
+			u: models.User{
+				Networks: []string{"n1"},
+				IsAdmin:  false,
+			},
+		},
+	}
+	for _, tc := range pass {
+		assert.True(t, UserHasNetworksAccess(tc.n, &tc.u))
+	}
+	for _, tc := range deny {
+		assert.False(t, UserHasNetworksAccess(tc.n, &tc.u))
+	}
+}

+ 22 - 25
logic/extpeers.go

@@ -174,6 +174,11 @@ func CreateExtClient(extclient *models.ExtClient) error {
 	}
 	}
 
 
 	extclient.LastModified = time.Now().Unix()
 	extclient.LastModified = time.Now().Unix()
+	return SaveExtClient(extclient)
+}
+
+// SaveExtClient - saves an ext client to database
+func SaveExtClient(extclient *models.ExtClient) error {
 	key, err := GetRecordKey(extclient.ClientID, extclient.Network)
 	key, err := GetRecordKey(extclient.ClientID, extclient.Network)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -188,35 +193,27 @@ func CreateExtClient(extclient *models.ExtClient) error {
 	return SetNetworkNodesLastModified(extclient.Network)
 	return SetNetworkNodesLastModified(extclient.Network)
 }
 }
 
 
-// UpdateExtClient - only supports name changes right now
-func UpdateExtClient(newclientid string, network string, enabled bool, client *models.ExtClient, newACLs map[string]struct{}) (*models.ExtClient, error) {
-	err := DeleteExtClient(network, client.ClientID)
+// UpdateExtClient - updates an ext client with new values
+func UpdateExtClient(old *models.ExtClient, update *models.CustomExtClient) (*models.ExtClient, error) {
+	new := old
+	err := DeleteExtClient(old.Network, old.ClientID)
 	if err != nil {
 	if err != nil {
-		return client, err
+		return new, err
 	}
 	}
-	if newclientid != client.ClientID { // name change only
-		client.ClientID = newclientid
-		client.LastModified = time.Now().Unix()
-		data, err := json.Marshal(&client)
-		if err != nil {
-			return nil, err
-		}
-		key, err := GetRecordKey(client.ClientID, client.Network)
-		if err != nil {
-			return nil, err
-		}
-		if err = database.Insert(key, string(data), database.EXT_CLIENT_TABLE_NAME); err != nil {
-			return client, err
-		}
-		return client, nil
+	new.ClientID = update.ClientID
+	if update.PublicKey != "" && old.PublicKey != update.PublicKey {
+		new.PublicKey = update.PublicKey
+	}
+	if update.DNS != "" && update.DNS != old.DNS {
+		new.DNS = update.DNS
+	}
+	if update.Enabled != old.Enabled {
+		new.Enabled = update.Enabled
 	}
 	}
-	client.ClientID = newclientid
-	client.Enabled = enabled
-	SetClientACLs(client, newACLs)
-	if err = CreateExtClient(client); err != nil {
-		return client, err
+	if update.ExtraAllowedIPs != nil && StringDifference(old.ExtraAllowedIPs, update.ExtraAllowedIPs) != nil {
+		new.ExtraAllowedIPs = update.ExtraAllowedIPs
 	}
 	}
-	return client, err
+	return new, CreateExtClient(new)
 }
 }
 
 
 // GetExtClientsByID - gets the clients of attached gateway
 // GetExtClientsByID - gets the clients of attached gateway

+ 6 - 2
logic/gateway.go

@@ -96,7 +96,7 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 }
 }
 
 
 // CreateIngressGateway - creates an ingress gateway
 // CreateIngressGateway - creates an ingress gateway
-func CreateIngressGateway(netid string, nodeid string, failover bool) (models.Node, error) {
+func CreateIngressGateway(netid string, nodeid string, ingress models.IngressRequest) (models.Node, error) {
 
 
 	node, err := GetNodeByID(nodeid)
 	node, err := GetNodeByID(nodeid)
 	if err != nil {
 	if err != nil {
@@ -112,6 +112,9 @@ func CreateIngressGateway(netid string, nodeid string, failover bool) (models.No
 	if host.FirewallInUse == models.FIREWALL_NONE {
 	if host.FirewallInUse == models.FIREWALL_NONE {
 		return models.Node{}, errors.New("firewall is not supported for ingress gateways")
 		return models.Node{}, errors.New("firewall is not supported for ingress gateways")
 	}
 	}
+	if host.NatType != models.NAT_Types.Public {
+		return models.Node{}, errors.New("ingress cannot be created on nodes behind NAT")
+	}
 
 
 	network, err := GetParentNetwork(netid)
 	network, err := GetParentNetwork(netid)
 	if err != nil {
 	if err != nil {
@@ -120,8 +123,9 @@ func CreateIngressGateway(netid string, nodeid string, failover bool) (models.No
 	node.IsIngressGateway = true
 	node.IsIngressGateway = true
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressGatewayRange6 = network.AddressRange6
+	node.IngressDNS = ingress.ExtclientDNS
 	node.SetLastModified()
 	node.SetLastModified()
-	if failover && servercfg.Is_EE {
+	if ingress.Failover && servercfg.Is_EE {
 		node.Failover = true
 		node.Failover = true
 	}
 	}
 	data, err := json.Marshal(&node)
 	data, err := json.Marshal(&node)

+ 5 - 0
logic/hosts.go

@@ -111,6 +111,7 @@ func CreateHost(h *models.Host) error {
 	}
 	}
 	h.HostPass = string(hash)
 	h.HostPass = string(hash)
 	h.AutoUpdate = servercfg.AutoUpdateEnabled()
 	h.AutoUpdate = servercfg.AutoUpdateEnabled()
+	h.EndpointDetection = servercfg.EndpointDetectionEnabled()
 	// if another server has already updated proxyenabled, leave it alone
 	// if another server has already updated proxyenabled, leave it alone
 	if !h.ProxyEnabledSet {
 	if !h.ProxyEnabledSet {
 		log.Println("checking default proxy", servercfg.GetServerConfig().DefaultProxyMode)
 		log.Println("checking default proxy", servercfg.GetServerConfig().DefaultProxyMode)
@@ -173,6 +174,10 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		currHost.ListenPort = newHost.ListenPort
 		currHost.ListenPort = newHost.ListenPort
 		sendPeerUpdate = true
 		sendPeerUpdate = true
 	}
 	}
+	if newHost.WgPublicListenPort != 0 && currHost.WgPublicListenPort != newHost.WgPublicListenPort {
+		currHost.WgPublicListenPort = newHost.WgPublicListenPort
+		sendPeerUpdate = true
+	}
 	if newHost.ProxyListenPort != 0 && currHost.ProxyListenPort != newHost.ProxyListenPort {
 	if newHost.ProxyListenPort != 0 && currHost.ProxyListenPort != newHost.ProxyListenPort {
 		currHost.ProxyListenPort = newHost.ProxyListenPort
 		currHost.ProxyListenPort = newHost.ProxyListenPort
 		sendPeerUpdate = true
 		sendPeerUpdate = true

+ 12 - 0
logic/nodes.go

@@ -41,6 +41,18 @@ func GetNetworkNodes(network string) ([]models.Node, error) {
 	return GetNetworkNodesMemory(allnodes, network), nil
 	return GetNetworkNodesMemory(allnodes, network), nil
 }
 }
 
 
+// GetHostNodes - fetches all nodes part of the host
+func GetHostNodes(host *models.Host) []models.Node {
+	nodes := []models.Node{}
+	for _, nodeID := range host.Nodes {
+		node, err := GetNodeByID(nodeID)
+		if err == nil {
+			nodes = append(nodes, node)
+		}
+	}
+	return nodes
+}
+
 // GetNetworkNodesMemory - gets all nodes belonging to a network from list in memory
 // GetNetworkNodesMemory - gets all nodes belonging to a network from list in memory
 func GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node {
 func GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node {
 	var nodes = []models.Node{}
 	var nodes = []models.Node{}

+ 17 - 1
logic/peers.go

@@ -120,6 +120,9 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 		HostNetworkInfo: models.HostInfoMap{},
 		HostNetworkInfo: models.HostInfoMap{},
 	}
 	}
 
 
+	// endpoint detection always comes from the server
+	hostPeerUpdate.Host.EndpointDetection = servercfg.EndpointDetectionEnabled()
+
 	logger.Log(1, "peer update for host", host.ID.String())
 	logger.Log(1, "peer update for host", host.ID.String())
 	peerIndexMap := make(map[string]int)
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {
 	for _, nodeID := range host.Nodes {
@@ -177,11 +180,12 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 				}
 				}
 				peerConfig.Endpoint = &net.UDPAddr{
 				peerConfig.Endpoint = &net.UDPAddr{
 					IP:   peerHost.EndpointIP,
 					IP:   peerHost.EndpointIP,
-					Port: peerHost.ListenPort,
+					Port: getPeerWgListenPort(peerHost),
 				}
 				}
 
 
 				if uselocal {
 				if uselocal {
 					peerConfig.Endpoint.IP = peer.LocalAddress.IP
 					peerConfig.Endpoint.IP = peer.LocalAddress.IP
+					peerConfig.Endpoint.Port = peerHost.ListenPort
 				}
 				}
 				allowedips := GetAllowedIPs(&node, &peer, nil)
 				allowedips := GetAllowedIPs(&node, &peer, nil)
 				if peer.IsIngressGateway {
 				if peer.IsIngressGateway {
@@ -382,9 +386,21 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 	return hostPeerUpdate, nil
 	return hostPeerUpdate, nil
 }
 }
 
 
+// getPeerWgListenPort - fetches the wg listen port for the host
+func getPeerWgListenPort(host *models.Host) int {
+	peerPort := host.ListenPort
+	if host.WgPublicListenPort != 0 {
+		peerPort = host.WgPublicListenPort
+	}
+	return peerPort
+}
+
 // GetPeerListenPort - given a host, retrieve it's appropriate listening port
 // GetPeerListenPort - given a host, retrieve it's appropriate listening port
 func GetPeerListenPort(host *models.Host) int {
 func GetPeerListenPort(host *models.Host) int {
 	peerPort := host.ListenPort
 	peerPort := host.ListenPort
+	if host.WgPublicListenPort != 0 {
+		peerPort = host.WgPublicListenPort
+	}
 	if host.ProxyEnabled {
 	if host.ProxyEnabled {
 		if host.PublicListenPort != 0 {
 		if host.PublicListenPort != 0 {
 			peerPort = host.PublicListenPort
 			peerPort = host.PublicListenPort

+ 14 - 6
logic/security.go

@@ -18,6 +18,8 @@ const (
 	ALL_NETWORK_ACCESS = "THIS_USER_HAS_ALL"
 	ALL_NETWORK_ACCESS = "THIS_USER_HAS_ALL"
 
 
 	master_uname     = "masteradministrator"
 	master_uname     = "masteradministrator"
+	Forbidden_Msg    = "forbidden"
+	Forbidden_Err    = models.Error(Forbidden_Msg)
 	Unauthorized_Msg = "unauthorized"
 	Unauthorized_Msg = "unauthorized"
 	Unauthorized_Err = models.Error(Unauthorized_Msg)
 	Unauthorized_Err = models.Error(Unauthorized_Msg)
 )
 )
@@ -27,8 +29,9 @@ func SecurityCheck(reqAdmin bool, 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.StatusUnauthorized, Message: Unauthorized_Msg,
+			Code: http.StatusForbidden, Message: Forbidden_Msg,
 		}
 		}
+		r.Header.Set("ismaster", "no")
 
 
 		var params = mux.Vars(r)
 		var params = mux.Vars(r)
 		bearerToken := r.Header.Get("Authorization")
 		bearerToken := r.Header.Get("Authorization")
@@ -51,6 +54,10 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 			ReturnErrorResponse(w, r, errorResponse)
 			ReturnErrorResponse(w, r, errorResponse)
 			return
 			return
 		}
 		}
+		// detect masteradmin
+		if len(networks) > 0 && networks[0] == ALL_NETWORK_ACCESS {
+			r.Header.Set("ismaster", "yes")
+		}
 		networksJson, err := json.Marshal(&networks)
 		networksJson, err := json.Marshal(&networks)
 		if err != nil {
 		if err != nil {
 			ReturnErrorResponse(w, r, errorResponse)
 			ReturnErrorResponse(w, r, errorResponse)
@@ -66,7 +73,7 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 func NetUserSecurityCheck(isNodes, isClients bool, next http.Handler) http.HandlerFunc {
 func NetUserSecurityCheck(isNodes, isClients bool, 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.StatusUnauthorized, Message: "unauthorized",
+			Code: http.StatusForbidden, Message: Forbidden_Msg,
 		}
 		}
 		r.Header.Set("ismaster", "no")
 		r.Header.Set("ismaster", "no")
 
 
@@ -145,6 +152,7 @@ func UserPermissions(reqAdmin bool, netname string, token string) ([]string, str
 	}
 	}
 	//all endpoints here require master so not as complicated
 	//all endpoints here require master so not as complicated
 	if authenticateMaster(authToken) {
 	if authenticateMaster(authToken) {
+		// TODO log in as an actual admin user
 		return []string{ALL_NETWORK_ACCESS}, master_uname, nil
 		return []string{ALL_NETWORK_ACCESS}, master_uname, nil
 	}
 	}
 	username, networks, isadmin, err := VerifyUserToken(authToken)
 	username, networks, isadmin, err := VerifyUserToken(authToken)
@@ -152,7 +160,7 @@ func UserPermissions(reqAdmin bool, netname string, token string) ([]string, str
 		return nil, username, Unauthorized_Err
 		return nil, username, Unauthorized_Err
 	}
 	}
 	if !isadmin && reqAdmin {
 	if !isadmin && reqAdmin {
-		return nil, username, Unauthorized_Err
+		return nil, username, Forbidden_Err
 	}
 	}
 	userNetworks = networks
 	userNetworks = networks
 	if isadmin {
 	if isadmin {
@@ -160,10 +168,10 @@ func UserPermissions(reqAdmin bool, netname string, token string) ([]string, str
 	}
 	}
 	// check network admin access
 	// check network admin access
 	if len(netname) > 0 && (len(userNetworks) == 0 || !authenticateNetworkUser(netname, userNetworks)) {
 	if len(netname) > 0 && (len(userNetworks) == 0 || !authenticateNetworkUser(netname, userNetworks)) {
-		return nil, username, Unauthorized_Err
+		return nil, username, Forbidden_Err
 	}
 	}
 	if isEE && len(netname) > 0 && !pro.IsUserNetAdmin(netname, username) {
 	if isEE && len(netname) > 0 && !pro.IsUserNetAdmin(netname, username) {
-		return nil, "", Unauthorized_Err
+		return nil, "", Forbidden_Err
 	}
 	}
 	return userNetworks, username, nil
 	return userNetworks, username, nil
 }
 }
@@ -193,7 +201,7 @@ func authenticateDNSToken(tokenString string) bool {
 func ContinueIfUserMatch(next http.Handler) http.HandlerFunc {
 func ContinueIfUserMatch(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.StatusUnauthorized, Message: Unauthorized_Msg,
+			Code: http.StatusForbidden, Message: Forbidden_Msg,
 		}
 		}
 		var params = mux.Vars(r)
 		var params = mux.Vars(r)
 		var requestedUser = params["username"]
 		var requestedUser = params["username"]

+ 1 - 0
logic/users.go

@@ -12,6 +12,7 @@ import (
 )
 )
 
 
 // GetUser - gets a user
 // GetUser - gets a user
+// TODO support "masteradmin"
 func GetUser(username string) (*models.User, error) {
 func GetUser(username string) (*models.User, error) {
 
 
 	var user models.User
 	var user models.User

+ 22 - 1
main.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
+	"path/filepath"
 	"runtime/debug"
 	"runtime/debug"
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
@@ -26,9 +27,10 @@ import (
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/serverctl"
 	"github.com/gravitl/netmaker/serverctl"
 	stunserver "github.com/gravitl/netmaker/stun-server"
 	stunserver "github.com/gravitl/netmaker/stun-server"
+	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v0.19.1"
+var version = "v0.20.2"
 
 
 // Start DB Connection and start API Request Handler
 // Start DB Connection and start API Request Handler
 func main() {
 func main() {
@@ -179,6 +181,25 @@ func runMessageQueue(wg *sync.WaitGroup, ctx context.Context) {
 func setVerbosity() {
 func setVerbosity() {
 	verbose := int(servercfg.GetVerbosity())
 	verbose := int(servercfg.GetVerbosity())
 	logger.Verbosity = verbose
 	logger.Verbosity = verbose
+	logLevel := &slog.LevelVar{}
+	replace := func(groups []string, a slog.Attr) slog.Attr {
+		if a.Key == slog.SourceKey {
+			a.Value = slog.StringValue(filepath.Base(a.Value.String()))
+		}
+		return a
+	}
+	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replace, Level: logLevel}))
+	slog.SetDefault(logger)
+	switch verbose {
+	case 4:
+		logLevel.Set(slog.LevelDebug)
+	case 3:
+		logLevel.Set(slog.LevelInfo)
+	case 2:
+		logLevel.Set(slog.LevelWarn)
+	default:
+		logLevel.Set(slog.LevelError)
+	}
 }
 }
 
 
 func setGarbageCollection() {
 func setGarbageCollection() {

+ 28 - 22
models/api_host.go

@@ -7,28 +7,33 @@ import (
 
 
 // ApiHost - the host struct for API usage
 // ApiHost - the host struct for API usage
 type ApiHost struct {
 type ApiHost struct {
-	ID               string   `json:"id"`
-	Verbosity        int      `json:"verbosity"`
-	FirewallInUse    string   `json:"firewallinuse"`
-	Version          string   `json:"version"`
-	Name             string   `json:"name"`
-	OS               string   `json:"os"`
-	Debug            bool     `json:"debug"`
-	IsStatic         bool     `json:"isstatic"`
-	ListenPort       int      `json:"listenport"`
-	LocalListenPort  int      `json:"locallistenport"`
-	ProxyListenPort  int      `json:"proxy_listen_port"`
-	PublicListenPort int      `json:"public_listen_port" yaml:"public_listen_port"`
-	MTU              int      `json:"mtu" yaml:"mtu"`
-	Interfaces       []Iface  `json:"interfaces" yaml:"interfaces"`
-	DefaultInterface string   `json:"defaultinterface" yaml:"defautlinterface"`
-	EndpointIP       string   `json:"endpointip" yaml:"endpointip"`
-	PublicKey        string   `json:"publickey"`
-	MacAddress       string   `json:"macaddress"`
-	InternetGateway  string   `json:"internetgateway"`
-	Nodes            []string `json:"nodes"`
-	ProxyEnabled     bool     `json:"proxy_enabled" yaml:"proxy_enabled"`
-	IsDefault        bool     `json:"isdefault" yaml:"isdefault"`
+	ID                 string   `json:"id"`
+	Verbosity          int      `json:"verbosity"`
+	FirewallInUse      string   `json:"firewallinuse"`
+	Version            string   `json:"version"`
+	Name               string   `json:"name"`
+	OS                 string   `json:"os"`
+	Debug              bool     `json:"debug"`
+	IsStatic           bool     `json:"isstatic"`
+	ListenPort         int      `json:"listenport"`
+	LocalListenPort    int      `json:"locallistenport"`
+	ProxyListenPort    int      `json:"proxy_listen_port"`
+	PublicListenPort   int      `json:"public_listen_port" yaml:"public_listen_port"`
+	WgPublicListenPort int      `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
+	MTU                int      `json:"mtu" yaml:"mtu"`
+	Interfaces         []Iface  `json:"interfaces" yaml:"interfaces"`
+	DefaultInterface   string   `json:"defaultinterface" yaml:"defautlinterface"`
+	EndpointIP         string   `json:"endpointip" yaml:"endpointip"`
+	PublicKey          string   `json:"publickey"`
+	MacAddress         string   `json:"macaddress"`
+	InternetGateway    string   `json:"internetgateway"`
+	Nodes              []string `json:"nodes"`
+	ProxyEnabled       bool     `json:"proxy_enabled" yaml:"proxy_enabled"`
+	IsDefault          bool     `json:"isdefault" yaml:"isdefault"`
+	IsRelayed          bool     `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
+	RelayedBy          string   `json:"relayed_by" bson:"relayed_by" yaml:"relayed_by"`
+	IsRelay            bool     `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
+	RelayedHosts       []string `json:"relay_hosts" bson:"relay_hosts" yaml:"relay_hosts"`
 }
 }
 
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -56,6 +61,7 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.Nodes = h.Nodes
 	a.Nodes = h.Nodes
 	a.ProxyEnabled = h.ProxyEnabled
 	a.ProxyEnabled = h.ProxyEnabled
 	a.PublicListenPort = h.PublicListenPort
 	a.PublicListenPort = h.PublicListenPort
+	a.WgPublicListenPort = h.WgPublicListenPort
 	a.ProxyListenPort = h.ProxyListenPort
 	a.ProxyListenPort = h.ProxyListenPort
 	a.PublicKey = h.PublicKey.String()
 	a.PublicKey = h.PublicKey.String()
 	a.Verbosity = h.Verbosity
 	a.Verbosity = h.Verbosity

+ 3 - 0
models/api_node.go

@@ -33,6 +33,7 @@ type ApiNode struct {
 	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
 	EgressGatewayNatEnabled bool     `json:"egressgatewaynatenabled"`
 	FailoverNode            string   `json:"failovernode"`
 	FailoverNode            string   `json:"failovernode"`
 	DNSOn                   bool     `json:"dnson"`
 	DNSOn                   bool     `json:"dnson"`
+	IngressDns              string   `json:"ingressdns"`
 	Server                  string   `json:"server"`
 	Server                  string   `json:"server"`
 	InternetGateway         string   `json:"internetgateway"`
 	InternetGateway         string   `json:"internetgateway"`
 	Connected               bool     `json:"connected"`
 	Connected               bool     `json:"connected"`
@@ -64,6 +65,7 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.DNSOn = a.DNSOn
 	convertedNode.DNSOn = a.DNSOn
+	convertedNode.IngressDNS = a.IngressDns
 	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)
@@ -152,6 +154,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 		apiNode.FailoverNode = ""
 		apiNode.FailoverNode = ""
 	}
 	}
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.DNSOn = nm.DNSOn
+	apiNode.IngressDns = nm.IngressDNS
 	apiNode.Server = nm.Server
 	apiNode.Server = nm.Server
 	apiNode.InternetGateway = nm.InternetGateway.String()
 	apiNode.InternetGateway = nm.InternetGateway.String()
 	if isEmptyAddr(apiNode.InternetGateway) {
 	if isEmptyAddr(apiNode.InternetGateway) {

+ 11 - 1
models/extclient.go

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

+ 54 - 37
models/host.go

@@ -41,38 +41,44 @@ const WIREGUARD_INTERFACE = "netmaker"
 
 
 // Host - represents a host on the network
 // Host - represents a host on the network
 type Host struct {
 type Host struct {
-	ID               uuid.UUID        `json:"id" yaml:"id"`
-	Verbosity        int              `json:"verbosity" yaml:"verbosity"`
-	FirewallInUse    string           `json:"firewallinuse" yaml:"firewallinuse"`
-	Version          string           `json:"version" yaml:"version"`
-	IPForwarding     bool             `json:"ipforwarding" yaml:"ipforwarding"`
-	DaemonInstalled  bool             `json:"daemoninstalled" yaml:"daemoninstalled"`
-	AutoUpdate       bool             `json:"autoupdate" yaml:"autoupdate"`
-	HostPass         string           `json:"hostpass" yaml:"hostpass"`
-	Name             string           `json:"name" yaml:"name"`
-	OS               string           `json:"os" yaml:"os"`
-	Interface        string           `json:"interface" yaml:"interface"`
-	Debug            bool             `json:"debug" yaml:"debug"`
-	ListenPort       int              `json:"listenport" yaml:"listenport"`
-	PublicListenPort int              `json:"public_listen_port" yaml:"public_listen_port"`
-	ProxyListenPort  int              `json:"proxy_listen_port" yaml:"proxy_listen_port"`
-	MTU              int              `json:"mtu" yaml:"mtu"`
-	PublicKey        wgtypes.Key      `json:"publickey" yaml:"publickey"`
-	MacAddress       net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
-	TrafficKeyPublic []byte           `json:"traffickeypublic" yaml:"traffickeypublic"`
-	InternetGateway  net.UDPAddr      `json:"internetgateway" yaml:"internetgateway"`
-	Nodes            []string         `json:"nodes" yaml:"nodes"`
-	Interfaces       []Iface          `json:"interfaces" yaml:"interfaces"`
-	DefaultInterface string           `json:"defaultinterface" yaml:"defaultinterface"`
-	EndpointIP       net.IP           `json:"endpointip" yaml:"endpointip"`
-	ProxyEnabled     bool             `json:"proxy_enabled" yaml:"proxy_enabled"`
-	ProxyEnabledSet  bool             `json:"proxy_enabled_updated" yaml:"proxy_enabled_updated"`
-	IsDocker         bool             `json:"isdocker" yaml:"isdocker"`
-	IsK8S            bool             `json:"isk8s" yaml:"isk8s"`
-	IsStatic         bool             `json:"isstatic" yaml:"isstatic"`
-	IsDefault        bool             `json:"isdefault" yaml:"isdefault"`
-	NatType          string           `json:"nat_type,omitempty" yaml:"nat_type,omitempty"`
-	TurnEndpoint     *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
+	ID                 uuid.UUID        `json:"id" yaml:"id"`
+	Verbosity          int              `json:"verbosity" yaml:"verbosity"`
+	FirewallInUse      string           `json:"firewallinuse" yaml:"firewallinuse"`
+	Version            string           `json:"version" yaml:"version"`
+	IPForwarding       bool             `json:"ipforwarding" yaml:"ipforwarding"`
+	DaemonInstalled    bool             `json:"daemoninstalled" yaml:"daemoninstalled"`
+	AutoUpdate         bool             `json:"autoupdate" yaml:"autoupdate"`
+	EndpointDetection  bool             `json:"endpointdetection" yaml:"endpointdetection"`
+	HostPass           string           `json:"hostpass" yaml:"hostpass"`
+	Name               string           `json:"name" yaml:"name"`
+	OS                 string           `json:"os" yaml:"os"`
+	Interface          string           `json:"interface" yaml:"interface"`
+	Debug              bool             `json:"debug" yaml:"debug"`
+	ListenPort         int              `json:"listenport" yaml:"listenport"`
+	PublicListenPort   int              `json:"public_listen_port" yaml:"public_listen_port"`
+	WgPublicListenPort int              `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
+	ProxyListenPort    int              `json:"proxy_listen_port" yaml:"proxy_listen_port"`
+	MTU                int              `json:"mtu" yaml:"mtu"`
+	PublicKey          wgtypes.Key      `json:"publickey" yaml:"publickey"`
+	MacAddress         net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
+	TrafficKeyPublic   []byte           `json:"traffickeypublic" yaml:"traffickeypublic"`
+	InternetGateway    net.UDPAddr      `json:"internetgateway" yaml:"internetgateway"`
+	Nodes              []string         `json:"nodes" yaml:"nodes"`
+	IsRelayed          bool             `json:"isrelayed" yaml:"isrelayed"`
+	RelayedBy          string           `json:"relayed_by" yaml:"relayed_by"`
+	IsRelay            bool             `json:"isrelay" yaml:"isrelay"`
+	RelayedHosts       []string         `json:"relay_hosts" yaml:"relay_hosts"`
+	Interfaces         []Iface          `json:"interfaces" yaml:"interfaces"`
+	DefaultInterface   string           `json:"defaultinterface" yaml:"defaultinterface"`
+	EndpointIP         net.IP           `json:"endpointip" yaml:"endpointip"`
+	ProxyEnabled       bool             `json:"proxy_enabled" yaml:"proxy_enabled"`
+	ProxyEnabledSet    bool             `json:"proxy_enabled_updated" yaml:"proxy_enabled_updated"`
+	IsDocker           bool             `json:"isdocker" yaml:"isdocker"`
+	IsK8S              bool             `json:"isk8s" yaml:"isk8s"`
+	IsStatic           bool             `json:"isstatic" yaml:"isstatic"`
+	IsDefault          bool             `json:"isdefault" yaml:"isdefault"`
+	NatType            string           `json:"nat_type,omitempty" yaml:"nat_type,omitempty"`
+	TurnEndpoint       *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
 }
 }
 
 
 // FormatBool converts a boolean to a [yes|no] string
 // FormatBool converts a boolean to a [yes|no] string
@@ -117,6 +123,16 @@ const (
 	UpdateKeys = "UPDATE_KEYS"
 	UpdateKeys = "UPDATE_KEYS"
 )
 )
 
 
+// SignalAction - turn peer signal action
+type SignalAction string
+
+const (
+	// Disconnect - action to stop using turn connection
+	Disconnect SignalAction = "DISCONNECT"
+	// ConnNegotiation - action to negotiate connection between peers
+	ConnNegotiation SignalAction = "CONNECTION_NEGOTIATION"
+)
+
 // HostUpdate - struct for host update
 // HostUpdate - struct for host update
 type HostUpdate struct {
 type HostUpdate struct {
 	Action HostMqAction
 	Action HostMqAction
@@ -133,11 +149,12 @@ type HostTurnRegister struct {
 
 
 // Signal - struct for signalling peer
 // Signal - struct for signalling peer
 type Signal struct {
 type Signal struct {
-	Server            string `json:"server"`
-	FromHostPubKey    string `json:"from_host_pubkey"`
-	TurnRelayEndpoint string `json:"turn_relay_addr"`
-	ToHostPubKey      string `json:"to_host_pubkey"`
-	Reply             bool   `json:"reply"`
+	Server            string       `json:"server"`
+	FromHostPubKey    string       `json:"from_host_pubkey"`
+	TurnRelayEndpoint string       `json:"turn_relay_addr"`
+	ToHostPubKey      string       `json:"to_host_pubkey"`
+	Reply             bool         `json:"reply"`
+	Action            SignalAction `json:"action"`
 }
 }
 
 
 // RegisterMsg - login message struct for hosts to join via SSO login
 // RegisterMsg - login message struct for hosts to join via SSO login

+ 0 - 1
models/network.go

@@ -23,7 +23,6 @@ type Network struct {
 	IsIPv4              string                `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
 	IsIPv4              string                `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
 	IsIPv6              string                `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`
 	IsIPv6              string                `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`
 	DefaultUDPHolePunch string                `json:"defaultudpholepunch" bson:"defaultudpholepunch" validate:"checkyesorno"`
 	DefaultUDPHolePunch string                `json:"defaultudpholepunch" bson:"defaultudpholepunch" validate:"checkyesorno"`
-	DefaultExtClientDNS string                `json:"defaultextclientdns" bson:"defaultextclientdns"`
 	DefaultMTU          int32                 `json:"defaultmtu" bson:"defaultmtu"`
 	DefaultMTU          int32                 `json:"defaultmtu" bson:"defaultmtu"`
 	DefaultACL          string                `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
 	DefaultACL          string                `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
 	ProSettings         *promodels.ProNetwork `json:"prosettings,omitempty" bson:"prosettings,omitempty" yaml:"prosettings,omitempty"`
 	ProSettings         *promodels.ProNetwork `json:"prosettings,omitempty" bson:"prosettings,omitempty" yaml:"prosettings,omitempty"`

+ 1 - 0
models/node.go

@@ -73,6 +73,7 @@ type CommonNode struct {
 	RelayedBy           string        `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	RelayedBy           string        `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	IsRelay             bool          `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
 	IsRelay             bool          `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
 	RelayedNodes        []string      `json:"relaynodes" yaml:"relayedNodes"`
 	RelayedNodes        []string      `json:"relaynodes" yaml:"relayedNodes"`
+	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"`
 }
 }

+ 7 - 6
models/structs.go

@@ -14,12 +14,6 @@ const (
 	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
 	PLACEHOLDER_TOKEN_TEXT = "ACCESS_TOKEN"
 )
 )
 
 
-// CustomExtClient - struct for CustomExtClient params
-type CustomExtClient struct {
-	ClientID  string `json:"clientid"`
-	PublicKey string `json:"publickey,omitempty"`
-}
-
 // AuthParams - struct for auth params
 // AuthParams - struct for auth params
 type AuthParams struct {
 type AuthParams struct {
 	MacAddress string `json:"macaddress"`
 	MacAddress string `json:"macaddress"`
@@ -170,6 +164,12 @@ type HostRelayRequest struct {
 	RelayedHosts []string `json:"relayed_hosts"`
 	RelayedHosts []string `json:"relayed_hosts"`
 }
 }
 
 
+// IngressRequest - ingress request struct
+type IngressRequest struct {
+	ExtclientDNS string `json:"extclientdns"`
+	Failover     bool   `json:"failover"`
+}
+
 // ServerUpdateData - contains data to configure server
 // ServerUpdateData - contains data to configure server
 // and if it should set peers
 // and if it should set peers
 type ServerUpdateData struct {
 type ServerUpdateData struct {
@@ -201,6 +201,7 @@ type TrafficKeys struct {
 // HostPull - response of a host's pull
 // HostPull - response of a host's pull
 type HostPull struct {
 type HostPull struct {
 	Host         Host                 `json:"host" yaml:"host"`
 	Host         Host                 `json:"host" yaml:"host"`
+	Nodes        []Node               `json:"nodes" yaml:"nodes"`
 	Peers        []wgtypes.PeerConfig `json:"peers" yaml:"peers"`
 	Peers        []wgtypes.PeerConfig `json:"peers" yaml:"peers"`
 	ServerConfig ServerConfig         `json:"server_config" yaml:"server_config"`
 	ServerConfig ServerConfig         `json:"server_config" yaml:"server_config"`
 	PeerIDs      PeerMap              `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
 	PeerIDs      PeerMap              `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`

+ 59 - 51
mq/handlers.go

@@ -10,86 +10,86 @@ import (
 	mqtt "github.com/eclipse/paho.mqtt.golang"
 	mqtt "github.com/eclipse/paho.mqtt.golang"
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/logic/hostactions"
 	"github.com/gravitl/netmaker/models"
 	"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/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 )
 
 
 // DefaultHandler default message queue handler  -- NOT USED
 // DefaultHandler default message queue handler  -- NOT USED
 func DefaultHandler(client mqtt.Client, msg mqtt.Message) {
 func DefaultHandler(client mqtt.Client, msg mqtt.Message) {
-	logger.Log(0, "MQTT Message: Topic: ", string(msg.Topic()), " Message: ", string(msg.Payload()))
+	slog.Info("mqtt default handler", "topic", msg.Topic(), "message", msg.Payload())
 }
 }
 
 
 // UpdateNode  message Handler -- handles updates from client nodes
 // UpdateNode  message Handler -- handles updates from client nodes
 func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 	id, err := getID(msg.Topic())
 	id, err := getID(msg.Topic())
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
+		slog.Error("error getting node.ID ", "topic", msg.Topic(), "error", err)
 		return
 		return
 	}
 	}
 	currentNode, err := logic.GetNodeByID(id)
 	currentNode, err := logic.GetNodeByID(id)
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting node ", id, err.Error())
+		slog.Error("error getting node", "id", id, "error", err)
 		return
 		return
 	}
 	}
 	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 	if decryptErr != nil {
 	if decryptErr != nil {
-		logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
+		slog.Error("failed to decrypt message for node", "id", id, "error", decryptErr)
 		return
 		return
 	}
 	}
 	var newNode models.Node
 	var newNode models.Node
 	if err := json.Unmarshal(decrypted, &newNode); err != nil {
 	if err := json.Unmarshal(decrypted, &newNode); err != nil {
-		logger.Log(1, "error unmarshaling payload ", err.Error())
+		slog.Error("error unmarshaling payload", "error", err)
 		return
 		return
 	}
 	}
 
 
 	ifaceDelta := logic.IfaceDelta(&currentNode, &newNode)
 	ifaceDelta := logic.IfaceDelta(&currentNode, &newNode)
 	if servercfg.Is_EE && ifaceDelta {
 	if servercfg.Is_EE && ifaceDelta {
 		if err = logic.EnterpriseResetAllPeersFailovers(currentNode.ID, currentNode.Network); err != nil {
 		if err = logic.EnterpriseResetAllPeersFailovers(currentNode.ID, currentNode.Network); err != nil {
-			logger.Log(1, "failed to reset failover list during node update", currentNode.ID.String(), currentNode.Network)
+			slog.Warn("failed to reset failover list during node update", "nodeid", currentNode.ID, "network", currentNode.Network)
 		}
 		}
 	}
 	}
 	newNode.SetLastCheckIn()
 	newNode.SetLastCheckIn()
 	if err := logic.UpdateNode(&currentNode, &newNode); err != nil {
 	if err := logic.UpdateNode(&currentNode, &newNode); err != nil {
-		logger.Log(1, "error saving node", err.Error())
+		slog.Error("error saving node", "id", id, "error", err)
 		return
 		return
 	}
 	}
 	if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
 	if ifaceDelta { // reduce number of unneeded updates, by only sending on iface changes
 		if err = PublishPeerUpdate(); err != nil {
 		if err = PublishPeerUpdate(); err != nil {
-			logger.Log(0, "error updating peers when node", currentNode.ID.String(), "informed the server of an interface change", err.Error())
+			slog.Warn("error updating peers when node informed the server of an interface change", "nodeid", currentNode.ID, "error", err)
 		}
 		}
 	}
 	}
 
 
-	logger.Log(1, "updated node", id, newNode.ID.String())
+	slog.Info("updated node", "id", id, "newnodeid", newNode.ID)
 }
 }
 
 
 // UpdateHost  message Handler -- handles host updates from clients
 // UpdateHost  message Handler -- handles host updates from clients
 func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	id, err := getID(msg.Topic())
 	id, err := getID(msg.Topic())
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting host.ID sent on ", msg.Topic(), err.Error())
+		slog.Error("error getting host.ID sent on ", "topic", msg.Topic(), "error", err)
 		return
 		return
 	}
 	}
 	currentHost, err := logic.GetHost(id)
 	currentHost, err := logic.GetHost(id)
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting host ", id, err.Error())
+		slog.Error("error getting host", "id", id, "error", err)
 		return
 		return
 	}
 	}
 	decrypted, decryptErr := decryptMsgWithHost(currentHost, msg.Payload())
 	decrypted, decryptErr := decryptMsgWithHost(currentHost, msg.Payload())
 	if decryptErr != nil {
 	if decryptErr != nil {
-		logger.Log(1, "failed to decrypt message for host ", id, decryptErr.Error())
+		slog.Error("failed to decrypt message for host", "id", id, "error", decryptErr)
 		return
 		return
 	}
 	}
 	var hostUpdate models.HostUpdate
 	var hostUpdate models.HostUpdate
 	if err := json.Unmarshal(decrypted, &hostUpdate); err != nil {
 	if err := json.Unmarshal(decrypted, &hostUpdate); err != nil {
-		logger.Log(1, "error unmarshaling payload ", err.Error())
+		slog.Error("error unmarshaling payload", "error", err)
 		return
 		return
 	}
 	}
-	logger.Log(3, fmt.Sprintf("recieved host update: %s\n", hostUpdate.Host.ID.String()))
+	slog.Info("recieved host update", "name", hostUpdate.Host.Name, "id", hostUpdate.Host.ID)
 	var sendPeerUpdate bool
 	var sendPeerUpdate bool
 	switch hostUpdate.Action {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 	case models.CheckIn:
@@ -98,21 +98,21 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 		hu := hostactions.GetAction(currentHost.ID.String())
 		hu := hostactions.GetAction(currentHost.ID.String())
 		if hu != nil {
 		if hu != nil {
 			if err = HostUpdate(hu); err != nil {
 			if err = HostUpdate(hu); err != nil {
-				logger.Log(0, "failed to send new node to host", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
+				slog.Error("failed to send new node to host", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 				return
 				return
 			} else {
 			} else {
 				if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 				if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 					if err = AppendNodeUpdateACL(hu.Host.ID.String(), hu.Node.Network, hu.Node.ID.String(), servercfg.GetServer()); err != nil {
 					if err = AppendNodeUpdateACL(hu.Host.ID.String(), hu.Node.Network, hu.Node.ID.String(), servercfg.GetServer()); err != nil {
-						logger.Log(0, "failed to add ACLs for EMQX node", err.Error())
+						slog.Error("failed to add ACLs for EMQX node", "error", err)
 						return
 						return
 					}
 					}
 				}
 				}
 				if err = PublishSingleHostPeerUpdate(context.Background(), currentHost, nil, nil); err != nil {
 				if err = PublishSingleHostPeerUpdate(context.Background(), currentHost, nil, nil); err != nil {
-					logger.Log(0, "failed peers publish after join acknowledged", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
+					slog.Error("failed peers publish after join acknowledged", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 					return
 				}
 				}
 				if err = handleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
 				if err = handleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
-					logger.Log(0, "failed to send dns update after node,", hu.Node.ID.String(), ", added to host", hu.Host.Name, err.Error())
+					slog.Error("failed to send dns update after node added to host", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 					return
 				}
 				}
 			}
 			}
@@ -131,7 +131,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 			}
 			}
 			data, err := json.Marshal(&peerUpdate)
 			data, err := json.Marshal(&peerUpdate)
 			if err != nil {
 			if err != nil {
-				logger.Log(2, "json error", err.Error())
+				slog.Error("failed to marshal peer update", "error", err)
 			}
 			}
 			hosts := logic.GetRelatedHosts(hostUpdate.Host.ID.String())
 			hosts := logic.GetRelatedHosts(hostUpdate.Host.ID.String())
 			server := servercfg.GetServer()
 			server := servercfg.GetServer()
@@ -143,23 +143,23 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 		sendPeerUpdate = logic.UpdateHostFromClient(&hostUpdate.Host, currentHost)
 		sendPeerUpdate = logic.UpdateHostFromClient(&hostUpdate.Host, currentHost)
 		err := logic.UpsertHost(currentHost)
 		err := logic.UpsertHost(currentHost)
 		if err != nil {
 		if err != nil {
-			logger.Log(0, "failed to update host: ", currentHost.ID.String(), err.Error())
+			slog.Error("failed to update host", "id", currentHost.ID, "error", err)
 			return
 			return
 		}
 		}
 	case models.DeleteHost:
 	case models.DeleteHost:
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			// delete EMQX credentials for host
 			// delete EMQX credentials for host
 			if err := DeleteEmqxUser(currentHost.ID.String()); err != nil {
 			if err := DeleteEmqxUser(currentHost.ID.String()); err != nil {
-				logger.Log(0, "failed to remove host credentials from EMQX: ", currentHost.ID.String(), err.Error())
+				slog.Error("failed to remove host credentials from EMQX", "id", currentHost.ID, "error", err)
 				return
 				return
 			}
 			}
 		}
 		}
 		if err := logic.DisassociateAllNodesFromHost(currentHost.ID.String()); err != nil {
 		if err := logic.DisassociateAllNodesFromHost(currentHost.ID.String()); err != nil {
-			logger.Log(0, "failed to delete all nodes of host: ", currentHost.ID.String(), err.Error())
+			slog.Error("failed to delete all nodes of host", "id", currentHost.ID, "error", err)
 			return
 			return
 		}
 		}
 		if err := logic.RemoveHostByID(currentHost.ID.String()); err != nil {
 		if err := logic.RemoveHostByID(currentHost.ID.String()); err != nil {
-			logger.Log(0, "failed to delete host: ", currentHost.ID.String(), err.Error())
+			slog.Error("failed to delete host", "id", currentHost.ID, "error", err)
 			return
 			return
 		}
 		}
 		sendPeerUpdate = true
 		sendPeerUpdate = true
@@ -167,7 +167,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 		if servercfg.IsUsingTurn() {
 		if servercfg.IsUsingTurn() {
 			err = logic.RegisterHostWithTurn(hostUpdate.Host.ID.String(), hostUpdate.Host.HostPass)
 			err = logic.RegisterHostWithTurn(hostUpdate.Host.ID.String(), hostUpdate.Host.HostPass)
 			if err != nil {
 			if err != nil {
-				logger.Log(0, "failed to register host with turn server: ", err.Error())
+				slog.Error("failed to register host with turn server", "id", currentHost.ID, "error", err)
 				return
 				return
 			}
 			}
 		}
 		}
@@ -177,7 +177,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	if sendPeerUpdate {
 	if sendPeerUpdate {
 		err := PublishPeerUpdate()
 		err := PublishPeerUpdate()
 		if err != nil {
 		if err != nil {
-			logger.Log(0, "failed to pulish peer update: ", err.Error())
+			slog.Error("failed to publish peer update", "error", err)
 		}
 		}
 	}
 	}
 	// if servercfg.Is_EE && ifaceDelta {
 	// if servercfg.Is_EE && ifaceDelta {
@@ -192,57 +192,55 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 	if servercfg.Is_EE {
 	if servercfg.Is_EE {
 		id, err := getID(msg.Topic())
 		id, err := getID(msg.Topic())
 		if err != nil {
 		if err != nil {
-			logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
+			slog.Error("error getting ID sent on ", "topic", msg.Topic(), "error", err)
 			return
 			return
 		}
 		}
 		currentNode, err := logic.GetNodeByID(id)
 		currentNode, err := logic.GetNodeByID(id)
 		if err != nil {
 		if err != nil {
-			logger.Log(1, "error getting node ", id, err.Error())
+			slog.Error("error getting node", "id", id, "error", err)
 			return
 			return
 		}
 		}
 		decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 		decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 		if decryptErr != nil {
 		if decryptErr != nil {
-			logger.Log(1, "failed to decrypt message for node ", id, decryptErr.Error())
+			slog.Error("failed to decrypt message for node", "id", id, "error", decryptErr)
 			return
 			return
 		}
 		}
 
 
 		var newMetrics models.Metrics
 		var newMetrics models.Metrics
 		if err := json.Unmarshal(decrypted, &newMetrics); err != nil {
 		if err := json.Unmarshal(decrypted, &newMetrics); err != nil {
-			logger.Log(1, "error unmarshaling payload ", err.Error())
+			slog.Error("error unmarshaling payload", "error", err)
 			return
 			return
 		}
 		}
 
 
 		shouldUpdate := updateNodeMetrics(&currentNode, &newMetrics)
 		shouldUpdate := updateNodeMetrics(&currentNode, &newMetrics)
 
 
 		if err = logic.UpdateMetrics(id, &newMetrics); err != nil {
 		if err = logic.UpdateMetrics(id, &newMetrics); err != nil {
-			logger.Log(1, "faield to update node metrics", id, err.Error())
+			slog.Error("failed to update node metrics", "id", id, "error", err)
 			return
 			return
 		}
 		}
 		if servercfg.IsMetricsExporter() {
 		if servercfg.IsMetricsExporter() {
 			if err := pushMetricsToExporter(newMetrics); err != nil {
 			if err := pushMetricsToExporter(newMetrics); err != nil {
-				logger.Log(2, fmt.Sprintf("failed to push node: [%s] metrics to exporter, err: %v",
-					currentNode.ID, err))
+				slog.Error("failed to push node metrics to exporter", "id", currentNode.ID, "error", err)
 			}
 			}
 		}
 		}
 
 
 		if newMetrics.Connectivity != nil {
 		if newMetrics.Connectivity != nil {
 			err := logic.EnterpriseFailoverFunc(&currentNode)
 			err := logic.EnterpriseFailoverFunc(&currentNode)
 			if err != nil {
 			if err != nil {
-				logger.Log(0, "failed to failover for node", currentNode.ID.String(), "on network", currentNode.Network, "-", err.Error())
+				slog.Error("failed to failover for node", "id", currentNode.ID, "network", currentNode.Network, "error", err)
 			}
 			}
 		}
 		}
 
 
 		if shouldUpdate {
 		if shouldUpdate {
-			logger.Log(2, "updating peers after node", currentNode.ID.String(), currentNode.Network, "detected connectivity issues")
+			slog.Info("updating peers after node detected connectivity issues", "id", currentNode.ID, "network", currentNode.Network)
 			host, err := logic.GetHost(currentNode.HostID.String())
 			host, err := logic.GetHost(currentNode.HostID.String())
 			if err == nil {
 			if err == nil {
 				if err = PublishSingleHostPeerUpdate(context.Background(), host, nil, nil); err != nil {
 				if err = PublishSingleHostPeerUpdate(context.Background(), host, nil, nil); err != nil {
-					logger.Log(0, "failed to publish update after failover peer change for node", currentNode.ID.String(), currentNode.Network)
+					slog.Warn("failed to publish update after failover peer change for node", "id", currentNode.ID, "network", currentNode.Network, "error", err)
 				}
 				}
 			}
 			}
 		}
 		}
-
-		logger.Log(1, "updated node metrics", id)
+		slog.Info("updated node metrics", "id", id)
 	}
 	}
 }
 }
 
 
@@ -250,17 +248,17 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 func ClientPeerUpdate(client mqtt.Client, msg mqtt.Message) {
 func ClientPeerUpdate(client mqtt.Client, msg mqtt.Message) {
 	id, err := getID(msg.Topic())
 	id, err := getID(msg.Topic())
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting node.ID sent on ", msg.Topic(), err.Error())
+		slog.Error("error getting node.ID sent on ", "topic", msg.Topic(), "error", err)
 		return
 		return
 	}
 	}
 	currentNode, err := logic.GetNodeByID(id)
 	currentNode, err := logic.GetNodeByID(id)
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error getting node ", id, err.Error())
+		slog.Error("error getting node", "id", id, "error", err)
 		return
 		return
 	}
 	}
 	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 	decrypted, decryptErr := decryptMsg(&currentNode, msg.Payload())
 	if decryptErr != nil {
 	if decryptErr != nil {
-		logger.Log(1, "failed to decrypt message during client peer update for node ", id, decryptErr.Error())
+		slog.Error("failed to decrypt message for node", "id", id, "error", decryptErr)
 		return
 		return
 	}
 	}
 	switch decrypted[0] {
 	switch decrypted[0] {
@@ -268,12 +266,12 @@ func ClientPeerUpdate(client mqtt.Client, msg mqtt.Message) {
 		// do we still need this
 		// do we still need this
 	case ncutils.DONE:
 	case ncutils.DONE:
 		if err = PublishPeerUpdate(); err != nil {
 		if err = PublishPeerUpdate(); err != nil {
-			logger.Log(1, "error publishing peer update for node", currentNode.ID.String(), err.Error())
+			slog.Error("error publishing peer update for node", "id", currentNode.ID, "error", err)
 			return
 			return
 		}
 		}
 	}
 	}
 
 
-	logger.Log(1, "sent peer updates after signal received from", id)
+	slog.Info("sent peer updates after signal received from", "id", id)
 }
 }
 
 
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) bool {
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) bool {
@@ -282,7 +280,7 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 	}
 	}
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	if err != nil {
 	if err != nil {
-		logger.Log(1, "error finding old metrics for node", currentNode.ID.String())
+		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
 		return false
 		return false
 	}
 	}
 	if oldMetrics.FailoverPeers == nil {
 	if oldMetrics.FailoverPeers == nil {
@@ -349,7 +347,7 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 	// add nodes that need failover
 	// add nodes that need failover
 	nodes, err := logic.GetNetworkNodes(currentNode.Network)
 	nodes, err := logic.GetNetworkNodes(currentNode.Network)
 	if err != nil {
 	if err != nil {
-		logger.Log(0, "failed to retrieve nodes while updating metrics")
+		slog.Error("failed to retrieve nodes while updating metrics", "error", err)
 		return false
 		return false
 	}
 	}
 	for _, node := range nodes {
 	for _, node := range nodes {
@@ -415,35 +413,45 @@ func handleHostCheckin(h, currentHost *models.Host) bool {
 				fakeNode.Action = models.NODE_DELETE
 				fakeNode.Action = models.NODE_DELETE
 				fakeNode.PendingDelete = true
 				fakeNode.PendingDelete = true
 				if err := NodeUpdate(&fakeNode); err != nil {
 				if err := NodeUpdate(&fakeNode); err != nil {
-					logger.Log(0, "failed to inform host", currentHost.Name, currentHost.ID.String(), "to remove node", currNodeID, err.Error())
+					slog.Warn("failed to inform host to remove node", "host", currentHost.Name, "hostid", currentHost.ID, "nodeid", currNodeID, "error", err)
 				}
 				}
 			}
 			}
 			continue
 			continue
 		}
 		}
 		if err := logic.UpdateNodeCheckin(&node); err != nil {
 		if err := logic.UpdateNodeCheckin(&node); err != nil {
-			logger.Log(0, "error updating node", node.ID.String(), " on checkin", err.Error())
+			slog.Warn("failed to update node on checkin", "nodeid", node.ID, "error", err)
 		}
 		}
 	}
 	}
 
 
 	for i := range h.Interfaces {
 	for i := range h.Interfaces {
 		h.Interfaces[i].AddressString = h.Interfaces[i].Address.String()
 		h.Interfaces[i].AddressString = h.Interfaces[i].Address.String()
 	}
 	}
+	/// version or firewall in use change does not require a peerUpdate
+	if h.Version != currentHost.Version || h.FirewallInUse != currentHost.FirewallInUse {
+		currentHost.FirewallInUse = h.FirewallInUse
+		currentHost.Version = h.Version
+		if err := logic.UpsertHost(currentHost); err != nil {
+			slog.Error("failed to update host after check-in", "name", h.Name, "id", h.ID, "error", err)
+			return false
+		}
+	}
 	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) ||
 	ifaceDelta := len(h.Interfaces) != len(currentHost.Interfaces) ||
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
-		h.DefaultInterface != currentHost.DefaultInterface
+		h.DefaultInterface != currentHost.DefaultInterface ||
+		h.EndpointDetection != servercfg.EndpointDetectionEnabled()
 	if ifaceDelta { // only save if something changes
 	if ifaceDelta { // only save if something changes
 		currentHost.EndpointIP = h.EndpointIP
 		currentHost.EndpointIP = h.EndpointIP
 		currentHost.Interfaces = h.Interfaces
 		currentHost.Interfaces = h.Interfaces
 		currentHost.DefaultInterface = h.DefaultInterface
 		currentHost.DefaultInterface = h.DefaultInterface
 		currentHost.NatType = h.NatType
 		currentHost.NatType = h.NatType
 		if err := logic.UpsertHost(currentHost); err != nil {
 		if err := logic.UpsertHost(currentHost); err != nil {
-			logger.Log(0, "failed to update host after check-in", h.Name, h.ID.String(), err.Error())
+			slog.Error("failed to update host after check-in", "name", h.Name, "id", h.ID, "error", err)
 			return false
 			return false
 		}
 		}
-		logger.Log(1, "updated host after check-in", currentHost.Name, currentHost.ID.String())
+		slog.Info("updated host after check-in", "name", currentHost.Name, "id", currentHost.ID)
 	}
 	}
 
 
-	logger.Log(2, "check-in processed for host", h.Name, h.ID.String())
+	slog.Info("check-in processed for host", "name", h.Name, "id", h.ID)
 	return ifaceDelta
 	return ifaceDelta
 }
 }

+ 7 - 3
release.md

@@ -1,14 +1,18 @@
 
 
-# Netmaker v0.19.1
+# Netmaker v0.20.2
 
 
 ## whats new
 ## whats new
 - 
 - 
     
     
 ## whats fixed
 ## whats fixed
-- status code for exceeding free tier limits  
+- enrollment keys for non-admins 
+- client version displayed correctly in UI
+- upd hole punching improvments
+- SSL fallback to letsencrypt
+- permission handling for non-admin users
+
 
 
 ## known issues
 ## known issues
-- Caddy does not handle netmaker exporter well for EE
 - Migration causes a listen port of 0 for some upgraded hosts
 - Migration causes a listen port of 0 for some upgraded hosts
 - Docker clients can not re-join after deletion
 - Docker clients can not re-join after deletion
 - Innacurate Ext Client Metrics 
 - Innacurate Ext Client Metrics 

+ 83 - 0
scripts/netmaker.default.env

@@ -0,0 +1,83 @@
+# Email used for SSL certificates
+NM_EMAIL=
+# The base domain of netmaker
+NM_DOMAIN=
+# Public IP of machine
+SERVER_HOST=
+# The admin master key for accessing the API. Change this in any production installation.
+MASTER_KEY=
+# The username to set for turn api access
+TURN_USERNAME=
+# The password to set for turn api access
+TURN_PASSWORD=
+# The username to set for MQ access
+MQ_USERNAME=
+# The password to set for MQ access
+MQ_PASSWORD=
+INSTALL_TYPE=
+NETMAKER_ACCOUNT_ID=
+LICENSE_KEY=
+SERVER_IMAGE_TAG=
+UI_IMAGE_TAG=
+NETCLIENT_ENDPOINT_DETECTION="disabled"
+# used for HA - identifies this server vs other servers
+NODE_ID="netmaker-server-1"
+METRICS_EXPORTER="off"
+PROMETHEUS="off"
+# Enables DNS Mode, meaning all nodes will set hosts file for private dns settings
+DNS_MODE="on"
+# Enable auto update of netclient ? ENUM:- enabled,disabled | default=enabled
+NETCLIENT_AUTO_UPDATE="enabled"
+# The HTTP API port for Netmaker. Used for API calls / communication from front end.
+# If changed, need to change port of BACKEND_URL for netmaker-ui.
+API_PORT="8081"
+EXPORTER_API_PORT="8085"
+# The "allowed origin" for API requests. Change to restrict where API requests can come from with comma-separated
+# URLs. ex:- https://dashboard.netmaker.domain1.com,https://dashboard.netmaker.domain2.com
+CORS_ALLOWED_ORIGIN="*"
+# Show keys permanently in UI (until deleted) as opposed to 1-time display.
+DISPLAY_KEYS="on"
+# Database to use - sqlite, postgres, or rqlite
+DATABASE="sqlite"
+# The address of the mq server. If running from docker compose it will be "mq". Otherwise, need to input address.
+# If using "host networking", it will find and detect the IP of the mq container.
+SERVER_BROKER_ENDPOINT="ws://mq:1883"
+# The reachable port of STUN on the server
+STUN_PORT="3478"
+# Logging verbosity level - 1, 2, or 3
+VERBOSITY="1"
+# If ON, all new clients will enable proxy by default
+# If OFF, all new clients will disable proxy by default
+# If AUTO, stick with the existing logic for NAT detection
+DEFAULT_PROXY_MODE="off"
+# Port to access turn server
+TURN_PORT="3479"
+# Config for using turn, accepts either true/false
+USE_TURN="true"
+DEBUG_MODE="off"
+TURN_API_PORT="8089"
+# Enables the REST backend (API running on API_PORT at SERVER_HTTP_HOST).
+# Change to "off" to turn off.
+REST_BACKEND="on"
+# If turned "on", Server will not set Host based on remote IP check.
+# This is already overridden if SERVER_HOST is set. Turned "off" by default.
+DISABLE_REMOTE_IP_CHECK="off"
+# Whether or not to send telemetry data to help improve Netmaker. Switch to "off" to opt out of sending telemetry.
+TELEMETRY="on"
+###
+#
+# OAuth section
+#
+###
+# "<azure-ad|github|google|oidc>"
+AUTH_PROVIDER=
+# "<client id of your oauth provider>"
+CLIENT_ID=
+# "<client secret of your oauth provider>"
+CLIENT_SECRET=
+# "https://dashboard.<netmaker base domain>"
+FRONTEND_URL=
+# "<only for azure, you may optionally specify the tenant for the OAuth>"
+AZURE_TENANT=
+# https://oidc.yourprovider.com - URL of oidc provider
+OIDC_ISSUER=

+ 18 - 17
scripts/nm-certs.sh

@@ -30,16 +30,16 @@ fi
 CERTBOT_PARAMS=$(cat <<EOF
 CERTBOT_PARAMS=$(cat <<EOF
 certonly --standalone \
 certonly --standalone \
 	--non-interactive --agree-tos \
 	--non-interactive --agree-tos \
-	-m "$NM_EMAIL" \
-	-d "stun.$NM_DOMAIN" \
-	-d "api.$NM_DOMAIN" \
-	-d "broker.$NM_DOMAIN" \
-	-d "dashboard.$NM_DOMAIN" \
-	-d "turn.$NM_DOMAIN" \
-	-d "turnapi.$NM_DOMAIN" \
-	-d "netmaker-exporter.$NM_DOMAIN" \
-	-d "grafana.$NM_DOMAIN" \
-	-d "prometheus.$NM_DOMAIN"
+	-m $NM_EMAIL \
+	-d stun.$NM_DOMAIN \
+	-d api.$NM_DOMAIN \
+	-d broker.$NM_DOMAIN \
+	-d dashboard.$NM_DOMAIN \
+	-d turn.$NM_DOMAIN \
+	-d turnapi.$NM_DOMAIN \
+	-d netmaker-exporter.$NM_DOMAIN \
+	-d grafana.$NM_DOMAIN \
+	-d prometheus.$NM_DOMAIN
 EOF
 EOF
 )
 )
 
 
@@ -47,6 +47,7 @@ EOF
 cat <<EOF >"$SCRIPT_DIR/certbot-entry.sh"
 cat <<EOF >"$SCRIPT_DIR/certbot-entry.sh"
 #!/bin/sh
 #!/bin/sh
 # deps
 # deps
+apk update
 apk add bash curl
 apk add bash curl
 # zerossl
 # zerossl
 wget -qO zerossl-bot.sh "https://github.com/zerossl/zerossl-bot/raw/master/zerossl-bot.sh"
 wget -qO zerossl-bot.sh "https://github.com/zerossl/zerossl-bot/raw/master/zerossl-bot.sh"
@@ -54,7 +55,8 @@ chmod +x zerossl-bot.sh
 # request the certs
 # request the certs
 ./zerossl-bot.sh "$CERTBOT_PARAMS"
 ./zerossl-bot.sh "$CERTBOT_PARAMS"
 EOF
 EOF
-chmod +x certbot-entry.sh
+
+chmod +x "$SCRIPT_DIR/certbot-entry.sh"
 
 
 # request certs
 # request certs
 sudo docker run -it --rm --name certbot \
 sudo docker run -it --rm --name certbot \
@@ -64,8 +66,8 @@ sudo docker run -it --rm --name certbot \
 	--entrypoint "/opt/certbot/certbot-entry.sh" \
 	--entrypoint "/opt/certbot/certbot-entry.sh" \
 	certbot/certbot
 	certbot/certbot
 
 
-# clean up TODO enable
-#rm "$SCRIPT_DIR/certbot-entry.sh"
+# clean up
+rm "$SCRIPT_DIR/certbot-entry.sh"
 
 
 # check if successful
 # check if successful
 if [ ! -f "$CERT_DIR"/fullchain.pem ]; then
 if [ ! -f "$CERT_DIR"/fullchain.pem ]; then
@@ -73,8 +75,7 @@ if [ ! -f "$CERT_DIR"/fullchain.pem ]; then
 	sudo docker run -it --rm --name certbot \
 	sudo docker run -it --rm --name certbot \
 		-p 80:80 -p 443:443 \
 		-p 80:80 -p 443:443 \
 		-v "$SCRIPT_DIR/letsencrypt:/etc/letsencrypt" \
 		-v "$SCRIPT_DIR/letsencrypt:/etc/letsencrypt" \
-		--entrypoint "/opt/certbot/certbot-entry.sh" \
-		certbot/certbot "$CERTBOT_PARAMS"
+		certbot/certbot $CERTBOT_PARAMS
 	if [ ! -f "$CERT_DIR"/fullchain.pem ]; then
 	if [ ! -f "$CERT_DIR"/fullchain.pem ]; then
 		echo "Missing file: $CERT_DIR/fullchain.pem"
 		echo "Missing file: $CERT_DIR/fullchain.pem"
 		echo "SSL certificates failed"
 		echo "SSL certificates failed"
@@ -84,8 +85,8 @@ fi
 
 
 # copy for mounting
 # copy for mounting
 mkdir -p certs
 mkdir -p certs
-cp -L "$CERT_DIR/fullchain.pem" /root/certs/fullchain.pem
-cp -L "$CERT_DIR/privkey.pem" /root/certs/privkey.pem
+cp -L "$CERT_DIR/fullchain.pem" "$SCRIPT_DIR/certs/fullchain.pem"
+cp -L "$CERT_DIR/privkey.pem" "$SCRIPT_DIR/certs/privkey.pem"
 
 
 echo "SSL certificates ready"
 echo "SSL certificates ready"
 
 

+ 240 - 123
scripts/nm-quick.sh

@@ -4,6 +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"
 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
@@ -11,12 +12,6 @@ if [ $(id -u) -ne 0 ]; then
 	exit 1
 	exit 1
 fi
 fi
 
 
-# read the config file
-if [ -f "$CONFIG_PATH" ]; then
-	echo "Reading config from $CONFIG_PATH"
-	source "$CONFIG_PATH"
-fi
-
 unset INSTALL_TYPE
 unset INSTALL_TYPE
 unset BUILD_TYPE
 unset BUILD_TYPE
 unset BUILD_TAG
 unset BUILD_TAG
@@ -25,6 +20,7 @@ unset AUTO_BUILD
 
 
 # usage - displays usage instructions
 # usage - displays usage instructions
 usage() {
 usage() {
+	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]"
 	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:"
@@ -141,6 +137,7 @@ set_buildinfo() {
 	echo "  Build Type: $BUILD_TYPE"
 	echo "  Build Type: $BUILD_TYPE"
 	echo "   Build Tag: $BUILD_TAG"
 	echo "   Build Tag: $BUILD_TAG"
 	echo "   Image Tag: $IMAGE_TAG"
 	echo "   Image Tag: $IMAGE_TAG"
+	echo "   Installer: v$NM_QUICK_VERSION"
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
 
 
 }
 }
@@ -148,13 +145,13 @@ set_buildinfo() {
 # install_yq - install yq if not present
 # install_yq - install yq if not present
 install_yq() {
 install_yq() {
 	if ! command -v yq &>/dev/null; then
 	if ! command -v yq &>/dev/null; then
-		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+		wget -qO /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
 		chmod +x /usr/bin/yq
 		chmod +x /usr/bin/yq
 	fi
 	fi
 	set +e
 	set +e
 	if ! command -v yq &>/dev/null; then
 	if ! command -v yq &>/dev/null; then
 		set -e
 		set -e
-		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
+		wget -qO /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
 		chmod +x /usr/bin/yq
 		chmod +x /usr/bin/yq
 	fi
 	fi
 	set -e
 	set -e
@@ -172,21 +169,44 @@ setup_netclient() {
 	netclient uninstall
 	netclient uninstall
 	set -e
 	set -e
 
 
-	wget -O netclient https://github.com/gravitl/netclient/releases/download/$LATEST/netclient-linux-amd64
+	# TODO arm support
+	wget -qO netclient https://github.com/gravitl/netclient/releases/download/$LATEST/netclient-linux-amd64
 	chmod +x netclient
 	chmod +x netclient
 	./netclient install
 	./netclient install
+	echo "Register token: $TOKEN"
 	netclient register -t $TOKEN
 	netclient register -t $TOKEN
 
 
-	echo "waiting for client to become available"
-	wait_seconds 10
+	echo "waiting for netclient to become available"
+	local found=false
+	local file=/etc/netclient/nodes.yml
+	for ((a = 1; a <= 90; a++)); do
+		if [ -f "$file" ]; then
+			found=true
+			break
+		fi
+		sleep 1
+	done
+
+	if [ "$found" = false ]; then
+		echo "Error - $file not present"
+		exit 1
+	fi
 }
 }
 
 
 # configure_netclient - configures server's netclient as a default host and an ingress gateway
 # configure_netclient - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
 configure_netclient() {
 
 
 	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
 	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
+	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
+		echo "Error obtaining NODE_ID for the new network"
+		exit 1
+	fi
 	echo "register complete. New node ID: $NODE_ID"
 	echo "register complete. New node ID: $NODE_ID"
 	HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
 	HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
+	if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then
+		echo "Error obtaining HOST_ID for the new network"
+		exit 1
+	fi
 	echo "making host a default"
 	echo "making host a default"
 	echo "Host ID: $HOST_ID"
 	echo "Host ID: $HOST_ID"
 	# set as a default host
 	# set as a default host
@@ -200,7 +220,15 @@ configure_netclient() {
 # setup_nmctl - pulls nmctl and makes it executable
 # setup_nmctl - pulls nmctl and makes it executable
 setup_nmctl() {
 setup_nmctl() {
 
 
-	wget -O /usr/bin/nmctl https://github.com/gravitl/netmaker/releases/download/$LATEST/nmctl-linux-amd64
+	# TODO arm support
+	local URL="https://github.com/gravitl/netmaker/releases/download/$LATEST/nmctl-linux-amd64"
+	echo "Downloading nmctl..."
+	wget -qO /usr/bin/nmctl "$URL"
+
+	if [ ! -f /usr/bin/nmctl ]; then
+		echo "Error downloading nmctl from '$URL'"
+		exit 1
+	fi
 
 
 	chmod +x /usr/bin/nmctl
 	chmod +x /usr/bin/nmctl
 	echo "using server api.$NETMAKER_BASE_DOMAIN"
 	echo "using server api.$NETMAKER_BASE_DOMAIN"
@@ -247,77 +275,142 @@ confirm() { (
 save_config() { (
 save_config() { (
 	echo "Saving the config to $CONFIG_PATH"
 	echo "Saving the config to $CONFIG_PATH"
 	touch "$CONFIG_PATH"
 	touch "$CONFIG_PATH"
-	# email
-	if grep -q "^NM_EMAIL=" "$CONFIG_PATH"; then
-		sed -i "s/NM_EMAIL=.*/NM_EMAIL=$EMAIL/" "$CONFIG_PATH"
+	save_config_item NM_EMAIL "$EMAIL"
+	save_config_item NM_DOMAIN "$NETMAKER_BASE_DOMAIN"
+	save_config_item UI_IMAGE_TAG "$IMAGE_TAG"
+	if [ "$BUILD_TYPE" = "local" ]; then
+		save_config_item UI_IMAGE_TAG "$LATEST"
 	else
 	else
-		echo "NM_EMAIL=$EMAIL" >>"$CONFIG_PATH"
+		save_config_item UI_IMAGE_TAG "$IMAGE_TAG"
 	fi
 	fi
-	# domain
-	if grep -q "^NM_DOMAIN=" "$CONFIG_PATH"; then
-		sed -i "s/NM_DOMAIN=.*/NM_DOMAIN=$NETMAKER_BASE_DOMAIN/" "$CONFIG_PATH"
+	# version-specific entries
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		save_config_item NETMAKER_ACCOUNT_ID "$ACCOUNT_ID"
+		save_config_item LICENSE_KEY "$LICENSE_KEY"
+		save_config_item METRICS_EXPORTER "on"
+		save_config_item PROMETHEUS "on"
+		if [ "$BUILD_TYPE" = "version" ]; then
+			save_config_item SERVER_IMAGE_TAG "$IMAGE_TAG-ee"
+		else
+			save_config_item SERVER_IMAGE_TAG "$IMAGE_TAG"
+		fi
 	else
 	else
-		echo "NM_DOMAIN=$NETMAKER_BASE_DOMAIN" >>"$CONFIG_PATH"
+		save_config_item METRICS_EXPORTER "off"
+		save_config_item PROMETHEUS "off"
+		save_config_item SERVER_IMAGE_TAG "$IMAGE_TAG"
+	fi
+	# copy entries from the previous config
+	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"
+		"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" "NETCLIENT_ENDPOINT_DETECTION" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT")
+	for name in "${toCopy[@]}"; do
+		save_config_item $name "${!name}"
+	done
+	# preserve debug entries
+	if test -n "$NM_SKIP_BUILD"; then
+		save_config_item NM_SKIP_BUILD "$NM_SKIP_BUILD"
+	fi
+	if test -n "$NM_SKIP_CLONE"; then
+		save_config_item NM_SKIP_CLONE "$NM_SKIP_CLONE"
+	fi
+	if test -n "$NM_SKIP_DEPS"; then
+		save_config_item NM_SKIP_DEPS "$NM_SKIP_DEPS"
+	fi
+); }
+
+save_config_item() { (
+	local NAME="$1"
+	local VALUE="$2"
+	#echo "$NAME=$VALUE"
+	if test -z "$VALUE"; then
+		# load the default for empty values
+		VALUE=$(awk -F'=' "/^$NAME/ { print \$2}"  "$SCRIPT_DIR/netmaker.default.env")
+		# trim quotes for docker
+		VALUE=$(echo "$VALUE" | sed -E "s|^(['\"])(.*)\1$|\2|g")
+		#echo "Default for $NAME=$VALUE"
+	fi
+	# TODO single quote passwords
+	if grep -q "^$NAME=" "$CONFIG_PATH"; then
+		# TODO escape | in the value
+		sed -i "s|$NAME=.*|$NAME=$VALUE|" "$CONFIG_PATH"
+	else
+		echo "$NAME=$VALUE" >>"$CONFIG_PATH"
 	fi
 	fi
 ); }
 ); }
 
 
 # local_install_setup - builds artifacts based on specified branch locally to use in install
 # local_install_setup - builds artifacts based on specified branch locally to use in install
 local_install_setup() { (
 local_install_setup() { (
-	rm -rf netmaker-tmp
-	mkdir netmaker-tmp
-	cd netmaker-tmp
-	git clone --single-branch --depth=1 --branch=$BUILD_TAG https://www.github.com/gravitl/netmaker
+	if test -z "$NM_SKIP_CLONE"; then
+		rm -rf netmaker-tmp
+		mkdir netmaker-tmp
+		cd netmaker-tmp
+		git clone --single-branch --depth=1 --branch=$BUILD_TAG https://www.github.com/gravitl/netmaker
+	else
+		cd netmaker-tmp
+		echo "Skipping git clone on NM_SKIP_CLONE"
+	fi
 	cd netmaker
 	cd netmaker
 	if test -z "$NM_SKIP_BUILD"; then
 	if test -z "$NM_SKIP_BUILD"; then
 		docker build --no-cache --build-arg version=$IMAGE_TAG -t gravitl/netmaker:$IMAGE_TAG .
 		docker build --no-cache --build-arg version=$IMAGE_TAG -t gravitl/netmaker:$IMAGE_TAG .
 	else
 	else
 		echo "Skipping build on NM_SKIP_BUILD"
 		echo "Skipping build on NM_SKIP_BUILD"
 	fi
 	fi
+	cp compose/docker-compose.yml "$SCRIPT_DIR/docker-compose.yml"
 	if [ "$INSTALL_TYPE" = "ee" ]; then
 	if [ "$INSTALL_TYPE" = "ee" ]; then
-		cp compose/docker-compose.ee.yml /root/docker-compose.yml
-		cp docker/Caddyfile-EE /root/Caddyfile
+		cp compose/docker-compose.ee.yml "$SCRIPT_DIR/docker-compose.override.yml"
+		cp docker/Caddyfile-EE "$SCRIPT_DIR/Caddyfile"
 	else
 	else
-		cp compose/docker-compose.yml /root/docker-compose.yml
-		cp docker/Caddyfile /root/Caddyfile
+		cp docker/Caddyfile "$SCRIPT_DIR/Caddyfile"
 	fi
 	fi
-	cp scripts/nm-certs.sh /root/nm-certs.sh
-	cp docker/mosquitto.conf /root/mosquitto.conf
-	cp docker/wait.sh /root/wait.sh
+	cp scripts/nm-certs.sh "$SCRIPT_DIR/nm-certs.sh"
+	cp scripts/netmaker.default.env "$SCRIPT_DIR/netmaker.default.env"
+	cp docker/mosquitto.conf "$SCRIPT_DIR/mosquitto.conf"
+	cp docker/wait.sh "$SCRIPT_DIR/wait.sh"
 	cd ../../
 	cd ../../
-	rm -rf netmaker-tmp
+	if test -z "$NM_SKIP_CLONE"; then
+		rm -rf netmaker-tmp
+	fi
 ); }
 ); }
 
 
 # install_dependencies - install necessary packages to run netmaker
 # install_dependencies - install necessary packages to run netmaker
 install_dependencies() {
 install_dependencies() {
+
+	if test -n "$NM_SKIP_DEPS"; then
+		return
+	fi
+
 	echo "checking dependencies..."
 	echo "checking dependencies..."
 
 
 	OS=$(uname)
 	OS=$(uname)
 	if [ -f /etc/debian_version ]; then
 	if [ -f /etc/debian_version ]; then
-		dependencies="git wireguard wireguard-tools dnsutils jq docker.io docker-compose"
+		dependencies="git wireguard wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
 		update_cmd='apt update'
 		update_cmd='apt update'
 		install_cmd='apt-get install -y'
 		install_cmd='apt-get install -y'
 	elif [ -f /etc/alpine-release ]; then
 	elif [ -f /etc/alpine-release ]; then
-		dependencies="git wireguard jq docker.io docker-compose"
+		dependencies="git wireguard jq docker.io docker-compose grep gawk"
 		update_cmd='apk update'
 		update_cmd='apk update'
 		install_cmd='apk --update add'
 		install_cmd='apk --update add'
 	elif [ -f /etc/centos-release ]; then
 	elif [ -f /etc/centos-release ]; then
-		dependencies="git wireguard jq bind-utils docker.io docker-compose"
+		dependencies="git wireguard jq bind-utils docker.io docker-compose grep gawk"
 		update_cmd='yum update'
 		update_cmd='yum update'
 		install_cmd='yum install -y'
 		install_cmd='yum install -y'
 	elif [ -f /etc/fedora-release ]; then
 	elif [ -f /etc/fedora-release ]; then
-		dependencies="git wireguard bind-utils jq docker.io docker-compose"
+		dependencies="git wireguard bind-utils jq docker.io docker-compose grep gawk"
 		update_cmd='dnf update'
 		update_cmd='dnf update'
 		install_cmd='dnf install -y'
 		install_cmd='dnf install -y'
 	elif [ -f /etc/redhat-release ]; then
 	elif [ -f /etc/redhat-release ]; then
-		dependencies="git wireguard jq docker.io bind-utils docker-compose"
+		dependencies="git wireguard jq docker.io bind-utils docker-compose grep gawk"
 		update_cmd='yum update'
 		update_cmd='yum update'
 		install_cmd='yum install -y'
 		install_cmd='yum install -y'
 	elif [ -f /etc/arch-release ]; then
 	elif [ -f /etc/arch-release ]; then
-		dependencies="git wireguard-tools dnsutils jq docker.io docker-compose"
+		dependencies="git wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
 		update_cmd='pacman -Sy'
 		update_cmd='pacman -Sy'
 		install_cmd='pacman -S --noconfirm'
 		install_cmd='pacman -S --noconfirm'
 	elif [ "${OS}" = "FreeBSD" ]; then
 	elif [ "${OS}" = "FreeBSD" ]; then
-		dependencies="git wireguard wget jq docker.io docker-compose"
+		dependencies="git wireguard wget jq docker.io docker-compose grep gawk"
 		update_cmd='pkg update'
 		update_cmd='pkg update'
 		install_cmd='pkg install -y'
 		install_cmd='pkg install -y'
 	else
 	else
@@ -399,17 +492,17 @@ set_install_vars() {
 	fi
 	fi
 
 
 	NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
 	NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
-	# TODO dead code?
-	# COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
-	SERVER_PUBLIC_IP=$IP_ADDR
-	MASTER_KEY=$(
-		tr -dc A-Za-z0-9 </dev/urandom | head -c 30
-		echo ''
-	)
+	SERVER_HOST=$IP_ADDR
+	if test -z "$MASTER_KEY"; then
+		MASTER_KEY=$(
+			tr -dc A-Za-z0-9 </dev/urandom | head -c 30
+			echo ''
+		)
+	fi
 	DOMAIN_TYPE=""
 	DOMAIN_TYPE=""
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
 	echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
 	echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
-	echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_PUBLIC_IP"
+	echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_HOST"
 	echo "IMPORTANT: Due to the high volume of requests, the auto-generated domain has been rate-limited by the certificate provider."
 	echo "IMPORTANT: Due to the high volume of requests, the auto-generated domain has been rate-limited by the certificate provider."
 	echo "For this reason, we STRONGLY RECOMMEND using your own domain. Using the auto-generated domain may lead to a failed installation due to rate limiting."
 	echo "For this reason, we STRONGLY RECOMMEND using your own domain. Using the auto-generated domain may lead to a failed installation due to rate limiting."
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
@@ -425,7 +518,7 @@ set_install_vars() {
 				break
 				break
 				;;
 				;;
 			2)
 			2)
-				read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_PUBLIC_IP first): " domain
+				read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_HOST first): " domain
 				NETMAKER_BASE_DOMAIN=$domain
 				NETMAKER_BASE_DOMAIN=$domain
 				echo "using $NETMAKER_BASE_DOMAIN"
 				echo "using $NETMAKER_BASE_DOMAIN"
 				DOMAIN_TYPE="custom"
 				DOMAIN_TYPE="custom"
@@ -456,7 +549,7 @@ set_install_vars() {
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
 
 
 	if [[ "$DOMAIN_TYPE" == "custom" ]]; then
 	if [[ "$DOMAIN_TYPE" == "custom" ]]; then
-		echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_PUBLIC_IP"
+		echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_HOST"
 		confirm
 		confirm
 	fi
 	fi
 
 
@@ -516,13 +609,15 @@ set_install_vars() {
 		MQ_USERNAME="$GET_MQ_USERNAME"
 		MQ_USERNAME="$GET_MQ_USERNAME"
 	fi
 	fi
 
 
-	MQ_PASSWORD=$(
-		tr -dc A-Za-z0-9 </dev/urandom | head -c 30
-		echo ''
-	)
+	if test -z "$MQ_PASSWORD"; then
+		MQ_PASSWORD=$(
+			tr -dc A-Za-z0-9 </dev/urandom | head -c 30
+			echo ''
+		)
+	fi
 
 
 	if [ -z $AUTO_BUILD ]; then
 	if [ -z $AUTO_BUILD ]; then
-		select domain_option in "Auto Generated Password" "Input Your Own Password"; do
+		select domain_option in "Auto Generated / Config Password" "Input Your Own Password"; do
 			case $REPLY in
 			case $REPLY in
 			1)
 			1)
 				echo "using random password for mq"
 				echo "using random password for mq"
@@ -563,13 +658,15 @@ set_install_vars() {
 		TURN_USERNAME="$GET_TURN_USERNAME"
 		TURN_USERNAME="$GET_TURN_USERNAME"
 	fi
 	fi
 
 
-	TURN_PASSWORD=$(
-		tr -dc A-Za-z0-9 </dev/urandom | head -c 30
-		echo ''
-	)
+	if test -z "$TURN_PASSWORD"; then
+		TURN_PASSWORD=$(
+			tr -dc A-Za-z0-9 </dev/urandom | head -c 30
+			echo ''
+		)
+	fi
 
 
 	if [ -z $AUTO_BUILD ]; then
 	if [ -z $AUTO_BUILD ]; then
-		select domain_option in "Auto Generated Password" "Input Your Own Password"; do
+		select domain_option in "Auto Generated / Config Password" "Input Your Own Password"; do
 			case $REPLY in
 			case $REPLY in
 			1)
 			1)
 				echo "using random password for turn"
 				echo "using random password for turn"
@@ -603,7 +700,7 @@ set_install_vars() {
 	echo "-----------------------------------------------------------------"
 	echo "-----------------------------------------------------------------"
 	echo "        domain: $NETMAKER_BASE_DOMAIN"
 	echo "        domain: $NETMAKER_BASE_DOMAIN"
 	echo "         email: $EMAIL"
 	echo "         email: $EMAIL"
-	echo "     public ip: $SERVER_PUBLIC_IP"
+	echo "     public ip: $SERVER_HOST"
 	if [ "$INSTALL_TYPE" = "ee" ]; then
 	if [ "$INSTALL_TYPE" = "ee" ]; then
 		echo "       license: $LICENSE_KEY"
 		echo "       license: $LICENSE_KEY"
 		echo "    account id: $ACCOUNT_ID"
 		echo "    account id: $ACCOUNT_ID"
@@ -612,9 +709,11 @@ set_install_vars() {
 	echo "Confirm Settings for Installation"
 	echo "Confirm Settings for Installation"
 	echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
 	echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
 
 
-	confirm
+	if [ ! "$BUILD_TYPE" = "local" ]; then
+		IMAGE_TAG="$LATEST"
+	fi
 
 
-	save_config
+	confirm
 }
 }
 
 
 # install_netmaker - sets the config files and starts docker-compose
 # install_netmaker - sets the config files and starts docker-compose
@@ -626,55 +725,40 @@ install_netmaker() {
 
 
 	wait_seconds 3
 	wait_seconds 3
 
 
-	# TODO extract wgets to setup(), mirror local_setup()
 	echo "Pulling config files..."
 	echo "Pulling config files..."
 
 
-	COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.yml"
-	CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile"
-	if [ "$INSTALL_TYPE" = "ee" ]; then
-		COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.ee.yml"
-		CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile-EE"
-	fi
+	if [ "$BUILD_TYPE" = "local" ]; then
+		local_install_setup
+	else
+		local BASE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG"
 
 
-	if [ ! "$BUILD_TYPE" = "local" ]; then
-		wget -qO /root/docker-compose.yml $COMPOSE_URL
-		wget -qO /root/Caddyfile $CADDY_URL
-		wget -qO /root/mosquitto.conf "https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/mosquitto.conf"
-		wget -qO /root/nm-certs.sh "https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/scripts/nm-certs.sh"
-		wget -qO /root/wait.sh "https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/wait.sh"
+		local COMPOSE_URL="$BASE_URL/compose/docker-compose.yml"
+		local CADDY_URL="$BASE_URL/docker/Caddyfile"
+		if [ "$INSTALL_TYPE" = "ee" ]; then
+			local COMPOSE_OVERRIDE_URL="$BASE_URL/compose/docker-compose.ee.yml"
+			local CADDY_URL="$BASE_URL/docker/Caddyfile-EE"
+		fi
+		wget -qO "$SCRIPT_DIR"/docker-compose.yml $COMPOSE_URL
+		if test -n "$COMPOSE_OVERRIDE_URL"; then
+			wget -qO "$SCRIPT_DIR"/docker-compose.override.yml $COMPOSE_OVERRIDE_URL
+		fi
+		wget -qO "$SCRIPT_DIR"/Caddyfile "$CADDY_URL"
+		wget -qO "$SCRIPT_DIR"/netmaker.default.env "$BASE_URL/scripts/netmaker.default.env"
+		wget -qO "$SCRIPT_DIR"/mosquitto.conf "$BASE_URL/docker/mosquitto.conf"
+		wget -qO "$SCRIPT_DIR"/nm-certs.sh "$BASE_URL/scripts/nm-certs.sh"
+		wget -qO "$SCRIPT_DIR"/wait.sh "$BASE_URL/docker/wait.sh"
 	fi
 	fi
 
 
-	chmod +x /root/wait.sh
+	chmod +x "$SCRIPT_DIR"/wait.sh
 	mkdir -p /etc/netmaker
 	mkdir -p /etc/netmaker
 
 
-	echo "Setting docker-compose and Caddyfile..."
-
-	sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
-	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/Caddyfile
-	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/docker-compose.yml
-	sed -i "s/REPLACE_MASTER_KEY/$MASTER_KEY/g" /root/docker-compose.yml
-	sed -i "s/YOUR_EMAIL/$EMAIL/g" /root/Caddyfile
-	sed -i "s/REPLACE_MQ_USERNAME/$MQ_USERNAME/g" /root/docker-compose.yml
-	sed -i "s/REPLACE_MQ_PASSWORD/$MQ_PASSWORD/g" /root/docker-compose.yml
-	sed -i "s/REPLACE_TURN_USERNAME/$TURN_USERNAME/g" /root/docker-compose.yml
-	sed -i "s/REPLACE_TURN_PASSWORD/$TURN_PASSWORD/g" /root/docker-compose.yml
-
-	if [ "$INSTALL_TYPE" = "ee" ]; then
-		sed -i "s~YOUR_LICENSE_KEY~$LICENSE_KEY~g" /root/docker-compose.yml
-		sed -i "s/YOUR_ACCOUNT_ID/$ACCOUNT_ID/g" /root/docker-compose.yml
-	fi
-
-	if [ "$BUILD_TYPE" = "version" ] && [ "$INSTALL_TYPE" = "ee" ]; then
-		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG-ee/g" /root/docker-compose.yml
-	else
-		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-	fi
+	# link .env to the user config
+	ln -fs "$SCRIPT_DIR/netmaker.env" "$SCRIPT_DIR/.env"
+	save_config
 
 
-	if [ "$BUILD_TYPE" = "local" ]; then
-		sed -i "s/REPLACE_UI_IMAGE_TAG/$LATEST/g" /root/docker-compose.yml
-	else
-		sed -i "s/REPLACE_UI_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-	fi
+	# Fetch / update certs using certbot
+	chmod +x "$SCRIPT_DIR"/nm-certs.sh
+	"$SCRIPT_DIR"/nm-certs.sh
 
 
 	echo "Starting containers..."
 	echo "Starting containers..."
 
 
@@ -683,7 +767,7 @@ install_netmaker() {
 	export COMPOSE_HTTP_TIMEOUT=120
 	export COMPOSE_HTTP_TIMEOUT=120
 
 
 	# start docker and rebuild containers / networks
 	# start docker and rebuild containers / networks
-	docker-compose -f /root/docker-compose.yml up -d --force-recreate
+	docker-compose -f "$SCRIPT_DIR"/docker-compose.yml up -d --force-recreate
 
 
 	wait_seconds 2
 	wait_seconds 2
 
 
@@ -720,16 +804,28 @@ setup_mesh() {
 
 
 	wait_seconds 5
 	wait_seconds 5
 
 
-	echo "Creating netmaker network (10.101.0.0/16)"
+	local networkCount=$(nmctl network list -o json | jq '. | length')
 
 
-	nmctl network create --name netmaker --ipv4_addr 10.101.0.0/16
+	# add a network if none present
+	if [ "$networkCount" -lt 1 ]; then
+		echo "Creating netmaker network (10.101.0.0/16)"
 
 
-	wait_seconds 5
+		# TODO causes "Error Status: 400 Response: {"Code":400,"Message":"could not find any records"}"
+		nmctl network create --name netmaker --ipv4_addr 10.101.0.0/16
 
 
-	echo "Creating netmaker enrollment key"
+		wait_seconds 5
+	fi
 
 
-	tokenJson=$(nmctl enrollment_key create --unlimited --networks netmaker)
+	echo "Obtaining a netmaker enrollment key..."
+
+	local tokenJson=$(nmctl enrollment_key create --unlimited --networks netmaker)
 	TOKEN=$(jq -r '.token' <<<${tokenJson})
 	TOKEN=$(jq -r '.token' <<<${tokenJson})
+	if test -z "$TOKEN"; then
+		echo "Error creating an enrollment key"
+		exit 1
+	else
+		echo "Enrollment key ready"
+	fi
 
 
 	wait_seconds 3
 	wait_seconds 3
 
 
@@ -745,9 +841,41 @@ print_success() {
 	echo "-----------------------------------------------------------------"
 	echo "-----------------------------------------------------------------"
 }
 }
 
 
+cleanup() {
+	# remove the existing netclient's instance from the existing network
+	if command -v nmctl >/dev/null 2>&1; then
+		local node_id=$(netclient list | jq '.[0].node_id' 2>/dev/null)
+		# trim doublequotes
+		node_id="${node_id//\"/}"
+		if test -n "$node_id"; then
+			echo "De-registering the existing netclient..."
+			nmctl node delete netmaker $node_id >/dev/null 2>&1
+		fi
+	fi
+
+	echo "Stopping all containers..."
+	local containers=("mq" "netmaker-ui" "coredns" "turn" "caddy" "netmaker" "netmaker-exporter" "prometheus" "grafana")
+	for name in "${containers[@]}"; do
+		local running=$(docker ps | grep -w "$name")
+		local exists=$(docker ps -a | grep -w "$name")
+		if test -n "$running"; then
+			docker stop "$name" 1>/dev/null
+		fi
+		if test -n "$exists"; then
+			docker rm "$name" 1>/dev/null
+		fi
+	done
+}
+
 # 1. print netmaker logo
 # 1. print netmaker logo
 print_logo
 print_logo
 
 
+# read the config
+if [ -f "$CONFIG_PATH" ]; then
+	echo "Using config: $CONFIG_PATH"
+	source "$CONFIG_PATH"
+fi
+
 # 2. setup the build instructions
 # 2. setup the build instructions
 set_buildinfo
 set_buildinfo
 
 
@@ -759,25 +887,14 @@ install_dependencies
 # 4. install yq if necessary
 # 4. install yq if necessary
 install_yq
 install_yq
 
 
-# 5. if running a local build, clone git and build artifacts
-if [ "$BUILD_TYPE" = "local" ]; then
-	local_install_setup
-fi
-
 set -e
 set -e
 
 
 # 6. get user input for variables
 # 6. get user input for variables
 set_install_vars
 set_install_vars
 
 
-# stop
-for name in "mq" "netmaker-ui" "coredns" "turn" "caddy" "netmaker"; do
-	if test -n "$(docker ps | grep name)"; then
-		docker stop $name
-	fi
-done
-
-# Fetch / update certs using certbot
-"$SCRIPT_DIR"/nm-certs.sh
+set +e
+cleanup
+set -e
 
 
 # 7. get and set config files, startup docker-compose
 # 7. get and set config files, startup docker-compose
 install_netmaker
 install_netmaker

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

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

+ 16 - 0
servercfg/serverconf.go

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

+ 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.19.0
+    version: 0.20.2
 paths:
 paths:
     /api/dns:
     /api/dns:
         get:
         get: