Browse Source

Merge branch 'develop' of https://github.com/gravitl/netmaker into mock_scale

abhishek9686 6 months ago
parent
commit
622be04a3e
72 changed files with 2303 additions and 977 deletions
  1. 1 1
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 2 2
      .github/workflows/deletedroplets.yml
  3. 3 1
      Dockerfile
  4. 1 1
      Dockerfile-quick
  5. 6 6
      README.md
  6. 11 0
      cli/cmd/ext_client/config.go
  7. 55 0
      cli/cmd/gateway/create.go
  8. 27 0
      cli/cmd/gateway/delete.go
  9. 18 0
      cli/cmd/gateway/root.go
  10. 6 5
      cli/cmd/node/create_ingress.go
  11. 5 4
      cli/cmd/node/create_relay.go
  12. 6 5
      cli/cmd/node/delete_ingress.go
  13. 5 4
      cli/cmd/node/delete_relay.go
  14. 2 0
      cli/cmd/root.go
  15. 5 0
      cli/functions/ext_client.go
  16. 18 0
      cli/functions/gateway.go
  17. 2 5
      compose/docker-compose.yml
  18. 8 7
      config/config.go
  19. 1 0
      controllers/controller.go
  20. 246 0
      controllers/ext_client.go
  21. 207 0
      controllers/gateway.go
  22. 137 8
      controllers/hosts.go
  23. 35 9
      controllers/network.go
  24. 19 2
      controllers/network_test.go
  25. 51 111
      controllers/node.go
  26. 4 0
      controllers/node_test.go
  27. 9 0
      controllers/user.go
  28. 3 0
      database/database.go
  29. 5 5
      go.mod
  30. 10 10
      go.sum
  31. 68 15
      logic/acls.go
  32. 57 1
      logic/enrollmentkey.go
  33. 24 19
      logic/extpeers.go
  34. 39 2
      logic/gateway.go
  35. 168 20
      logic/networks.go
  36. 90 44
      logic/nodes.go
  37. 116 32
      logic/peers.go
  38. 225 13
      logic/relay.go
  39. 6 2
      logic/status.go
  40. 1 1
      logic/tags.go
  41. 1 0
      logic/user_mgmt.go
  42. 25 0
      logic/util.go
  43. 26 0
      migrate/migrate.go
  44. 51 29
      models/api_node.go
  45. 4 0
      models/extclient.go
  46. 9 0
      models/gateway.go
  47. 2 0
      models/host.go
  48. 1 0
      models/metrics.go
  49. 33 23
      models/mqtt.go
  50. 29 17
      models/network.go
  51. 13 9
      models/node.go
  52. 42 33
      models/structs.go
  53. 1 0
      models/user_mgmt.go
  54. 1 0
      mq/handlers.go
  55. 5 1
      mq/publishers.go
  56. 6 0
      pro/auth/auth.go
  57. 4 0
      pro/auth/azure-ad.go
  58. 137 4
      pro/controllers/failover.go
  59. 3 0
      pro/controllers/inet_gws.go
  60. 0 152
      pro/controllers/relay.go
  61. 13 0
      pro/controllers/users.go
  62. 1 1
      pro/email/email.go
  63. 2 8
      pro/initialize.go
  64. 67 12
      pro/logic/failover.go
  65. 0 255
      pro/logic/relays.go
  66. 53 17
      pro/logic/status.go
  67. 6 4
      pro/logic/user_mgmt.go
  68. 4 0
      scripts/netmaker.default.env
  69. 6 49
      scripts/nm-quick.sh
  70. 33 1
      servercfg/serverconf.go
  71. 2 26
      serverctl/serverctl.go
  72. 21 1
      utils/utils.go

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

@@ -2,7 +2,7 @@ name: Bug Report
 description: File a bug report
 description: File a bug report
 title: "[Bug]: "
 title: "[Bug]: "
 labels: ["bug", "triage"]
 labels: ["bug", "triage"]
-assignees: ["ok-john", "0xdcarns", "afeiszli",  "mattkasun"]
+assignees: ["abhishek9686","VishalDalwadi","Aceix","dentadlp"]
 body:
 body:
   - type: markdown
   - type: markdown
     attributes:
     attributes:

+ 2 - 2
.github/workflows/deletedroplets.yml

@@ -12,7 +12,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v7
+        uses: dawidd6/action-download-artifact@v8
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn
@@ -75,7 +75,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v7
+        uses: dawidd6/action-download-artifact@v8
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn

+ 3 - 1
Dockerfile

@@ -6,11 +6,13 @@ 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.21.0
+FROM alpine:3.21.2
 
 
 # add a c lib
 # add a c lib
 # set the working directory
 # set the working directory
 WORKDIR /root/
 WORKDIR /root/
+RUN apk update && apk upgrade
+RUN apk add --no-cache sqlite
 RUN mkdir -p /etc/netclient/config
 RUN mkdir -p /etc/netclient/config
 COPY --from=builder /app/netmaker .
 COPY --from=builder /app/netmaker .
 COPY --from=builder /app/config config
 COPY --from=builder /app/config config

+ 1 - 1
Dockerfile-quick

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

+ 6 - 6
README.md

@@ -49,14 +49,14 @@
 
 
 # Try Netmaker SaaS  
 # Try Netmaker SaaS  
 
 
-If you're looking for a managed service, you can get started with just few clicks, visit [netmaker.io](https://account.netmaker.io) to create your netmaker server.  
+If you're looking for a managed service, you can get started with just a few clicks, visit [netmaker.io](https://account.netmaker.io) to create your netmaker server.  
 
 
 # Self-Hosted Open Source Quick Start  
 # Self-Hosted Open Source Quick Start  
 
 
-These are the instructions for deploying a Netmaker server on your own cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/docs/server-installation/quick-install#quick-install-script).  
+These are the instructions for deploying a Netmaker server on your cloud VM as quickly as possible. For more detailed instructions, visit the [Install Docs](https://docs.netmaker.io/docs/server-installation/quick-install#quick-install-script).  
 
 
-1. Get a cloud VM with Ubuntu 22.04 and a public IP.
-2. Open ports 443, 80, 3479, 8089 and 51821-51830/udp on the VM firewall and in cloud security settings.
+1. Get a cloud VM with Ubuntu 24.04 and a static public IP.
+2. Allow inbound traffic on port 443,51821 TCP and UDP to the VM firewall in cloud security settings, and for simplicity, allow outbound on All TCP and All UDP.
 3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP.
 3. (recommended) Prepare DNS - Set a wildcard subdomain in your DNS settings for Netmaker, e.g. *.netmaker.example.com, which points to your VM's public IP.
 4. Run the script to setup open source version of Netmaker: 
 4. Run the script to setup open source version of Netmaker: 
 
 
@@ -82,11 +82,11 @@ After installing Netmaker, check out the [Walkthrough](https://itnext.io/getting
 
 
 # Why Netmaker + WireGuard?
 # Why Netmaker + WireGuard?
 
 
-- Netmaker automates virtual networks between data centers, clouds, and edge devices, so you don't have to.
+- Netmaker automates virtual networks between data centres, clouds, and edge devices, so you don't have to.
 
 
 - Kernel WireGuard offers maximum speed, performance, and security. 
 - Kernel WireGuard offers maximum speed, performance, and security. 
 
 
-- Netmaker is built to scale from the small business to the enterprise. 
+- Netmaker is built to scale from small businesses to enterprises. 
 
 
 - Netmaker with WireGuard can be highly customized for peer-to-peer, site-to-site, Kubernetes, and more.
 - Netmaker with WireGuard can be highly customized for peer-to-peer, site-to-site, Kubernetes, and more.
 
 

+ 11 - 0
cli/cmd/ext_client/config.go

@@ -17,6 +17,17 @@ var extClientConfigCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+var extClientHAConfigCmd = &cobra.Command{
+	Use:   "auto_config [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Get an External Client Configuration",
+	Long:  `Get an External Client Configuration`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(functions.GetExtClientHAConfig(args[0]))
+	},
+}
+
 func init() {
 func init() {
 	rootCmd.AddCommand(extClientConfigCmd)
 	rootCmd.AddCommand(extClientConfigCmd)
+	rootCmd.AddCommand(extClientHAConfigCmd)
 }
 }

+ 55 - 0
cli/cmd/gateway/create.go

@@ -0,0 +1,55 @@
+package gateway
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+	"strings"
+)
+
+var externalClientDNS string
+var isInternetGateway bool
+var metadata string
+var persistentKeepAlive uint
+var mtu uint
+
+var gatewayCreateCmd = &cobra.Command{
+	Use:   "create [NETWORK NAME] [NODE ID] [RELAYED NODES ID (comma separated)]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Create a new Gateway on a Netmaker network.",
+	Long: `
+Configures a node as a gateway in a specified network, allowing it to relay traffic for other nodes. The gateway can also function as an internet gateway if specified.
+
+Arguments:
+NETWORK NAME:		The name of the network where the gateway will be created.
+NODE ID:			The ID of the node to be configured as a gateway.
+RELAYED NODES ID:	A comma-separated list of node IDs that will be relayed through this gateway.
+`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(
+			functions.CreateGateway(
+				models.IngressRequest{
+					ExtclientDNS:        externalClientDNS,
+					IsInternetGateway:   isInternetGateway,
+					Metadata:            metadata,
+					PersistentKeepalive: int32(persistentKeepAlive),
+					MTU:                 int32(mtu),
+				},
+				models.RelayRequest{
+					NodeID:       args[0],
+					NetID:        args[1],
+					RelayedNodes: strings.Split(args[2], ","),
+				},
+			),
+		)
+	},
+}
+
+func init() {
+	gatewayCreateCmd.Flags().StringVarP(&externalClientDNS, "dns", "d", "", "the IP address of the DNS server to be used by external clients")
+	gatewayCreateCmd.Flags().BoolVarP(&isInternetGateway, "internet", "i", false, "if set, the gateway will route traffic to the internet")
+	gatewayCreateCmd.Flags().StringVarP(&metadata, "note", "n", "", "description or metadata to be associated with the gateway")
+	gatewayCreateCmd.Flags().UintVarP(&persistentKeepAlive, "keep-alive", "k", 20, "the keep-alive interval (in seconds) for maintaining persistent connections")
+	gatewayCreateCmd.Flags().UintVarP(&mtu, "mtu", "m", 1420, "the maximum transmission unit (MTU) size in bytes")
+	rootCmd.AddCommand(gatewayCreateCmd)
+}

+ 27 - 0
cli/cmd/gateway/delete.go

@@ -0,0 +1,27 @@
+package gateway
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var gatewayDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete a Gateway.",
+	Long: `
+Removes the gateway configuration from a node in a specified network. The node itself remains, but it will no longer function as a gateway.
+
+Arguments:
+NETWORK NAME:	The name of the network from which the gateway configuration should be removed.
+NODE ID:		The ID of the node that is currently acting as a gateway.
+`,
+	Aliases: []string{"rm"},
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteGateway(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(gatewayDeleteCmd)
+}

+ 18 - 0
cli/cmd/gateway/root.go

@@ -0,0 +1,18 @@
+package gateway
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands.
+var rootCmd = &cobra.Command{
+	Use:     "gateway",
+	Short:   "Manage Gateways.",
+	Long:    `Manage Gateways.`,
+	Aliases: []string{"gw"},
+}
+
+// GetRoot returns the root subcommand.
+func GetRoot() *cobra.Command {
+	return rootCmd
+}

+ 6 - 5
cli/cmd/node/create_ingress.go

@@ -6,11 +6,12 @@ import (
 )
 )
 
 
 var nodeCreateIngressCmd = &cobra.Command{
 var nodeCreateIngressCmd = &cobra.Command{
-	Use:     "create_remote_access_gateway [NETWORK NAME] [NODE ID]",
-	Args:    cobra.ExactArgs(2),
-	Short:   "Turn a Node into a Remote Access Gateway (Ingress)",
-	Long:    `Turn a Node into a Remote Access Gateway (Ingress) for a Network.`,
-	Aliases: []string{"create_rag"},
+	Use:        "create_remote_access_gateway [NETWORK NAME] [NODE ID]",
+	Args:       cobra.ExactArgs(2),
+	Short:      "Turn a Node into a Remote Access Gateway (Ingress)",
+	Long:       `Turn a Node into a Remote Access Gateway (Ingress) for a Network.`,
+	Deprecated: "in favour of the `gateway` subcommand, in Netmaker v0.90.0.",
+	Aliases:    []string{"create_rag"},
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
 		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
 	},
 	},

+ 5 - 4
cli/cmd/node/create_relay.go

@@ -8,10 +8,11 @@ import (
 )
 )
 
 
 var hostCreateRelayCmd = &cobra.Command{
 var hostCreateRelayCmd = &cobra.Command{
-	Use:   "create_relay [NETWORK][NODE ID] [RELAYED NODE IDS (comma separated)]",
-	Args:  cobra.ExactArgs(3),
-	Short: "Turn a Node into a Relay",
-	Long:  `Turn a Node into a Relay`,
+	Use:        "create_relay [NETWORK][NODE ID] [RELAYED NODE IDS (comma separated)]",
+	Args:       cobra.ExactArgs(3),
+	Short:      "Turn a Node into a Relay",
+	Long:       `Turn a Node into a Relay`,
+	Deprecated: "in favour of the `gateway` subcommand, in Netmaker v0.90.0.",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.CreateRelay(args[0], args[1], strings.Split(args[2], ",")))
 		functions.PrettyPrint(functions.CreateRelay(args[0], args[1], strings.Split(args[2], ",")))
 	},
 	},

+ 6 - 5
cli/cmd/node/delete_ingress.go

@@ -6,11 +6,12 @@ import (
 )
 )
 
 
 var nodeDeleteIngressCmd = &cobra.Command{
 var nodeDeleteIngressCmd = &cobra.Command{
-	Use:     "delete_remote_access_gateway [NETWORK NAME] [NODE ID]",
-	Args:    cobra.ExactArgs(2),
-	Short:   "Delete Remote Access Gateway role from a Node",
-	Long:    `Delete Remote Access Gateway role from a Node`,
-	Aliases: []string{"delete_rag"},
+	Use:        "delete_remote_access_gateway [NETWORK NAME] [NODE ID]",
+	Args:       cobra.ExactArgs(2),
+	Short:      "Delete Remote Access Gateway role from a Node",
+	Long:       `Delete Remote Access Gateway role from a Node`,
+	Deprecated: "in favour of the `gateway` subcommand, in Netmaker v0.90.0.",
+	Aliases:    []string{"delete_rag"},
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
 		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
 	},
 	},

+ 5 - 4
cli/cmd/node/delete_relay.go

@@ -6,10 +6,11 @@ import (
 )
 )
 
 
 var hostDeleteRelayCmd = &cobra.Command{
 var hostDeleteRelayCmd = &cobra.Command{
-	Use:   "delete_relay [NETWORK] [NODE ID]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Delete Relay from a node",
-	Long:  `Delete Relay from a node`,
+	Use:        "delete_relay [NETWORK] [NODE ID]",
+	Args:       cobra.ExactArgs(2),
+	Short:      "Delete Relay from a node",
+	Long:       `Delete Relay from a node`,
+	Deprecated: "in favour of the `gateway` subcommand, in Netmaker v0.90.0.",
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteRelay(args[0], args[1]))
 		functions.PrettyPrint(functions.DeleteRelay(args[0], args[1]))
 	},
 	},

+ 2 - 0
cli/cmd/root.go

@@ -1,6 +1,7 @@
 package cmd
 package cmd
 
 
 import (
 import (
+	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"os"
 	"os"
 
 
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
@@ -55,4 +56,5 @@ func init() {
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
+	rootCmd.AddCommand(gateway.GetRoot())
 }
 }

+ 5 - 0
cli/functions/ext_client.go

@@ -27,6 +27,11 @@ func GetExtClientConfig(networkName, clientID string) string {
 	return get(fmt.Sprintf("/api/extclients/%s/%s/file", networkName, clientID))
 	return get(fmt.Sprintf("/api/extclients/%s/%s/file", networkName, clientID))
 }
 }
 
 
+// GetExtClientConfig - auto fetch a client config
+func GetExtClientHAConfig(networkName string) string {
+	return get(fmt.Sprintf("/api/v1/client_conf/%s", networkName))
+}
+
 // CreateExtClient - create an external client
 // CreateExtClient - create an external client
 func CreateExtClient(networkName, nodeID string, extClient models.CustomExtClient) {
 func CreateExtClient(networkName, nodeID string, extClient models.CustomExtClient) {
 	request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), extClient)
 	request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), extClient)

+ 18 - 0
cli/functions/gateway.go

@@ -0,0 +1,18 @@
+package functions
+
+import (
+	"fmt"
+	"github.com/gravitl/netmaker/models"
+	"net/http"
+)
+
+func CreateGateway(ingressRequest models.IngressRequest, relayRequest models.RelayRequest) *models.ApiNode {
+	return request[models.ApiNode](http.MethodPost, fmt.Sprintf("/api/nodes/%s/%s/gateway", relayRequest.NetID, relayRequest.NodeID), &models.CreateGwReq{
+		IngressRequest: ingressRequest,
+		RelayRequest:   relayRequest,
+	})
+}
+
+func DeleteGateway(networkID, nodeID string) *models.ApiNode {
+	return request[models.ApiNode](http.MethodDelete, fmt.Sprintf("/api/nodes/%s/%s/gateway", networkID, nodeID), nil)
+}

+ 2 - 5
compose/docker-compose.yml

@@ -52,11 +52,8 @@ services:
       - caddy_data:/data
       - caddy_data:/data
       - caddy_conf:/config
       - caddy_conf:/config
     ports:
     ports:
-      - "$SERVER_HOST:80:80/tcp"
-      - "$SERVER_HOST:443:443/tcp"
-    #uncomment to enable IPv6 communication
-     # - "$SERVER_HOST6:80:80/tcp"
-     # - "$SERVER_HOST6:443:443/tcp"
+      - "80:80/tcp"
+      - "443:443/tcp"
 
 
   coredns:
   coredns:
     #network_mode: host
     #network_mode: host

+ 8 - 7
config/config.go

@@ -37,7 +37,7 @@ type ServerConfig struct {
 	APIConnString              string        `yaml:"apiconn"`
 	APIConnString              string        `yaml:"apiconn"`
 	APIHost                    string        `yaml:"apihost"`
 	APIHost                    string        `yaml:"apihost"`
 	APIPort                    string        `yaml:"apiport"`
 	APIPort                    string        `yaml:"apiport"`
-	Broker                     string        `yam:"broker"`
+	Broker                     string        `yaml:"broker"`
 	ServerBrokerEndpoint       string        `yaml:"serverbrokerendpoint"`
 	ServerBrokerEndpoint       string        `yaml:"serverbrokerendpoint"`
 	BrokerType                 string        `yaml:"brokertype"`
 	BrokerType                 string        `yaml:"brokertype"`
 	EmqxRestEndpoint           string        `yaml:"emqxrestendpoint"`
 	EmqxRestEndpoint           string        `yaml:"emqxrestendpoint"`
@@ -92,14 +92,15 @@ type ServerConfig struct {
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
-	EndpointDetection          bool          `json:"endpoint_detection"`
+	EndpointDetection          bool          `yaml:"endpoint_detection"`
 	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
-	EmailSenderAddr            string        `json:"email_sender_addr"`
-	EmailSenderUser            string        `json:"email_sender_user"`
-	EmailSenderPassword        string        `json:"email_sender_password"`
-	SmtpHost                   string        `json:"smtp_host"`
-	SmtpPort                   int           `json:"smtp_port"`
+	EmailSenderAddr            string        `yaml:"email_sender_addr"`
+	EmailSenderUser            string        `yaml:"email_sender_user"`
+	EmailSenderPassword        string        `yaml:"email_sender_password"`
+	SmtpHost                   string        `yaml:"smtp_host"`
+	SmtpPort                   int           `yaml:"smtp_port"`
 	MetricInterval             string        `yaml:"metric_interval"`
 	MetricInterval             string        `yaml:"metric_interval"`
+	MetricsPort                int           `yaml:"metrics_port"`
 	ManageDNS                  bool          `yaml:"manage_dns"`
 	ManageDNS                  bool          `yaml:"manage_dns"`
 	Stun                       bool          `yaml:"stun"`
 	Stun                       bool          `yaml:"stun"`
 	StunServers                string        `yaml:"stun_servers"`
 	StunServers                string        `yaml:"stun_servers"`

+ 1 - 0
controllers/controller.go

@@ -24,6 +24,7 @@ var HttpMiddlewares = []mux.MiddlewareFunc{
 // HttpHandlers - handler functions for REST interactions
 // HttpHandlers - handler functions for REST interactions
 var HttpHandlers = []interface{}{
 var HttpHandlers = []interface{}{
 	nodeHandlers,
 	nodeHandlers,
+	gwHandlers,
 	userHandlers,
 	userHandlers,
 	networkHandlers,
 	networkHandlers,
 	dnsHandlers,
 	dnsHandlers,

+ 246 - 0
controllers/ext_client.go

@@ -42,6 +42,7 @@ func extClientHandlers(r *mux.Router) {
 		Methods(http.MethodDelete)
 		Methods(http.MethodDelete)
 	r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).
 	r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/client_conf/{network}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientHAConf))).Methods(http.MethodGet)
 }
 }
 
 
 func checkIngressExists(nodeID string) bool {
 func checkIngressExists(nodeID string) bool {
@@ -387,6 +388,251 @@ Endpoint = %s
 	json.NewEncoder(w).Encode(client)
 	json.NewEncoder(w).Encode(client)
 }
 }
 
 
+// @Summary     Get an individual remote access client
+// @Router      /api/extclients/{network}/{clientid}/{type} [get]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
+func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
+
+	var params = mux.Vars(r)
+	networkid := params["network"]
+	network, err := logic.GetParentNetwork(networkid)
+	if err != nil {
+		logger.Log(
+			1,
+			r.Header.Get("user"),
+			"Could not retrieve Ingress Gateway Network",
+			networkid,
+		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	// fetch client based on availability
+	nodes, _ := logic.GetNetworkNodes(networkid)
+	defaultPolicy, _ := logic.GetDefaultPolicy(models.NetworkID(networkid), models.DevicePolicy)
+	var targetGwID string
+	var connectionCnt int = -1
+	for _, nodeI := range nodes {
+		if nodeI.IsGw {
+			// check health status
+			logic.GetNodeStatus(&nodeI, defaultPolicy.Enabled)
+			if nodeI.Status != models.OnlineSt {
+				continue
+			}
+			// Get Total connections on the gw
+			clients := logic.GetGwExtclients(nodeI.ID.String(), networkid)
+
+			if connectionCnt == -1 || len(clients) < connectionCnt {
+				connectionCnt = len(clients)
+				targetGwID = nodeI.ID.String()
+			}
+
+		}
+	}
+	gwnode, err := logic.GetNodeByID(targetGwID)
+	if err != nil {
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to get ingress gateway node [%s] info: %v",
+				gwnode.ID,
+				err,
+			),
+		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	host, err := logic.GetHost(gwnode.HostID.String())
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", gwnode.ID, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	var userName string
+	if r.Header.Get("ismaster") == "yes" {
+		userName = logic.MasterUser
+	} else {
+		caller, err := logic.GetUser(r.Header.Get("user"))
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		userName = caller.UserName
+	}
+	// create client
+	var extclient models.ExtClient
+	extclient.OwnerID = userName
+	extclient.IngressGatewayID = targetGwID
+	extclient.Network = networkid
+	extclient.Tags = make(map[models.TagID]struct{})
+	// extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
+	// 	models.RemoteAccessTagName))] = struct{}{}
+	// set extclient dns to ingressdns if extclient dns is not explicitly set
+	if (extclient.DNS == "") && (gwnode.IngressDNS != "") {
+		extclient.DNS = gwnode.IngressDNS
+	}
+
+	listenPort := logic.GetPeerListenPort(host)
+	extclient.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
+	extclient.Enabled = true
+
+	if err = logic.CreateExtClient(&extclient); err != nil {
+		slog.Error(
+			"failed to create extclient",
+			"user",
+			r.Header.Get("user"),
+			"network",
+			networkid,
+			"error",
+			err,
+		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	client, err := logic.GetExtClient(extclient.ClientID, networkid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	addrString := client.Address
+	if addrString != "" {
+		addrString += "/32"
+	}
+	if client.Address6 != "" {
+		if addrString != "" {
+			addrString += ","
+		}
+		addrString += client.Address6 + "/128"
+	}
+
+	keepalive := ""
+	if network.DefaultKeepalive != 0 {
+		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
+	}
+	if gwnode.IngressPersistentKeepalive != 0 {
+		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
+	}
+	var newAllowedIPs string
+	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+		egressrange := "0.0.0.0/0"
+		if gwnode.Address6.IP != nil && client.Address6 != "" {
+			egressrange += "," + "::/0"
+		}
+		newAllowedIPs = egressrange
+	} else {
+		newAllowedIPs = network.AddressRange
+		if newAllowedIPs != "" && network.AddressRange6 != "" {
+			newAllowedIPs += ","
+		}
+		if network.AddressRange6 != "" {
+			newAllowedIPs += network.AddressRange6
+		}
+		if egressGatewayRanges, err := logic.GetEgressRangesOnNetwork(&client); err == nil {
+			for _, egressGatewayRange := range egressGatewayRanges {
+				newAllowedIPs += "," + egressGatewayRange
+			}
+		}
+	}
+	gwendpoint := ""
+	if host.EndpointIP.To4() == nil {
+		gwendpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), host.ListenPort)
+	} else {
+		gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
+	}
+	defaultDNS := ""
+	if client.DNS != "" {
+		defaultDNS = "DNS = " + client.DNS
+	} else if gwnode.IngressDNS != "" {
+		defaultDNS = "DNS = " + gwnode.IngressDNS
+	}
+
+	defaultMTU := 1420
+	if host.MTU != 0 {
+		defaultMTU = host.MTU
+	}
+	if gwnode.IngressMTU != 0 {
+		defaultMTU = int(gwnode.IngressMTU)
+	}
+
+	postUp := strings.Builder{}
+	if client.PostUp != "" && params["type"] != "qr" {
+		for _, loc := range strings.Split(client.PostUp, "\n") {
+			postUp.WriteString(fmt.Sprintf("PostUp = %s\n", loc))
+		}
+	}
+
+	postDown := strings.Builder{}
+	if client.PostDown != "" && params["type"] != "qr" {
+		for _, loc := range strings.Split(client.PostDown, "\n") {
+			postDown.WriteString(fmt.Sprintf("PostDown = %s\n", loc))
+		}
+	}
+
+	config := fmt.Sprintf(`[Interface]
+Address = %s
+PrivateKey = %s
+MTU = %d
+%s
+%s
+%s
+
+[Peer]
+PublicKey = %s
+AllowedIPs = %s
+Endpoint = %s
+%s
+
+`, addrString,
+		client.PrivateKey,
+		defaultMTU,
+		defaultDNS,
+		postUp.String(),
+		postDown.String(),
+		host.PublicKey,
+		newAllowedIPs,
+		gwendpoint,
+		keepalive,
+	)
+
+	go func() {
+		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
+			slog.Error(
+				"failed to set default acls for extclient",
+				"user",
+				r.Header.Get("user"),
+				"network",
+				networkid,
+				"error",
+				err,
+			)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		if err := mq.PublishPeerUpdate(false); err != nil {
+			logger.Log(1, "error publishing peer update ", err.Error())
+		}
+		if servercfg.IsDNSMode() {
+			logic.SetDNS()
+		}
+	}()
+
+	name := client.ClientID + ".conf"
+	w.Header().Set("Content-Type", "application/config")
+	w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
+	w.WriteHeader(http.StatusOK)
+	_, err = fmt.Fprint(w, config)
+	if err != nil {
+		logger.Log(1, r.Header.Get("user"), "response writer error (file) ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	}
+}
+
 // @Summary     Create an individual remote access client
 // @Summary     Create an individual remote access client
 // @Router      /api/extclients/{network}/{nodeid} [post]
 // @Router      /api/extclients/{network}/{nodeid} [post]
 // @Tags        Remote Access Client
 // @Tags        Remote Access Client

+ 207 - 0
controllers/gateway.go

@@ -0,0 +1,207 @@
+package controller
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
+)
+
+func gwHandlers(r *mux.Router) {
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/gateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createGateway)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/gateway", logic.SecurityCheck(true, http.HandlerFunc(deleteGateway))).Methods(http.MethodDelete)
+	// old relay handlers
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", logic.SecurityCheck(true, http.HandlerFunc(createGateway))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", logic.SecurityCheck(true, http.HandlerFunc(deleteGateway))).Methods(http.MethodDelete)
+}
+
+// @Summary     Create a gateway
+// @Router      /api/nodes/{network}/{nodeid}/gateway [post]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
+func createGateway(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	node, err := logic.ValidateParams(nodeid, netid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var req models.CreateGwReq
+	err = json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	node, err = logic.CreateIngressGateway(netid, nodeid, req.IngressRequest)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to create gateway on node [%s] on network [%s]: %v",
+				nodeid, netid, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	req.RelayRequest.NetID = netid
+	req.RelayRequest.NodeID = nodeid
+	_, relayNode, err := logic.CreateRelay(req.RelayRequest)
+	if err != nil {
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to create relay on node [%s] on network [%s]: %v",
+				req.RelayRequest.NodeID,
+				req.RelayRequest.NetID,
+				err,
+			),
+		)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, relayedNodeID := range relayNode.RelayedNodes {
+		relayedNode, err := logic.GetNodeByID(relayedNodeID)
+		if err == nil {
+			if relayedNode.FailedOverBy != uuid.Nil {
+				go logic.ResetFailedOverPeer(&relayedNode)
+			}
+
+		}
+	}
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created gw node",
+		req.RelayRequest.NodeID,
+		"on network",
+		req.RelayRequest.NetID,
+	)
+	apiNode := relayNode.ConvertToAPINode()
+
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+	go func() {
+		if err := mq.NodeUpdate(&node); err != nil {
+			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
+		}
+		mq.PublishPeerUpdate(false)
+	}()
+
+}
+
+// @Summary     Delete a gateway
+// @Router      /api/nodes/{network}/{nodeid}/gateway [delete]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
+func deleteGateway(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	netid := params["network"]
+	node, err := logic.ValidateParams(nodeid, netid)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	node, removedClients, err := logic.DeleteIngressGateway(nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to delete ingress gateway on node [%s] on network [%s]: %v",
+				nodeid, netid, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	updateNodes, node, err := logic.DeleteRelay(netid, nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	node, err = logic.GetNodeByID(node.ID.String())
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get node", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	node.IsGw = false
+	logic.UpsertNode(&node)
+	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
+
+	go func() {
+		host, err := logic.GetHost(node.HostID.String())
+		if err == nil {
+			allNodes, err := logic.GetAllNodes()
+			if err != nil {
+				return
+			}
+
+			for _, relayedNode := range updateNodes {
+				err = mq.NodeUpdate(&relayedNode)
+				if err != nil {
+					logger.Log(
+						1,
+						"relayed node update ",
+						relayedNode.ID.String(),
+						"on network",
+						relayedNode.Network,
+						": ",
+						err.Error(),
+					)
+
+				}
+				h, err := logic.GetHost(relayedNode.HostID.String())
+				if err == nil {
+					if h.OS == models.OS_Types.IoT {
+						nodes, err := logic.GetAllNodes()
+						if err != nil {
+							return
+						}
+						node.IsRelay = true // for iot update to recognise that it has to delete relay peer
+						if err = mq.PublishSingleHostPeerUpdate(h, nodes, &node, nil, false, nil); err != nil {
+							logger.Log(1, "failed to publish peer update to host", h.ID.String(), ": ", err.Error())
+						}
+					}
+				}
+			}
+			if len(removedClients) > 0 {
+				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil {
+					slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err)
+				}
+			}
+			mq.PublishPeerUpdate(false)
+			if err := mq.NodeUpdate(&node); err != nil {
+				slog.Error(
+					"error publishing node update to node",
+					"node",
+					node.ID,
+					"error",
+					err,
+				)
+			}
+			if servercfg.IsDNSMode() {
+				logic.SetDNS()
+			}
+
+		}
+
+	}()
+
+	apiNode := node.ConvertToAPINode()
+	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(apiNode)
+}

+ 137 - 8
controllers/hosts.go

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"time"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
@@ -23,6 +24,10 @@ func hostHandlers(r *mux.Router) {
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).
 	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).
 		Methods(http.MethodPut)
 		Methods(http.MethodPut)
+	r.HandleFunc("/api/hosts/sync", logic.SecurityCheck(true, http.HandlerFunc(syncHosts))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/hosts/upgrade", logic.SecurityCheck(true, http.HandlerFunc(upgradeHosts))).
+		Methods(http.MethodPost)
 	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).
 	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).
 		Methods(http.MethodPut)
 		Methods(http.MethodPut)
 	r.HandleFunc("/api/hosts/{hostid}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncHost))).
 	r.HandleFunc("/api/hosts/{hostid}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncHost))).
@@ -44,16 +49,64 @@ func hostHandlers(r *mux.Router) {
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).
 		Methods(http.MethodPut)
 		Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/host/{hostid}/peer_info", Authorize(true, false, "host", http.HandlerFunc(getHostPeerInfo))).
+		Methods(http.MethodGet)
 	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).
 	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).
 		Methods(http.MethodDelete)
 		Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 }
 
 
+// @Summary     Requests all the hosts to upgrade their version
+// @Router      /api/hosts/upgrade [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       force query bool false "Force upgrade"
+// @Success     200 {string} string "upgrade all hosts request received"
+func upgradeHosts(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+
+	action := models.Upgrade
+
+	if r.URL.Query().Get("force") == "true" {
+		action = models.ForceUpgrade
+	}
+
+	user := r.Header.Get("user")
+
+	go func() {
+		slog.Info("requesting all hosts to upgrade", "user", user)
+
+		hosts, err := logic.GetAllHosts()
+		if err != nil {
+			slog.Error("failed to retrieve all hosts", "user", user, "error", err)
+			return
+		}
+
+		for _, host := range hosts {
+			go func(host models.Host) {
+				hostUpdate := models.HostUpdate{
+					Action: action,
+					Host:   host,
+				}
+				if err = mq.HostUpdate(&hostUpdate); err != nil {
+					slog.Error("failed to request host to upgrade", "user", user, "host", host.ID.String(), "error", err)
+				} else {
+					slog.Info("host upgrade requested", "user", user, "host", host.ID.String())
+				}
+			}(host)
+		}
+	}()
+
+	slog.Info("upgrade all hosts request received", "user", user)
+	logic.ReturnSuccessResponse(w, r, "upgrade all hosts request received")
+}
+
 // @Summary     Upgrade a host
 // @Summary     Upgrade a host
 // @Router      /api/hosts/{hostid}/upgrade [put]
 // @Router      /api/hosts/{hostid}/upgrade [put]
 // @Tags        Hosts
 // @Tags        Hosts
 // @Security    oauth
 // @Security    oauth
 // @Param       hostid path string true "Host ID"
 // @Param       hostid path string true "Host ID"
+// @Param       force query bool false "Force upgrade"
 // @Success     200 {string} string "passed message to upgrade host"
 // @Success     200 {string} string "passed message to upgrade host"
 // @Failure     500 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 // upgrade host is a handler to send upgrade message to a host
 // upgrade host is a handler to send upgrade message to a host
@@ -64,7 +117,14 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "notfound"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "notfound"))
 		return
 		return
 	}
 	}
-	if err := mq.HostUpdate(&models.HostUpdate{Action: models.Upgrade, Host: *host}); err != nil {
+
+	action := models.Upgrade
+
+	if r.URL.Query().Get("force") == "true" {
+		action = models.ForceUpgrade
+	}
+
+	if err := mq.HostUpdate(&models.HostUpdate{Action: action, Host: *host}); err != nil {
 		slog.Error("failed to upgrade host", "error", err)
 		slog.Error("failed to upgrade host", "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
@@ -175,13 +235,13 @@ func pull(w http.ResponseWriter, r *http.Request) {
 			slog.Error("failed to get node:", "id", node.ID, "error", err)
 			slog.Error("failed to get node:", "id", node.ID, "error", err)
 			continue
 			continue
 		}
 		}
-		if node.FailedOverBy != uuid.Nil {
+		if node.FailedOverBy != uuid.Nil && r.URL.Query().Get("reset_failovered") == "true" {
 			logic.ResetFailedOverPeer(&node)
 			logic.ResetFailedOverPeer(&node)
 			sendPeerUpdate = true
 			sendPeerUpdate = true
 		}
 		}
 	}
 	}
 	if sendPeerUpdate {
 	if sendPeerUpdate {
-		if err := mq.PublishPeerUpdate(true); err != nil {
+		if err := mq.PublishPeerUpdate(false); err != nil {
 			logger.Log(0, "fail to publish peer update: ", err.Error())
 			logger.Log(0, "fail to publish peer update: ", err.Error())
 		}
 		}
 	}
 	}
@@ -311,11 +371,11 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	var hostUpdate models.HostUpdate
 	var hostUpdate models.HostUpdate
 	err = json.NewDecoder(r.Body).Decode(&hostUpdate)
 	err = json.NewDecoder(r.Body).Decode(&hostUpdate)
 	if err != nil {
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
+		slog.Error("failed to update a host:", "user", r.Header.Get("user"), "error", err.Error(), "host", currentHost.Name)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	slog.Info("recieved host update", "name", hostUpdate.Host.Name, "id", hostUpdate.Host.ID)
+	slog.Info("recieved host update", "name", hostUpdate.Host.Name, "id", hostUpdate.Host.ID, "action", hostUpdate.Action)
 	switch hostUpdate.Action {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 	case models.CheckIn:
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
@@ -532,7 +592,7 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 					w,
 					w,
 					r,
 					r,
 					logic.FormatError(
 					logic.FormatError(
-						fmt.Errorf("failed to force delete daemon node: "+err.Error()),
+						fmt.Errorf("failed to force delete daemon node: %s", err.Error()),
 						"internal",
 						"internal",
 					),
 					),
 				)
 				)
@@ -572,7 +632,7 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 					w,
 					w,
 					r,
 					r,
 					logic.FormatError(
 					logic.FormatError(
-						fmt.Errorf("failed to force delete daemon node: "+err.Error()),
+						fmt.Errorf("failed to force delete daemon node: %s", err.Error()),
 						"internal",
 						"internal",
 					),
 					),
 				)
 				)
@@ -855,6 +915,45 @@ func updateKeys(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
 
 
+// @Summary     Requests all the hosts to pull
+// @Router      /api/hosts/sync [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {string} string "sync all hosts request received"
+func syncHosts(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+
+	user := r.Header.Get("user")
+
+	go func() {
+		slog.Info("requesting all hosts to sync", "user", user)
+
+		hosts, err := logic.GetAllHosts()
+		if err != nil {
+			slog.Error("failed to retrieve all hosts", "user", user, "error", err)
+			return
+		}
+
+		for _, host := range hosts {
+			go func(host models.Host) {
+				hostUpdate := models.HostUpdate{
+					Action: models.RequestPull,
+					Host:   host,
+				}
+				if err = mq.HostUpdate(&hostUpdate); err != nil {
+					slog.Error("failed to request host to sync", "user", user, "host", host.ID.String(), "error", err)
+				} else {
+					slog.Info("host sync requested", "user", user, "host", host.ID.String())
+				}
+			}(host)
+			time.Sleep(time.Millisecond * 100)
+		}
+	}()
+
+	slog.Info("sync all hosts request received", "user", user)
+	logic.ReturnSuccessResponse(w, r, "sync all hosts request received")
+}
+
 // @Summary     Requests a host to pull
 // @Summary     Requests a host to pull
 // @Router      /api/hosts/{hostid}/sync [post]
 // @Router      /api/hosts/{hostid}/sync [post]
 // @Tags        Hosts
 // @Tags        Hosts
@@ -887,7 +986,7 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}()
 	}()
 
 
-	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
+	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
 
 
@@ -922,3 +1021,33 @@ func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
 	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
 }
 }
+
+// @Summary     Fetches host peerinfo
+// @Router      /api/host/{hostid}/peer_info [get]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getHostPeerInfo(w http.ResponseWriter, r *http.Request) {
+	hostId := mux.Vars(r)["hostid"]
+	var errorResponse = models.ErrorResponse{}
+
+	host, err := logic.GetHost(hostId)
+	if err != nil {
+		slog.Error("failed to retrieve host", "error", err)
+		errorResponse.Code = http.StatusBadRequest
+		errorResponse.Message = err.Error()
+		logic.ReturnErrorResponse(w, r, errorResponse)
+		return
+	}
+	peerInfo, err := logic.GetHostPeerInfo(host)
+	if err != nil {
+		slog.Error("failed to retrieve host peerinfo", "error", err)
+		errorResponse.Code = http.StatusBadRequest
+		errorResponse.Message = err.Error()
+		logic.ReturnErrorResponse(w, r, errorResponse)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, peerInfo, "fetched host peer info")
+}

+ 35 - 9
controllers/network.go

@@ -434,6 +434,7 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 // @Tags        Networks
 // @Tags        Networks
 // @Security    oauth
 // @Security    oauth
 // @Param       networkname path string true "Network name"
 // @Param       networkname path string true "Network name"
+// @Param       force query bool false "Force Delete"
 // @Produce     json
 // @Produce     json
 // @Success     200 {object} models.SuccessResponse
 // @Success     200 {object} models.SuccessResponse
 // @Failure     400 {object} models.ErrorResponse
 // @Failure     400 {object} models.ErrorResponse
@@ -441,10 +442,18 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
-
+	force := r.URL.Query().Get("force") == "true"
 	var params = mux.Vars(r)
 	var params = mux.Vars(r)
 	network := params["networkname"]
 	network := params["networkname"]
-	err := logic.DeleteNetwork(network)
+	doneCh := make(chan struct{}, 1)
+	networkNodes, err := logic.GetNetworkNodes(network)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to get network nodes [%s]: %v", network, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	err = logic.DeleteNetwork(network, force, doneCh)
 	if err != nil {
 	if err != nil {
 		errtype := "badrequest"
 		errtype := "badrequest"
 		if strings.Contains(err.Error(), "Node check failed") {
 		if strings.Contains(err.Error(), "Node check failed") {
@@ -455,11 +464,27 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
 		return
 		return
 	}
 	}
+	go logic.UnlinkNetworkAndTagsFromEnrollmentKeys(network, true)
 	go logic.DeleteNetworkRoles(network)
 	go logic.DeleteNetworkRoles(network)
 	go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network))
 	go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network))
 	//delete network from allocated ip map
 	//delete network from allocated ip map
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
-
+	go func() {
+		<-doneCh
+		mq.PublishPeerUpdate(true)
+		// send node update to clean up locally
+		for _, node := range networkNodes {
+			node := node
+			node.PendingDelete = true
+			node.Action = models.NODE_DELETE
+			if err := mq.NodeUpdate(&node); err != nil {
+				slog.Error("error publishing node update to node", "node", node.ID, "error", err)
+			}
+		}
+		if servercfg.IsDNSMode() {
+			logic.SetDNS()
+		}
+	}()
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode("success")
 	json.NewEncoder(w).Encode("success")
@@ -534,7 +559,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
-	//add new network to allocated ip map
+
 	go logic.AddNetworkToAllocatedIpMap(network.NetID)
 	go logic.AddNetworkToAllocatedIpMap(network.NetID)
 
 
 	go func() {
 	go func() {
@@ -608,21 +633,22 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	netOld1, err := logic.GetNetwork(payload.NetID)
+	netOld, err := logic.GetNetwork(payload.NetID)
 	if err != nil {
 	if err != nil {
 		slog.Info("error fetching network", "user", r.Header.Get("user"), "err", err)
 		slog.Info("error fetching network", "user", r.Header.Get("user"), "err", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	// partial update
-	netOld2 := netOld1
-	_, _, _, err = logic.UpdateNetwork(&netOld1, &netOld2)
+	netNew := netOld
+	netNew.NameServers = payload.NameServers
+	netNew.DefaultACL = payload.DefaultACL
+	_, _, _, err = logic.UpdateNetwork(&netOld, &netNew)
 	if err != nil {
 	if err != nil {
 		slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
 		slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-
+	go mq.PublishPeerUpdate(false)
 	slog.Info("updated network", "network", payload.NetID, "user", r.Header.Get("user"))
 	slog.Info("updated network", "network", payload.NetID, "user", r.Header.Get("user"))
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(payload)
 	json.NewEncoder(w).Encode(payload)

+ 19 - 2
controllers/network_test.go

@@ -75,11 +75,19 @@ func TestDeleteNetwork(t *testing.T) {
 	t.Run("NetworkwithNodes", func(t *testing.T) {
 	t.Run("NetworkwithNodes", func(t *testing.T) {
 	})
 	})
 	t.Run("DeleteExistingNetwork", func(t *testing.T) {
 	t.Run("DeleteExistingNetwork", func(t *testing.T) {
-		err := logic.DeleteNetwork("skynet")
+		doneCh := make(chan struct{}, 1)
+		err := logic.DeleteNetwork("skynet", false, doneCh)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 	})
 	})
 	t.Run("NonExistentNetwork", func(t *testing.T) {
 	t.Run("NonExistentNetwork", func(t *testing.T) {
-		err := logic.DeleteNetwork("skynet")
+		doneCh := make(chan struct{}, 1)
+		err := logic.DeleteNetwork("skynet", false, doneCh)
+		assert.Nil(t, err)
+	})
+	createNetv1("test")
+	t.Run("ForceDeleteNetwork", func(t *testing.T) {
+		doneCh := make(chan struct{}, 1)
+		err := logic.DeleteNetwork("test", true, doneCh)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 	})
 	})
 }
 }
@@ -214,6 +222,15 @@ func createNet() {
 		logic.CreateNetwork(network)
 		logic.CreateNetwork(network)
 	}
 	}
 }
 }
+func createNetv1(netId string) {
+	var network models.Network
+	network.NetID = netId
+	network.AddressRange = "100.0.0.1/24"
+	_, err := logic.GetNetwork(netId)
+	if err != nil {
+		logic.CreateNetwork(network)
+	}
+}
 
 
 func createNetDualStack() {
 func createNetDualStack() {
 	var network models.Network
 	var network models.Network

+ 51 - 111
controllers/node.go

@@ -28,9 +28,10 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/creategateway", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceEgress, http.HandlerFunc(createEgressGateway)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deletegateway", logic.SecurityCheck(true, http.HandlerFunc(deleteEgressGateway))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createIngressGateway)))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createingress", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceIngress, http.HandlerFunc(createGateway)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(true, http.HandlerFunc(deleteGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/nodes/{network}/status", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodeStatus))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
 }
 
 
@@ -328,7 +329,7 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	nodes = logic.AddStaticNodestoList(nodes)
 	nodes = logic.AddStaticNodestoList(nodes)
-	nodes = logic.AddStatusToNodes(nodes)
+	nodes = logic.AddStatusToNodes(nodes, false)
 	// returns all the nodes in JSON/API format
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(2, r.Header.Get("user"), "fetched nodes on network", networkName)
 	logger.Log(2, r.Header.Get("user"), "fetched nodes on network", networkName)
@@ -368,7 +369,7 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 
 
 	}
 	}
 	nodes = logic.AddStaticNodestoList(nodes)
 	nodes = logic.AddStaticNodestoList(nodes)
-	nodes = logic.AddStatusToNodes(nodes)
+	nodes = logic.AddStatusToNodes(nodes, false)
 	// return all the nodes in JSON/API format
 	// return all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
@@ -377,6 +378,52 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiNodes)
 	json.NewEncoder(w).Encode(apiNodes)
 }
 }
 
 
+// @Summary     Get all nodes status on the network
+// @Router      /api/v1/nodes/{network}/status [get]
+// @Tags        Nodes
+// @Securitydefinitions.oauth2.application OAuth2Application
+// @Success     200 {array} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
+// Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
+func getNetworkNodeStatus(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	netID := params["network"]
+	// validate network
+	_, err := logic.GetNetwork(netID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to get network %v", err), "badrequest"))
+		return
+	}
+	var nodes []models.Node
+	nodes, err = logic.GetNetworkNodes(netID)
+	if err != nil {
+		logger.Log(0, "error fetching all nodes info: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	username := r.Header.Get("user")
+	if r.Header.Get("ismaster") == "no" {
+		user, err := logic.GetUser(username)
+		if err != nil {
+			return
+		}
+		userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			return
+		}
+		if !userPlatformRole.FullAccess {
+			nodes = logic.GetFilteredNodesByUserAccess(*user, nodes)
+		}
+
+	}
+	nodes = logic.AddStaticNodestoList(nodes)
+	nodes = logic.AddStatusToNodes(nodes, false)
+	// return all the nodes in JSON/API format
+	apiNodesStatusMap := logic.GetNodesStatusAPI(nodes[:])
+	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
+	logic.ReturnSuccessResponseWithJson(w, r, apiNodesStatusMap, "fetched nodes with metric status")
+}
+
 // @Summary     Get an individual node
 // @Summary     Get an individual node
 // @Router      /api/nodes/{network}/{nodeid} [get]
 // @Router      /api/nodes/{network}/{nodeid} [get]
 // @Tags        Nodes
 // @Tags        Nodes
@@ -548,113 +595,6 @@ func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}()
 	}()
 }
 }
 
 
-// == INGRESS ==
-
-// @Summary     Create an remote access gateway
-// @Router      /api/nodes/{network}/{nodeid}/createingress [post]
-// @Tags        Nodes
-// @Security    oauth2
-// @Success     200 {object} models.ApiNode
-// @Failure     500 {object} models.ErrorResponse
-func createIngressGateway(w http.ResponseWriter, r *http.Request) {
-	var params = mux.Vars(r)
-	w.Header().Set("Content-Type", "application/json")
-	nodeid := params["nodeid"]
-	netid := params["network"]
-	node, err := logic.ValidateParams(nodeid, netid)
-	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	var request models.IngressRequest
-	json.NewDecoder(r.Body).Decode(&request)
-	node, err = logic.CreateIngressGateway(netid, nodeid, request)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create ingress gateway on node [%s] on network [%s]: %v",
-				nodeid, netid, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	apiNode := node.ConvertToAPINode()
-	logger.Log(
-		1,
-		r.Header.Get("user"),
-		"created ingress gateway on node",
-		nodeid,
-		"on network",
-		netid,
-	)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-	go func() {
-		if err := mq.NodeUpdate(&node); err != nil {
-			slog.Error("error publishing node update to node", "node", node.ID, "error", err)
-		}
-		mq.PublishPeerUpdate(false)
-	}()
-}
-
-// @Summary     Delete an remote access gateway
-// @Router      /api/nodes/{network}/{nodeid}/deleteingress [delete]
-// @Tags        Nodes
-// @Security    oauth2
-// @Success     200 {object} models.ApiNode
-// @Failure     500 {object} models.ErrorResponse
-func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	nodeid := params["nodeid"]
-	netid := params["network"]
-	node, err := logic.ValidateParams(nodeid, netid)
-	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	node, removedClients, err := logic.DeleteIngressGateway(nodeid)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to delete ingress gateway on node [%s] on network [%s]: %v",
-				nodeid, netid, err))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-
-	if len(removedClients) > 0 {
-		host, err := logic.GetHost(node.HostID.String())
-		if err == nil {
-			allNodes, err := logic.GetAllNodes()
-			if err != nil {
-				return
-			}
-			go func() {
-				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil {
-					slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err)
-				}
-				mq.PublishPeerUpdate(false)
-				if err := mq.NodeUpdate(&node); err != nil {
-					slog.Error(
-						"error publishing node update to node",
-						"node",
-						node.ID,
-						"error",
-						err,
-					)
-				}
-				if servercfg.IsDNSMode() {
-					logic.SetDNS()
-				}
-			}()
-		}
-	}
-}
-
 // @Summary     Update an individual node
 // @Summary     Update an individual node
 // @Router      /api/nodes/{network}/{nodeid} [put]
 // @Router      /api/nodes/{network}/{nodeid} [put]
 // @Tags        Nodes
 // @Tags        Nodes

+ 4 - 0
controllers/node_test.go

@@ -21,6 +21,10 @@ var linuxHost models.Host
 func TestCreateEgressGateway(t *testing.T) {
 func TestCreateEgressGateway(t *testing.T) {
 	var gateway models.EgressGatewayRequest
 	var gateway models.EgressGatewayRequest
 	gateway.Ranges = []string{"10.100.100.0/24"}
 	gateway.Ranges = []string{"10.100.100.0/24"}
+	gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
+		Network:     "10.100.100.0/24",
+		RouteMetric: 256,
+	})
 	gateway.NetID = "skynet"
 	gateway.NetID = "skynet"
 	deleteAllNetworks()
 	deleteAllNetworks()
 	createNet()
 	createNet()

+ 9 - 0
controllers/user.go

@@ -258,6 +258,15 @@ func getUserV1(w http.ResponseWriter, r *http.Request) {
 	resp := models.ReturnUserWithRolesAndGroups{
 	resp := models.ReturnUserWithRolesAndGroups{
 		ReturnUser:   user,
 		ReturnUser:   user,
 		PlatformRole: userRoleTemplate,
 		PlatformRole: userRoleTemplate,
+		UserGroups:   map[models.UserGroupID]models.UserGroup{},
+	}
+	for gId := range user.UserGroups {
+		grp, err := logic.GetUserGroup(gId)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		resp.UserGroups[gId] = grp
 	}
 	}
 	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
 	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
 	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
 	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")

+ 3 - 0
database/database.go

@@ -71,6 +71,8 @@ const (
 	USER_INVITES_TABLE_NAME = "user_invites"
 	USER_INVITES_TABLE_NAME = "user_invites"
 	// TAG_TABLE_NAME - table for tags
 	// TAG_TABLE_NAME - table for tags
 	TAG_TABLE_NAME = "tags"
 	TAG_TABLE_NAME = "tags"
+	// PEER_ACK_TABLE - table for failover peer ack
+	PEER_ACK_TABLE = "peer_ack"
 	// == ERROR CONSTS ==
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
 	NO_RECORD = "no result found"
@@ -158,6 +160,7 @@ func createTables() {
 	CreateTable(USER_INVITES_TABLE_NAME)
 	CreateTable(USER_INVITES_TABLE_NAME)
 	CreateTable(TAG_TABLE_NAME)
 	CreateTable(TAG_TABLE_NAME)
 	CreateTable(ACLS_TABLE_NAME)
 	CreateTable(ACLS_TABLE_NAME)
+	CreateTable(PEER_ACK_TABLE)
 }
 }
 
 
 func CreateTable(tableName string) error {
 func CreateTable(tableName string) error {

+ 5 - 5
go.mod

@@ -5,7 +5,7 @@ go 1.23
 require (
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/eclipse/paho.mqtt.golang v1.4.3
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.23.0
+	github.com/go-playground/validator/v10 v10.24.0
 	github.com/golang-jwt/jwt/v4 v4.5.1
 	github.com/golang-jwt/jwt/v4 v4.5.1
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
@@ -18,10 +18,10 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/stretchr/testify v1.10.0
 	github.com/txn2/txeh v1.5.5
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.30.0
-	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/crypto v0.32.0
+	golang.org/x/net v0.34.0 // indirect
 	golang.org/x/oauth2 v0.24.0
 	golang.org/x/oauth2 v0.24.0
-	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -51,7 +51,7 @@ require (
 
 
 require (
 require (
 	cloud.google.com/go/compute/metadata v0.3.0 // indirect
 	cloud.google.com/go/compute/metadata v0.3.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/kr/text v0.2.0 // indirect

+ 10 - 10
go.sum

@@ -19,8 +19,8 @@ github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQ
 github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
 github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
 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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -29,8 +29,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.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
-github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
+github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
 github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
 github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
 github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
@@ -105,8 +105,8 @@ go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwE
 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
-golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -116,8 +116,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
 golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
 golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -133,8 +133,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

+ 68 - 15
logic/acls.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"maps"
 	"sort"
 	"sort"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -163,6 +164,7 @@ func storeAclInCache(a models.Acl) {
 	aclCacheMutex.Lock()
 	aclCacheMutex.Lock()
 	defer aclCacheMutex.Unlock()
 	defer aclCacheMutex.Unlock()
 	aclCacheMap[a.ID] = a
 	aclCacheMap[a.ID] = a
+
 }
 }
 
 
 func removeAclFromCache(a models.Acl) {
 func removeAclFromCache(a models.Acl) {
@@ -574,6 +576,22 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 	if peer.IsStatic {
 	if peer.IsStatic {
 		peer = peer.StaticNode.ConvertToStaticNode()
 		peer = peer.StaticNode.ConvertToStaticNode()
 	}
 	}
+	var nodeTags, peerTags map[models.TagID]struct{}
+	if node.Mutex != nil {
+		node.Mutex.Lock()
+		nodeTags = maps.Clone(node.Tags)
+		node.Mutex.Unlock()
+	} else {
+		nodeTags = node.Tags
+	}
+	if peer.Mutex != nil {
+		peer.Mutex.Lock()
+		peerTags = maps.Clone(peer.Tags)
+		peer.Mutex.Unlock()
+	} else {
+		peerTags = peer.Tags
+	}
+
 	if checkDefaultPolicy {
 	if checkDefaultPolicy {
 		// check default policy if all allowed return true
 		// check default policy if all allowed return true
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
@@ -582,6 +600,7 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 				return true
 				return true
 			}
 			}
 		}
 		}
+
 	}
 	}
 	// list device policies
 	// list device policies
 	policies := listDevicePolicies(models.NetworkID(peer.Network))
 	policies := listDevicePolicies(models.NetworkID(peer.Network))
@@ -597,12 +616,12 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 		}
 		}
 		srcMap = convAclTagToValueMap(policy.Src)
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = convAclTagToValueMap(policy.Dst)
 		dstMap = convAclTagToValueMap(policy.Dst)
-		for tagID := range node.Tags {
+		for tagID := range nodeTags {
 			if _, ok := dstMap[tagID.String()]; ok {
 			if _, ok := dstMap[tagID.String()]; ok {
 				if _, ok := srcMap["*"]; ok {
 				if _, ok := srcMap["*"]; ok {
 					return true
 					return true
 				}
 				}
-				for tagID := range peer.Tags {
+				for tagID := range peerTags {
 					if _, ok := srcMap[tagID.String()]; ok {
 					if _, ok := srcMap[tagID.String()]; ok {
 						return true
 						return true
 					}
 					}
@@ -612,19 +631,20 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 				if _, ok := dstMap["*"]; ok {
 				if _, ok := dstMap["*"]; ok {
 					return true
 					return true
 				}
 				}
-				for tagID := range peer.Tags {
+				for tagID := range peerTags {
 					if _, ok := dstMap[tagID.String()]; ok {
 					if _, ok := dstMap[tagID.String()]; ok {
 						return true
 						return true
 					}
 					}
 				}
 				}
 			}
 			}
 		}
 		}
-		for tagID := range peer.Tags {
+
+		for tagID := range peerTags {
 			if _, ok := dstMap[tagID.String()]; ok {
 			if _, ok := dstMap[tagID.String()]; ok {
 				if _, ok := srcMap["*"]; ok {
 				if _, ok := srcMap["*"]; ok {
 					return true
 					return true
 				}
 				}
-				for tagID := range node.Tags {
+				for tagID := range nodeTags {
 
 
 					if _, ok := srcMap[tagID.String()]; ok {
 					if _, ok := srcMap[tagID.String()]; ok {
 						return true
 						return true
@@ -635,7 +655,7 @@ func IsPeerAllowed(node, peer models.Node, checkDefaultPolicy bool) bool {
 				if _, ok := dstMap["*"]; ok {
 				if _, ok := dstMap["*"]; ok {
 					return true
 					return true
 				}
 				}
-				for tagID := range node.Tags {
+				for tagID := range nodeTags {
 					if _, ok := dstMap[tagID.String()]; ok {
 					if _, ok := dstMap[tagID.String()]; ok {
 						return true
 						return true
 					}
 					}
@@ -654,6 +674,21 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 	if peer.IsStatic {
 	if peer.IsStatic {
 		peer = peer.StaticNode.ConvertToStaticNode()
 		peer = peer.StaticNode.ConvertToStaticNode()
 	}
 	}
+	var nodeTags, peerTags map[models.TagID]struct{}
+	if node.Mutex != nil {
+		node.Mutex.Lock()
+		nodeTags = maps.Clone(node.Tags)
+		node.Mutex.Unlock()
+	} else {
+		nodeTags = node.Tags
+	}
+	if peer.Mutex != nil {
+		peer.Mutex.Lock()
+		peerTags = maps.Clone(peer.Tags)
+		peer.Mutex.Unlock()
+	} else {
+		peerTags = peer.Tags
+	}
 	if checkDefaultPolicy {
 	if checkDefaultPolicy {
 		// check default policy if all allowed return true
 		// check default policy if all allowed return true
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 		defaultPolicy, err := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
@@ -678,7 +713,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 		}
 		}
 		srcMap = convAclTagToValueMap(policy.Src)
 		srcMap = convAclTagToValueMap(policy.Src)
 		dstMap = convAclTagToValueMap(policy.Dst)
 		dstMap = convAclTagToValueMap(policy.Dst)
-		for tagID := range node.Tags {
+		for tagID := range nodeTags {
 			allowed := false
 			allowed := false
 			if _, ok := dstMap[tagID.String()]; policy.AllowedDirection == models.TrafficDirectionBi && ok {
 			if _, ok := dstMap[tagID.String()]; policy.AllowedDirection == models.TrafficDirectionBi && ok {
 				if _, ok := srcMap["*"]; ok {
 				if _, ok := srcMap["*"]; ok {
@@ -686,7 +721,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 					allowedPolicies = append(allowedPolicies, policy)
 					allowedPolicies = append(allowedPolicies, policy)
 					break
 					break
 				}
 				}
-				for tagID := range peer.Tags {
+				for tagID := range peerTags {
 					if _, ok := srcMap[tagID.String()]; ok {
 					if _, ok := srcMap[tagID.String()]; ok {
 						allowed = true
 						allowed = true
 						break
 						break
@@ -703,7 +738,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 					allowedPolicies = append(allowedPolicies, policy)
 					allowedPolicies = append(allowedPolicies, policy)
 					break
 					break
 				}
 				}
-				for tagID := range peer.Tags {
+				for tagID := range peerTags {
 					if _, ok := dstMap[tagID.String()]; ok {
 					if _, ok := dstMap[tagID.String()]; ok {
 						allowed = true
 						allowed = true
 						break
 						break
@@ -715,7 +750,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 				break
 				break
 			}
 			}
 		}
 		}
-		for tagID := range peer.Tags {
+		for tagID := range peerTags {
 			allowed := false
 			allowed := false
 			if _, ok := dstMap[tagID.String()]; ok {
 			if _, ok := dstMap[tagID.String()]; ok {
 				if _, ok := srcMap["*"]; ok {
 				if _, ok := srcMap["*"]; ok {
@@ -723,7 +758,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 					allowedPolicies = append(allowedPolicies, policy)
 					allowedPolicies = append(allowedPolicies, policy)
 					break
 					break
 				}
 				}
-				for tagID := range node.Tags {
+				for tagID := range nodeTags {
 
 
 					if _, ok := srcMap[tagID.String()]; ok {
 					if _, ok := srcMap[tagID.String()]; ok {
 						allowed = true
 						allowed = true
@@ -742,7 +777,7 @@ func IsNodeAllowedToCommunicate(node, peer models.Node, checkDefaultPolicy bool)
 					allowedPolicies = append(allowedPolicies, policy)
 					allowedPolicies = append(allowedPolicies, policy)
 					break
 					break
 				}
 				}
-				for tagID := range node.Tags {
+				for tagID := range nodeTags {
 					if _, ok := dstMap[tagID.String()]; ok {
 					if _, ok := dstMap[tagID.String()]; ok {
 						allowed = true
 						allowed = true
 						break
 						break
@@ -851,7 +886,15 @@ func getUserAclRulesForNode(targetnode *models.Node,
 	userGrpMap := GetUserGrpMap()
 	userGrpMap := GetUserGrpMap()
 	allowedUsers := make(map[string][]models.Acl)
 	allowedUsers := make(map[string][]models.Acl)
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
 	acls := listUserPolicies(models.NetworkID(targetnode.Network))
-	for nodeTag := range targetnode.Tags {
+	var targetNodeTags = make(map[models.TagID]struct{})
+	if targetnode.Mutex != nil {
+		targetnode.Mutex.Lock()
+		targetNodeTags = maps.Clone(targetnode.Tags)
+		targetnode.Mutex.Unlock()
+	} else {
+		targetNodeTags = maps.Clone(targetnode.Tags)
+	}
+	for nodeTag := range targetNodeTags {
 		for _, acl := range acls {
 		for _, acl := range acls {
 			if !acl.Enabled {
 			if !acl.Enabled {
 				continue
 				continue
@@ -875,6 +918,7 @@ func getUserAclRulesForNode(targetnode *models.Node,
 			}
 			}
 		}
 		}
 	}
 	}
+
 	for _, userNode := range userNodes {
 	for _, userNode := range userNodes {
 		if !userNode.StaticNode.Enabled {
 		if !userNode.StaticNode.Enabled {
 			continue
 			continue
@@ -931,8 +975,17 @@ func GetAclRulesForNode(targetnode *models.Node) (rules map[string]models.AclRul
 	}
 	}
 
 
 	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
 	acls := listDevicePolicies(models.NetworkID(targetnode.Network))
-	targetnode.Tags["*"] = struct{}{}
-	for nodeTag := range targetnode.Tags {
+
+	var targetNodeTags = make(map[models.TagID]struct{})
+	if targetnode.Mutex != nil {
+		targetnode.Mutex.Lock()
+		targetNodeTags = maps.Clone(targetnode.Tags)
+		targetnode.Mutex.Unlock()
+	} else {
+		targetNodeTags = maps.Clone(targetnode.Tags)
+	}
+	targetNodeTags["*"] = struct{}{}
+	for nodeTag := range targetNodeTags {
 		for _, acl := range acls {
 		for _, acl := range acls {
 			if !acl.Enabled {
 			if !acl.Enabled {
 				continue
 				continue

+ 57 - 1
logic/enrollmentkey.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
@@ -120,7 +121,6 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID, groups []models.TagID)
 }
 }
 
 
 // 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 {
@@ -335,3 +335,59 @@ func RemoveTagFromEnrollmentKeys(deletedTagID models.TagID) {
 
 
 	}
 	}
 }
 }
+
+func UnlinkNetworkAndTagsFromEnrollmentKeys(network string, delete bool) error {
+	keys, err := GetAllEnrollmentKeys()
+	if err != nil {
+		return fmt.Errorf("failed to retrieve keys: %w", err)
+	}
+
+	var errs []error
+	for _, key := range keys {
+		newNetworks := []string{}
+		newTags := []models.TagID{}
+		update := false
+
+		// Check and update networks
+		for _, net := range key.Networks {
+			if net == network {
+				update = true
+				continue
+			}
+			newNetworks = append(newNetworks, net)
+		}
+
+		// Check and update tags
+		for _, tag := range key.Groups {
+			tagParts := strings.Split(tag.String(), ".")
+			if len(tagParts) == 0 {
+				continue
+			}
+			tagNetwork := tagParts[0]
+			if tagNetwork == network {
+				update = true
+				continue
+			}
+			newTags = append(newTags, tag)
+		}
+
+		if update && len(newNetworks) == 0 && delete {
+			if err := DeleteEnrollmentKey(key.Value, true); err != nil {
+				errs = append(errs, fmt.Errorf("failed to delete key %s: %w", key.Value, err))
+			}
+			continue
+		}
+		if update {
+			key.Networks = newNetworks
+			key.Groups = newTags
+			if err := upsertEnrollmentKey(&key); err != nil {
+				errs = append(errs, fmt.Errorf("failed to update key %s: %w", key.Value, err))
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return fmt.Errorf("errors unlinking network/tags from keys: %v", errs)
+	}
+	return nil
+}

+ 24 - 19
logic/extpeers.go

@@ -28,6 +28,9 @@ var (
 func getAllExtClientsFromCache() (extClients []models.ExtClient) {
 func getAllExtClientsFromCache() (extClients []models.ExtClient) {
 	extClientCacheMutex.RLock()
 	extClientCacheMutex.RLock()
 	for _, extclient := range extClientCacheMap {
 	for _, extclient := range extClientCacheMap {
+		if extclient.Mutex == nil {
+			extclient.Mutex = &sync.Mutex{}
+		}
 		extClients = append(extClients, extclient)
 		extClients = append(extClients, extclient)
 	}
 	}
 	extClientCacheMutex.RUnlock()
 	extClientCacheMutex.RUnlock()
@@ -43,12 +46,18 @@ func deleteExtClientFromCache(key string) {
 func getExtClientFromCache(key string) (extclient models.ExtClient, ok bool) {
 func getExtClientFromCache(key string) (extclient models.ExtClient, ok bool) {
 	extClientCacheMutex.RLock()
 	extClientCacheMutex.RLock()
 	extclient, ok = extClientCacheMap[key]
 	extclient, ok = extClientCacheMap[key]
+	if extclient.Mutex == nil {
+		extclient.Mutex = &sync.Mutex{}
+	}
 	extClientCacheMutex.RUnlock()
 	extClientCacheMutex.RUnlock()
 	return
 	return
 }
 }
 
 
 func storeExtClientInCache(key string, extclient models.ExtClient) {
 func storeExtClientInCache(key string, extclient models.ExtClient) {
 	extClientCacheMutex.Lock()
 	extClientCacheMutex.Lock()
+	if extclient.Mutex == nil {
+		extclient.Mutex = &sync.Mutex{}
+	}
 	extClientCacheMap[key] = extclient
 	extClientCacheMap[key] = extclient
 	extClientCacheMutex.Unlock()
 	extClientCacheMutex.Unlock()
 }
 }
@@ -96,14 +105,14 @@ func DeleteExtClient(network string, clientid string) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	//recycle ip address
-	if extClient.Address != "" {
-		RemoveIpFromAllocatedIpMap(network, extClient.Address)
-	}
-	if extClient.Address6 != "" {
-		RemoveIpFromAllocatedIpMap(network, extClient.Address6)
-	}
 	if servercfg.CacheEnabled() {
 	if servercfg.CacheEnabled() {
+		// recycle ip address
+		if extClient.Address != "" {
+			RemoveIpFromAllocatedIpMap(network, extClient.Address)
+		}
+		if extClient.Address6 != "" {
+			RemoveIpFromAllocatedIpMap(network, extClient.Address6)
+		}
 		deleteExtClientFromCache(key)
 		deleteExtClientFromCache(key)
 	}
 	}
 	return nil
 	return nil
@@ -333,15 +342,16 @@ func SaveExtClient(extclient *models.ExtClient) error {
 	}
 	}
 	if servercfg.CacheEnabled() {
 	if servercfg.CacheEnabled() {
 		storeExtClientInCache(key, *extclient)
 		storeExtClientInCache(key, *extclient)
-	}
-	if _, ok := allocatedIpMap[extclient.Network]; ok {
-		if extclient.Address != "" {
-			AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address))
-		}
-		if extclient.Address6 != "" {
-			AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address6))
+		if _, ok := allocatedIpMap[extclient.Network]; ok {
+			if extclient.Address != "" {
+				AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address))
+			}
+			if extclient.Address6 != "" {
+				AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address6))
+			}
 		}
 		}
 	}
 	}
+
 	return SetNetworkNodesLastModified(extclient.Network)
 	return SetNetworkNodesLastModified(extclient.Network)
 }
 }
 
 
@@ -456,17 +466,12 @@ func GetStaticNodeIps(node models.Node) (ips []net.IP) {
 
 
 func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 	// fetch user access to static clients via policies
 	// fetch user access to static clients via policies
-	defer func() {
-		logger.Log(0, fmt.Sprintf("node.ID: %s, Rules: %+v\n", node.ID, rules))
-	}()
 
 
 	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 	nodes, _ := GetNetworkNodes(node.Network)
 	nodes, _ := GetNetworkNodes(node.Network)
 	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
 	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
-	//fmt.Printf("=====> NODES: %+v \n\n", nodes)
 	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
 	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
-	//fmt.Printf("=====> USER NODES %+v \n\n", userNodes)
 	for _, userNodeI := range userNodes {
 	for _, userNodeI := range userNodes {
 		for _, peer := range nodes {
 		for _, peer := range nodes {
 			if peer.IsUserNode {
 			if peer.IsUserNode {

+ 39 - 2
logic/gateway.go

@@ -3,6 +3,8 @@ package logic
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"slices"
+	"sort"
 	"time"
 	"time"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
@@ -77,6 +79,14 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if host.FirewallInUse == models.FIREWALL_NONE {
 	if host.FirewallInUse == models.FIREWALL_NONE {
 		return models.Node{}, errors.New("please install iptables or nftables on the device")
 		return models.Node{}, errors.New("please install iptables or nftables on the device")
 	}
 	}
+	if len(gateway.RangesWithMetric) == 0 && len(gateway.Ranges) > 0 {
+		for _, rangeI := range gateway.Ranges {
+			gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
+				Network:     rangeI,
+				RouteMetric: 256,
+			})
+		}
+	}
 	for i := len(gateway.Ranges) - 1; i >= 0; i-- {
 	for i := len(gateway.Ranges) - 1; i >= 0; i-- {
 		// check if internet gateway IPv4
 		// check if internet gateway IPv4
 		if gateway.Ranges[i] == "0.0.0.0/0" || gateway.Ranges[i] == "::/0" {
 		if gateway.Ranges[i] == "0.0.0.0/0" || gateway.Ranges[i] == "::/0" {
@@ -91,6 +101,28 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 		gateway.Ranges[i] = normalized
 		gateway.Ranges[i] = normalized
 
 
 	}
 	}
+	rangesWithMetric := []string{}
+	for i := len(gateway.RangesWithMetric) - 1; i >= 0; i-- {
+		if gateway.RangesWithMetric[i].Network == "0.0.0.0/0" || gateway.RangesWithMetric[i].Network == "::/0" {
+			// remove inet range
+			gateway.RangesWithMetric = append(gateway.RangesWithMetric[:i], gateway.RangesWithMetric[i+1:]...)
+			continue
+		}
+		normalized, err := NormalizeCIDR(gateway.RangesWithMetric[i].Network)
+		if err != nil {
+			return models.Node{}, err
+		}
+		gateway.RangesWithMetric[i].Network = normalized
+		rangesWithMetric = append(rangesWithMetric, gateway.RangesWithMetric[i].Network)
+		if gateway.RangesWithMetric[i].RouteMetric <= 0 || gateway.RangesWithMetric[i].RouteMetric > 999 {
+			gateway.RangesWithMetric[i].RouteMetric = 256
+		}
+	}
+	sort.Strings(gateway.Ranges)
+	sort.Strings(rangesWithMetric)
+	if !slices.Equal(gateway.Ranges, rangesWithMetric) {
+		return models.Node{}, errors.New("invalid ranges")
+	}
 	if gateway.NatEnabled == "" {
 	if gateway.NatEnabled == "" {
 		gateway.NatEnabled = "yes"
 		gateway.NatEnabled = "yes"
 	}
 	}
@@ -104,6 +136,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	node.IsEgressGateway = true
 	node.IsEgressGateway = true
 	node.EgressGatewayRanges = gateway.Ranges
 	node.EgressGatewayRanges = gateway.Ranges
 	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
 	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
+
 	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	node.SetLastModified()
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 	if err = UpsertNode(&node); err != nil {
@@ -141,14 +174,14 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 		return models.Node{}, err
 		return models.Node{}, err
 	}
 	}
 	if node.IsRelayed {
 	if node.IsRelayed {
-		return models.Node{}, errors.New("ingress cannot be created on a relayed node")
+		return models.Node{}, errors.New("gateway cannot be created on a relayed node")
 	}
 	}
 	host, err := GetHost(node.HostID.String())
 	host, err := GetHost(node.HostID.String())
 	if err != nil {
 	if err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
 	}
 	}
 	if host.OS != "linux" {
 	if host.OS != "linux" {
-		return models.Node{}, errors.New("ingress can only be created on linux based node")
+		return models.Node{}, errors.New("gateway can only be created on linux based node")
 	}
 	}
 
 
 	network, err := GetParentNetwork(netid)
 	network, err := GetParentNetwork(netid)
@@ -156,12 +189,16 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 		return models.Node{}, err
 		return models.Node{}, err
 	}
 	}
 	node.IsIngressGateway = true
 	node.IsIngressGateway = true
+	node.IsGw = true
 	if !servercfg.IsPro {
 	if !servercfg.IsPro {
 		node.IsInternetGateway = ingress.IsInternetGateway
 		node.IsInternetGateway = ingress.IsInternetGateway
 	}
 	}
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
 	node.IngressDNS = ingress.ExtclientDNS
+	if node.IsInternetGateway && node.IngressDNS == "" {
+		node.IngressDNS = "1.1.1.1"
+	}
 	node.IngressPersistentKeepalive = 20
 	node.IngressPersistentKeepalive = 20
 	if ingress.PersistentKeepalive != 0 {
 	if ingress.PersistentKeepalive != 0 {
 		node.IngressPersistentKeepalive = ingress.PersistentKeepalive
 		node.IngressPersistentKeepalive = ingress.PersistentKeepalive

+ 168 - 20
logic/networks.go

@@ -30,6 +30,9 @@ var (
 
 
 // SetAllocatedIpMap - set allocated ip map for networks
 // SetAllocatedIpMap - set allocated ip map for networks
 func SetAllocatedIpMap() error {
 func SetAllocatedIpMap() error {
+	if !servercfg.CacheEnabled() {
+		return nil
+	}
 	logger.Log(0, "start setting up allocated ip map")
 	logger.Log(0, "start setting up allocated ip map")
 	if allocatedIpMap == nil {
 	if allocatedIpMap == nil {
 		allocatedIpMap = map[string]map[string]net.IP{}
 		allocatedIpMap = map[string]map[string]net.IP{}
@@ -84,16 +87,25 @@ func SetAllocatedIpMap() error {
 
 
 // ClearAllocatedIpMap - set allocatedIpMap to nil
 // ClearAllocatedIpMap - set allocatedIpMap to nil
 func ClearAllocatedIpMap() {
 func ClearAllocatedIpMap() {
+	if !servercfg.CacheEnabled() {
+		return
+	}
 	allocatedIpMap = nil
 	allocatedIpMap = nil
 }
 }
 
 
 func AddIpToAllocatedIpMap(networkName string, ip net.IP) {
 func AddIpToAllocatedIpMap(networkName string, ip net.IP) {
+	if !servercfg.CacheEnabled() {
+		return
+	}
 	networkCacheMutex.Lock()
 	networkCacheMutex.Lock()
 	allocatedIpMap[networkName][ip.String()] = ip
 	allocatedIpMap[networkName][ip.String()] = ip
 	networkCacheMutex.Unlock()
 	networkCacheMutex.Unlock()
 }
 }
 
 
 func RemoveIpFromAllocatedIpMap(networkName string, ip string) {
 func RemoveIpFromAllocatedIpMap(networkName string, ip string) {
+	if !servercfg.CacheEnabled() {
+		return
+	}
 	networkCacheMutex.Lock()
 	networkCacheMutex.Lock()
 	delete(allocatedIpMap[networkName], ip)
 	delete(allocatedIpMap[networkName], ip)
 	networkCacheMutex.Unlock()
 	networkCacheMutex.Unlock()
@@ -101,13 +113,20 @@ func RemoveIpFromAllocatedIpMap(networkName string, ip string) {
 
 
 // AddNetworkToAllocatedIpMap - add network to allocated ip map when network is added
 // AddNetworkToAllocatedIpMap - add network to allocated ip map when network is added
 func AddNetworkToAllocatedIpMap(networkName string) {
 func AddNetworkToAllocatedIpMap(networkName string) {
+	//add new network to allocated ip map
+	if !servercfg.CacheEnabled() {
+		return
+	}
 	networkCacheMutex.Lock()
 	networkCacheMutex.Lock()
-	allocatedIpMap[networkName] = map[string]net.IP{}
+	allocatedIpMap[networkName] = make(map[string]net.IP)
 	networkCacheMutex.Unlock()
 	networkCacheMutex.Unlock()
 }
 }
 
 
 // RemoveNetworkFromAllocatedIpMap - remove network from allocated ip map when network is deleted
 // RemoveNetworkFromAllocatedIpMap - remove network from allocated ip map when network is deleted
 func RemoveNetworkFromAllocatedIpMap(networkName string) {
 func RemoveNetworkFromAllocatedIpMap(networkName string) {
+	if !servercfg.CacheEnabled() {
+		return
+	}
 	networkCacheMutex.Lock()
 	networkCacheMutex.Lock()
 	delete(allocatedIpMap, networkName)
 	delete(allocatedIpMap, networkName)
 	networkCacheMutex.Unlock()
 	networkCacheMutex.Unlock()
@@ -171,23 +190,8 @@ func GetNetworks() ([]models.Network, error) {
 }
 }
 
 
 // DeleteNetwork - deletes a network
 // DeleteNetwork - deletes a network
-func DeleteNetwork(network string) error {
-	// remove ACL for network
-	err := nodeacls.DeleteACLContainer(nodeacls.NetworkID(network))
-	if err != nil {
-		logger.Log(1, "failed to remove the node acls during network delete for network,", network)
-	}
-	// Delete default network enrollment key
-	keys, _ := GetAllEnrollmentKeys()
-	for _, key := range keys {
-		if key.Tags[0] == network {
-			if key.Default {
-				DeleteEnrollmentKey(key.Value, true)
-				break
-			}
+func DeleteNetwork(network string, force bool, done chan struct{}) error {
 
 
-		}
-	}
 	nodeCount, err := GetNetworkNonServerNodeCount(network)
 	nodeCount, err := GetNetworkNonServerNodeCount(network)
 	if nodeCount == 0 || database.IsEmptyRecord(err) {
 	if nodeCount == 0 || database.IsEmptyRecord(err) {
 		// delete server nodes first then db records
 		// delete server nodes first then db records
@@ -200,7 +204,50 @@ func DeleteNetwork(network string) error {
 		}
 		}
 		return nil
 		return nil
 	}
 	}
-	return errors.New("node check failed. All nodes must be deleted before deleting network")
+
+	// Remove All Nodes
+	go func() {
+		nodes, err := GetNetworkNodes(network)
+		if err == nil {
+			for _, node := range nodes {
+				node := node
+				host, err := GetHost(node.HostID.String())
+				if err != nil {
+					continue
+				}
+				DissasociateNodeFromHost(&node, host)
+			}
+		}
+		// remove ACL for network
+		err = nodeacls.DeleteACLContainer(nodeacls.NetworkID(network))
+		if err != nil {
+			logger.Log(1, "failed to remove the node acls during network delete for network,", network)
+		}
+		// delete server nodes first then db records
+		err = database.DeleteRecord(database.NETWORKS_TABLE_NAME, network)
+		if err != nil {
+			return
+		}
+		if servercfg.CacheEnabled() {
+			deleteNetworkFromCache(network)
+		}
+		done <- struct{}{}
+		close(done)
+	}()
+
+	// Delete default network enrollment key
+	keys, _ := GetAllEnrollmentKeys()
+	for _, key := range keys {
+		if key.Tags[0] == network {
+			if key.Default {
+				DeleteEnrollmentKey(key.Value, true)
+				break
+			}
+
+		}
+	}
+
+	return nil
 }
 }
 
 
 // CreateNetwork - creates a network in database
 // CreateNetwork - creates a network in database
@@ -326,7 +373,7 @@ func GetNetworkSettings(networkname string) (models.Network, error) {
 }
 }
 
 
 // UniqueAddress - get a unique ipv4 address
 // UniqueAddress - get a unique ipv4 address
-func UniqueAddress(networkName string, reverse bool) (net.IP, error) {
+func UniqueAddressCache(networkName string, reverse bool) (net.IP, error) {
 	add := net.IP{}
 	add := net.IP{}
 	var network models.Network
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)
@@ -368,6 +415,49 @@ func UniqueAddress(networkName string, reverse bool) (net.IP, error) {
 	return add, errors.New("ERROR: No unique addresses available. Check network subnet")
 	return add, errors.New("ERROR: No unique addresses available. Check network subnet")
 }
 }
 
 
+// UniqueAddress - get a unique ipv4 address
+func UniqueAddressDB(networkName string, reverse bool) (net.IP, error) {
+	add := net.IP{}
+	var network models.Network
+	network, err := GetParentNetwork(networkName)
+	if err != nil {
+		logger.Log(0, "UniqueAddressServer encountered  an error")
+		return add, err
+	}
+
+	if network.IsIPv4 == "no" {
+		return add, fmt.Errorf("IPv4 not active on network " + networkName)
+	}
+	//ensure AddressRange is valid
+	if _, _, err := net.ParseCIDR(network.AddressRange); err != nil {
+		logger.Log(0, "UniqueAddress encountered  an error")
+		return add, err
+	}
+	net4 := iplib.Net4FromStr(network.AddressRange)
+	newAddrs := net4.FirstAddress()
+
+	if reverse {
+		newAddrs = net4.LastAddress()
+	}
+
+	for {
+		if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, false) &&
+			IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, false) {
+			return newAddrs, nil
+		}
+		if reverse {
+			newAddrs, err = net4.PreviousIP(newAddrs)
+		} else {
+			newAddrs, err = net4.NextIP(newAddrs)
+		}
+		if err != nil {
+			break
+		}
+	}
+
+	return add, errors.New("ERROR: No unique addresses available. Check network subnet")
+}
+
 // IsIPUnique - checks if an IP is unique
 // IsIPUnique - checks if an IP is unique
 func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool {
 func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool {
 
 
@@ -411,9 +501,67 @@ func IsIPUnique(network string, ip string, tableName string, isIpv6 bool) bool {
 
 
 	return isunique
 	return isunique
 }
 }
+func UniqueAddress(networkName string, reverse bool) (net.IP, error) {
+	if servercfg.CacheEnabled() {
+		return UniqueAddressCache(networkName, reverse)
+	}
+	return UniqueAddressDB(networkName, reverse)
+}
 
 
-// UniqueAddress6 - see if ipv6 address is unique
 func UniqueAddress6(networkName string, reverse bool) (net.IP, error) {
 func UniqueAddress6(networkName string, reverse bool) (net.IP, error) {
+	if servercfg.CacheEnabled() {
+		return UniqueAddress6Cache(networkName, reverse)
+	}
+	return UniqueAddress6DB(networkName, reverse)
+}
+
+// UniqueAddress6DB - see if ipv6 address is unique
+func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) {
+	add := net.IP{}
+	var network models.Network
+	network, err := GetParentNetwork(networkName)
+	if err != nil {
+		fmt.Println("Network Not Found")
+		return add, err
+	}
+	if network.IsIPv6 == "no" {
+		return add, fmt.Errorf("IPv6 not active on network " + networkName)
+	}
+
+	//ensure AddressRange is valid
+	if _, _, err := net.ParseCIDR(network.AddressRange6); err != nil {
+		return add, err
+	}
+	net6 := iplib.Net6FromStr(network.AddressRange6)
+
+	newAddrs, err := net6.NextIP(net6.FirstAddress())
+	if reverse {
+		newAddrs, err = net6.PreviousIP(net6.LastAddress())
+	}
+	if err != nil {
+		return add, err
+	}
+
+	for {
+		if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, true) &&
+			IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, true) {
+			return newAddrs, nil
+		}
+		if reverse {
+			newAddrs, err = net6.PreviousIP(newAddrs)
+		} else {
+			newAddrs, err = net6.NextIP(newAddrs)
+		}
+		if err != nil {
+			break
+		}
+	}
+
+	return add, errors.New("ERROR: No unique IPv6 addresses available. Check network subnet")
+}
+
+// UniqueAddress6Cache - see if ipv6 address is unique using cache
+func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 	add := net.IP{}
 	add := net.IP{}
 	var network models.Network
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)

+ 90 - 44
logic/nodes.go

@@ -35,12 +35,18 @@ var (
 func getNodeFromCache(nodeID string) (node models.Node, ok bool) {
 func getNodeFromCache(nodeID string) (node models.Node, ok bool) {
 	nodeCacheMutex.RLock()
 	nodeCacheMutex.RLock()
 	node, ok = nodesCacheMap[nodeID]
 	node, ok = nodesCacheMap[nodeID]
+	if node.Mutex == nil {
+		node.Mutex = &sync.Mutex{}
+	}
 	nodeCacheMutex.RUnlock()
 	nodeCacheMutex.RUnlock()
 	return
 	return
 }
 }
 func getNodesFromCache() (nodes []models.Node) {
 func getNodesFromCache() (nodes []models.Node) {
 	nodeCacheMutex.RLock()
 	nodeCacheMutex.RLock()
 	for _, node := range nodesCacheMap {
 	for _, node := range nodesCacheMap {
+		if node.Mutex == nil {
+			node.Mutex = &sync.Mutex{}
+		}
 		nodes = append(nodes, node)
 		nodes = append(nodes, node)
 	}
 	}
 	nodeCacheMutex.RUnlock()
 	nodeCacheMutex.RUnlock()
@@ -141,7 +147,7 @@ func GetNetworkNodesMemory(allNodes []models.Node, network string) []models.Node
 		defer nodeNetworkCacheMutex.Unlock()
 		defer nodeNetworkCacheMutex.Unlock()
 		return slices.Collect(maps.Values(networkNodes))
 		return slices.Collect(maps.Values(networkNodes))
 	}
 	}
-	var nodes = []models.Node{}
+	var nodes = make([]models.Node, 0, len(allNodes))
 	for i := range allNodes {
 	for i := range allNodes {
 		node := allNodes[i]
 		node := allNodes[i]
 		if node.Network == network {
 		if node.Network == network {
@@ -239,7 +245,7 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 		}
 		}
 	}
 	}
 
 
-	return fmt.Errorf("failed to update node " + currentNode.ID.String() + ", cannot change ID.")
+	return fmt.Errorf("failed to update node %s, cannot change ID", currentNode.ID.String())
 }
 }
 
 
 // DeleteNode - marks node for deletion (and adds to zombie list) if called by UI or deletes node if called by node
 // DeleteNode - marks node for deletion (and adds to zombie list) if called by UI or deletes node if called by node
@@ -358,12 +364,15 @@ func DeleteNodeByID(node *models.Node) error {
 		logger.Log(1, "unable to remove metrics from DB for node", node.ID.String(), err.Error())
 		logger.Log(1, "unable to remove metrics from DB for node", node.ID.String(), err.Error())
 	}
 	}
 	//recycle ip address
 	//recycle ip address
-	if node.Address.IP != nil {
-		RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String())
-	}
-	if node.Address6.IP != nil {
-		RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String())
+	if servercfg.CacheEnabled() {
+		if node.Address.IP != nil {
+			RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String())
+		}
+		if node.Address6.IP != nil {
+			RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String())
+		}
 	}
 	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -425,6 +434,9 @@ func GetAllNodes() ([]models.Node, error) {
 		}
 		}
 		// add node to our array
 		// add node to our array
 		nodes = append(nodes, node)
 		nodes = append(nodes, node)
+		if node.Mutex == nil {
+			node.Mutex = &sync.Mutex{}
+		}
 		nodesMap[node.ID.String()] = node
 		nodesMap[node.ID.String()] = node
 	}
 	}
 
 
@@ -445,7 +457,7 @@ func AddStaticNodestoList(nodes []models.Node) []models.Node {
 	return nodes
 	return nodes
 }
 }
 
 
-func AddStatusToNodes(nodes []models.Node) (nodesWithStatus []models.Node) {
+func AddStatusToNodes(nodes []models.Node, statusCall bool) (nodesWithStatus []models.Node) {
 	aclDefaultPolicyStatusMap := make(map[string]bool)
 	aclDefaultPolicyStatusMap := make(map[string]bool)
 	for _, node := range nodes {
 	for _, node := range nodes {
 		if _, ok := aclDefaultPolicyStatusMap[node.Network]; !ok {
 		if _, ok := aclDefaultPolicyStatusMap[node.Network]; !ok {
@@ -453,7 +465,12 @@ func AddStatusToNodes(nodes []models.Node) (nodesWithStatus []models.Node) {
 			defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 			defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 			aclDefaultPolicyStatusMap[node.Network] = defaultPolicy.Enabled
 			aclDefaultPolicyStatusMap[node.Network] = defaultPolicy.Enabled
 		}
 		}
-		GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
+		if statusCall {
+			GetNodeStatus(&node, aclDefaultPolicyStatusMap[node.Network])
+		} else {
+			GetNodeCheckInStatus(&node, true)
+		}
+
 		nodesWithStatus = append(nodesWithStatus, node)
 		nodesWithStatus = append(nodesWithStatus, node)
 	}
 	}
 	return
 	return
@@ -574,6 +591,16 @@ func GetAllNodesAPI(nodes []models.Node) []models.ApiNode {
 	return apiNodes[:]
 	return apiNodes[:]
 }
 }
 
 
+// GetNodesStatusAPI - gets nodes status
+func GetNodesStatusAPI(nodes []models.Node) map[string]models.ApiNodeStatus {
+	apiStatusNodesMap := make(map[string]models.ApiNodeStatus)
+	for i := range nodes {
+		newApiNode := nodes[i].ConvertToStatusNode()
+		apiStatusNodesMap[newApiNode.ID] = *newApiNode
+	}
+	return apiStatusNodesMap
+}
+
 // DeleteExpiredNodes - goroutine which deletes nodes which are expired
 // DeleteExpiredNodes - goroutine which deletes nodes which are expired
 func DeleteExpiredNodes(ctx context.Context, peerUpdate chan *models.Node) {
 func DeleteExpiredNodes(ctx context.Context, peerUpdate chan *models.Node) {
 	// Delete Expired Nodes Every Hour
 	// Delete Expired Nodes Every Hour
@@ -685,15 +712,16 @@ func createNode(node *models.Node) error {
 	if servercfg.CacheEnabled() {
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(*node)
 		storeNodeInCache(*node)
 		storeNodeInNetworkCache(*node, node.Network)
 		storeNodeInNetworkCache(*node, node.Network)
-	}
-	if _, ok := allocatedIpMap[node.Network]; ok {
-		if node.Address.IP != nil {
-			AddIpToAllocatedIpMap(node.Network, node.Address.IP)
-		}
-		if node.Address6.IP != nil {
-			AddIpToAllocatedIpMap(node.Network, node.Address6.IP)
+		if _, ok := allocatedIpMap[node.Network]; ok {
+			if node.Address.IP != nil {
+				AddIpToAllocatedIpMap(node.Network, node.Address.IP)
+			}
+			if node.Address6.IP != nil {
+				AddIpToAllocatedIpMap(node.Network, node.Address6.IP)
+			}
 		}
 		}
 	}
 	}
+
 	_, err = nodeacls.CreateNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), defaultACLVal)
 	_, err = nodeacls.CreateNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), defaultACLVal)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "failed to create node ACL for node,", node.ID.String(), "err:", err.Error())
 		logger.Log(1, "failed to create node ACL for node,", node.ID.String(), "err:", err.Error())
@@ -739,16 +767,14 @@ func ValidateParams(nodeid, netid string) (models.Node, error) {
 func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
 func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
 
 
 	if currentNode.Address.IP != nil && currentNode.Address.String() != newNode.Address {
 	if currentNode.Address.IP != nil && currentNode.Address.String() != newNode.Address {
-		newIp, _, _ := net.ParseCIDR(newNode.Address)
-		ipAllocated := allocatedIpMap[currentNode.Network]
-		if _, ok := ipAllocated[newIp.String()]; ok {
+		if !IsIPUnique(newNode.Network, newNode.Address, database.NODES_TABLE_NAME, false) ||
+			!IsIPUnique(newNode.Network, newNode.Address, database.EXT_CLIENT_TABLE_NAME, false) {
 			return errors.New("ip specified is already allocated:  " + newNode.Address)
 			return errors.New("ip specified is already allocated:  " + newNode.Address)
 		}
 		}
 	}
 	}
 	if currentNode.Address6.IP != nil && currentNode.Address6.String() != newNode.Address6 {
 	if currentNode.Address6.IP != nil && currentNode.Address6.String() != newNode.Address6 {
-		newIp, _, _ := net.ParseCIDR(newNode.Address6)
-		ipAllocated := allocatedIpMap[currentNode.Network]
-		if _, ok := ipAllocated[newIp.String()]; ok {
+		if !IsIPUnique(newNode.Network, newNode.Address6, database.NODES_TABLE_NAME, false) ||
+			!IsIPUnique(newNode.Network, newNode.Address6, database.EXT_CLIENT_TABLE_NAME, false) {
 			return errors.New("ip specified is already allocated:  " + newNode.Address6)
 			return errors.New("ip specified is already allocated:  " + newNode.Address6)
 		}
 		}
 	}
 	}
@@ -811,9 +837,16 @@ func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) {
 		if nodeI.Tags == nil {
 		if nodeI.Tags == nil {
 			continue
 			continue
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Lock()
+		}
 		for nodeTagID := range nodeI.Tags {
 		for nodeTagID := range nodeI.Tags {
 			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Unlock()
+		}
+
 	}
 	}
 	return
 	return
 }
 }
@@ -825,9 +858,15 @@ func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (
 		if nodeI.Tags == nil {
 		if nodeI.Tags == nil {
 			continue
 			continue
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Lock()
+		}
 		for nodeTagID := range nodeI.Tags {
 		for nodeTagID := range nodeI.Tags {
 			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Unlock()
+		}
 	}
 	}
 	tagNodesMap["*"] = nodes
 	tagNodesMap["*"] = nodes
 	if !withStaticNodes {
 	if !withStaticNodes {
@@ -846,17 +885,16 @@ func AddTagMapWithStaticNodes(netID models.NetworkID,
 		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {
 		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {
 			continue
 			continue
 		}
 		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Lock()
+		}
 		for tagID := range extclient.Tags {
 		for tagID := range extclient.Tags {
-			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{
-				IsStatic:   true,
-				StaticNode: extclient,
-			})
-			tagNodesMap["*"] = append(tagNodesMap["*"], models.Node{
-				IsStatic:   true,
-				StaticNode: extclient,
-			})
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
+			tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
+		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Unlock()
 		}
 		}
-
 	}
 	}
 	return tagNodesMap
 	return tagNodesMap
 }
 }
@@ -871,11 +909,14 @@ func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
 		if extclient.Tags == nil {
 		if extclient.Tags == nil {
 			continue
 			continue
 		}
 		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Lock()
+		}
 		for tagID := range extclient.Tags {
 		for tagID := range extclient.Tags {
-			tagNodesMap[tagID] = append(tagNodesMap[tagID], models.Node{
-				IsStatic:   true,
-				StaticNode: extclient,
-			})
+			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
+		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Unlock()
 		}
 		}
 
 
 	}
 	}
@@ -893,9 +934,15 @@ func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
 		if nodeI.Tags == nil {
 		if nodeI.Tags == nil {
 			continue
 			continue
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Lock()
+		}
 		if _, ok := nodeI.Tags[tagID]; ok {
 		if _, ok := nodeI.Tags[tagID]; ok {
 			nMap[nodeI.ID.String()] = nodeI
 			nMap[nodeI.ID.String()] = nodeI
 		}
 		}
+		if nodeI.Mutex != nil {
+			nodeI.Mutex.Unlock()
+		}
 	}
 	}
 	return AddStaticNodesWithTag(tag, nMap)
 	return AddStaticNodesWithTag(tag, nMap)
 }
 }
@@ -909,13 +956,15 @@ func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[stri
 		if extclient.RemoteAccessClientID != "" {
 		if extclient.RemoteAccessClientID != "" {
 			continue
 			continue
 		}
 		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Lock()
+		}
 		if _, ok := extclient.Tags[tag.ID]; ok {
 		if _, ok := extclient.Tags[tag.ID]; ok {
-			nMap[extclient.ClientID] = models.Node{
-				IsStatic:   true,
-				StaticNode: extclient,
-			}
+			nMap[extclient.ClientID] = extclient.ConvertToStaticNode()
+		}
+		if extclient.Mutex != nil {
+			extclient.Mutex.Unlock()
 		}
 		}
-
 	}
 	}
 	return nMap
 	return nMap
 }
 }
@@ -931,10 +980,7 @@ func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node {
 		return nMap
 		return nMap
 	}
 	}
 	for _, extclient := range extclients {
 	for _, extclient := range extclients {
-		nMap[extclient.ClientID] = models.Node{
-			IsStatic:   true,
-			StaticNode: extclient,
-		}
+		nMap[extclient.ClientID] = extclient.ConvertToStaticNode()
 	}
 	}
 	return nMap
 	return nMap
 }
 }

+ 116 - 32
logic/peers.go

@@ -59,6 +59,80 @@ var (
 	}
 	}
 )
 )
 
 
+// GetHostPeerInfo - fetches required peer info per network
+func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
+	peerInfo := models.HostPeerInfo{
+		NetworkPeerIDs: make(map[models.NetworkID]models.PeerMap),
+	}
+	allNodes, err := GetAllNodes()
+	if err != nil {
+		return peerInfo, err
+	}
+	for _, nodeID := range host.Nodes {
+		nodeID := nodeID
+		node, err := GetNodeByID(nodeID)
+		if err != nil {
+			continue
+		}
+
+		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
+			continue
+		}
+		networkPeersInfo := make(models.PeerMap)
+		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+
+		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
+		for _, peer := range currentPeers {
+			peer := peer
+			if peer.ID.String() == node.ID.String() {
+				logger.Log(2, "peer update, skipping self")
+				// skip yourself
+				continue
+			}
+
+			peerHost, err := GetHost(peer.HostID.String())
+			if err != nil {
+				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+				continue
+			}
+
+			var allowedToComm bool
+			if defaultDevicePolicy.Enabled {
+				allowedToComm = true
+			} else {
+				allowedToComm = IsPeerAllowed(node, peer, false)
+			}
+			if peer.Action != models.NODE_DELETE &&
+				!peer.PendingDelete &&
+				peer.Connected &&
+				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
+				(defaultDevicePolicy.Enabled || allowedToComm) {
+
+				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
+					ID:         peer.ID.String(),
+					HostID:     peerHost.ID.String(),
+					Address:    peer.PrimaryAddress(),
+					Name:       peerHost.Name,
+					Network:    peer.Network,
+					ListenPort: peerHost.ListenPort,
+				}
+
+			}
+		}
+		var extPeerIDAndAddrs []models.IDandAddr
+		if node.IsIngressGateway {
+			_, extPeerIDAndAddrs, _, err = GetExtPeers(&node, &node)
+			if err == nil {
+				for _, extPeerIdAndAddr := range extPeerIDAndAddrs {
+					networkPeersInfo[extPeerIdAndAddr.ID] = extPeerIdAndAddr
+				}
+			}
+		}
+		peerInfo.NetworkPeerIDs[models.NetworkID(node.Network)] = networkPeersInfo
+	}
+	return peerInfo, nil
+}
+
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
 func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.Node,
 func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.Node,
 	deletedNode *models.Node, deletedClients []models.ExtClient) (models.HostPeerUpdate, error) {
 	deletedNode *models.Node, deletedClients []models.ExtClient) (models.HostPeerUpdate, error) {
@@ -79,11 +153,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			IngressInfo: make(map[string]models.IngressInfo),
 			IngressInfo: make(map[string]models.IngressInfo),
 			AclRules:    make(map[string]models.AclRule),
 			AclRules:    make(map[string]models.AclRule),
 		},
 		},
-		PeerIDs:           make(models.PeerMap, 0),
-		Peers:             []wgtypes.PeerConfig{},
-		NodePeers:         []wgtypes.PeerConfig{},
-		HostNetworkInfo:   models.HostInfoMap{},
-		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
+		PeerIDs:         make(models.PeerMap, 0),
+		Peers:           []wgtypes.PeerConfig{},
+		NodePeers:       []wgtypes.PeerConfig{},
+		HostNetworkInfo: models.HostInfoMap{},
+		ServerConfig:    servercfg.ServerInfo,
 	}
 	}
 	defer func() {
 	defer func() {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
@@ -187,9 +261,20 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 
 
 		} else {
 		} else {
 			hostPeerUpdate.FwUpdate.AllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
-			hostPeerUpdate.FwUpdate.AclRules = GetAclRulesForNode(&node)
+			rules := GetAclRulesForNode(&node)
+			if len(hostPeerUpdate.FwUpdate.AclRules) == 0 {
+				hostPeerUpdate.FwUpdate.AclRules = rules
+			} else {
+				for aclID, rule := range rules {
+					hostPeerUpdate.FwUpdate.AclRules[aclID] = rule
+				}
+			}
 		}
 		}
-
+		networkSettings, err := GetNetwork(node.Network)
+		if err != nil {
+			continue
+		}
+		hostPeerUpdate.NameServers = append(hostPeerUpdate.NameServers, networkSettings.NameServers...)
 		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		for _, peer := range currentPeers {
 		for _, peer := range currentPeers {
 			peer := peer
 			peer := peer
@@ -211,32 +296,31 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 			}
 			if peer.IsEgressGateway {
 			if peer.IsEgressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
-					EgressGwAddr:  peer.Address,
-					EgressGwAddr6: peer.Address6,
-					NodeAddr:      node.Address,
-					NodeAddr6:     node.Address6,
-					EgressRanges:  peer.EgressGatewayRanges,
+					EgressGwAddr:           peer.Address,
+					EgressGwAddr6:          peer.Address6,
+					NodeAddr:               node.Address,
+					NodeAddr6:              node.Address6,
+					EgressRanges:           peer.EgressGatewayRanges,
+					EgressRangesWithMetric: peer.EgressGatewayRequest.RangesWithMetric,
 				})
 				})
 			}
 			}
 			if peer.IsIngressGateway {
 			if peer.IsIngressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 			}
 			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
-			if servercfg.IsPro {
-				if (node.IsRelayed && node.RelayedBy != peer.ID.String()) ||
-					(peer.IsRelayed && peer.RelayedBy != node.ID.String()) || isFailOverPeer {
-					// if node is relayed and peer is not the relay, set remove to true
-					if _, ok := peerIndexMap[peerHost.PublicKey.String()]; ok {
-						continue
-					}
-					peerConfig.Remove = true
-					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
-					peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
+			if (node.IsRelayed && node.RelayedBy != peer.ID.String()) ||
+				(peer.IsRelayed && peer.RelayedBy != node.ID.String()) || isFailOverPeer {
+				// if node is relayed and peer is not the relay, set remove to true
+				if _, ok := peerIndexMap[peerHost.PublicKey.String()]; ok {
 					continue
 					continue
 				}
 				}
-				if node.IsRelayed && node.RelayedBy == peer.ID.String() {
-					hostPeerUpdate = SetDefaultGwForRelayedUpdate(node, peer, hostPeerUpdate)
-				}
+				peerConfig.Remove = true
+				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
+				peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
+				continue
+			}
+			if node.IsRelayed && node.RelayedBy == peer.ID.String() {
+				hostPeerUpdate = SetDefaultGwForRelayedUpdate(node, peer, hostPeerUpdate)
 			}
 			}
 
 
 			uselocal := false
 			uselocal := false
@@ -290,15 +374,19 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				peerConfig.Endpoint.IP = peer.LocalAddress.IP
 				peerConfig.Endpoint.IP = peer.LocalAddress.IP
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 			}
 			}
-			allowedips := GetAllowedIPs(&node, &peer, nil)
-			allowedToComm := IsPeerAllowed(node, peer, false)
+			var allowedToComm bool
+			if defaultDevicePolicy.Enabled {
+				allowedToComm = true
+			} else {
+				allowedToComm = IsPeerAllowed(node, peer, false)
+			}
 			if peer.Action != models.NODE_DELETE &&
 			if peer.Action != models.NODE_DELETE &&
 				!peer.PendingDelete &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				peer.Connected &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
 				(defaultDevicePolicy.Enabled || allowedToComm) &&
 				(defaultDevicePolicy.Enabled || allowedToComm) &&
 				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
 				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
-				peerConfig.AllowedIPs = allowedips // only append allowed IPs if valid connection
+				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 			}
 			}
 
 
 			var nodePeer wgtypes.PeerConfig
 			var nodePeer wgtypes.PeerConfig
@@ -459,10 +547,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 			}
 		}
 		}
 	}
 	}
-
-	hostPeerUpdate.ManageDNS = servercfg.GetManageDNS()
-	hostPeerUpdate.Stun = servercfg.IsStunEnabled()
-	hostPeerUpdate.StunServers = servercfg.GetStunServers()
 	return hostPeerUpdate, nil
 	return hostPeerUpdate, nil
 }
 }
 
 

+ 225 - 13
logic/relay.go

@@ -1,34 +1,246 @@
 package logic
 package logic
 
 
 import (
 import (
+	"errors"
+	"fmt"
 	"net"
 	"net"
 
 
+	"github.com/google/uuid"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 )
 )
 
 
-var GetRelays = func() ([]models.Node, error) {
-	return []models.Node{}, nil
+// GetRelays - gets all the nodes that are relays
+func GetRelays() ([]models.Node, error) {
+	nodes, err := GetAllNodes()
+	if err != nil {
+		return nil, err
+	}
+	relays := make([]models.Node, 0)
+	for _, node := range nodes {
+		if node.IsRelay {
+			relays = append(relays, node)
+		}
+	}
+	return relays, nil
 }
 }
 
 
-var RelayedAllowedIPs = func(peer, node *models.Node) []net.IPNet {
-	return []net.IPNet{}
+// CreateRelay - creates a relay
+func CreateRelay(relay models.RelayRequest) ([]models.Node, models.Node, error) {
+	var returnnodes []models.Node
+
+	node, err := GetNodeByID(relay.NodeID)
+	if err != nil {
+		return returnnodes, models.Node{}, err
+	}
+	host, err := GetHost(node.HostID.String())
+	if err != nil {
+		return returnnodes, models.Node{}, err
+	}
+	if host.OS != "linux" {
+		return returnnodes, models.Node{}, fmt.Errorf("only linux machines can be gateway nodes")
+	}
+	err = ValidateRelay(relay, false)
+	if err != nil {
+		return returnnodes, models.Node{}, err
+	}
+	node.IsRelay = true
+	node.IsGw = true
+	node.RelayedNodes = relay.RelayedNodes
+	node.SetLastModified()
+	err = UpsertNode(&node)
+	if err != nil {
+		return returnnodes, node, err
+	}
+	returnnodes = SetRelayedNodes(true, relay.NodeID, relay.RelayedNodes)
+	return returnnodes, node, nil
+}
+
+// SetRelayedNodes- sets and saves node as relayed
+func SetRelayedNodes(setRelayed bool, relay string, relayed []string) []models.Node {
+	var returnnodes []models.Node
+	for _, id := range relayed {
+		node, err := GetNodeByID(id)
+		if err != nil {
+			logger.Log(0, "setRelayedNodes.GetNodebyID", err.Error())
+			continue
+		}
+		node.IsRelayed = setRelayed
+		if setRelayed {
+			node.RelayedBy = relay
+		} else {
+			node.RelayedBy = ""
+		}
+		node.SetLastModified()
+		if err := UpsertNode(&node); err != nil {
+			logger.Log(0, "setRelayedNodes.Insert", err.Error())
+			continue
+		}
+		returnnodes = append(returnnodes, node)
+	}
+	return returnnodes
+}
+
+// func GetRelayedNodes(relayNode *models.Node) (models.Node, error) {
+//	var returnnodes []models.Node
+//	networkNodes, err := GetNetworkNodes(relayNode.Network)
+//	if err != nil {
+//		return returnnodes, err
+//	}
+//	for _, node := range networkNodes {
+//		for _, addr := range relayNode.RelayAddrs {
+//			if addr == node.Address.IP.String() || addr == node.Address6.IP.String() {
+//				returnnodes = append(returnnodes, node)
+//			}
+//		}
+//	}
+//	return returnnodes, nil
+// }
+
+// ValidateRelay - checks if relay is valid
+func ValidateRelay(relay models.RelayRequest, update bool) error {
+	var err error
+
+	node, err := GetNodeByID(relay.NodeID)
+	if err != nil {
+		return err
+	}
+	if !update && node.IsRelay {
+		return errors.New("node is already acting as a relay")
+	}
+	for _, relayedNodeID := range relay.RelayedNodes {
+		relayedNode, err := GetNodeByID(relayedNodeID)
+		if err != nil {
+			return err
+		}
+		if relayedNode.IsIngressGateway {
+			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
+		}
+		if relayedNode.IsInternetGateway {
+			return errors.New("cannot relay an internet gateway (" + relayedNodeID + ")")
+		}
+		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
+			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
+		}
+		if relayedNode.IsFailOver {
+			return errors.New("cannot relay a failOver (" + relayedNodeID + ")")
+		}
+		if relayedNode.FailedOverBy != uuid.Nil {
+			ResetFailedOverPeer(&relayedNode)
+		}
+	}
+	return err
+}
+
+// UpdateRelayNodes - updates relay nodes
+func updateRelayNodes(relay string, oldNodes []string, newNodes []string) []models.Node {
+	_ = SetRelayedNodes(false, relay, oldNodes)
+	return SetRelayedNodes(true, relay, newNodes)
+}
+
+func RelayUpdates(currentNode, newNode *models.Node) bool {
+	relayUpdates := false
+	if newNode.IsRelay {
+		if len(newNode.RelayedNodes) != len(currentNode.RelayedNodes) {
+			relayUpdates = true
+		} else {
+			for i, node := range newNode.RelayedNodes {
+				if node != currentNode.RelayedNodes[i] {
+					relayUpdates = true
+				}
+			}
+		}
+	}
+	return relayUpdates
 }
 }
 
 
-var GetAllowedIpsForRelayed = func(relayed, relay *models.Node) []net.IPNet {
-	return []net.IPNet{}
+// UpdateRelayed - updates a relay's relayed nodes, and sends updates to the relayed nodes over MQ
+func UpdateRelayed(currentNode, newNode *models.Node) {
+	updatenodes := updateRelayNodes(currentNode.ID.String(), currentNode.RelayedNodes, newNode.RelayedNodes)
+	if len(updatenodes) > 0 {
+		for _, relayedNode := range updatenodes {
+			node := relayedNode
+			ResetFailedOverPeer(&node)
+		}
+	}
 }
 }
 
 
-var UpdateRelayed = func(currentNode, newNode *models.Node) {
+// DeleteRelay - deletes a relay
+func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
+	var returnnodes []models.Node
+	node, err := GetNodeByID(nodeid)
+	if err != nil {
+		return returnnodes, models.Node{}, err
+	}
+	returnnodes = SetRelayedNodes(false, nodeid, node.RelayedNodes)
+	node.IsRelay = false
+	node.RelayedNodes = []string{}
+	node.SetLastModified()
+	if err = UpsertNode(&node); err != nil {
+		return returnnodes, models.Node{}, err
+	}
+	return returnnodes, node, nil
 }
 }
 
 
-var SetRelayedNodes = func(setRelayed bool, relay string, relayed []string) []models.Node {
-	return []models.Node{}
+func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
+	var allowedIPs = []net.IPNet{}
+	for _, relayedNodeID := range peer.RelayedNodes {
+		if node.ID.String() == relayedNodeID {
+			continue
+		}
+		relayedNode, err := GetNodeByID(relayedNodeID)
+		if err != nil {
+			continue
+		}
+		allowed := getRelayedAddresses(relayedNodeID)
+		if relayedNode.IsEgressGateway {
+			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
+		}
+		allowedIPs = append(allowedIPs, allowed...)
+	}
+	return allowedIPs
 }
 }
 
 
-var RelayUpdates = func(currentNode, newNode *models.Node) bool {
-	return false
+// GetAllowedIpsForRelayed - returns the peerConfig for a node relayed by relay
+func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNet) {
+	if relayed.RelayedBy != relay.ID.String() {
+		logger.Log(0, "RelayedByRelay called with invalid parameters")
+		return
+	}
+	if relay.InternetGwID != "" {
+		return GetAllowedIpForInetNodeClient(relayed, relay)
+	}
+	peers, err := GetNetworkNodes(relay.Network)
+	if err != nil {
+		logger.Log(0, "error getting network clients", err.Error())
+		return
+	}
+	for _, peer := range peers {
+		if peer.ID == relayed.ID || peer.ID == relay.ID {
+			continue
+		}
+		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
+			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
+		}
+	}
+	return
 }
 }
 
 
-var ValidateRelay = func(relay models.RelayRequest, update bool) error {
-	return nil
+func getRelayedAddresses(id string) []net.IPNet {
+	addrs := []net.IPNet{}
+	node, err := GetNodeByID(id)
+	if err != nil {
+		logger.Log(0, "getRelayedAddresses: "+err.Error())
+		return addrs
+	}
+	if node.Address.IP != nil {
+		node.Address.Mask = net.CIDRMask(32, 32)
+		addrs = append(addrs, node.Address)
+	}
+	if node.Address6.IP != nil {
+		node.Address6.Mask = net.CIDRMask(128, 128)
+		addrs = append(addrs, node.Address6)
+	}
+	return addrs
 }
 }

+ 6 - 2
logic/status.go

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

+ 1 - 1
logic/tags.go

@@ -259,7 +259,7 @@ func CheckIDSyntax(id string) error {
 	if len(id) < 3 {
 	if len(id) < 3 {
 		return errors.New("name should have min 3 characters")
 		return errors.New("name should have min 3 characters")
 	}
 	}
-	reg, err := regexp.Compile("^[a-zA-Z-]+$")
+	reg, err := regexp.Compile("^[a-zA-Z0-9- ]+$")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 1 - 0
logic/user_mgmt.go

@@ -60,6 +60,7 @@ var DeleteNetworkRoles = func(netID string) {}
 var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
 var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
+var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
 var AddGlobalNetRolesToAdmins = func(u models.User) {}
 var AddGlobalNetRolesToAdmins = func(u models.User) {}
 
 
 // GetRole - fetches role template by id
 // GetRole - fetches role template by id

+ 25 - 0
logic/util.go

@@ -7,6 +7,7 @@ import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
+	"log/slog"
 	"net"
 	"net"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -91,6 +92,30 @@ func StringSliceContains(slice []string, item string) bool {
 	}
 	}
 	return false
 	return false
 }
 }
+func SetVerbosity(logLevel int) {
+	var level slog.Level
+	switch logLevel {
+
+	case 0:
+		level = slog.LevelInfo
+	case 1:
+		level = slog.LevelError
+	case 2:
+		level = slog.LevelWarn
+	case 3:
+		level = slog.LevelDebug
+
+	default:
+		level = slog.LevelInfo
+	}
+	// Create the logger with the chosen level
+	handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+		Level: level,
+	})
+	logger := slog.New(handler)
+	slog.SetDefault(logger)
+
+}
 
 
 // NormalizeCIDR - returns the first address of CIDR
 // NormalizeCIDR - returns the first address of CIDR
 func NormalizeCIDR(address string) (string, error) {
 func NormalizeCIDR(address string) (string, error) {

+ 26 - 0
migrate/migrate.go

@@ -28,6 +28,7 @@ func Run() {
 	updateHosts()
 	updateHosts()
 	updateNodes()
 	updateNodes()
 	updateAcls()
 	updateAcls()
+	migrateToGws()
 }
 }
 
 
 func assignSuperAdmin() {
 func assignSuperAdmin() {
@@ -224,6 +225,16 @@ func updateNodes() {
 				node.EgressGatewayRanges = egressRanges
 				node.EgressGatewayRanges = egressRanges
 				logic.UpsertNode(&node)
 				logic.UpsertNode(&node)
 			}
 			}
+			if len(node.EgressGatewayRequest.Ranges) > 0 && len(node.EgressGatewayRequest.RangesWithMetric) == 0 {
+				for _, egressRangeI := range node.EgressGatewayRequest.Ranges {
+					node.EgressGatewayRequest.RangesWithMetric = append(node.EgressGatewayRequest.RangesWithMetric, models.EgressRangeMetric{
+						Network:     egressRangeI,
+						RouteMetric: 256,
+					})
+				}
+				logic.UpsertNode(&node)
+			}
+
 		}
 		}
 	}
 	}
 }
 }
@@ -441,3 +452,18 @@ func createDefaultTagsAndPolicies() {
 	}
 	}
 	logic.MigrateAclPolicies()
 	logic.MigrateAclPolicies()
 }
 }
+
+func migrateToGws() {
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+	for _, node := range nodes {
+		if node.IsIngressGateway || node.IsRelay {
+			node.IsGw = true
+			node.IsIngressGateway = true
+			node.IsRelay = true
+			logic.UpsertNode(&node)
+		}
+	}
+}

+ 51 - 29
models/api_node.go

@@ -8,37 +8,45 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
+type ApiNodeStatus struct {
+	ID         string     `json:"id"`
+	IsStatic   bool       `json:"is_static"`
+	IsUserNode bool       `json:"is_user_node"`
+	Status     NodeStatus `json:"status"`
+}
+
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 type ApiNode struct {
 type ApiNode struct {
-	ID                         string   `json:"id,omitempty" validate:"required,min=5,id_unique"`
-	HostID                     string   `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
-	Address                    string   `json:"address" validate:"omitempty,cidrv4"`
-	Address6                   string   `json:"address6" validate:"omitempty,cidrv6"`
-	LocalAddress               string   `json:"localaddress" validate:"omitempty,cidr"`
-	AllowedIPs                 []string `json:"allowedips"`
-	LastModified               int64    `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
-	ExpirationDateTime         int64    `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
-	LastCheckIn                int64    `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
-	LastPeerUpdate             int64    `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
-	Network                    string   `json:"network"`
-	NetworkRange               string   `json:"networkrange"`
-	NetworkRange6              string   `json:"networkrange6"`
-	IsRelayed                  bool     `json:"isrelayed"`
-	IsRelay                    bool     `json:"isrelay"`
-	RelayedBy                  string   `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
-	RelayedNodes               []string `json:"relaynodes" yaml:"relayedNodes"`
-	IsEgressGateway            bool     `json:"isegressgateway"`
-	IsIngressGateway           bool     `json:"isingressgateway"`
-	EgressGatewayRanges        []string `json:"egressgatewayranges"`
-	EgressGatewayNatEnabled    bool     `json:"egressgatewaynatenabled"`
-	DNSOn                      bool     `json:"dnson"`
-	IngressDns                 string   `json:"ingressdns"`
-	IngressPersistentKeepalive int32    `json:"ingresspersistentkeepalive"`
-	IngressMTU                 int32    `json:"ingressmtu"`
-	Server                     string   `json:"server"`
-	Connected                  bool     `json:"connected"`
-	PendingDelete              bool     `json:"pendingdelete"`
-	Metadata                   string   `json:"metadata"`
+	ID                            string              `json:"id,omitempty" validate:"required,min=5,id_unique"`
+	HostID                        string              `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
+	Address                       string              `json:"address" validate:"omitempty,cidrv4"`
+	Address6                      string              `json:"address6" validate:"omitempty,cidrv6"`
+	LocalAddress                  string              `json:"localaddress" validate:"omitempty,cidr"`
+	AllowedIPs                    []string            `json:"allowedips"`
+	LastModified                  int64               `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
+	ExpirationDateTime            int64               `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
+	LastCheckIn                   int64               `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
+	LastPeerUpdate                int64               `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
+	Network                       string              `json:"network"`
+	NetworkRange                  string              `json:"networkrange"`
+	NetworkRange6                 string              `json:"networkrange6"`
+	IsRelayed                     bool                `json:"isrelayed"`
+	IsRelay                       bool                `json:"isrelay"`
+	RelayedBy                     string              `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
+	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
+	IsEgressGateway               bool                `json:"isegressgateway"`
+	IsIngressGateway              bool                `json:"isingressgateway"`
+	EgressGatewayRanges           []string            `json:"egressgatewayranges"`
+	EgressGatewayNatEnabled       bool                `json:"egressgatewaynatenabled"`
+	EgressGatewayRangesWithMetric []EgressRangeMetric `json:"egressgatewayranges_with_metric"`
+	DNSOn                         bool                `json:"dnson"`
+	IngressDns                    string              `json:"ingressdns"`
+	IngressPersistentKeepalive    int32               `json:"ingresspersistentkeepalive"`
+	IngressMTU                    int32               `json:"ingressmtu"`
+	Server                        string              `json:"server"`
+	Connected                     bool                `json:"connected"`
+	PendingDelete                 bool                `json:"pendingdelete"`
+	Metadata                      string              `json:"metadata"`
 	// == PRO ==
 	// == PRO ==
 	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	IsFailOver        bool                `json:"is_fail_over"`
 	IsFailOver        bool                `json:"is_fail_over"`
@@ -132,6 +140,19 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	return &convertedNode
 	return &convertedNode
 }
 }
 
 
+func (nm *Node) ConvertToStatusNode() *ApiNodeStatus {
+	apiNode := ApiNodeStatus{}
+	if nm.IsStatic {
+		apiNode.ID = nm.StaticNode.ClientID
+	} else {
+		apiNode.ID = nm.ID.String()
+	}
+	apiNode.IsStatic = nm.IsStatic
+	apiNode.IsUserNode = nm.IsUserNode
+	apiNode.Status = nm.Status
+	return &apiNode
+}
+
 // Node.ConvertToAPINode - converts a node to an API node
 // Node.ConvertToAPINode - converts a node to an API node
 func (nm *Node) ConvertToAPINode() *ApiNode {
 func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode := ApiNode{}
 	apiNode := ApiNode{}
@@ -169,6 +190,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
 	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
+	apiNode.EgressGatewayRangesWithMetric = nm.EgressGatewayRequest.RangesWithMetric
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressDns = nm.IngressDNS

+ 4 - 0
models/extclient.go

@@ -1,5 +1,7 @@
 package models
 package models
 
 
+import "sync"
+
 // 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"`
@@ -25,6 +27,7 @@ type ExtClient struct {
 	DeviceName             string              `json:"device_name"`
 	DeviceName             string              `json:"device_name"`
 	PublicEndpoint         string              `json:"public_endpoint"`
 	PublicEndpoint         string              `json:"public_endpoint"`
 	Country                string              `json:"country"`
 	Country                string              `json:"country"`
+	Mutex                  *sync.Mutex         `json:"-"`
 }
 }
 
 
 // CustomExtClient - struct for CustomExtClient params
 // CustomExtClient - struct for CustomExtClient params
@@ -55,5 +58,6 @@ func (ext *ExtClient) ConvertToStaticNode() Node {
 		Tags:       ext.Tags,
 		Tags:       ext.Tags,
 		IsStatic:   true,
 		IsStatic:   true,
 		StaticNode: *ext,
 		StaticNode: *ext,
+		Mutex:      ext.Mutex,
 	}
 	}
 }
 }

+ 9 - 0
models/gateway.go

@@ -0,0 +1,9 @@
+package models
+
+type CreateGwReq struct {
+	IngressRequest
+	RelayRequest
+}
+
+type DeleteGw struct {
+}

+ 2 - 0
models/host.go

@@ -98,6 +98,8 @@ type HostMqAction string
 const (
 const (
 	// Upgrade - const to request host to update it's client
 	// Upgrade - const to request host to update it's client
 	Upgrade HostMqAction = "UPGRADE"
 	Upgrade HostMqAction = "UPGRADE"
+	// ForceUpgrade - const for forcing a host to upgrade its client binary
+	ForceUpgrade HostMqAction = "FORCE_UPGRADE"
 	// SignalHost - const for host signal action
 	// SignalHost - const for host signal action
 	SignalHost HostMqAction = "SIGNAL_HOST"
 	SignalHost HostMqAction = "SIGNAL_HOST"
 	// UpdateHost - constant for host update action
 	// UpdateHost - constant for host update action

+ 1 - 0
models/metrics.go

@@ -48,6 +48,7 @@ type HostNetworkInfo struct {
 	ListenPort   int     `json:"listen_port" yaml:"listen_port"`
 	ListenPort   int     `json:"listen_port" yaml:"listen_port"`
 	IsStaticPort bool    `json:"is_static_port"`
 	IsStaticPort bool    `json:"is_static_port"`
 	IsStatic     bool    `json:"is_static"`
 	IsStatic     bool    `json:"is_static"`
+	Version      string  `json:"version"`
 }
 }
 
 
 // PeerMap - peer map for ids and addresses in metrics
 // PeerMap - peer map for ids and addresses in metrics

+ 33 - 23
models/mqtt.go

@@ -6,27 +6,36 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 )
 
 
+type HostPeerInfo struct {
+	NetworkPeerIDs map[NetworkID]PeerMap `json:"network_peers"`
+}
+
 // HostPeerUpdate - struct for host peer updates
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 type HostPeerUpdate struct {
-	Host              Host                 `json:"host" bson:"host" yaml:"host"`
-	ChangeDefaultGw   bool                 `json:"change_default_gw"`
-	DefaultGwIp       net.IP               `json:"default_gw_ip"`
-	IsInternetGw      bool                 `json:"is_inet_gw"`
-	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
-	Server            string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	Host            Host                  `json:"host"`
+	ChangeDefaultGw bool                  `json:"change_default_gw"`
+	DefaultGwIp     net.IP                `json:"default_gw_ip"`
+	IsInternetGw    bool                  `json:"is_inet_gw"`
+	NodeAddrs       []net.IPNet           `json:"nodes_addrs"`
+	Server          string                `json:"server"`
+	ServerVersion   string                `json:"serverversion"`
+	ServerAddrs     []ServerAddr          `json:"serveraddrs"`
+	NodePeers       []wgtypes.PeerConfig  `json:"node_peers"`
+	Peers           []wgtypes.PeerConfig  `json:"host_peers"`
+	PeerIDs         PeerMap               `json:"peerids"`
+	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"`
+	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate        FwUpdate              `json:"fw_update"`
+	ReplacePeers    bool                  `json:"replace_peers"`
+	NameServers     []string              `json:"name_servers"`
+	ServerConfig
+	OldPeerUpdateFields
+}
+
+type OldPeerUpdateFields struct {
 	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
 	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers             []wgtypes.PeerConfig
-	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
-	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate          FwUpdate              `json:"fw_update"`
-	ReplacePeers      bool                  `json:"replace_peers"`
-	EndpointDetection bool                  `json:"endpoint_detection"`
-	ManageDNS         bool                  `yaml:"manage_dns"`
-	Stun              bool                  `yaml:"stun"`
-	StunServers       string                `yaml:"stun_servers"`
+	OldPeers          []wgtypes.PeerConfig `json:"Peers"`
+	EndpointDetection bool                 `json:"endpoint_detection"`
 }
 }
 
 
 type FwRule struct {
 type FwRule struct {
@@ -61,11 +70,12 @@ type EgressInfo struct {
 
 
 // EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
 // EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
 type EgressNetworkRoutes struct {
 type EgressNetworkRoutes struct {
-	EgressGwAddr  net.IPNet `json:"egress_gw_addr" yaml:"egress_gw_addr"`
-	EgressGwAddr6 net.IPNet `json:"egress_gw_addr6" yaml:"egress_gw_addr6"`
-	NodeAddr      net.IPNet `json:"node_addr"`
-	NodeAddr6     net.IPNet `json:"node_addr6"`
-	EgressRanges  []string  `json:"egress_ranges"`
+	EgressGwAddr           net.IPNet           `json:"egress_gw_addr" yaml:"egress_gw_addr"`
+	EgressGwAddr6          net.IPNet           `json:"egress_gw_addr6" yaml:"egress_gw_addr6"`
+	NodeAddr               net.IPNet           `json:"node_addr"`
+	NodeAddr6              net.IPNet           `json:"node_addr6"`
+	EgressRanges           []string            `json:"egress_ranges"`
+	EgressRangesWithMetric []EgressRangeMetric `json:"egress_ranges_metric"`
 }
 }
 
 
 // PeerRouteInfo - struct for peer info for an ext. client
 // PeerRouteInfo - struct for peer info for an ext. client

+ 29 - 17
models/network.go

@@ -8,22 +8,23 @@ import (
 // Network Struct - contains info for a given unique network
 // Network Struct - contains info for a given unique network
 // At  some point, need to replace all instances of Name with something else like  Identifier
 // At  some point, need to replace all instances of Name with something else like  Identifier
 type Network struct {
 type Network struct {
-	AddressRange        string `json:"addressrange" bson:"addressrange" validate:"omitempty,cidrv4"`
-	AddressRange6       string `json:"addressrange6" bson:"addressrange6" validate:"omitempty,cidrv6"`
-	NetID               string `json:"netid" bson:"netid" validate:"required,min=1,max=32,netid_valid"`
-	NodesLastModified   int64  `json:"nodeslastmodified" bson:"nodeslastmodified" swaggertype:"primitive,integer" format:"int64"`
-	NetworkLastModified int64  `json:"networklastmodified" bson:"networklastmodified" swaggertype:"primitive,integer" format:"int64"`
-	DefaultInterface    string `json:"defaultinterface" bson:"defaultinterface" validate:"min=1,max=35"`
-	DefaultListenPort   int32  `json:"defaultlistenport,omitempty" bson:"defaultlistenport,omitempty" validate:"omitempty,min=1024,max=65535"`
-	NodeLimit           int32  `json:"nodelimit" bson:"nodelimit"`
-	DefaultPostDown     string `json:"defaultpostdown" bson:"defaultpostdown"`
-	DefaultKeepalive    int32  `json:"defaultkeepalive" bson:"defaultkeepalive" validate:"omitempty,max=1000"`
-	AllowManualSignUp   string `json:"allowmanualsignup" bson:"allowmanualsignup" validate:"checkyesorno"`
-	IsIPv4              string `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
-	IsIPv6              string `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`
-	DefaultUDPHolePunch string `json:"defaultudpholepunch" bson:"defaultudpholepunch" validate:"checkyesorno"`
-	DefaultMTU          int32  `json:"defaultmtu" bson:"defaultmtu"`
-	DefaultACL          string `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
+	AddressRange        string   `json:"addressrange" bson:"addressrange" validate:"omitempty,cidrv4"`
+	AddressRange6       string   `json:"addressrange6" bson:"addressrange6" validate:"omitempty,cidrv6"`
+	NetID               string   `json:"netid" bson:"netid" validate:"required,min=1,max=32,netid_valid"`
+	NodesLastModified   int64    `json:"nodeslastmodified" bson:"nodeslastmodified" swaggertype:"primitive,integer" format:"int64"`
+	NetworkLastModified int64    `json:"networklastmodified" bson:"networklastmodified" swaggertype:"primitive,integer" format:"int64"`
+	DefaultInterface    string   `json:"defaultinterface" bson:"defaultinterface" validate:"min=1,max=35"`
+	DefaultListenPort   int32    `json:"defaultlistenport,omitempty" bson:"defaultlistenport,omitempty" validate:"omitempty,min=1024,max=65535"`
+	NodeLimit           int32    `json:"nodelimit" bson:"nodelimit"`
+	DefaultPostDown     string   `json:"defaultpostdown" bson:"defaultpostdown"`
+	DefaultKeepalive    int32    `json:"defaultkeepalive" bson:"defaultkeepalive" validate:"omitempty,max=1000"`
+	AllowManualSignUp   string   `json:"allowmanualsignup" bson:"allowmanualsignup" validate:"checkyesorno"`
+	IsIPv4              string   `json:"isipv4" bson:"isipv4" validate:"checkyesorno"`
+	IsIPv6              string   `json:"isipv6" bson:"isipv6" validate:"checkyesorno"`
+	DefaultUDPHolePunch string   `json:"defaultudpholepunch" bson:"defaultudpholepunch" validate:"checkyesorno"`
+	DefaultMTU          int32    `json:"defaultmtu" bson:"defaultmtu"`
+	DefaultACL          string   `json:"defaultacl" bson:"defaultacl" yaml:"defaultacl" validate:"checkyesorno"`
+	NameServers         []string `json:"dns_nameservers"`
 }
 }
 
 
 // SaveData - sensitive fields of a network that should be kept the same
 // SaveData - sensitive fields of a network that should be kept the same
@@ -42,9 +43,10 @@ func (network *Network) SetNetworkLastModified() {
 }
 }
 
 
 // Network.SetDefaults - sets default values for a network struct
 // Network.SetDefaults - sets default values for a network struct
-func (network *Network) SetDefaults() {
+func (network *Network) SetDefaults() (upsert bool) {
 	if network.DefaultUDPHolePunch == "" {
 	if network.DefaultUDPHolePunch == "" {
 		network.DefaultUDPHolePunch = "no"
 		network.DefaultUDPHolePunch = "no"
+		upsert = true
 	}
 	}
 	if network.DefaultInterface == "" {
 	if network.DefaultInterface == "" {
 		if len(network.NetID) < 33 {
 		if len(network.NetID) < 33 {
@@ -52,35 +54,45 @@ func (network *Network) SetDefaults() {
 		} else {
 		} else {
 			network.DefaultInterface = network.NetID
 			network.DefaultInterface = network.NetID
 		}
 		}
+		upsert = true
 	}
 	}
 	if network.DefaultListenPort == 0 {
 	if network.DefaultListenPort == 0 {
 		network.DefaultListenPort = 51821
 		network.DefaultListenPort = 51821
+		upsert = true
 	}
 	}
 	if network.NodeLimit == 0 {
 	if network.NodeLimit == 0 {
 		network.NodeLimit = 999999999
 		network.NodeLimit = 999999999
+		upsert = true
 	}
 	}
 	if network.DefaultKeepalive == 0 {
 	if network.DefaultKeepalive == 0 {
 		network.DefaultKeepalive = 20
 		network.DefaultKeepalive = 20
+		upsert = true
 	}
 	}
 	if network.AllowManualSignUp == "" {
 	if network.AllowManualSignUp == "" {
 		network.AllowManualSignUp = "no"
 		network.AllowManualSignUp = "no"
+		upsert = true
 	}
 	}
 
 
 	if network.IsIPv4 == "" {
 	if network.IsIPv4 == "" {
 		network.IsIPv4 = "yes"
 		network.IsIPv4 = "yes"
+		upsert = true
 	}
 	}
 
 
 	if network.IsIPv6 == "" {
 	if network.IsIPv6 == "" {
 		network.IsIPv6 = "no"
 		network.IsIPv6 = "no"
+		upsert = true
 	}
 	}
 
 
 	if network.DefaultMTU == 0 {
 	if network.DefaultMTU == 0 {
 		network.DefaultMTU = 1280
 		network.DefaultMTU = 1280
+		upsert = true
 	}
 	}
 
 
 	if network.DefaultACL == "" {
 	if network.DefaultACL == "" {
 		network.DefaultACL = "yes"
 		network.DefaultACL = "yes"
+		upsert = true
 	}
 	}
+	return
 }
 }
 
 
 func (network *Network) GetNetworkNetworkCIDR4() *net.IPNet {
 func (network *Network) GetNetworkNetworkCIDR4() *net.IPNet {

+ 13 - 9
models/node.go

@@ -5,6 +5,7 @@ import (
 	"math/rand"
 	"math/rand"
 	"net"
 	"net"
 	"strings"
 	"strings"
+	"sync"
 	"time"
 	"time"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
@@ -14,11 +15,12 @@ import (
 type NodeStatus string
 type NodeStatus string
 
 
 const (
 const (
-	OnlineSt  NodeStatus = "online"
-	OfflineSt NodeStatus = "offline"
-	WarningSt NodeStatus = "warning"
-	ErrorSt   NodeStatus = "error"
-	UnKnown   NodeStatus = "unknown"
+	OnlineSt     NodeStatus = "online"
+	OfflineSt    NodeStatus = "offline"
+	WarningSt    NodeStatus = "warning"
+	ErrorSt      NodeStatus = "error"
+	UnKnown      NodeStatus = "unknown"
+	Disconnected NodeStatus = "disconnected"
 )
 )
 
 
 // LastCheckInThreshold - if node's checkin more than this threshold,then node is declared as offline
 // LastCheckInThreshold - if node's checkin more than this threshold,then node is declared as offline
@@ -77,11 +79,12 @@ type CommonNode struct {
 	Action              string    `json:"action"              yaml:"action"`
 	Action              string    `json:"action"              yaml:"action"`
 	LocalAddress        net.IPNet `json:"localaddress"        yaml:"localaddress"`
 	LocalAddress        net.IPNet `json:"localaddress"        yaml:"localaddress"`
 	IsEgressGateway     bool      `json:"isegressgateway"     yaml:"isegressgateway"`
 	IsEgressGateway     bool      `json:"isegressgateway"     yaml:"isegressgateway"`
-	EgressGatewayRanges []string  `json:"egressgatewayranges" yaml:"egressgatewayranges"                                 bson:"egressgatewayranges"`
+	EgressGatewayRanges []string  `json:"egressgatewayranges" yaml:"egressgatewayranges"`
 	IsIngressGateway    bool      `json:"isingressgateway"    yaml:"isingressgateway"`
 	IsIngressGateway    bool      `json:"isingressgateway"    yaml:"isingressgateway"`
-	IsRelayed           bool      `json:"isrelayed"           yaml:"isrelayed"                                           bson:"isrelayed"`
-	RelayedBy           string    `json:"relayedby"           yaml:"relayedby"                                           bson:"relayedby"`
-	IsRelay             bool      `json:"isrelay"             yaml:"isrelay"                                             bson:"isrelay"`
+	IsRelayed           bool      `json:"isrelayed"           yaml:"isrelayed"`
+	RelayedBy           string    `json:"relayedby"           yaml:"relayedby"`
+	IsRelay             bool      `json:"isrelay"             yaml:"isrelay"`
+	IsGw                bool      `json:"is_gw"             yaml:"is_gw"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
 	DNSOn               bool      `json:"dnson"               yaml:"dnson"`
 	DNSOn               bool      `json:"dnson"               yaml:"dnson"`
@@ -117,6 +120,7 @@ type Node struct {
 	IsUserNode        bool                `json:"is_user_node"`
 	IsUserNode        bool                `json:"is_user_node"`
 	StaticNode        ExtClient           `json:"static_node"`
 	StaticNode        ExtClient           `json:"static_node"`
 	Status            NodeStatus          `json:"node_status"`
 	Status            NodeStatus          `json:"node_status"`
+	Mutex             *sync.Mutex         `json:"-"`
 }
 }
 
 
 // LegacyNode - legacy struct for node model
 // LegacyNode - legacy struct for node model

+ 42 - 33
models/structs.go

@@ -32,17 +32,18 @@ type IngressGwUsers struct {
 
 
 // UserRemoteGws - struct to hold user's remote gws
 // UserRemoteGws - struct to hold user's remote gws
 type UserRemoteGws struct {
 type UserRemoteGws struct {
-	GwID              string    `json:"remote_access_gw_id"`
-	GWName            string    `json:"gw_name"`
-	Network           string    `json:"network"`
-	Connected         bool      `json:"connected"`
-	IsInternetGateway bool      `json:"is_internet_gateway"`
-	GwClient          ExtClient `json:"gw_client"`
-	GwPeerPublicKey   string    `json:"gw_peer_public_key"`
-	GwListenPort      int       `json:"gw_listen_port"`
-	Metadata          string    `json:"metadata"`
-	AllowedEndpoints  []string  `json:"allowed_endpoints"`
-	NetworkAddresses  []string  `json:"network_addresses"`
+	GwID              string     `json:"remote_access_gw_id"`
+	GWName            string     `json:"gw_name"`
+	Network           string     `json:"network"`
+	Connected         bool       `json:"connected"`
+	IsInternetGateway bool       `json:"is_internet_gateway"`
+	GwClient          ExtClient  `json:"gw_client"`
+	GwPeerPublicKey   string     `json:"gw_peer_public_key"`
+	GwListenPort      int        `json:"gw_listen_port"`
+	Metadata          string     `json:"metadata"`
+	AllowedEndpoints  []string   `json:"allowed_endpoints"`
+	NetworkAddresses  []string   `json:"network_addresses"`
+	Status            NodeStatus `json:"status"`
 }
 }
 
 
 // UserRAGs - struct for user access gws
 // UserRAGs - struct for user access gws
@@ -150,12 +151,18 @@ type ExtPeersResponse struct {
 	KeepAlive       int32  `json:"persistentkeepalive" bson:"persistentkeepalive"`
 	KeepAlive       int32  `json:"persistentkeepalive" bson:"persistentkeepalive"`
 }
 }
 
 
+type EgressRangeMetric struct {
+	Network     string `json:"network"`
+	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
+}
+
 // EgressGatewayRequest - egress gateway request
 // EgressGatewayRequest - egress gateway request
 type EgressGatewayRequest struct {
 type EgressGatewayRequest struct {
-	NodeID     string   `json:"nodeid" bson:"nodeid"`
-	NetID      string   `json:"netid" bson:"netid"`
-	NatEnabled string   `json:"natenabled" bson:"natenabled"`
-	Ranges     []string `json:"ranges" bson:"ranges"`
+	NodeID           string              `json:"nodeid" bson:"nodeid"`
+	NetID            string              `json:"netid" bson:"netid"`
+	NatEnabled       string              `json:"natenabled" bson:"natenabled"`
+	Ranges           []string            `json:"ranges" bson:"ranges"`
+	RangesWithMetric []EgressRangeMetric `json:"ranges_with_metric"`
 }
 }
 
 
 // RelayRequest - relay request struct
 // RelayRequest - relay request struct
@@ -252,24 +259,26 @@ type NodeJoinResponse struct {
 
 
 // ServerConfig - struct for dealing with the server information for a netclient
 // ServerConfig - struct for dealing with the server information for a netclient
 type ServerConfig struct {
 type ServerConfig struct {
-	CoreDNSAddr    string `yaml:"corednsaddr"`
-	API            string `yaml:"api"`
-	APIPort        string `yaml:"apiport"`
-	DNSMode        string `yaml:"dnsmode"`
-	Version        string `yaml:"version"`
-	MQPort         string `yaml:"mqport"`
-	MQUserName     string `yaml:"mq_username"`
-	MQPassword     string `yaml:"mq_password"`
-	BrokerType     string `yaml:"broker_type"`
-	Server         string `yaml:"server"`
-	Broker         string `yaml:"broker"`
-	IsPro          bool   `yaml:"isee" json:"Is_EE"`
-	TrafficKey     []byte `yaml:"traffickey"`
-	MetricInterval string `yaml:"metric_interval"`
-	ManageDNS      bool   `yaml:"manage_dns"`
-	Stun           bool   `yaml:"stun"`
-	StunServers    string `yaml:"stun_servers"`
-	DefaultDomain  string `yaml:"default_domain"`
+	CoreDNSAddr       string `yaml:"corednsaddr"`
+	API               string `yaml:"api"`
+	APIPort           string `yaml:"apiport"`
+	DNSMode           string `yaml:"dnsmode"`
+	Version           string `yaml:"version"`
+	MQPort            string `yaml:"mqport"`
+	MQUserName        string `yaml:"mq_username"`
+	MQPassword        string `yaml:"mq_password"`
+	BrokerType        string `yaml:"broker_type"`
+	Server            string `yaml:"server"`
+	Broker            string `yaml:"broker"`
+	IsPro             bool   `yaml:"isee" json:"Is_EE"`
+	TrafficKey        []byte `yaml:"traffickey"`
+	MetricInterval    string `yaml:"metric_interval"`
+	MetricsPort       int    `yaml:"metrics_port"`
+	ManageDNS         bool   `yaml:"manage_dns"`
+	Stun              bool   `yaml:"stun"`
+	StunServers       string `yaml:"stun_servers"`
+	EndpointDetection bool   `yaml:"endpoint_detection"`
+	DefaultDomain     string `yaml:"default_domain"`
 }
 }
 
 
 // User.NameInCharset - returns if name is in charset below or not
 // User.NameInCharset - returns if name is in charset below or not

+ 1 - 0
models/user_mgmt.go

@@ -159,6 +159,7 @@ type User struct {
 type ReturnUserWithRolesAndGroups struct {
 type ReturnUserWithRolesAndGroups struct {
 	ReturnUser
 	ReturnUser
 	PlatformRole UserRolePermissionTemplate `json:"platform_role"`
 	PlatformRole UserRolePermissionTemplate `json:"platform_role"`
+	UserGroups   map[UserGroupID]UserGroup  `json:"user_group_ids"`
 }
 }
 
 
 // ReturnUser - return user struct
 // ReturnUser - return user struct

+ 1 - 0
mq/handlers.go

@@ -280,6 +280,7 @@ func HandleHostCheckin(h, currentHost *models.Host) bool {
 		(h.ListenPort != 0 && h.ListenPort != currentHost.ListenPort) ||
 		(h.ListenPort != 0 && h.ListenPort != currentHost.ListenPort) ||
 		(h.WgPublicListenPort != 0 && h.WgPublicListenPort != currentHost.WgPublicListenPort) || (!h.EndpointIPv6.Equal(currentHost.EndpointIPv6))
 		(h.WgPublicListenPort != 0 && h.WgPublicListenPort != currentHost.WgPublicListenPort) || (!h.EndpointIPv6.Equal(currentHost.EndpointIPv6))
 	if ifaceDelta { // only save if something changes
 	if ifaceDelta { // only save if something changes
+
 		currentHost.EndpointIP = h.EndpointIP
 		currentHost.EndpointIP = h.EndpointIP
 		currentHost.EndpointIPv6 = h.EndpointIPv6
 		currentHost.EndpointIPv6 = h.EndpointIPv6
 		currentHost.Interfaces = h.Interfaces
 		currentHost.Interfaces = h.Interfaces

+ 5 - 1
mq/publishers.go

@@ -17,7 +17,6 @@ import (
 
 
 // PublishPeerUpdate --- determines and publishes a peer update to all the hosts
 // PublishPeerUpdate --- determines and publishes a peer update to all the hosts
 func PublishPeerUpdate(replacePeers bool) error {
 func PublishPeerUpdate(replacePeers bool) error {
-
 	if !servercfg.IsMessageQueueBackend() {
 	if !servercfg.IsMessageQueueBackend() {
 		return nil
 		return nil
 	}
 	}
@@ -114,6 +113,11 @@ func PublishSingleHostPeerUpdate(host *models.Host, allNodes []models.Node, dele
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	peerUpdate.OldPeerUpdateFields = models.OldPeerUpdateFields{
+		NodePeers:         peerUpdate.NodePeers,
+		OldPeers:          peerUpdate.Peers,
+		EndpointDetection: peerUpdate.ServerConfig.EndpointDetection,
+	}
 	peerUpdate.ReplacePeers = replacePeers
 	peerUpdate.ReplacePeers = replacePeers
 	data, err := json.Marshal(&peerUpdate)
 	data, err := json.Marshal(&peerUpdate)
 	if err != nil {
 	if err != nil {

+ 6 - 0
pro/auth/auth.go

@@ -245,6 +245,12 @@ func getUserEmailFromClaims(token string) string {
 		return ""
 		return ""
 	}
 	}
 	claims, _ := accessToken.Claims.(jwt.MapClaims)
 	claims, _ := accessToken.Claims.(jwt.MapClaims)
+	if claims == nil {
+		return ""
+	}
+	if claims["email"] == nil {
+		return ""
+	}
 	return claims["email"].(string)
 	return claims["email"].(string)
 }
 }
 
 

+ 4 - 0
pro/auth/azure-ad.go

@@ -199,6 +199,10 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	if userInfo.Email == "" {
 	if userInfo.Email == "" {
 		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
 		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
 	}
 	}
+	if userInfo.Email == "" && userInfo.UserPrincipalName != "" {
+		userInfo.Email = userInfo.UserPrincipalName
+
+	}
 	if userInfo.Email == "" {
 	if userInfo.Email == "" {
 		err = errors.New("failed to fetch user email from SSO state")
 		err = errors.New("failed to fetch user email from SSO state")
 		return userInfo, err
 		return userInfo, err

+ 137 - 4
pro/controllers/failover.go

@@ -19,7 +19,7 @@ import (
 
 
 // FailOverHandlers - handlers for FailOver
 // FailOverHandlers - handlers for FailOver
 func FailOverHandlers(r *mux.Router) {
 func FailOverHandlers(r *mux.Router) {
-	r.HandleFunc("/api/v1/node/{nodeid}/failover", http.HandlerFunc(getfailOver)).
+	r.HandleFunc("/api/v1/node/{nodeid}/failover", controller.Authorize(true, false, "host", http.HandlerFunc(getfailOver))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(createfailOver))).
 	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(createfailOver))).
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
@@ -29,6 +29,8 @@ func FailOverHandlers(r *mux.Router) {
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/node/{nodeid}/failover_me", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).
 	r.HandleFunc("/api/v1/node/{nodeid}/failover_me", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).
 		Methods(http.MethodPost)
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/failover_check", controller.Authorize(true, false, "host", http.HandlerFunc(checkfailOverCtx))).
+		Methods(http.MethodGet)
 }
 }
 
 
 // @Summary     Get failover node
 // @Summary     Get failover node
@@ -44,7 +46,6 @@ func getfailOver(w http.ResponseWriter, r *http.Request) {
 	// confirm host exists
 	// confirm host exists
 	node, err := logic.GetNodeByID(nodeid)
 	node, err := logic.GetNodeByID(nodeid)
 	if err != nil {
 	if err != nil {
-		slog.Error("failed to get node:", "node", nodeid, "error", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
@@ -140,6 +141,7 @@ func deletefailOver(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	proLogic.RemoveFailOverFromCache(node.Network)
 	go func() {
 	go func() {
 		proLogic.ResetFailOver(&node)
 		proLogic.ResetFailOver(&node)
 		mq.PublishPeerUpdate(false)
 		mq.PublishPeerUpdate(false)
@@ -265,10 +267,9 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
-
 	err = proLogic.SetFailOverCtx(failOverNode, node, peerNode)
 	err = proLogic.SetFailOverCtx(failOverNode, node, peerNode)
 	if err != nil {
 	if err != nil {
-		slog.Error("failed to create failover", "id", node.ID.String(),
+		slog.Debug("failed to create failover", "id", node.ID.String(),
 			"network", node.Network, "error", err)
 			"network", node.Network, "error", err)
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
@@ -293,3 +294,135 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
 	logic.ReturnSuccessResponse(w, r, "relayed successfully")
 	logic.ReturnSuccessResponse(w, r, "relayed successfully")
 }
 }
+
+// @Summary     checkfailOverCtx
+// @Router      /api/v1/node/{nodeid}/failover_check [get]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Accept      json
+// @Param       body body models.FailOverMeReq true "Failover request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func checkfailOverCtx(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	nodeid := params["nodeid"]
+	// confirm host exists
+	node, err := logic.GetNodeByID(nodeid)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get node:", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	failOverNode, exists := proLogic.FailOverExists(node.Network)
+	if !exists {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("req-from: %s, failover node doesn't exist in the network", host.Name),
+				"badrequest",
+			),
+		)
+		return
+	}
+	var failOverReq models.FailOverMeReq
+	err = json.NewDecoder(r.Body).Decode(&failOverReq)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	peerNode, err := logic.GetNodeByID(failOverReq.NodeID)
+	if err != nil {
+		slog.Error("peer not found: ", "nodeid", failOverReq.NodeID, "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer not found"), "badrequest"),
+		)
+		return
+	}
+	if peerNode.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as failover"), "badrequest"),
+		)
+		return
+	}
+	if node.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is acting as failover"), "badrequest"),
+		)
+		return
+	}
+	if peerNode.IsFailOver {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer is acting as failover"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelayed && node.RelayedBy == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is relayed by peer node"), "badrequest"),
+		)
+		return
+	}
+	if node.IsRelay && peerNode.RelayedBy == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node acting as relay for the peer node"), "badrequest"),
+		)
+		return
+	}
+	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node acting as internet gw for the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
+		return
+	}
+
+	err = proLogic.CheckFailOverCtx(failOverNode, node, peerNode)
+	if err != nil {
+		slog.Error("failover ctx cannot be set ", "error", err)
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("failover ctx cannot be set: %v", err), "internal"),
+		)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	logic.ReturnSuccessResponse(w, r, "failover can be set")
+}

+ 3 - 0
pro/controllers/inet_gws.go

@@ -84,6 +84,9 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 			}()
 			}()
 		}
 		}
 	}
 	}
+	if node.IsGw && node.IngressDNS == "" {
+		node.IngressDNS = "1.1.1.1"
+	}
 	err = logic.UpsertNode(&node)
 	err = logic.UpsertNode(&node)
 	if err != nil {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))

+ 0 - 152
pro/controllers/relay.go

@@ -1,152 +0,0 @@
-package controllers
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-
-	"github.com/google/uuid"
-	proLogic "github.com/gravitl/netmaker/pro/logic"
-
-	"github.com/gorilla/mux"
-	controller "github.com/gravitl/netmaker/controllers"
-	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/mq"
-)
-
-// RelayHandlers - handle Pro Relays
-func RelayHandlers(r *mux.Router) {
-
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", logic.SecurityCheck(true, http.HandlerFunc(createRelay))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", logic.SecurityCheck(true, http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).Methods(http.MethodPost)
-}
-
-// @Summary     Create a relay
-// @Router      /api/nodes/{network}/{nodeid}/createrelay [post]
-// @Tags        PRO
-// @Accept      json
-// @Produce     json
-// @Param       network path string true "Network ID"
-// @Param       nodeid path string true "Node ID"
-// @Param       body body models.RelayRequest true "Relay request parameters"
-// @Success     200 {object} models.ApiNode
-// @Failure     400 {object} models.ErrorResponse
-// @Failure     500 {object} models.ErrorResponse
-func createRelay(w http.ResponseWriter, r *http.Request) {
-	var relayRequest models.RelayRequest
-	var params = mux.Vars(r)
-	w.Header().Set("Content-Type", "application/json")
-	err := json.NewDecoder(r.Body).Decode(&relayRequest)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	relayRequest.NetID = params["network"]
-	relayRequest.NodeID = params["nodeid"]
-	_, relayNode, err := proLogic.CreateRelay(relayRequest)
-	if err != nil {
-		logger.Log(
-			0,
-			r.Header.Get("user"),
-			fmt.Sprintf(
-				"failed to create relay on node [%s] on network [%s]: %v",
-				relayRequest.NodeID,
-				relayRequest.NetID,
-				err,
-			),
-		)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	for _, relayedNodeID := range relayNode.RelayedNodes {
-		relayedNode, err := logic.GetNodeByID(relayedNodeID)
-		if err == nil {
-			if relayedNode.FailedOverBy != uuid.Nil {
-				go logic.ResetFailedOverPeer(&relayedNode)
-			}
-
-		}
-	}
-	go mq.PublishPeerUpdate(false)
-	logger.Log(
-		1,
-		r.Header.Get("user"),
-		"created relay on node",
-		relayRequest.NodeID,
-		"on network",
-		relayRequest.NetID,
-	)
-	apiNode := relayNode.ConvertToAPINode()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-}
-
-// @Summary     Remove a relay
-// @Router      /api/nodes/{network}/{nodeid}/deleterelay [delete]
-// @Tags        PRO
-// @Accept      json
-// @Produce     json
-// @Param       network path string true "Network ID"
-// @Param       nodeid path string true "Node ID"
-// @Success     200 {object} models.ApiNode
-// @Failure     400 {object} models.ErrorResponse
-// @Failure     500 {object} models.ErrorResponse
-func deleteRelay(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	nodeid := params["nodeid"]
-	netid := params["network"]
-	updateNodes, node, err := proLogic.DeleteRelay(netid, nodeid)
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
-	}
-	logger.Log(1, r.Header.Get("user"), "deleted relay server", nodeid, "on network", netid)
-	go func() {
-		for _, relayedNode := range updateNodes {
-			err = mq.NodeUpdate(&relayedNode)
-			if err != nil {
-				logger.Log(
-					1,
-					"relayed node update ",
-					relayedNode.ID.String(),
-					"on network",
-					relayedNode.Network,
-					": ",
-					err.Error(),
-				)
-
-			}
-			h, err := logic.GetHost(relayedNode.HostID.String())
-			if err == nil {
-				if h.OS == models.OS_Types.IoT {
-					nodes, err := logic.GetAllNodes()
-					if err != nil {
-						return
-					}
-					node.IsRelay = true // for iot update to recognise that it has to delete relay peer
-					if err = mq.PublishSingleHostPeerUpdate(h, nodes, &node, nil, false, nil); err != nil {
-						logger.Log(1, "failed to publish peer update to host", h.ID.String(), ": ", err.Error())
-					}
-				}
-			}
-		}
-		mq.PublishPeerUpdate(false)
-	}()
-	logger.Log(
-		1,
-		r.Header.Get("user"),
-		"deleted relay on node",
-		node.ID.String(),
-		"on network",
-		node.Network,
-	)
-	apiNode := node.ConvertToAPINode()
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(apiNode)
-}

+ 13 - 0
pro/controllers/users.go

@@ -1102,8 +1102,15 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				slog.Error("failed to get node network", "error", err)
 				slog.Error("failed to get node network", "error", err)
 				continue
 				continue
 			}
 			}
+			nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
+			if len(nodesWithStatus) > 0 {
+				node = nodesWithStatus[0]
+			}
 
 
 			gws := userGws[node.Network]
 			gws := userGws[node.Network]
+			if extClient.DNS == "" {
+				extClient.DNS = node.IngressDNS
+			}
 			extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
 			extClient.AllowedIPs = logic.GetExtclientAllowedIPs(extClient)
 			gws = append(gws, models.UserRemoteGws{
 			gws = append(gws, models.UserRemoteGws{
 				GwID:              node.ID.String(),
 				GwID:              node.ID.String(),
@@ -1117,6 +1124,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				Metadata:          node.Metadata,
 				Metadata:          node.Metadata,
 				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 				NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+				Status:            node.Status,
 			})
 			})
 			userGws[node.Network] = gws
 			userGws[node.Network] = gws
 			delete(userGwNodes, node.ID.String())
 			delete(userGwNodes, node.ID.String())
@@ -1138,6 +1146,10 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
+		nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
+		if len(nodesWithStatus) > 0 {
+			node = nodesWithStatus[0]
+		}
 		network, err := logic.GetNetwork(node.Network)
 		network, err := logic.GetNetwork(node.Network)
 		if err != nil {
 		if err != nil {
 			slog.Error("failed to get node network", "error", err)
 			slog.Error("failed to get node network", "error", err)
@@ -1154,6 +1166,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			Metadata:          node.Metadata,
 			Metadata:          node.Metadata,
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+			Status:            node.Status,
 		})
 		})
 		userGws[node.Network] = gws
 		userGws[node.Network] = gws
 	}
 	}

+ 1 - 1
pro/email/email.go

@@ -55,6 +55,6 @@ func GetClient() (e EmailSender) {
 }
 }
 
 
 func IsValid(email string) bool {
 func IsValid(email string) bool {
-	emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
+	emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
 	return emailRegex.MatchString(email)
 	return emailRegex.MatchString(email)
 }
 }

+ 2 - 8
pro/initialize.go

@@ -29,7 +29,6 @@ func InitPro() {
 	controller.HttpHandlers = append(
 	controller.HttpHandlers = append(
 		controller.HttpHandlers,
 		controller.HttpHandlers,
 		proControllers.MetricHandlers,
 		proControllers.MetricHandlers,
-		proControllers.RelayHandlers,
 		proControllers.UserHandlers,
 		proControllers.UserHandlers,
 		proControllers.FailOverHandlers,
 		proControllers.FailOverHandlers,
 		proControllers.InetHandlers,
 		proControllers.InetHandlers,
@@ -91,6 +90,7 @@ func InitPro() {
 			slog.Error("no OAuth provider found or not configured, continuing without OAuth")
 			slog.Error("no OAuth provider found or not configured, continuing without OAuth")
 		}
 		}
 		proLogic.LoadNodeMetricsToCache()
 		proLogic.LoadNodeMetricsToCache()
+		proLogic.InitFailOverCache()
 	})
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
@@ -106,13 +106,6 @@ func InitPro() {
 	logic.GetMetrics = proLogic.GetMetrics
 	logic.GetMetrics = proLogic.GetMetrics
 	logic.UpdateMetrics = proLogic.UpdateMetrics
 	logic.UpdateMetrics = proLogic.UpdateMetrics
 	logic.DeleteMetrics = proLogic.DeleteMetrics
 	logic.DeleteMetrics = proLogic.DeleteMetrics
-	logic.GetRelays = proLogic.GetRelays
-	logic.GetAllowedIpsForRelayed = proLogic.GetAllowedIpsForRelayed
-	logic.RelayedAllowedIPs = proLogic.RelayedAllowedIPs
-	logic.UpdateRelayed = proLogic.UpdateRelayed
-	logic.SetRelayedNodes = proLogic.SetRelayedNodes
-	logic.RelayUpdates = proLogic.RelayUpdates
-	logic.ValidateRelay = proLogic.ValidateRelay
 	logic.GetTrialEndDate = getTrialEndDate
 	logic.GetTrialEndDate = getTrialEndDate
 	logic.SetDefaultGw = proLogic.SetDefaultGw
 	logic.SetDefaultGw = proLogic.SetDefaultGw
 	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
 	logic.SetDefaultGwForRelayedUpdate = proLogic.SetDefaultGwForRelayedUpdate
@@ -140,6 +133,7 @@ func InitPro() {
 	logic.IntialiseGroups = proLogic.UserGroupsInit
 	logic.IntialiseGroups = proLogic.UserGroupsInit
 	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
 	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
 	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
 	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
+	logic.GetUserGroup = proLogic.GetUserGroup
 	logic.GetNodeStatus = proLogic.GetNodeStatus
 	logic.GetNodeStatus = proLogic.GetNodeStatus
 }
 }
 
 

+ 67 - 12
pro/logic/failover.go

@@ -13,7 +13,49 @@ import (
 )
 )
 
 
 var failOverCtxMutex = &sync.RWMutex{}
 var failOverCtxMutex = &sync.RWMutex{}
+var failOverCacheMutex = &sync.RWMutex{}
+var failOverCache = make(map[models.NetworkID]string)
 
 
+func InitFailOverCache() {
+	failOverCacheMutex.Lock()
+	defer failOverCacheMutex.Unlock()
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		return
+	}
+	allNodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+
+	for _, network := range networks {
+		networkNodes := logic.GetNetworkNodesMemory(allNodes, network.NetID)
+		for _, node := range networkNodes {
+			if node.IsFailOver {
+				failOverCache[models.NetworkID(network.NetID)] = node.ID.String()
+				break
+			}
+		}
+	}
+}
+
+func CheckFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
+	failOverCtxMutex.RLock()
+	defer failOverCtxMutex.RUnlock()
+	if peerNode.FailOverPeers == nil {
+		return nil
+	}
+	if victimNode.FailOverPeers == nil {
+		return nil
+	}
+	_, peerHasFailovered := peerNode.FailOverPeers[victimNode.ID.String()]
+	_, victimHasFailovered := victimNode.FailOverPeers[peerNode.ID.String()]
+	if peerHasFailovered && victimHasFailovered &&
+		victimNode.FailedOverBy == failOverNode.ID && peerNode.FailedOverBy == failOverNode.ID {
+		return errors.New("failover ctx is already set")
+	}
+	return nil
+}
 func SetFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
 func SetFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
 	failOverCtxMutex.Lock()
 	failOverCtxMutex.Lock()
 	defer failOverCtxMutex.Unlock()
 	defer failOverCtxMutex.Unlock()
@@ -23,13 +65,16 @@ func SetFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
 	if victimNode.FailOverPeers == nil {
 	if victimNode.FailOverPeers == nil {
 		victimNode.FailOverPeers = make(map[string]struct{})
 		victimNode.FailOverPeers = make(map[string]struct{})
 	}
 	}
+	_, peerHasFailovered := peerNode.FailOverPeers[victimNode.ID.String()]
+	_, victimHasFailovered := victimNode.FailOverPeers[peerNode.ID.String()]
+	if peerHasFailovered && victimHasFailovered &&
+		victimNode.FailedOverBy == failOverNode.ID && peerNode.FailedOverBy == failOverNode.ID {
+		return errors.New("failover ctx is already set")
+	}
 	peerNode.FailOverPeers[victimNode.ID.String()] = struct{}{}
 	peerNode.FailOverPeers[victimNode.ID.String()] = struct{}{}
 	victimNode.FailOverPeers[peerNode.ID.String()] = struct{}{}
 	victimNode.FailOverPeers[peerNode.ID.String()] = struct{}{}
 	victimNode.FailedOverBy = failOverNode.ID
 	victimNode.FailedOverBy = failOverNode.ID
 	peerNode.FailedOverBy = failOverNode.ID
 	peerNode.FailedOverBy = failOverNode.ID
-	if err := logic.UpsertNode(&failOverNode); err != nil {
-		return err
-	}
 	if err := logic.UpsertNode(&victimNode); err != nil {
 	if err := logic.UpsertNode(&victimNode); err != nil {
 		return err
 		return err
 	}
 	}
@@ -50,17 +95,26 @@ func GetFailOverNode(network string, allNodes []models.Node) (models.Node, error
 	return models.Node{}, errors.New("auto relay not found")
 	return models.Node{}, errors.New("auto relay not found")
 }
 }
 
 
+func RemoveFailOverFromCache(network string) {
+	failOverCacheMutex.Lock()
+	defer failOverCacheMutex.Unlock()
+	delete(failOverCache, models.NetworkID(network))
+}
+
+func SetFailOverInCache(node models.Node) {
+	failOverCacheMutex.Lock()
+	defer failOverCacheMutex.Unlock()
+	failOverCache[models.NetworkID(node.Network)] = node.ID.String()
+}
+
 // FailOverExists - checks if failOver exists already in the network
 // FailOverExists - checks if failOver exists already in the network
 func FailOverExists(network string) (failOverNode models.Node, exists bool) {
 func FailOverExists(network string) (failOverNode models.Node, exists bool) {
-	nodes, err := logic.GetNetworkNodes(network)
-	if err != nil {
-		return
-	}
-	for _, node := range nodes {
-		if node.IsFailOver {
-			exists = true
-			failOverNode = node
-			return
+	failOverCacheMutex.RLock()
+	defer failOverCacheMutex.RUnlock()
+	if nodeID, ok := failOverCache[models.NetworkID(network)]; ok {
+		failOverNode, err := logic.GetNodeByID(nodeID)
+		if err == nil {
+			return failOverNode, true
 		}
 		}
 	}
 	}
 	return
 	return
@@ -185,5 +239,6 @@ func CreateFailOver(node models.Node) error {
 		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
 		slog.Error("failed to upsert node", "node", node.ID.String(), "error", err)
 		return err
 		return err
 	}
 	}
+	SetFailOverInCache(node)
 	return nil
 	return nil
 }
 }

+ 0 - 255
pro/logic/relays.go

@@ -1,255 +0,0 @@
-package logic
-
-import (
-	"errors"
-	"fmt"
-	"net"
-
-	"github.com/google/uuid"
-	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/acls/nodeacls"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/mq"
-	"github.com/gravitl/netmaker/servercfg"
-	"golang.org/x/exp/slog"
-)
-
-// GetRelays - gets all the nodes that are relays
-func GetRelays() ([]models.Node, error) {
-	nodes, err := logic.GetAllNodes()
-	if err != nil {
-		return nil, err
-	}
-	relays := make([]models.Node, 0)
-	for _, node := range nodes {
-		if node.IsRelay {
-			relays = append(relays, node)
-		}
-	}
-	return relays, nil
-}
-
-// CreateRelay - creates a relay
-func CreateRelay(relay models.RelayRequest) ([]models.Node, models.Node, error) {
-	var returnnodes []models.Node
-
-	node, err := logic.GetNodeByID(relay.NodeID)
-	if err != nil {
-		return returnnodes, models.Node{}, err
-	}
-	host, err := logic.GetHost(node.HostID.String())
-	if err != nil {
-		return returnnodes, models.Node{}, err
-	}
-	if host.OS != "linux" {
-		return returnnodes, models.Node{}, fmt.Errorf("only linux machines can be relay nodes")
-	}
-	err = ValidateRelay(relay, false)
-	if err != nil {
-		return returnnodes, models.Node{}, err
-	}
-	node.IsRelay = true
-	node.RelayedNodes = relay.RelayedNodes
-	node.SetLastModified()
-	err = logic.UpsertNode(&node)
-	if err != nil {
-		return returnnodes, node, err
-	}
-	returnnodes = SetRelayedNodes(true, relay.NodeID, relay.RelayedNodes)
-	return returnnodes, node, nil
-}
-
-// SetRelayedNodes- sets and saves node as relayed
-func SetRelayedNodes(setRelayed bool, relay string, relayed []string) []models.Node {
-	var returnnodes []models.Node
-	for _, id := range relayed {
-		node, err := logic.GetNodeByID(id)
-		if err != nil {
-			logger.Log(0, "setRelayedNodes.GetNodebyID", err.Error())
-			continue
-		}
-		node.IsRelayed = setRelayed
-		if setRelayed {
-			node.RelayedBy = relay
-		} else {
-			node.RelayedBy = ""
-		}
-		node.SetLastModified()
-		if err := logic.UpsertNode(&node); err != nil {
-			logger.Log(0, "setRelayedNodes.Insert", err.Error())
-			continue
-		}
-		returnnodes = append(returnnodes, node)
-	}
-	return returnnodes
-}
-
-// func GetRelayedNodes(relayNode *models.Node) (models.Node, error) {
-//	var returnnodes []models.Node
-//	networkNodes, err := GetNetworkNodes(relayNode.Network)
-//	if err != nil {
-//		return returnnodes, err
-//	}
-//	for _, node := range networkNodes {
-//		for _, addr := range relayNode.RelayAddrs {
-//			if addr == node.Address.IP.String() || addr == node.Address6.IP.String() {
-//				returnnodes = append(returnnodes, node)
-//			}
-//		}
-//	}
-//	return returnnodes, nil
-// }
-
-// ValidateRelay - checks if relay is valid
-func ValidateRelay(relay models.RelayRequest, update bool) error {
-	var err error
-
-	node, err := logic.GetNodeByID(relay.NodeID)
-	if err != nil {
-		return err
-	}
-	if !update && node.IsRelay {
-		return errors.New("node is already acting as a relay")
-	}
-	for _, relayedNodeID := range relay.RelayedNodes {
-		relayedNode, err := logic.GetNodeByID(relayedNodeID)
-		if err != nil {
-			return err
-		}
-		if relayedNode.IsIngressGateway {
-			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
-		}
-		if relayedNode.IsInternetGateway {
-			return errors.New("cannot relay an internet gateway (" + relayedNodeID + ")")
-		}
-		if relayedNode.InternetGwID != "" && relayedNode.InternetGwID != relay.NodeID {
-			return errors.New("cannot relay an internet client (" + relayedNodeID + ")")
-		}
-		if relayedNode.IsFailOver {
-			return errors.New("cannot relay a failOver (" + relayedNodeID + ")")
-		}
-		if relayedNode.FailedOverBy != uuid.Nil {
-			ResetFailedOverPeer(&relayedNode)
-		}
-	}
-	return err
-}
-
-// UpdateRelayNodes - updates relay nodes
-func updateRelayNodes(relay string, oldNodes []string, newNodes []string) []models.Node {
-	_ = SetRelayedNodes(false, relay, oldNodes)
-	return SetRelayedNodes(true, relay, newNodes)
-}
-
-func RelayUpdates(currentNode, newNode *models.Node) bool {
-	relayUpdates := false
-	if servercfg.IsPro && newNode.IsRelay {
-		if len(newNode.RelayedNodes) != len(currentNode.RelayedNodes) {
-			relayUpdates = true
-		} else {
-			for i, node := range newNode.RelayedNodes {
-				if node != currentNode.RelayedNodes[i] {
-					relayUpdates = true
-				}
-			}
-		}
-	}
-	return relayUpdates
-}
-
-// UpdateRelayed - updates a relay's relayed nodes, and sends updates to the relayed nodes over MQ
-func UpdateRelayed(currentNode, newNode *models.Node) {
-	updatenodes := updateRelayNodes(currentNode.ID.String(), currentNode.RelayedNodes, newNode.RelayedNodes)
-	if len(updatenodes) > 0 {
-		for _, relayedNode := range updatenodes {
-			node := relayedNode
-			ResetFailedOverPeer(&node)
-			go func() {
-				if err := mq.NodeUpdate(&node); err != nil {
-					slog.Error("error publishing node update to node", "node", node.ID, "error", err)
-				}
-
-			}()
-		}
-	}
-}
-
-// DeleteRelay - deletes a relay
-func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
-	var returnnodes []models.Node
-	node, err := logic.GetNodeByID(nodeid)
-	if err != nil {
-		return returnnodes, models.Node{}, err
-	}
-	returnnodes = SetRelayedNodes(false, nodeid, node.RelayedNodes)
-	node.IsRelay = false
-	node.RelayedNodes = []string{}
-	node.SetLastModified()
-	if err = logic.UpsertNode(&node); err != nil {
-		return returnnodes, models.Node{}, err
-	}
-	return returnnodes, node, nil
-}
-
-func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
-	var allowedIPs = []net.IPNet{}
-	for _, relayedNodeID := range peer.RelayedNodes {
-		if node.ID.String() == relayedNodeID {
-			continue
-		}
-		relayedNode, err := logic.GetNodeByID(relayedNodeID)
-		if err != nil {
-			continue
-		}
-		allowed := getRelayedAddresses(relayedNodeID)
-		if relayedNode.IsEgressGateway {
-			allowed = append(allowed, logic.GetEgressIPs(&relayedNode)...)
-		}
-		allowedIPs = append(allowedIPs, allowed...)
-	}
-	return allowedIPs
-}
-
-// GetAllowedIpsForRelayed - returns the peerConfig for a node relayed by relay
-func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNet) {
-	if relayed.RelayedBy != relay.ID.String() {
-		logger.Log(0, "RelayedByRelay called with invalid parameters")
-		return
-	}
-	if relay.InternetGwID != "" {
-		return GetAllowedIpForInetNodeClient(relayed, relay)
-	}
-	peers, err := logic.GetNetworkNodes(relay.Network)
-	if err != nil {
-		logger.Log(0, "error getting network clients", err.Error())
-		return
-	}
-	for _, peer := range peers {
-		if peer.ID == relayed.ID || peer.ID == relay.ID {
-			continue
-		}
-		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
-			allowedIPs = append(allowedIPs, logic.GetAllowedIPs(relayed, &peer, nil)...)
-		}
-	}
-	return
-}
-
-func getRelayedAddresses(id string) []net.IPNet {
-	addrs := []net.IPNet{}
-	node, err := logic.GetNodeByID(id)
-	if err != nil {
-		logger.Log(0, "getRelayedAddresses: "+err.Error())
-		return addrs
-	}
-	if node.Address.IP != nil {
-		node.Address.Mask = net.CIDRMask(32, 32)
-		addrs = append(addrs, node.Address)
-	}
-	if node.Address6.IP != nil {
-		node.Address6.Mask = net.CIDRMask(128, 128)
-		addrs = append(addrs, node.Address6)
-	}
-	return addrs
-}

+ 53 - 17
pro/logic/status.go

@@ -17,6 +17,10 @@ func getNodeStatusOld(node *models.Node) {
 		node.Status = models.OnlineSt
 		node.Status = models.OnlineSt
 		return
 		return
 	}
 	}
+	if !node.Connected {
+		node.Status = models.Disconnected
+		return
+	}
 	if time.Since(node.LastCheckIn) > time.Minute*10 {
 	if time.Since(node.LastCheckIn) > time.Minute*10 {
 		node.Status = models.OfflineSt
 		node.Status = models.OfflineSt
 		return
 		return
@@ -26,21 +30,30 @@ func getNodeStatusOld(node *models.Node) {
 
 
 func GetNodeStatus(node *models.Node, defaultEnabledPolicy bool) {
 func GetNodeStatus(node *models.Node, defaultEnabledPolicy bool) {
 
 
-	if time.Since(node.LastCheckIn) > models.LastCheckInThreshold {
-		node.Status = models.OfflineSt
-		return
-	}
 	if node.IsStatic {
 	if node.IsStatic {
 		if !node.StaticNode.Enabled {
 		if !node.StaticNode.Enabled {
 			node.Status = models.OfflineSt
 			node.Status = models.OfflineSt
 			return
 			return
 		}
 		}
+		ingNode, err := logic.GetNodeByID(node.StaticNode.IngressGatewayID)
+		if err != nil {
+			node.Status = models.OfflineSt
+			return
+		}
+		if !defaultEnabledPolicy {
+			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, ingNode, false)
+			if !allowed {
+				node.Status = models.OnlineSt
+				return
+			}
+		}
 		// check extclient connection from metrics
 		// check extclient connection from metrics
 		ingressMetrics, err := GetMetrics(node.StaticNode.IngressGatewayID)
 		ingressMetrics, err := GetMetrics(node.StaticNode.IngressGatewayID)
 		if err != nil || ingressMetrics == nil || ingressMetrics.Connectivity == nil {
 		if err != nil || ingressMetrics == nil || ingressMetrics.Connectivity == nil {
 			node.Status = models.UnKnown
 			node.Status = models.UnKnown
 			return
 			return
 		}
 		}
+
 		if metric, ok := ingressMetrics.Connectivity[node.StaticNode.ClientID]; ok {
 		if metric, ok := ingressMetrics.Connectivity[node.StaticNode.ClientID]; ok {
 			if metric.Connected {
 			if metric.Connected {
 				node.Status = models.OnlineSt
 				node.Status = models.OnlineSt
@@ -50,9 +63,18 @@ func GetNodeStatus(node *models.Node, defaultEnabledPolicy bool) {
 				return
 				return
 			}
 			}
 		}
 		}
+
 		node.Status = models.UnKnown
 		node.Status = models.UnKnown
 		return
 		return
 	}
 	}
+	if !node.Connected {
+		node.Status = models.Disconnected
+		return
+	}
+	if time.Since(node.LastCheckIn) > models.LastCheckInThreshold {
+		node.Status = models.OfflineSt
+		return
+	}
 	host, err := logic.GetHost(node.HostID.String())
 	host, err := logic.GetHost(node.HostID.String())
 	if err != nil {
 	if err != nil {
 		node.Status = models.UnKnown
 		node.Status = models.UnKnown
@@ -71,11 +93,15 @@ func GetNodeStatus(node *models.Node, defaultEnabledPolicy bool) {
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
-	if metrics == nil || metrics.Connectivity == nil {
+	if metrics == nil || metrics.Connectivity == nil || len(metrics.Connectivity) == 0 {
 		if time.Since(node.LastCheckIn) < models.LastCheckInThreshold {
 		if time.Since(node.LastCheckIn) < models.LastCheckInThreshold {
 			node.Status = models.OnlineSt
 			node.Status = models.OnlineSt
 			return
 			return
 		}
 		}
+		if node.LastCheckIn.IsZero() {
+			node.Status = models.OfflineSt
+			return
+		}
 	}
 	}
 	// if node.IsFailOver {
 	// if node.IsFailOver {
 	// 	if time.Since(node.LastCheckIn) < models.LastCheckInThreshold {
 	// 	if time.Since(node.LastCheckIn) < models.LastCheckInThreshold {
@@ -133,9 +159,12 @@ func checkPeerStatus(node *models.Node, defaultAclPolicy bool) {
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
-		allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
-		if !defaultAclPolicy && !allowed {
-			continue
+
+		if !defaultAclPolicy {
+			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
+			if !allowed {
+				continue
+			}
 		}
 		}
 
 
 		if time.Since(peer.LastCheckIn) > models.LastCheckInThreshold {
 		if time.Since(peer.LastCheckIn) > models.LastCheckInThreshold {
@@ -154,7 +183,7 @@ func checkPeerStatus(node *models.Node, defaultAclPolicy bool) {
 		node.Status = models.OnlineSt
 		node.Status = models.OnlineSt
 		return
 		return
 	}
 	}
-	if peerNotConnectedCnt == len(metrics.Connectivity) {
+	if len(metrics.Connectivity) > 0 && peerNotConnectedCnt == len(metrics.Connectivity) {
 		node.Status = models.ErrorSt
 		node.Status = models.ErrorSt
 		return
 		return
 	}
 	}
@@ -168,9 +197,12 @@ func checkPeerConnectivity(node *models.Node, metrics *models.Metrics, defaultAc
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
-		allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
-		if !defaultAclPolicy && !allowed {
-			continue
+
+		if !defaultAclPolicy {
+			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
+			if !allowed {
+				continue
+			}
 		}
 		}
 
 
 		if time.Since(peer.LastCheckIn) > models.LastCheckInThreshold {
 		if time.Since(peer.LastCheckIn) > models.LastCheckInThreshold {
@@ -181,19 +213,23 @@ func checkPeerConnectivity(node *models.Node, metrics *models.Metrics, defaultAc
 		}
 		}
 		// check if peer is in error state
 		// check if peer is in error state
 		checkPeerStatus(&peer, defaultAclPolicy)
 		checkPeerStatus(&peer, defaultAclPolicy)
-		if peer.Status == models.ErrorSt {
+		if peer.Status == models.ErrorSt || peer.Status == models.WarningSt {
 			continue
 			continue
 		}
 		}
 		peerNotConnectedCnt++
 		peerNotConnectedCnt++
 
 
 	}
 	}
-	if peerNotConnectedCnt == 0 {
-		node.Status = models.OnlineSt
+
+	if peerNotConnectedCnt > len(metrics.Connectivity)/2 {
+		node.Status = models.WarningSt
 		return
 		return
 	}
 	}
-	if peerNotConnectedCnt == len(metrics.Connectivity) {
+
+	if len(metrics.Connectivity) > 0 && peerNotConnectedCnt == len(metrics.Connectivity) {
 		node.Status = models.ErrorSt
 		node.Status = models.ErrorSt
 		return
 		return
 	}
 	}
-	node.Status = models.WarningSt
+
+	node.Status = models.OnlineSt
+
 }
 }

+ 6 - 4
pro/logic/user_mgmt.go

@@ -216,8 +216,9 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 
 
 	// create default network groups
 	// create default network groups
 	var NetworkAdminGroup = models.UserGroup{
 	var NetworkAdminGroup = models.UserGroup{
-		ID:   models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
-		Name: fmt.Sprintf("%s Admin Group", netID),
+		ID:      models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
+		Name:    fmt.Sprintf("%s Admin Group", netID),
+		Default: true,
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 			netID: {
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
@@ -226,8 +227,9 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 		MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID),
 		MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID),
 	}
 	}
 	var NetworkUserGroup = models.UserGroup{
 	var NetworkUserGroup = models.UserGroup{
-		ID:   models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
-		Name: fmt.Sprintf("%s User Group", netID),
+		ID:      models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
+		Name:    fmt.Sprintf("%s User Group", netID),
+		Default: true,
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
 			netID: {
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},

+ 4 - 0
scripts/netmaker.default.env

@@ -96,3 +96,7 @@ MANAGE_DNS=false
 OLD_ACL_SUPPORT=true
 OLD_ACL_SUPPORT=true
 # if STUN is set to true, hole punch is called
 # if STUN is set to true, hole punch is called
 STUN=true
 STUN=true
+# Metrics Collection Port
+METRICS_PORT=51821
+# Metrics Collection interval in minutes
+PUBLISH_METRIC_INTERVAL=15

+ 6 - 49
scripts/nm-quick.sh

@@ -6,7 +6,7 @@ SCRIPT_DIR=$(dirname "$(realpath "$0")")
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
 CONFIG_PATH="$SCRIPT_DIR/$CONFIG_FILE"
 NM_QUICK_VERSION="0.1.1"
 NM_QUICK_VERSION="0.1.1"
 LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\")
 LATEST=$(curl -s https://api.github.com/repos/gravitl/netmaker/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d [:space:],\")
-
+BRANCH=master
 if [ $(id -u) -ne 0 ]; then
 if [ $(id -u) -ne 0 ]; then
 	echo "This script must be run as root"
 	echo "This script must be run as root"
 	exit 1
 	exit 1
@@ -167,19 +167,11 @@ configure_netclient() {
 	nmctl host update $HOST_ID --default
 	nmctl host update $HOST_ID --default
 	sleep 5
 	sleep 5
 	nmctl node create_remote_access_gateway netmaker $NODE_ID
 	nmctl node create_remote_access_gateway netmaker $NODE_ID
-	
 	sleep 2
 	sleep 2
-	# create network for internet access vpn
+	# set failover
 	if [ "$INSTALL_TYPE" = "pro" ]; then
 	if [ "$INSTALL_TYPE" = "pro" ]; then
 	    #setup failOver
 	    #setup failOver
 		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
 		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/v1/node/${NODE_ID}/failover" --header "Authorization: Bearer ${MASTER_KEY}"
-		INET_NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r '."internet-access-vpn".id')
-		nmctl node create_remote_access_gateway internet-access-vpn $INET_NODE_ID
-		out=$(nmctl node list -o json | jq -r '.[] | select(.id=='\"$INET_NODE_ID\"') | .ingressdns = "8.8.8.8"')
-		curl --location --request PUT "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}" --data "$out" --header "Authorization: Bearer ${MASTER_KEY}"
-		out=$(nmctl node list -o json | jq -r '.[] | select(.id=='\"$INET_NODE_ID\"') | .metadata = "This host can be used for secure internet access"')
-		curl --location --request PUT "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}" --data "$out" --header "Authorization: Bearer ${MASTER_KEY}"
-		curl --location --request POST "https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/internet-access-vpn/${INET_NODE_ID}/inet_gw" --data '{}' --header "Authorization: Bearer ${MASTER_KEY}"
 	fi
 	fi
 	set -e
 	set -e
 }
 }
@@ -625,8 +617,7 @@ install_netmaker() {
 
 
 	echo "Pulling config files..."
 	echo "Pulling config files..."
 
 
-	
-	local BASE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG"
+	local BASE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BRANCH"
 	local COMPOSE_URL="$BASE_URL/compose/docker-compose.yml"
 	local COMPOSE_URL="$BASE_URL/compose/docker-compose.yml"
 	local CADDY_URL="$BASE_URL/docker/Caddyfile"
 	local CADDY_URL="$BASE_URL/docker/Caddyfile"
 	if [ "$INSTALL_TYPE" = "pro" ]; then
 	if [ "$INSTALL_TYPE" = "pro" ]; then
@@ -705,7 +696,6 @@ setup_mesh() {
 	networks=$(nmctl network list -o json)
 	networks=$(nmctl network list -o json)
 	if [[ ${networks} != "null" ]]; then
 	if [[ ${networks} != "null" ]]; then
 		netmakerNet=$(nmctl network list -o json | jq -r '.[] | .netid' | grep -w "netmaker")
 		netmakerNet=$(nmctl network list -o json | jq -r '.[] | .netid' | grep -w "netmaker")
-		inetNet=$(nmctl network list -o json | jq -r '.[] | .netid' | grep -w "internet-access-vpn")
 	fi
 	fi
 	# create netmaker network
 	# create netmaker network
 	if [[ ${netmakerNet} = "" ]]; then
 	if [[ ${netmakerNet} = "" ]]; then
@@ -718,43 +708,10 @@ setup_mesh() {
 	if [[ ${netmakerTag} = "" ]]; then
 	if [[ ${netmakerTag} = "" ]]; then
 		nmctl enrollment_key create --tags netmaker --unlimited --networks netmaker
 		nmctl enrollment_key create --tags netmaker --unlimited --networks netmaker
 	fi
 	fi
-
-	# create internet-access-vpn
-	if [ "$INSTALL_TYPE" = "pro" ]; then
-		if [[ ${inetNet} = "" ]]; then
-			echo "Creating internet-access-vpn network (100.65.0.0/16)"
-			# TODO causes "Error Status: 400 Response: {"Code":400,"Message":"could not find any records"}"
-			nmctl network create --name internet-access-vpn --ipv4_addr 100.65.0.0/16
-		fi
-
-		# create enrollment key for internet-access-vpn network
-		local inetTag=$(nmctl enrollment_key list | jq -r '.[] | .tags[0]' | grep -w "internet-access-vpn")
-		if [[ ${inetTag} = "" ]]; then
-			nmctl enrollment_key create --tags internet-access-vpn --unlimited --networks internet-access-vpn
-		fi
-
-		# create enrollment key for both networks
-		local netInetTag=$(nmctl enrollment_key list | jq -r '.[] | .tags[0]' | grep -w "netmaker-inet")
-		if [[ ${netInetTag} = "" ]]; then
-			nmctl enrollment_key create --tags netmaker-inet --unlimited --networks netmaker,internet-access-vpn
-		fi
-	fi
-
-	if [ "$INSTALL_TYPE" = "pro" ]; then
-		# create enrollment key for both setup networks
-		echo "Obtaining enrollment key..."
-		# key exists already, fetch token
-		TOKEN=$(nmctl enrollment_key list | jq -r '.[] | select(.tags[0]=="netmaker-inet") | .token')
-		
-	else
-
-		echo "Obtaining enrollment key..."
-		# key exists already, fetch token
-		TOKEN=$(nmctl enrollment_key list | jq -r '.[] | select(.tags[0]=="netmaker") | .token')
-	fi
-	
+	echo "Obtaining enrollment key..."
+	# key exists already, fetch token
+	TOKEN=$(nmctl enrollment_key list | jq -r '.[] | select(.tags[0]=="netmaker") | .token')
 	wait_seconds 3
 	wait_seconds 3
-
 }
 }
 
 
 # print_success - prints a success message upon completion
 # print_success - prints a success message upon completion

+ 33 - 1
servercfg/serverconf.go

@@ -14,6 +14,8 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 )
 )
 
 
+var ServerInfo = GetServerInfo()
+
 // EmqxBrokerType denotes the broker type for EMQX MQTT
 // EmqxBrokerType denotes the broker type for EMQX MQTT
 const EmqxBrokerType = "emqx"
 const EmqxBrokerType = "emqx"
 
 
@@ -141,10 +143,12 @@ func GetServerInfo() models.ServerConfig {
 	cfg.Version = GetVersion()
 	cfg.Version = GetVersion()
 	cfg.IsPro = IsPro
 	cfg.IsPro = IsPro
 	cfg.MetricInterval = GetMetricInterval()
 	cfg.MetricInterval = GetMetricInterval()
+	cfg.MetricsPort = GetMetricsPort()
 	cfg.ManageDNS = GetManageDNS()
 	cfg.ManageDNS = GetManageDNS()
 	cfg.Stun = IsStunEnabled()
 	cfg.Stun = IsStunEnabled()
 	cfg.StunServers = GetStunServers()
 	cfg.StunServers = GetStunServers()
 	cfg.DefaultDomain = GetDefaultDomain()
 	cfg.DefaultDomain = GetDefaultDomain()
+	cfg.EndpointDetection = IsEndpointDetectionEnabled()
 	return cfg
 	return cfg
 }
 }
 
 
@@ -509,7 +513,7 @@ func GetPublicIP() (string, error) {
 	endpoint := ""
 	endpoint := ""
 	var err error
 	var err error
 
 
-	iplist := []string{"https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
+	iplist := []string{"https://ifconfig.me/ip", "https://api.ipify.org", "https://ipinfo.io/ip"}
 	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
 	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
 	if publicIpService != "" {
 	if publicIpService != "" {
 		// prepend the user-specified service so it's checked first
 		// prepend the user-specified service so it's checked first
@@ -654,6 +658,34 @@ func GetMqUserName() string {
 	return password
 	return password
 }
 }
 
 
+// GetMetricsPort - get metrics port
+func GetMetricsPort() int {
+	p := 51821
+	if os.Getenv("METRICS_PORT") != "" {
+		pStr := os.Getenv("METRICS_PORT")
+		pInt, err := strconv.Atoi(pStr)
+		if err == nil && pInt != 0 {
+			p = pInt
+		}
+	}
+	return p
+}
+
+// GetMetricInterval - get the publish metric interval
+func GetMetricIntervalInMinutes() time.Duration {
+	//default 15 minutes
+	mi := "15"
+	if os.Getenv("PUBLISH_METRIC_INTERVAL") != "" {
+		mi = os.Getenv("PUBLISH_METRIC_INTERVAL")
+	}
+	interval, err := strconv.Atoi(mi)
+	if err != nil {
+		interval = 15
+	}
+
+	return time.Duration(interval) * time.Minute
+}
+
 // GetMetricInterval - get the publish metric interval
 // GetMetricInterval - get the publish metric interval
 func GetMetricInterval() string {
 func GetMetricInterval() string {
 	//default 15 minutes
 	//default 15 minutes

+ 2 - 26
serverctl/serverctl.go

@@ -59,32 +59,8 @@ func setNetworkDefaults() error {
 		return err
 		return err
 	}
 	}
 	for _, network := range networks {
 	for _, network := range networks {
-		update := false
-		newNet := network
-		if strings.Contains(network.NetID, ".") {
-			newNet.NetID = strings.ReplaceAll(network.NetID, ".", "")
-			newNet.DefaultInterface = strings.ReplaceAll(network.DefaultInterface, ".", "")
-			update = true
-		}
-		if strings.ContainsAny(network.NetID, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
-			newNet.NetID = strings.ToLower(network.NetID)
-			newNet.DefaultInterface = strings.ToLower(network.DefaultInterface)
-			update = true
-		}
-		if update {
-			newNet.SetDefaults()
-			if err := logic.SaveNetwork(&newNet); err != nil {
-				logger.Log(0, "error saving networks during initial update:", err.Error())
-			}
-			if err := logic.DeleteNetwork(network.NetID); err != nil {
-				logger.Log(0, "error deleting old network:", err.Error())
-			}
-		} else {
-			network.SetDefaults()
-			_, _, _, err = logic.UpdateNetwork(&network, &network)
-			if err != nil {
-				logger.Log(0, "could not set defaults on network", network.NetID)
-			}
+		if network.SetDefaults() {
+			logic.SaveNetwork(&network)
 		}
 		}
 	}
 	}
 	return nil
 	return nil

+ 21 - 1
utils/utils.go

@@ -1,6 +1,10 @@
 package utils
 package utils
 
 
-import "time"
+import (
+	"log/slog"
+	"runtime"
+	"time"
+)
 
 
 // RetryStrategy specifies a strategy to retry an operation after waiting a while,
 // RetryStrategy specifies a strategy to retry an operation after waiting a while,
 // with hooks for successful and unsuccessful (>=max) tries.
 // with hooks for successful and unsuccessful (>=max) tries.
@@ -39,3 +43,19 @@ func (rs RetryStrategy) DoStrategy() {
 		return
 		return
 	}
 	}
 }
 }
+
+func TraceCaller() {
+	// Skip 1 frame to get the caller of this function
+	pc, file, line, ok := runtime.Caller(2)
+	if !ok {
+		slog.Debug("Unable to get caller information")
+		return
+	}
+
+	// Get function name from the program counter (pc)
+	funcName := runtime.FuncForPC(pc).Name()
+
+	// Print trace details
+	slog.Debug("Called from function: %s\n", "func-name", funcName)
+	slog.Debug("File: %s, Line: %d\n", "file", file, "line-no", line)
+}