abhishek9686 5 месяцев назад
Родитель
Сommit
137303a687
65 измененных файлов с 2036 добавлено и 777 удалено
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      README.md
  3. 11 0
      cli/cmd/ext_client/config.go
  4. 55 0
      cli/cmd/gateway/create.go
  5. 27 0
      cli/cmd/gateway/delete.go
  6. 18 0
      cli/cmd/gateway/root.go
  7. 6 5
      cli/cmd/node/create_ingress.go
  8. 5 4
      cli/cmd/node/create_relay.go
  9. 6 5
      cli/cmd/node/delete_ingress.go
  10. 5 4
      cli/cmd/node/delete_relay.go
  11. 2 0
      cli/cmd/root.go
  12. 5 0
      cli/functions/ext_client.go
  13. 18 0
      cli/functions/gateway.go
  14. 1 1
      compose/docker-compose.netclient.yml
  15. 1 0
      config/config.go
  16. 49 8
      controllers/acls.go
  17. 284 4
      controllers/ext_client.go
  18. 5 53
      controllers/hosts.go
  19. 33 2
      controllers/network.go
  20. 51 2
      controllers/node.go
  21. 4 0
      controllers/node_test.go
  22. 9 0
      controllers/tags.go
  23. 9 0
      controllers/user.go
  24. 1 1
      k8s/client/netclient-daemonset.yaml
  25. 1 1
      k8s/client/netclient.yaml
  26. 1 1
      k8s/server/netmaker-ui.yaml
  27. 543 173
      logic/acls.go
  28. 1 0
      logic/auth.go
  29. 57 1
      logic/enrollmentkey.go
  30. 269 225
      logic/extpeers.go
  31. 35 2
      logic/gateway.go
  32. 1 1
      logic/hosts.go
  33. 44 3
      logic/nodes.go
  34. 108 66
      logic/peers.go
  35. 2 2
      logic/status.go
  36. 18 4
      logic/tags.go
  37. 1 0
      logic/user_mgmt.go
  38. 2 2
      main.go
  39. 42 10
      migrate/migrate.go
  40. 6 3
      models/acl.go
  41. 51 29
      models/api_node.go
  42. 4 1
      models/extclient.go
  43. 10 6
      models/mqtt.go
  44. 17 16
      models/network.go
  45. 24 15
      models/structs.go
  46. 5 1
      models/tags.go
  47. 1 0
      models/user_mgmt.go
  48. 1 1
      pro/LICENSE
  49. 21 2
      pro/auth/azure-ad.go
  50. 10 1
      pro/auth/error.go
  51. 8 0
      pro/auth/github.go
  52. 13 2
      pro/auth/google.go
  53. 12 2
      pro/auth/oidc.go
  54. 57 0
      pro/controllers/users.go
  55. 3 2
      pro/email/invite.go
  56. 1 0
      pro/initialize.go
  57. 3 3
      pro/logic/status.go
  58. 17 95
      pro/logic/user_mgmt.go
  59. 11 12
      release.md
  60. 3 1
      scripts/netmaker.default.env
  61. 2 2
      scripts/nm-quick.sh
  62. 1 1
      scripts/nm-upgrade.sh
  63. 6 0
      servercfg/serverconf.go
  64. 1 1
      swagger.yaml
  65. 16 0
      utils/utils.go

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

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

+ 1 - 1
README.md

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

+ 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() {
 	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{
-	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) {
 		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{
-	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) {
 		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{
-	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) {
 		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{
-	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) {
 		functions.PrettyPrint(functions.DeleteRelay(args[0], args[1]))
 	},

+ 2 - 0
cli/cmd/root.go

@@ -1,6 +1,7 @@
 package cmd
 
 import (
+	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"os"
 
 	"github.com/gravitl/netmaker/cli/cmd/acl"
@@ -55,4 +56,5 @@ func init() {
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(enrollment_key.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))
 }
 
+// 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
 func CreateExtClient(networkName, nodeID string, extClient models.CustomExtClient) {
 	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)
+}

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

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

+ 1 - 0
config/config.go

@@ -91,6 +91,7 @@ type ServerConfig struct {
 	Environment                string        `yaml:"environment"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration" swaggertype:"primitive,integer" format:"int64"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
+	RacRestrictToSingleNetwork bool          `yaml:"rac_restrict_to_single_network"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `yaml:"endpoint_detection"`
 	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`

+ 49 - 8
controllers/acls.go

@@ -45,10 +45,13 @@ func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
 		SrcGroupTypes: []models.AclGroupType{
 			models.UserAclID,
 			models.UserGroupAclID,
-			models.DeviceAclID,
+			models.NodeTagID,
+			models.NodeID,
 		},
 		DstGroupTypes: []models.AclGroupType{
-			models.DeviceAclID,
+			models.NodeTagID,
+			models.NodeID,
+			models.EgressRange,
 			// models.NetmakerIPAclID,
 			// models.NetmakerSubNetRangeAClID,
 		},
@@ -117,6 +120,13 @@ func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
 				},
 				PortRange: "",
 			},
+			{
+				Name: models.SSH,
+				AllowedProtocols: []models.Protocol{
+					models.TCP,
+				},
+				PortRange: "22",
+			},
 			{
 				Name: models.Custom,
 				AllowedProtocols: []models.Protocol{
@@ -134,18 +144,49 @@ func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
 func aclDebug(w http.ResponseWriter, r *http.Request) {
 	nodeID, _ := url.QueryUnescape(r.URL.Query().Get("node"))
 	peerID, _ := url.QueryUnescape(r.URL.Query().Get("peer"))
+	peerIsStatic, _ := url.QueryUnescape(r.URL.Query().Get("peer_is_static"))
 	node, err := logic.GetNodeByID(nodeID)
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	peer, err := logic.GetNodeByID(peerID)
-	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
+	var peer models.Node
+	if peerIsStatic == "true" {
+		extclient, err := logic.GetExtClient(peerID, node.Network)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		peer = extclient.ConvertToStaticNode()
+
+	} else {
+		peer, err = logic.GetNodeByID(peerID)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
+	type resp struct {
+		IsNodeAllowed bool
+		IsPeerAllowed bool
+		Policies      []models.Acl
+		IngressRules  []models.FwRule
+	}
+
+	allowed, ps := logic.IsNodeAllowedToCommunicateV1(node, peer, true)
+	isallowed := logic.IsPeerAllowed(node, peer, true)
+	re := resp{
+		IsNodeAllowed: allowed,
+		IsPeerAllowed: isallowed,
+		Policies:      ps,
+	}
+	if peerIsStatic == "true" {
+		ingress, err := logic.GetNodeByID(peer.StaticNode.IngressGatewayID)
+		if err == nil {
+			re.IngressRules = logic.GetFwRulesOnIngressGateway(ingress)
+		}
 	}
-	allowed, _ := logic.IsNodeAllowedToCommunicate(node, peer, true)
-	logic.ReturnSuccessResponseWithJson(w, r, allowed, "fetched all acls in the network ")
+	logic.ReturnSuccessResponseWithJson(w, r, re, "fetched all acls in the network ")
 }
 
 // @Summary     List Acls in a network

+ 284 - 4
controllers/ext_client.go

@@ -42,6 +42,7 @@ func extClientHandlers(r *mux.Router) {
 		Methods(http.MethodDelete)
 	r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).
 		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/client_conf/{network}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientHAConf))).Methods(http.MethodGet)
 }
 
 func checkIngressExists(nodeID string) bool {
@@ -287,6 +288,16 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	} else if gwnode.IngressDNS != "" {
 		defaultDNS = "DNS = " + gwnode.IngressDNS
 	}
+	if client.DNS == "" {
+		if len(network.NameServers) > 0 {
+			if defaultDNS == "" {
+				defaultDNS = "DNS = " + strings.Join(network.NameServers, ",")
+			} else {
+				defaultDNS += "," + strings.Join(network.NameServers, ",")
+			}
+
+		}
+	}
 	// if servercfg.GetManageDNS() {
 	// 	if gwnode.Address6.IP != nil {
 	// 		if defaultDNS == "" {
@@ -387,6 +398,262 @@ Endpoint = %s
 	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 != "") {
+		network, _ := logic.GetNetwork(gwnode.Network)
+		dns := gwnode.IngressDNS
+		if len(network.NameServers) > 0 {
+			if dns == "" {
+				dns = strings.Join(network.NameServers, ",")
+			} else {
+				dns += "," + strings.Join(network.NameServers, ",")
+			}
+
+		}
+		extclient.DNS = dns
+
+	}
+
+	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
 // @Router      /api/extclients/{network}/{nodeid} [post]
 // @Tags        Remote Access Client
@@ -474,7 +741,17 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	// 	models.RemoteAccessTagName))] = struct{}{}
 	// set extclient dns to ingressdns if extclient dns is not explicitly set
 	if (extclient.DNS == "") && (node.IngressDNS != "") {
-		extclient.DNS = node.IngressDNS
+		network, _ := logic.GetNetwork(node.Network)
+		dns := node.IngressDNS
+		if len(network.NameServers) > 0 {
+			if dns == "" {
+				dns = strings.Join(network.NameServers, ",")
+			} else {
+				dns += "," + strings.Join(network.NameServers, ",")
+			}
+
+		}
+		extclient.DNS = dns
 	}
 	host, err := logic.GetHost(node.HostID.String())
 	if err != nil {
@@ -797,10 +1074,13 @@ func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bo
 	}
 	//validate DNS
 	if customExtClient.DNS != "" {
-		if ip := net.ParseIP(customExtClient.DNS); ip == nil {
-			return errInvalidExtClientDNS
+		ips := strings.Split(customExtClient.DNS, ",")
+		for _, ip := range ips {
+			trimmedIp := strings.TrimSpace(ip)
+			if ip := net.ParseIP(trimmedIp); ip == nil {
+				return errInvalidExtClientDNS
+			}
 		}
-		//extclient.DNS = customExtClient.DNS
 	}
 	return nil
 }

+ 5 - 53
controllers/hosts.go

@@ -140,60 +140,12 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 // @Failure     500 {object} models.ErrorResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
-	currentHosts := []models.Host{}
-	var err error
-	if r.Header.Get("ismaster") == "yes" {
-		currentHosts, err = logic.GetAllHosts()
-		if err != nil {
-			logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-			return
-		}
-	} else {
-		username := r.Header.Get("user")
-		user, err := logic.GetUser(username)
-		if err != nil {
-			return
-		}
-		userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
-		if err != nil {
-			return
-		}
-		respHostsMap := make(map[string]struct{})
-		if !userPlatformRole.FullAccess {
-			nodes, err := logic.GetAllNodes()
-			if err != nil {
-				logger.Log(0, "error fetching all nodes info: ", err.Error())
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-				return
-			}
-			filteredNodes := logic.GetFilteredNodesByUserAccess(*user, nodes)
-			if len(filteredNodes) > 0 {
-				currentHostsMap, err := logic.GetHostsMap()
-				if err != nil {
-					logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
-					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-					return
-				}
-				for _, node := range filteredNodes {
-					if _, ok := respHostsMap[node.HostID.String()]; ok {
-						continue
-					}
-					if host, ok := currentHostsMap[node.HostID.String()]; ok {
-						currentHosts = append(currentHosts, host)
-						respHostsMap[host.ID.String()] = struct{}{}
-					}
-				}
 
-			}
-		} else {
-			currentHosts, err = logic.GetAllHosts()
-			if err != nil {
-				logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-				return
-			}
-		}
+	currentHosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
 	}
 
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])

+ 33 - 2
controllers/network.go

@@ -41,6 +41,7 @@ func networkHandlers(r *mux.Router) {
 		Methods(http.MethodPut)
 	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/networks/{networkname}/egress_routes", logic.SecurityCheck(true, http.HandlerFunc(getNetworkEgressRoutes)))
 }
 
 // @Summary     Lists all networks
@@ -429,6 +430,33 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(networkACL)
 }
 
+// @Summary     Get a network Egress routes
+// @Router      /api/networks/{networkname}/egress_routes [get]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Produce     json
+// @Success     200 {object} acls.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getNetworkEgressRoutes(w http.ResponseWriter, r *http.Request) {
+	var params = mux.Vars(r)
+	netname := params["networkname"]
+	// check if network exists
+	_, err := logic.GetNetwork(netname)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to fetch ACLs for network [%s]: %v", netname, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	nodeEgressRoutes, _, err := logic.GetEgressRanges(models.NetworkID(netname))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, nodeEgressRoutes, "fetched network egress routes")
+}
+
 // @Summary     Delete a network
 // @Router      /api/networks/{networkname} [delete]
 // @Tags        Networks
@@ -464,8 +492,10 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
 		return
 	}
+	go logic.UnlinkNetworkAndTagsFromEnrollmentKeys(network, true)
 	go logic.DeleteNetworkRoles(network)
-	go logic.DeleteDefaultNetworkPolicies(models.NetworkID(network))
+	go logic.DeleteAllNetworkTags(models.NetworkID(network))
+	go logic.DeleteNetworkPolicies(models.NetworkID(network))
 	//delete network from allocated ip map
 	go logic.RemoveNetworkFromAllocatedIpMap(network)
 	go func() {
@@ -639,6 +669,7 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	netNew := netOld
+	netNew.NameServers = payload.NameServers
 	netNew.DefaultACL = payload.DefaultACL
 	_, _, _, err = logic.UpdateNetwork(&netOld, &netNew)
 	if err != nil {
@@ -646,7 +677,7 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-
+	go mq.PublishPeerUpdate(false)
 	slog.Info("updated network", "network", payload.NetID, "user", r.Header.Get("user"))
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(payload)

+ 51 - 2
controllers/node.go

@@ -31,6 +31,7 @@ func nodeHandlers(r *mux.Router) {
 	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/v1/nodes/{network}/status", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodeStatus))).Methods(http.MethodGet)
 	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.AddStatusToNodes(nodes)
+	nodes = logic.AddStatusToNodes(nodes, false)
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	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.AddStatusToNodes(nodes)
+	nodes = logic.AddStatusToNodes(nodes, false)
 	// return all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
 	logger.Log(3, r.Header.Get("user"), "fetched all nodes they have access to")
@@ -377,6 +378,52 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	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
 // @Router      /api/nodes/{network}/{nodeid} [get]
 // @Tags        Nodes
@@ -631,6 +678,8 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.UpdateRelayed(&currentNode, newNode)
 	}
 
+	logic.GetNodeStatus(newNode, false)
+
 	apiNode := newNode.ConvertToAPINode()
 	logger.Log(
 		1,

+ 4 - 0
controllers/node_test.go

@@ -21,6 +21,10 @@ var linuxHost models.Host
 func TestCreateEgressGateway(t *testing.T) {
 	var gateway models.EgressGatewayRequest
 	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"
 	deleteAllNetworks()
 	createNet()

+ 9 - 0
controllers/tags.go

@@ -88,6 +88,7 @@ func createTag(w http.ResponseWriter, r *http.Request) {
 		TagName:   req.TagName,
 		Network:   req.Network,
 		CreatedBy: user.UserName,
+		ColorCode: req.ColorCode,
 		CreatedAt: time.Now(),
 	}
 	_, err = logic.GetTag(tag.ID)
@@ -182,6 +183,14 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
 		// delete old Tag entry
 		logic.DeleteTag(updateTag.ID, false)
 	}
+	if updateTag.ColorCode != "" && updateTag.ColorCode != tag.ColorCode {
+		tag.ColorCode = updateTag.ColorCode
+		err = logic.UpsertTag(tag)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
 	go func() {
 		logic.UpdateTag(updateTag, newID)
 		if updateTag.NewName != "" {

+ 9 - 0
controllers/user.go

@@ -258,6 +258,15 @@ func getUserV1(w http.ResponseWriter, r *http.Request) {
 	resp := models.ReturnUserWithRolesAndGroups{
 		ReturnUser:   user,
 		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)
 	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

Разница между файлами не показана из-за своего большого размера
+ 543 - 173
logic/acls.go


+ 1 - 0
logic/auth.go

@@ -359,6 +359,7 @@ func DeleteUser(user string) (bool, error) {
 	if err != nil {
 		return false, err
 	}
+	go RemoveUserFromAclPolicy(user)
 
 	return true, nil
 }

+ 57 - 1
logic/enrollmentkey.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strings"
 	"sync"
 	"time"
 
@@ -120,7 +121,6 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID, groups []models.TagID)
 }
 
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB
-// TODO drop double pointer
 func GetAllEnrollmentKeys() ([]models.EnrollmentKey, error) {
 	currentKeys, err := getEnrollmentKeysMap()
 	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
+}

+ 269 - 225
logic/extpeers.go

@@ -116,6 +116,7 @@ func DeleteExtClient(network string, clientid string) error {
 		}
 		deleteExtClientFromCache(key)
 	}
+	go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
 	return nil
 }
 
@@ -428,7 +429,7 @@ func GetAllExtClientsWithStatus(status models.NodeStatus) ([]models.ExtClient, e
 	var validExtClients []models.ExtClient
 	for _, extClient := range extClients {
 		node := extClient.ConvertToStaticNode()
-		getNodeStatus(&node, false)
+		GetNodeCheckInStatus(&node, false)
 
 		if node.Status == status {
 			validExtClients = append(validExtClients, extClient)
@@ -497,217 +498,192 @@ func GetStaticNodeIps(node models.Node) (ips []net.IP) {
 	return
 }
 
-func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
-	// fetch user access to static clients via policies
-	defer func() {
-		sort.Slice(rules, func(i, j int) bool {
-			if !rules[i].SrcIP.IP.Equal(rules[j].SrcIP.IP) {
-				return string(rules[i].SrcIP.IP.To16()) < string(rules[j].SrcIP.IP.To16())
-			}
-			return string(rules[i].DstIP.IP.To16()) < string(rules[j].DstIP.IP.To16())
-		})
-	}()
-	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
-	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
-	nodes, _ := GetNetworkNodes(node.Network)
-	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
-	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
-	for _, userNodeI := range userNodes {
-		for _, peer := range nodes {
-			if peer.IsUserNode {
-				continue
-			}
-			if ok, allowedPolicies := IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer); ok {
-				if peer.IsStatic {
-					if userNodeI.StaticNode.Address != "" {
-						if !defaultUserPolicy.Enabled {
-							for _, policy := range allowedPolicies {
-								rules = append(rules, models.FwRule{
-									SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
-									DstIP:           peer.StaticNode.AddressIPNet4(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-								rules = append(rules, models.FwRule{
-									SrcIP:           peer.StaticNode.AddressIPNet4(),
-									DstIP:           userNodeI.StaticNode.AddressIPNet4(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
-						}
-
-					}
-					if userNodeI.StaticNode.Address6 != "" {
-						if !defaultUserPolicy.Enabled {
-							for _, policy := range allowedPolicies {
-								rules = append(rules, models.FwRule{
-									SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
-									DstIP:           peer.StaticNode.AddressIPNet6(),
-									Allow:           true,
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-								})
-								rules = append(rules, models.FwRule{
-									SrcIP:           peer.StaticNode.AddressIPNet6(),
-									DstIP:           userNodeI.StaticNode.AddressIPNet6(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-
-							}
-						}
-
-					}
-					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
-						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
-							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
-							if err != nil {
-								continue
-							}
-							if ipNet.IP.To4() != nil {
-								rules = append(rules, models.FwRule{
-									SrcIP: userNodeI.StaticNode.AddressIPNet4(),
-									DstIP: *ipNet,
-									Allow: true,
-								})
-							} else {
-								rules = append(rules, models.FwRule{
-									SrcIP: userNodeI.StaticNode.AddressIPNet6(),
-									DstIP: *ipNet,
-									Allow: true,
-								})
-							}
-
-						}
+func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []models.Acl) (rules []models.FwRule) {
+
+	for _, policy := range allowedPolicies {
+		// if static peer dst rule not for ingress node -> skip
+		if node.Address.IP != nil {
+			rules = append(rules, models.FwRule{
+				SrcIP: net.IPNet{
+					IP:   node.Address.IP,
+					Mask: net.CIDRMask(32, 32),
+				},
+				DstIP: net.IPNet{
+					IP:   peer.Address.IP,
+					Mask: net.CIDRMask(32, 32),
+				},
+				AllowedProtocol: policy.Proto,
+				AllowedPorts:    policy.Port,
+				Allow:           true,
+			})
+		}
 
-					}
-				} else {
+		if node.Address6.IP != nil {
+			rules = append(rules, models.FwRule{
+				SrcIP: net.IPNet{
+					IP:   node.Address6.IP,
+					Mask: net.CIDRMask(128, 128),
+				},
+				DstIP: net.IPNet{
+					IP:   peer.Address6.IP,
+					Mask: net.CIDRMask(128, 128),
+				},
+				AllowedProtocol: policy.Proto,
+				AllowedPorts:    policy.Port,
+				Allow:           true,
+			})
+		}
+		if policy.AllowedDirection == models.TrafficDirectionBi {
+			if node.Address.IP != nil {
+				rules = append(rules, models.FwRule{
+					SrcIP: net.IPNet{
+						IP:   peer.Address.IP,
+						Mask: net.CIDRMask(32, 32),
+					},
+					DstIP: net.IPNet{
+						IP:   node.Address.IP,
+						Mask: net.CIDRMask(32, 32),
+					},
+					AllowedProtocol: policy.Proto,
+					AllowedPorts:    policy.Port,
+					Allow:           true,
+				})
+			}
 
-					if userNodeI.StaticNode.Address != "" {
-						if !defaultUserPolicy.Enabled {
-							for _, policy := range allowedPolicies {
-								rules = append(rules, models.FwRule{
-									SrcIP: userNodeI.StaticNode.AddressIPNet4(),
-									DstIP: net.IPNet{
-										IP:   peer.Address.IP,
-										Mask: net.CIDRMask(32, 32),
-									},
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
+			if node.Address6.IP != nil {
+				rules = append(rules, models.FwRule{
+					SrcIP: net.IPNet{
+						IP:   peer.Address6.IP,
+						Mask: net.CIDRMask(128, 128),
+					},
+					DstIP: net.IPNet{
+						IP:   node.Address6.IP,
+						Mask: net.CIDRMask(128, 128),
+					},
+					AllowedProtocol: policy.Proto,
+					AllowedPorts:    policy.Port,
+					Allow:           true,
+				})
+			}
+		}
+		if len(node.StaticNode.ExtraAllowedIPs) > 0 {
+			for _, additionalAllowedIPNet := range node.StaticNode.ExtraAllowedIPs {
+				_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+				if err != nil {
+					continue
+				}
+				if ipNet.IP.To4() != nil && peer.Address.IP != nil {
+					rules = append(rules, models.FwRule{
+						SrcIP: net.IPNet{
+							IP:   peer.Address.IP,
+							Mask: net.CIDRMask(32, 32),
+						},
+						DstIP: *ipNet,
+						Allow: true,
+					})
+				} else if peer.Address6.IP != nil {
+					rules = append(rules, models.FwRule{
+						SrcIP: net.IPNet{
+							IP:   peer.Address6.IP,
+							Mask: net.CIDRMask(128, 128),
+						},
+						DstIP: *ipNet,
+						Allow: true,
+					})
+				}
 
-						}
-					}
+			}
 
-					if userNodeI.StaticNode.Address6 != "" {
-						if !defaultUserPolicy.Enabled {
-							for _, policy := range allowedPolicies {
-								rules = append(rules, models.FwRule{
-									SrcIP: userNodeI.StaticNode.AddressIPNet6(),
-									DstIP: net.IPNet{
-										IP:   peer.Address6.IP,
-										Mask: net.CIDRMask(128, 128),
-									},
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
-						}
-					}
+		}
+		if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
+			for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
+				_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
+				if err != nil {
+					continue
+				}
+				if ipNet.IP.To4() != nil && node.Address.IP != nil {
+					rules = append(rules, models.FwRule{
+						SrcIP: net.IPNet{
+							IP:   node.Address.IP,
+							Mask: net.CIDRMask(32, 32),
+						},
+						DstIP: *ipNet,
+						Allow: true,
+					})
+				} else if node.Address6.IP != nil {
+					rules = append(rules, models.FwRule{
+						SrcIP: net.IPNet{
+							IP:   node.Address6.IP,
+							Mask: net.CIDRMask(128, 128),
+						},
+						DstIP: *ipNet,
+						Allow: true,
+					})
 				}
+
 			}
-		}
-	}
 
-	if defaultDevicePolicy.Enabled {
-		return
-	}
-	for _, nodeI := range nodes {
-		if !nodeI.IsStatic || nodeI.IsUserNode {
-			continue
 		}
-		for _, peer := range nodes {
-			if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode {
-				continue
-			}
-			if ok, allowedPolicies := IsNodeAllowedToCommunicate(nodeI, peer, true); ok {
-				if peer.IsStatic {
-					if nodeI.StaticNode.Address != "" {
-						for _, policy := range allowedPolicies {
+
+		// add egress range rules
+		for _, dstI := range policy.Dst {
+			if dstI.ID == models.EgressRange {
+				ip, cidr, err := net.ParseCIDR(dstI.Value)
+				if err == nil {
+					if ip.To4() != nil {
+						if node.Address.IP != nil {
 							rules = append(rules, models.FwRule{
-								SrcIP:           nodeI.StaticNode.AddressIPNet4(),
-								DstIP:           peer.StaticNode.AddressIPNet4(),
+								SrcIP: net.IPNet{
+									IP:   node.Address.IP,
+									Mask: net.CIDRMask(32, 32),
+								},
+								DstIP:           *cidr,
 								AllowedProtocol: policy.Proto,
 								AllowedPorts:    policy.Port,
 								Allow:           true,
 							})
-							if policy.AllowedDirection == models.TrafficDirectionBi {
-								rules = append(rules, models.FwRule{
-									SrcIP:           peer.StaticNode.AddressIPNet4(),
-									DstIP:           nodeI.StaticNode.AddressIPNet4(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
 						}
-
-					}
-					if nodeI.StaticNode.Address6 != "" {
-						for _, policy := range allowedPolicies {
+					} else {
+						if node.Address6.IP != nil {
 							rules = append(rules, models.FwRule{
-								SrcIP:           nodeI.StaticNode.AddressIPNet6(),
-								DstIP:           peer.StaticNode.AddressIPNet6(),
+								SrcIP: net.IPNet{
+									IP:   node.Address6.IP,
+									Mask: net.CIDRMask(128, 128),
+								},
+								DstIP:           *cidr,
 								AllowedProtocol: policy.Proto,
 								AllowedPorts:    policy.Port,
 								Allow:           true,
 							})
-							if policy.AllowedDirection == models.TrafficDirectionBi {
-								rules = append(rules, models.FwRule{
-									SrcIP:           peer.StaticNode.AddressIPNet6(),
-									DstIP:           nodeI.StaticNode.AddressIPNet6(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
 						}
 					}
-					if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
-						for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
-							_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
-							if err != nil {
-								continue
-							}
-							if ipNet.IP.To4() != nil {
-								rules = append(rules, models.FwRule{
-									SrcIP: nodeI.StaticNode.AddressIPNet4(),
-									DstIP: *ipNet,
-									Allow: true,
-								})
-							} else {
-								rules = append(rules, models.FwRule{
-									SrcIP: nodeI.StaticNode.AddressIPNet6(),
-									DstIP: *ipNet,
-									Allow: true,
-								})
-							}
 
-						}
+				}
+			}
+		}
+	}
 
-					}
-				} else {
-					if nodeI.StaticNode.Address != "" {
-						for _, policy := range allowedPolicies {
+	return
+}
+
+func getFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []models.FwRule) {
+	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
+	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
+	for _, userNodeI := range userNodes {
+		for _, peer := range nodes {
+			if peer.IsUserNode {
+				continue
+			}
+
+			if ok, allowedPolicies := IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer); ok {
+				if peer.IsStatic {
+					peer = peer.StaticNode.ConvertToStaticNode()
+				}
+				if !defaultUserPolicy.Enabled {
+					for _, policy := range allowedPolicies {
+						if userNodeI.StaticNode.Address != "" {
 							rules = append(rules, models.FwRule{
-								SrcIP: nodeI.StaticNode.AddressIPNet4(),
+								SrcIP: userNodeI.StaticNode.AddressIPNet4(),
 								DstIP: net.IPNet{
 									IP:   peer.Address.IP,
 									Mask: net.CIDRMask(32, 32),
@@ -716,24 +692,10 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 								AllowedPorts:    policy.Port,
 								Allow:           true,
 							})
-							if policy.AllowedDirection == models.TrafficDirectionBi {
-								rules = append(rules, models.FwRule{
-									SrcIP: net.IPNet{
-										IP:   peer.Address.IP,
-										Mask: net.CIDRMask(32, 32),
-									},
-									DstIP:           nodeI.StaticNode.AddressIPNet4(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
-							}
 						}
-					}
-					if nodeI.StaticNode.Address6 != "" {
-						for _, policy := range allowedPolicies {
+						if userNodeI.StaticNode.Address6 != "" {
 							rules = append(rules, models.FwRule{
-								SrcIP: nodeI.StaticNode.AddressIPNet6(),
+								SrcIP: userNodeI.StaticNode.AddressIPNet6(),
 								DstIP: net.IPNet{
 									IP:   peer.Address6.IP,
 									Mask: net.CIDRMask(128, 128),
@@ -742,19 +704,34 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 								AllowedPorts:    policy.Port,
 								Allow:           true,
 							})
-							if policy.AllowedDirection == models.TrafficDirectionBi {
-								rules = append(rules, models.FwRule{
-									SrcIP: net.IPNet{
-										IP:   peer.Address6.IP,
-										Mask: net.CIDRMask(128, 128),
-									},
-									DstIP:           nodeI.StaticNode.AddressIPNet6(),
-									AllowedProtocol: policy.Proto,
-									AllowedPorts:    policy.Port,
-									Allow:           true,
-								})
+						}
+
+						// add egress ranges
+						for _, dstI := range policy.Dst {
+							if dstI.ID == models.EgressRange {
+								ip, cidr, err := net.ParseCIDR(dstI.Value)
+								if err == nil {
+									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									}
+								}
 							}
 						}
+
 					}
 				}
 
@@ -764,6 +741,70 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 	return
 }
 
+func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
+	// fetch user access to static clients via policies
+	defer func() {
+		sort.Slice(rules, func(i, j int) bool {
+			if !rules[i].SrcIP.IP.Equal(rules[j].SrcIP.IP) {
+				return string(rules[i].SrcIP.IP.To16()) < string(rules[j].SrcIP.IP.To16())
+			}
+			return string(rules[i].DstIP.IP.To16()) < string(rules[j].DstIP.IP.To16())
+		})
+	}()
+	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
+	nodes, _ := GetNetworkNodes(node.Network)
+	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
+	rules = getFwRulesForUserNodesOnGw(node, nodes)
+	if defaultDevicePolicy.Enabled {
+		return
+	}
+	for _, nodeI := range nodes {
+		if !nodeI.IsStatic || nodeI.IsUserNode {
+			continue
+		}
+		// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
+		// 	continue
+		// }
+		for _, peer := range nodes {
+			if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode {
+				continue
+			}
+			if nodeI.StaticNode.IngressGatewayID != node.ID.String() &&
+				((!peer.IsStatic && peer.ID.String() != node.ID.String()) ||
+					(peer.IsStatic && peer.StaticNode.IngressGatewayID != node.ID.String())) {
+				continue
+			}
+			if peer.IsStatic {
+				peer = peer.StaticNode.ConvertToStaticNode()
+			}
+			var allowedPolicies1 []models.Acl
+			var ok bool
+			if ok, allowedPolicies1 = IsNodeAllowedToCommunicateV1(nodeI.StaticNode.ConvertToStaticNode(), peer, true); ok {
+				rules = append(rules, getFwRulesForNodeAndPeerOnGw(nodeI.StaticNode.ConvertToStaticNode(), peer, allowedPolicies1)...)
+			}
+			if ok, allowedPolicies2 := IsNodeAllowedToCommunicateV1(peer, nodeI.StaticNode.ConvertToStaticNode(), true); ok {
+				rules = append(rules,
+					getFwRulesForNodeAndPeerOnGw(peer, nodeI.StaticNode.ConvertToStaticNode(),
+						GetUniquePolicies(allowedPolicies1, allowedPolicies2))...)
+			}
+		}
+	}
+	return
+}
+
+func GetUniquePolicies(policies1, policies2 []models.Acl) []models.Acl {
+	policies1Map := make(map[string]struct{})
+	for _, policy1I := range policies1 {
+		policies1Map[policy1I.ID] = struct{}{}
+	}
+	for i := len(policies2) - 1; i >= 0; i-- {
+		if _, ok := policies1Map[policies2[i].ID]; ok {
+			policies2 = append(policies2[:i], policies2[i+1:]...)
+		}
+	}
+	return policies2
+}
+
 func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) {
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
@@ -852,13 +893,21 @@ func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandA
 }
 
 func getExtPeerEgressRoute(node models.Node, extPeer models.ExtClient) (egressRoutes []models.EgressNetworkRoutes) {
-	egressRoutes = append(egressRoutes, models.EgressNetworkRoutes{
+	r := models.EgressNetworkRoutes{
+		PeerKey:       extPeer.PublicKey,
 		EgressGwAddr:  extPeer.AddressIPNet4(),
 		EgressGwAddr6: extPeer.AddressIPNet6(),
 		NodeAddr:      node.Address,
 		NodeAddr6:     node.Address6,
 		EgressRanges:  extPeer.ExtraAllowedIPs,
-	})
+	}
+	for _, extraAllowedIP := range extPeer.ExtraAllowedIPs {
+		r.EgressRangesWithMetric = append(r.EgressRangesWithMetric, models.EgressRangeMetric{
+			Network:     extraAllowedIP,
+			RouteMetric: 256,
+		})
+	}
+	egressRoutes = append(egressRoutes, r)
 	return
 }
 
@@ -871,7 +920,7 @@ func getExtpeerEgressRanges(node models.Node) (ranges, ranges6 []net.IPNet) {
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 		}
-		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
+		if ok, _ := IsNodeAllowedToCommunicateV1(extPeer.ConvertToStaticNode(), node, true); !ok {
 			continue
 		}
 		for _, allowedRange := range extPeer.ExtraAllowedIPs {
@@ -898,7 +947,7 @@ func getExtpeersExtraRoutes(node models.Node) (egressRoutes []models.EgressNetwo
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 		}
-		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
+		if ok, _ := IsNodeAllowedToCommunicateV1(extPeer.ConvertToStaticNode(), node, true); !ok {
 			continue
 		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...)
@@ -946,16 +995,11 @@ func GetStaticUserNodesByNetwork(network models.NetworkID) (staticNode []models.
 	for _, extI := range extClients {
 		if extI.Network == network.String() {
 			if extI.RemoteAccessClientID != "" {
-				n := models.Node{
-					IsStatic:   true,
-					StaticNode: extI,
-					IsUserNode: extI.RemoteAccessClientID != "",
-				}
+				n := extI.ConvertToStaticNode()
 				staticNode = append(staticNode, n)
 			}
 		}
 	}
-
 	return
 }
 

+ 35 - 2
logic/gateway.go

@@ -3,6 +3,8 @@ package logic
 import (
 	"errors"
 	"fmt"
+	"slices"
+	"sort"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -77,6 +79,14 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if host.FirewallInUse == models.FIREWALL_NONE {
 		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-- {
 		// check if internet gateway IPv4
 		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
 
 	}
+	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 == "" {
 		gateway.NatEnabled = "yes"
 	}
@@ -104,6 +136,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	node.IsEgressGateway = true
 	node.EgressGatewayRanges = gateway.Ranges
 	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
+
 	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
@@ -187,7 +220,7 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if node.Tags == nil {
 		node.Tags = make(map[models.TagID]struct{})
 	}
-	node.Tags[models.TagID(fmt.Sprintf("%s.%s", netid, models.RemoteAccessTagName))] = struct{}{}
+	node.Tags[models.TagID(fmt.Sprintf("%s.%s", netid, models.GwTagName))] = struct{}{}
 	err = UpsertNode(&node)
 	if err != nil {
 		return models.Node{}, err
@@ -239,7 +272,7 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	if !servercfg.IsPro {
 		node.IsInternetGateway = false
 	}
-	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.RemoteAccessTagName)))
+	delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName)))
 	node.IngressGatewayRange = ""
 	node.Metadata = ""
 	err = UpsertNode(&node)

+ 1 - 1
logic/hosts.go

@@ -122,7 +122,7 @@ func GetAllHostsWithStatus(status models.NodeStatus) ([]models.Host, error) {
 
 		nodes := GetHostNodes(&host)
 		for _, node := range nodes {
-			getNodeStatus(&node, false)
+			GetNodeCheckInStatus(&node, false)
 			if node.Status == status {
 				validHosts = append(validHosts, host)
 				break

+ 44 - 3
logic/nodes.go

@@ -320,6 +320,7 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 		return err
 	}
+	go RemoveNodeFromAclPolicy(*node)
 
 	return nil
 }
@@ -457,7 +458,7 @@ func AddStaticNodestoList(nodes []models.Node) []models.Node {
 	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)
 	for _, node := range nodes {
 		if _, ok := aclDefaultPolicyStatusMap[node.Network]; !ok {
@@ -465,7 +466,12 @@ func AddStatusToNodes(nodes []models.Node) (nodesWithStatus []models.Node) {
 			defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 			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)
 	}
 	return
@@ -586,6 +592,16 @@ func GetAllNodesAPI(nodes []models.Node) []models.ApiNode {
 	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
 func DeleteExpiredNodes(ctx context.Context, peerUpdate chan *models.Node) {
 	// Delete Expired Nodes Every Hour
@@ -840,6 +856,9 @@ func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (
 	tagNodesMap = make(map[models.TagID][]models.Node)
 	nodes, _ := GetNetworkNodes(netID.String())
 	for _, nodeI := range nodes {
+		tagNodesMap[models.TagID(nodeI.ID.String())] = []models.Node{
+			nodeI,
+		}
 		if nodeI.Tags == nil {
 			continue
 		}
@@ -847,6 +866,9 @@ func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (
 			nodeI.Mutex.Lock()
 		}
 		for nodeTagID := range nodeI.Tags {
+			if nodeTagID == models.TagID(nodeI.ID.String()) {
+				continue
+			}
 			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
 		}
 		if nodeI.Mutex != nil {
@@ -867,13 +889,26 @@ func AddTagMapWithStaticNodes(netID models.NetworkID,
 		return tagNodesMap
 	}
 	for _, extclient := range extclients {
-		if extclient.Tags == nil || extclient.RemoteAccessClientID != "" {
+		if extclient.RemoteAccessClientID != "" {
+			continue
+		}
+		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
+			{
+				IsStatic:   true,
+				StaticNode: extclient,
+			},
+		}
+		if extclient.Tags == nil {
 			continue
 		}
+
 		if extclient.Mutex != nil {
 			extclient.Mutex.Lock()
 		}
 		for tagID := range extclient.Tags {
+			if tagID == models.TagID(extclient.ClientID) {
+				continue
+			}
 			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
 			tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
 		}
@@ -891,6 +926,12 @@ func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
 		return tagNodesMap
 	}
 	for _, extclient := range extclients {
+		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
+			{
+				IsStatic:   true,
+				StaticNode: extclient,
+			},
+		}
 		if extclient.Tags == nil {
 			continue
 		}

+ 108 - 66
logic/peers.go

@@ -175,12 +175,19 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				}
 			}
 			hostPeerUpdate.FwUpdate.AclRules["allowed-network-rules"] = aclRule
+			hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
+				EgressID: "allowed-network-rules",
+				EgressFwRules: map[string]models.AclRule{
+					"allowed-network-rules": aclRule,
+				},
+			}
 		}
 	}()
 
 	slog.Debug("peer update for host", "hostId", host.ID.String())
 	peerIndexMap := make(map[string]int)
 	for _, nodeID := range host.Nodes {
+		networkAllowAll := true
 		nodeID := nodeID
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
@@ -190,60 +197,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
 			continue
 		}
-		if host.OS == models.OS_Types.IoT {
-			hostPeerUpdate.NodeAddrs = append(hostPeerUpdate.NodeAddrs, node.PrimaryAddressIPNet())
-			if node.IsRelayed {
-				relayNode, err := GetNodeByID(node.RelayedBy)
-				if err != nil {
-					continue
-				}
-				relayHost, err := GetHost(relayNode.HostID.String())
-				if err != nil {
-					continue
-				}
-				relayPeer := wgtypes.PeerConfig{
-					PublicKey:                   relayHost.PublicKey,
-					PersistentKeepaliveInterval: &relayHost.PersistentKeepalive,
-					ReplaceAllowedIPs:           true,
-					AllowedIPs:                  GetAllowedIPs(&node, &relayNode, nil),
-				}
-				uselocal := false
-				if host.EndpointIP.String() == relayHost.EndpointIP.String() {
-					// peer is on same network
-					// set to localaddress
-					uselocal = true
-					if node.LocalAddress.IP == nil {
-						// use public endpint
-						uselocal = false
-					}
-					if node.LocalAddress.String() == relayNode.LocalAddress.String() {
-						uselocal = false
-					}
-				}
-				relayPeer.Endpoint = &net.UDPAddr{
-					IP:   relayHost.EndpointIP,
-					Port: GetPeerListenPort(relayHost),
-				}
-
-				if uselocal {
-					relayPeer.Endpoint.IP = relayNode.LocalAddress.IP
-					relayPeer.Endpoint.Port = relayHost.ListenPort
-				}
-
-				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, relayPeer)
-			} else if deletedNode != nil && deletedNode.IsRelay {
-				relayHost, err := GetHost(deletedNode.HostID.String())
-				if err != nil {
-					continue
-				}
-				relayPeer := wgtypes.PeerConfig{
-					PublicKey: relayHost.PublicKey,
-					Remove:    true,
-				}
-				hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, relayPeer)
-			}
-			continue
-		}
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
@@ -251,15 +204,15 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 
-		if defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled {
+		if (defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) || !checkIfAnyPolicyisUniDirectional(node) {
 			if node.NetworkRange.IP != nil {
 				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange)
 			}
 			if node.NetworkRange6.IP != nil {
 				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange6)
 			}
-
 		} else {
+			networkAllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
 			rules := GetAclRulesForNode(&node)
 			if len(hostPeerUpdate.FwUpdate.AclRules) == 0 {
@@ -270,7 +223,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				}
 			}
 		}
-
+		networkSettings, err := GetNetwork(node.Network)
+		if err != nil {
+			continue
+		}
+		hostPeerUpdate.NameServers = append(hostPeerUpdate.NameServers, networkSettings.NameServers...)
 		currentPeers := GetNetworkNodesMemory(allNodes, node.Network)
 		for _, peer := range currentPeers {
 			peer := peer
@@ -290,19 +247,45 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				ReplaceAllowedIPs:           true,
 			}
+			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			if peer.IsEgressGateway {
+				peerKey := peerHost.PublicKey.String()
+				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
+					// get relay host
+					failOverNode, err := GetNodeByID(peer.FailedOverBy.String())
+					if err == nil {
+						relayHost, err := GetHost(failOverNode.HostID.String())
+						if err == nil {
+							peerKey = relayHost.PublicKey.String()
+						}
+					}
+				}
+				if peer.IsRelayed && (peer.RelayedBy != node.ID.String()) {
+					// get relay host
+					relayNode, err := GetNodeByID(peer.RelayedBy)
+					if err == nil {
+						relayHost, err := GetHost(relayNode.HostID.String())
+						if err == nil {
+							peerKey = relayHost.PublicKey.String()
+						}
+					}
+				}
+
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
-					EgressGwAddr:  peer.Address,
-					EgressGwAddr6: peer.Address6,
-					NodeAddr:      node.Address,
-					NodeAddr6:     node.Address6,
-					EgressRanges:  peer.EgressGatewayRanges,
+					PeerKey:                peerKey,
+					EgressGwAddr:           peer.Address,
+					EgressGwAddr6:          peer.Address6,
+					NodeAddr:               node.Address,
+					NodeAddr6:              node.Address6,
+					EgressRanges:           filterConflictingEgressRoutes(node, peer),
+					EgressRangesWithMetric: filterConflictingEgressRoutesWithMetric(node, peer),
+					Network:                peer.Network,
 				})
 			}
 			if peer.IsIngressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, getExtpeersExtraRoutes(node)...)
 			}
-			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
+
 			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
@@ -366,7 +349,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 
 			if uselocal {
-				peerConfig.Endpoint.IP = peer.LocalAddress.IP
 				peerConfig.Endpoint.Port = peerHost.ListenPort
 			}
 			var allowedToComm bool
@@ -434,7 +416,6 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 						IngressID:     node.ID.String(),
 						Network:       node.NetworkRange,
 						Network6:      node.NetworkRange6,
-						AllowAll:      defaultDevicePolicy.Enabled && defaultUserPolicy.Default,
 						StaticNodeIps: GetStaticNodeIps(node),
 						Rules:         GetFwRulesOnIngressGateway(node),
 					}
@@ -468,10 +449,23 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   node.Address6.IP,
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 				},
-				EgressGWCfg: node.EgressGatewayRequest,
+				EgressGWCfg:   node.EgressGatewayRequest,
+				EgressFwRules: make(map[string]models.AclRule),
+			}
+
+		}
+		if node.IsEgressGateway {
+			if !networkAllowAll {
+				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
+				if egressInfo.EgressFwRules == nil {
+					egressInfo.EgressFwRules = make(map[string]models.AclRule)
+				}
+				egressInfo.EgressFwRules = GetEgressRulesForNode(node)
+				hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = egressInfo
 			}
 
 		}
+
 		if IsInternetGw(node) {
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
 			egressrange := []string{"0.0.0.0/0"}
@@ -554,6 +548,42 @@ func GetPeerListenPort(host *models.Host) int {
 	return peerPort
 }
 
+func filterConflictingEgressRoutes(node, peer models.Node) []string {
+	egressIPs := slices.Clone(peer.EgressGatewayRanges)
+	if node.IsEgressGateway {
+		// filter conflicting addrs
+		nodeEgressMap := make(map[string]struct{})
+		for _, rangeI := range node.EgressGatewayRanges {
+			nodeEgressMap[rangeI] = struct{}{}
+		}
+		for i := len(egressIPs) - 1; i >= 0; i-- {
+			if _, ok := nodeEgressMap[egressIPs[i]]; ok {
+				egressIPs = append(egressIPs[:i], egressIPs[i+1:]...)
+			}
+		}
+	}
+
+	return egressIPs
+}
+
+func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
+	egressIPs := slices.Clone(peer.EgressGatewayRequest.RangesWithMetric)
+	if node.IsEgressGateway {
+		// filter conflicting addrs
+		nodeEgressMap := make(map[string]struct{})
+		for _, rangeI := range node.EgressGatewayRanges {
+			nodeEgressMap[rangeI] = struct{}{}
+		}
+		for i := len(egressIPs) - 1; i >= 0; i-- {
+			if _, ok := nodeEgressMap[egressIPs[i].Network]; ok {
+				egressIPs = append(egressIPs[:i], egressIPs[i+1:]...)
+			}
+		}
+	}
+
+	return egressIPs
+}
+
 // GetAllowedIPs - calculates the wireguard allowedip field for a peer of a node based on the peer and node settings
 func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet {
 	var allowedips []net.IPNet
@@ -642,6 +672,18 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 	if peer.IsEgressGateway {
 		// hasGateway = true
 		egressIPs := GetEgressIPs(peer)
+		if node.IsEgressGateway {
+			// filter conflicting addrs
+			nodeEgressMap := make(map[string]struct{})
+			for _, rangeI := range node.EgressGatewayRanges {
+				nodeEgressMap[rangeI] = struct{}{}
+			}
+			for i := len(egressIPs) - 1; i >= 0; i-- {
+				if _, ok := nodeEgressMap[egressIPs[i].String()]; ok {
+					egressIPs = append(egressIPs[:i], egressIPs[i+1:]...)
+				}
+			}
+		}
 		allowedips = append(allowedips, egressIPs...)
 	}
 	if peer.IsRelay {

+ 2 - 2
logic/status.go

@@ -6,9 +6,9 @@ import (
 	"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
 	if node.IsStatic {
 		if !node.StaticNode.Enabled {

+ 18 - 4
logic/tags.go

@@ -30,6 +30,14 @@ func GetTag(tagID models.TagID) (models.Tag, error) {
 	return tag, nil
 }
 
+func UpsertTag(tag models.Tag) error {
+	d, err := json.Marshal(tag)
+	if err != nil {
+		return err
+	}
+	return database.Insert(tag.ID.String(), string(d), database.TAG_TABLE_NAME)
+}
+
 // InsertTag - creates new tag
 func InsertTag(tag models.Tag) error {
 	tagMutex.Lock()
@@ -97,6 +105,12 @@ func ListTagsWithNodes(netID models.NetworkID) ([]models.TagListResp, error) {
 	}
 	return resp, nil
 }
+func DeleteAllNetworkTags(networkID models.NetworkID) {
+	tags, _ := ListNetworkTags(networkID)
+	for _, tagI := range tags {
+		DeleteTag(tagI.ID, false)
+	}
+}
 
 // ListTags - lists all tags from DB
 func ListTags() ([]models.Tag, error) {
@@ -270,10 +284,10 @@ func CheckIDSyntax(id string) error {
 }
 
 func CreateDefaultTags(netID models.NetworkID) {
-	// create tag for remote access gws in the network
+	// create tag for gws in the network
 	tag := models.Tag{
-		ID:        models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.RemoteAccessTagName)),
-		TagName:   models.RemoteAccessTagName,
+		ID:        models.TagID(fmt.Sprintf("%s.%s", netID.String(), models.GwTagName)),
+		TagName:   models.GwTagName,
 		Network:   netID,
 		CreatedBy: "auto",
 		CreatedAt: time.Now(),
@@ -284,7 +298,7 @@ func CreateDefaultTags(netID models.NetworkID) {
 	}
 	err = InsertTag(tag)
 	if err != nil {
-		slog.Error("failed to create remote access gw tag", "error", err.Error())
+		slog.Error("failed to create gw tag", "error", err.Error())
 		return
 	}
 }

+ 1 - 0
logic/user_mgmt.go

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

+ 2 - 2
main.go

@@ -28,10 +28,10 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.30.0"
+var version = "v0.90.0"
 
 //	@title			NetMaker
-//	@version		0.30.0
+//	@version		0.90.0
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.description.markdown

+ 42 - 10
migrate/migrate.go

@@ -204,15 +204,6 @@ func updateNodes() {
 			logic.UpsertNode(&node)
 		}
 		if node.IsIngressGateway {
-			tagID := models.TagID(fmt.Sprintf("%s.%s", node.Network,
-				models.RemoteAccessTagName))
-			if node.Tags == nil {
-				node.Tags = make(map[models.TagID]struct{})
-			}
-			if _, ok := node.Tags[tagID]; !ok {
-				node.Tags[tagID] = struct{}{}
-				logic.UpsertNode(&node)
-			}
 			host, err := logic.GetHost(node.HostID.String())
 			if err == nil {
 				go logic.DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
@@ -225,6 +216,16 @@ func updateNodes() {
 				node.EgressGatewayRanges = egressRanges
 				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)
+			}
+
 		}
 	}
 	extclients, _ := logic.GetAllExtClients()
@@ -445,7 +446,8 @@ func createDefaultTagsAndPolicies() {
 	for _, network := range networks {
 		logic.CreateDefaultTags(models.NetworkID(network.NetID))
 		logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
-
+		// delete old remote access gws policy
+		logic.DeleteAcl(models.Acl{ID: fmt.Sprintf("%s.%s", network.NetID, "all-remote-access-gws")})
 	}
 	logic.MigrateAclPolicies()
 }
@@ -460,7 +462,37 @@ func migrateToGws() {
 			node.IsGw = true
 			node.IsIngressGateway = true
 			node.IsRelay = true
+			if node.Tags == nil {
+				node.Tags = make(map[models.TagID]struct{})
+			}
+			node.Tags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
+			delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.OldRemoteAccessTagName)))
 			logic.UpsertNode(&node)
 		}
 	}
+	acls := logic.ListAcls()
+	for _, acl := range acls {
+		upsert := false
+		for i, srcI := range acl.Src {
+			if srcI.ID == models.NodeTagID && srcI.Value == fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.OldRemoteAccessTagName) {
+				srcI.Value = fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.GwTagName)
+				acl.Src[i] = srcI
+				upsert = true
+			}
+		}
+		for i, dstI := range acl.Dst {
+			if dstI.ID == models.NodeTagID && dstI.Value == fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.OldRemoteAccessTagName) {
+				dstI.Value = fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.GwTagName)
+				acl.Dst[i] = dstI
+				upsert = true
+			}
+		}
+		if upsert {
+			logic.UpsertAcl(acl)
+		}
+	}
+	nets, _ := logic.GetNetworks()
+	for _, netI := range nets {
+		logic.DeleteTag(models.TagID(fmt.Sprintf("%s.%s", netI.NetID, models.OldRemoteAccessTagName)), true)
+	}
 }

+ 6 - 3
models/acl.go

@@ -25,14 +25,13 @@ const (
 	ICMP Protocol = "icmp"
 )
 
-type ServiceType string
-
 const (
 	Http        = "HTTP"
 	Https       = "HTTPS"
 	AllTCP      = "All TCP"
 	AllUDP      = "All UDP"
 	ICMPService = "ICMP"
+	SSH         = "SSH"
 	Custom      = "Custom"
 	Any         = "Any"
 )
@@ -58,7 +57,9 @@ type AclGroupType string
 const (
 	UserAclID                AclGroupType = "user"
 	UserGroupAclID           AclGroupType = "user-group"
-	DeviceAclID              AclGroupType = "tag"
+	NodeTagID                AclGroupType = "tag"
+	NodeID                   AclGroupType = "device"
+	EgressRange              AclGroupType = "egress-range"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerSubNetRangeAClID AclGroupType = "ipset"
 )
@@ -116,5 +117,7 @@ type AclRule struct {
 	AllowedProtocol Protocol                `json:"allowed_protocols"` // tcp, udp, etc.
 	AllowedPorts    []string                `json:"allowed_ports"`
 	Direction       AllowedTrafficDirection `json:"direction"` // single or two-way
+	Dst             []net.IPNet             `json:"dst"`
+	Dst6            []net.IPNet             `json:"dst6"`
 	Allowed         bool
 }

+ 51 - 29
models/api_node.go

@@ -8,37 +8,45 @@ import (
 	"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
 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 ==
 	DefaultACL        string              `json:"defaultacl,omitempty" validate:"checkyesornoorunset"`
 	IsFailOver        bool                `json:"is_fail_over"`
@@ -132,6 +140,19 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	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
 func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode := ApiNode{}
@@ -169,6 +190,7 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
+	apiNode.EgressGatewayRangesWithMetric = nm.EgressGatewayRequest.RangesWithMetric
 	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
 	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS

+ 4 - 1
models/extclient.go

@@ -55,11 +55,14 @@ func (ext *ExtClient) ConvertToStaticNode() Node {
 	}
 	return Node{
 		CommonNode: CommonNode{
-			Network: ext.Network,
+			Network:  ext.Network,
+			Address:  ext.AddressIPNet4(),
+			Address6: ext.AddressIPNet6(),
 		},
 		Tags:       ext.Tags,
 		IsStatic:   true,
 		StaticNode: *ext,
+		IsUserNode: ext.RemoteAccessClientID != "",
 		Mutex:      ext.Mutex,
 	}
 }

+ 10 - 6
models/mqtt.go

@@ -27,6 +27,7 @@ type HostPeerUpdate struct {
 	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
 	FwUpdate        FwUpdate              `json:"fw_update"`
 	ReplacePeers    bool                  `json:"replace_peers"`
+	NameServers     []string              `json:"name_servers"`
 	ServerConfig
 	OldPeerUpdateFields
 }
@@ -52,7 +53,6 @@ type IngressInfo struct {
 	Network6      net.IPNet   `json:"network6"`
 	StaticNodeIps []net.IP    `json:"static_node_ips"`
 	Rules         []FwRule    `json:"rules"`
-	AllowAll      bool        `json:"allow_all"`
 	EgressRanges  []net.IPNet `json:"egress_ranges"`
 	EgressRanges6 []net.IPNet `json:"egress_ranges6"`
 }
@@ -65,15 +65,19 @@ type EgressInfo struct {
 	Network6      net.IPNet            `json:"network6" yaml:"network6"`
 	EgressGwAddr6 net.IPNet            `json:"egress_gw_addr6" yaml:"egress_gw_addr6"`
 	EgressGWCfg   EgressGatewayRequest `json:"egress_gateway_cfg" yaml:"egress_gateway_cfg"`
+	EgressFwRules map[string]AclRule   `json:"egress_fw_rules"`
 }
 
 // EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
 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"`
+	PeerKey                string              `json:"peer_key"`
+	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"`
+	Network                string              `json:"network"`
 }
 
 // PeerRouteInfo - struct for peer info for an ext. client

+ 17 - 16
models/network.go

@@ -8,22 +8,23 @@ import (
 // Network Struct - contains info for a given unique network
 // At  some point, need to replace all instances of Name with something else like  Identifier
 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

+ 24 - 15
models/structs.go

@@ -32,17 +32,20 @@ type IngressGwUsers struct {
 
 // UserRemoteGws - struct to hold user's remote gws
 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"`
+	DnsAddress        string     `json:"dns_address"`
+	Addresses         string     `json:"addresses"`
 }
 
 // UserRAGs - struct for user access gws
@@ -150,12 +153,18 @@ type ExtPeersResponse struct {
 	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
 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

+ 5 - 1
models/tags.go

@@ -8,7 +8,8 @@ import (
 type TagID string
 
 const (
-	RemoteAccessTagName = "remote-access-gws"
+	OldRemoteAccessTagName = "remote-access-gws"
+	GwTagName              = "gateways"
 )
 
 func (id TagID) String() string {
@@ -23,6 +24,7 @@ type Tag struct {
 	ID        TagID     `json:"id"`
 	TagName   string    `json:"tag_name"`
 	Network   NetworkID `json:"network"`
+	ColorCode string    `json:"color_code"`
 	CreatedBy string    `json:"created_by"`
 	CreatedAt time.Time `json:"created_at"`
 }
@@ -30,6 +32,7 @@ type Tag struct {
 type CreateTagReq struct {
 	TagName     string    `json:"tag_name"`
 	Network     NetworkID `json:"network"`
+	ColorCode   string    `json:"color_code"`
 	TaggedNodes []ApiNode `json:"tagged_nodes"`
 }
 
@@ -48,5 +51,6 @@ type TagListRespNodes struct {
 type UpdateTagReq struct {
 	Tag
 	NewName     string    `json:"new_name"`
+	ColorCode   string    `json:"color_code"`
 	TaggedNodes []ApiNode `json:"tagged_nodes"`
 }

+ 1 - 0
models/user_mgmt.go

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

+ 1 - 1
pro/LICENSE

@@ -3,7 +3,7 @@ Copyright (c) 2022 Netmaker, Inc.
 
 With regard to the Netmaker Software:
 
-This software and associated documentation files (the "Software") may only be used in production, if you (and any entity that you represent) have agreed to, and are in compliance with, the Netmaker Subscription Terms of Service, available at https://netmaker.io/terms (the “Enterprise Terms”), or other agreement governing the use of the Software, as agreed by you and Netmaker, and otherwise have a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients. Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients as allocated by the license. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications. You are not granted any other rights beyond what is expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software.
+This software and associated documentation files (the "Software") may only be used in production, if you (and any entity that you represent) have agreed to, and are in compliance with, the Netmaker Subscription Terms of Service, available at https://www.netmaker.io/terms-and-conditions (the “Enterprise Terms”), or other agreement governing the use of the Software, as agreed by you and Netmaker, and otherwise have a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients. Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications and/or patches, and all such modifications and/or patches may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid Netmaker Enterprise license for the correct number of users, networks, nodes, servers, and external clients as allocated by the license. Notwithstanding the foregoing, you may copy and modify the Software for development and testing purposes, without requiring a subscription. You agree that Netmaker and/or its licensors (as applicable) retain all right, title and interest in and to all such modifications. You are not granted any other rights beyond what is expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 

+ 21 - 2
pro/auth/azure-ad.go

@@ -79,9 +79,18 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
-	// if user exists with provider ID, convert them into email ID
+
 	user, err := logic.GetUser(content.UserPrincipalName)
 	if err == nil {
+		// if user exists, then ensure user's auth type is
+		// oauth before proceeding.
+		if user.AuthType == models.BasicAuth {
+			logger.Log(0, "invalid auth type: basic_auth")
+			handleAuthTypeMismatch(w)
+			return
+		}
+
+		// if user exists with provider ID, convert them into email ID
 		_, err := logic.GetUser(content.Email)
 		if err != nil {
 			user.UserName = content.Email
@@ -91,7 +100,8 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 			database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME)
 		}
 	}
-	_, err = logic.GetUser(content.Email)
+
+	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			if inviteExists {
@@ -127,7 +137,16 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 			handleSomethingWentWrong(w)
 			return
 		}
+	} else {
+		// if user exists, then ensure user's auth type is
+		// oauth before proceeding.
+		if user.AuthType == models.BasicAuth {
+			logger.Log(0, "invalid auth type: basic_auth")
+			handleAuthTypeMismatch(w)
+			return
+		}
 	}
+
 	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		handleOauthUserNotFound(w)

+ 10 - 1
pro/auth/error.go

@@ -98,7 +98,7 @@ var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `<h2>Your Netmaker server
 var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
 
 var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account does not have access to the dashboard. Please contact your administrator for more information about your account.</h2>
-<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/docs/remote-access-client-rac#downloadinstallation" target="_blank" rel="noopener">our Remote Access Client.</a></p>`)
+<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/docs/remote-access-client-rac#downloadinstallation" target="_blank" rel="noopener">our Netmaker Desktop App.</a></p>`)
 
 var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
 
@@ -110,6 +110,9 @@ var somethingwentwrong = fmt.Sprintf(htmlBaseTemplate, `<h2>Something went wrong
 
 var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not allowed. Please contact your administrator.</h2>`)
 
+var authTypeMismatch = fmt.Sprintf(htmlBaseTemplate, `<h2>It looks like you already have an account with us using Basic Authentication.</h2>
+<p>To continue, please log in with your existing credentials or reset your password if needed.</p>`)
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
@@ -157,3 +160,9 @@ func handleSomethingWentWrong(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusInternalServerError)
 	response.Write([]byte(somethingwentwrong))
 }
+
+func handleAuthTypeMismatch(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusBadRequest)
+	response.Write([]byte(authTypeMismatch))
+}

+ 8 - 0
pro/auth/github.go

@@ -82,6 +82,14 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	// if user exists with provider ID, convert them into email ID
 	user, err := logic.GetUser(content.Login)
 	if err == nil {
+		// if user exists, then ensure user's auth type is
+		// oauth before proceeding.
+		if user.AuthType == models.BasicAuth {
+			logger.Log(0, "invalid auth type: basic_auth")
+			handleAuthTypeMismatch(w)
+			return
+		}
+
 		// checks if user exists with email
 		_, err := logic.GetUser(content.Email)
 		if err != nil {

+ 13 - 2
pro/auth/google.go

@@ -80,7 +80,8 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
-	_, err = logic.GetUser(content.Email)
+
+	user, err := logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			if inviteExists {
@@ -117,13 +118,23 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 			handleSomethingWentWrong(w)
 			return
 		}
+	} else {
+		// if user exists, then ensure user's auth type is
+		// oauth before proceeding.
+		if user.AuthType == models.BasicAuth {
+			logger.Log(0, "invalid auth type: basic_auth")
+			handleAuthTypeMismatch(w)
+			return
+		}
 	}
-	user, err := logic.GetUser(content.Email)
+
+	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		logger.Log(0, "error fetching user: ", err.Error())
 		handleOauthUserNotFound(w)
 		return
 	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 		handleSomethingWentWrong(w)

+ 12 - 2
pro/auth/oidc.go

@@ -91,7 +91,8 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
-	_, err = logic.GetUser(content.Email)
+
+	user, err := logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
 			if inviteExists {
@@ -127,8 +128,17 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 			handleSomethingWentWrong(w)
 			return
 		}
+	} else {
+		// if user exists, then ensure user's auth type is
+		// oauth before proceeding.
+		if user.AuthType == models.BasicAuth {
+			logger.Log(0, "invalid auth type: basic_auth")
+			handleAuthTypeMismatch(w)
+			return
+		}
 	}
-	user, err := logic.GetUser(content.Email)
+
+	user, err = logic.GetUser(content.Email)
 	if err != nil {
 		handleOauthUserNotFound(w)
 		return

+ 57 - 0
pro/controllers/users.go

@@ -8,7 +8,9 @@ import (
 	"net/http"
 	"net/url"
 	"strings"
+	"time"
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -19,6 +21,7 @@ import (
 	"github.com/gravitl/netmaker/pro/email"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
+	"github.com/gravitl/netmaker/utils"
 	"golang.org/x/exp/slog"
 )
 
@@ -411,6 +414,44 @@ func createUserGroup(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	networks, err := logic.GetNetworks()
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, network := range networks {
+		acl := models.Acl{
+			ID:          uuid.New().String(),
+			Name:        fmt.Sprintf("%s group", userGroupReq.Group.Name),
+			MetaData:    "This Policy allows user group to communicate with all gateways",
+			Default:     false,
+			ServiceType: models.Any,
+			NetworkID:   models.NetworkID(network.NetID),
+			Proto:       models.ALL,
+			RuleType:    models.UserPolicy,
+			Src: []models.AclPolicyTag{
+				{
+					ID:    models.UserGroupAclID,
+					Value: userGroupReq.Group.ID.String(),
+				},
+			},
+			Dst: []models.AclPolicyTag{
+				{
+					ID:    models.NodeTagID,
+					Value: fmt.Sprintf("%s.%s", models.NetworkID(network.NetID), models.GwTagName),
+				}},
+			AllowedDirection: models.TrafficDirectionUni,
+			Enabled:          true,
+			CreatedBy:        "auto",
+			CreatedAt:        time.Now().UTC(),
+		}
+		err = logic.InsertAcl(acl)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	}
+
 	for _, userID := range userGroupReq.Members {
 		user, err := logic.GetUser(userID)
 		if err != nil {
@@ -1034,6 +1075,8 @@ func getRemoteAccessGatewayConf(w http.ResponseWriter, r *http.Request) {
 		Metadata:          node.Metadata,
 		AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 		NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+		DnsAddress:        node.IngressDNS,
+		Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 	}
 
 	slog.Debug("returned user gw config", "user", user.UserName, "gws", userGw)
@@ -1102,6 +1145,10 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				slog.Error("failed to get node network", "error", err)
 				continue
 			}
+			nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
+			if len(nodesWithStatus) > 0 {
+				node = nodesWithStatus[0]
+			}
 
 			gws := userGws[node.Network]
 			if extClient.DNS == "" {
@@ -1120,6 +1167,9 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				Metadata:          node.Metadata,
 				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+				Status:            node.Status,
+				DnsAddress:        node.IngressDNS,
+				Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 			})
 			userGws[node.Network] = gws
 			delete(userGwNodes, node.ID.String())
@@ -1141,6 +1191,10 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 		if err != nil {
 			continue
 		}
+		nodesWithStatus := logic.AddStatusToNodes([]models.Node{node}, false)
+		if len(nodesWithStatus) > 0 {
+			node = nodesWithStatus[0]
+		}
 		network, err := logic.GetNetwork(node.Network)
 		if err != nil {
 			slog.Error("failed to get node network", "error", err)
@@ -1157,6 +1211,9 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 			Metadata:          node.Metadata,
 			AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			NetworkAddresses:  []string{network.AddressRange, network.AddressRange6},
+			Status:            node.Status,
+			DnsAddress:        node.IngressDNS,
+			Addresses:         utils.NoEmptyStringToCsv(node.Address.String(), node.Address6.String()),
 		})
 		userGws[node.Network] = gws
 	}

+ 3 - 2
pro/email/invite.go

@@ -2,6 +2,7 @@ package email
 
 import (
 	"fmt"
+
 	"github.com/gravitl/netmaker/models"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
@@ -31,11 +32,11 @@ func (invite UserInvitedMail) GetBody(info Notification) string {
 
 	content := invite.BodyBuilder.
 		WithParagraph("Hi,").
-		WithParagraph("You've been invited to access a secure network via Netmaker's Remote Access Client (RAC). Follow these simple steps to get connected:").
+		WithParagraph("You've been invited to access a secure network via Netmaker Desktop App. Follow these simple steps to get connected:").
 		WithHtml("<ol>").
 		WithHtml(fmt.Sprintf("<li>Click <a href=\"%s\">here</a> to accept your invitation and setup your account.</li>", invite.InviteURL)).
 		WithHtml("<br>").
-		WithHtml(fmt.Sprintf("<li><a href=\"%s\">Download the Remote Access Client (RAC)</a>.</li>", downloadLink))
+		WithHtml(fmt.Sprintf("<li><a href=\"%s\">Download the Netmaker Desktop App</a>.</li>", downloadLink))
 
 	if invite.PlatformRoleID == models.AdminRole.String() || invite.PlatformRoleID == models.PlatformUser.String() {
 		content = content.

+ 1 - 0
pro/initialize.go

@@ -133,6 +133,7 @@ func InitPro() {
 	logic.IntialiseGroups = proLogic.UserGroupsInit
 	logic.AddGlobalNetRolesToAdmins = proLogic.AddGlobalNetRolesToAdmins
 	logic.GetUserGroupsInNetwork = proLogic.GetUserGroupsInNetwork
+	logic.GetUserGroup = proLogic.GetUserGroup
 	logic.GetNodeStatus = proLogic.GetNodeStatus
 }
 

+ 3 - 3
pro/logic/status.go

@@ -41,7 +41,7 @@ func GetNodeStatus(node *models.Node, defaultEnabledPolicy bool) {
 			return
 		}
 		if !defaultEnabledPolicy {
-			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, ingNode, false)
+			allowed, _ := logic.IsNodeAllowedToCommunicateV1(*node, ingNode, false)
 			if !allowed {
 				node.Status = models.OnlineSt
 				return
@@ -161,7 +161,7 @@ func checkPeerStatus(node *models.Node, defaultAclPolicy bool) {
 		}
 
 		if !defaultAclPolicy {
-			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
+			allowed, _ := logic.IsNodeAllowedToCommunicateV1(*node, peer, false)
 			if !allowed {
 				continue
 			}
@@ -199,7 +199,7 @@ func checkPeerConnectivity(node *models.Node, metrics *models.Metrics, defaultAc
 		}
 
 		if !defaultAclPolicy {
-			allowed, _ := logic.IsNodeAllowedToCommunicate(*node, peer, false)
+			allowed, _ := logic.IsNodeAllowedToCommunicateV1(*node, peer, false)
 			if !allowed {
 				continue
 			}

+ 17 - 95
pro/logic/user_mgmt.go

@@ -40,7 +40,7 @@ var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
 var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{
 	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)),
 	Name:       "Network Users",
-	MetaData:   "Can connect to nodes in your networks via Remote Access Client.",
+	MetaData:   "Can connect to nodes in your networks via Netmaker Desktop App.",
 	Default:    true,
 	FullAccess: false,
 	NetworkID:  models.AllNetworks,
@@ -131,7 +131,7 @@ func UserGroupsInit() {
 				models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)): {},
 			},
 		},
-		MetaData: "Provides read-only dashboard access to platform users and allows connection to network nodes via the Remote Access Client.",
+		MetaData: "Provides read-only dashboard access to platform users and allows connection to network nodes via the Netmaker Desktop App.",
 	}
 	d, _ := json.Marshal(NetworkGlobalAdminGroup)
 	database.Insert(NetworkGlobalAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
@@ -156,7 +156,7 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 	var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{
 		ID:                  models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)),
 		Name:                fmt.Sprintf("%s User", netID),
-		MetaData:            fmt.Sprintf("Can connect to nodes in your network `%s` via Remote Access Client.", netID),
+		MetaData:            fmt.Sprintf("Can connect to nodes in your network `%s` via Netmaker Desktop App.", netID),
 		Default:             true,
 		FullAccess:          false,
 		NetworkID:           netID,
@@ -216,8 +216,9 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 
 	// create default network groups
 	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{}{
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
@@ -226,14 +227,15 @@ func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
 		MetaData: fmt.Sprintf("can manage your network `%s` configuration including adding and removing devices.", netID),
 	}
 	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{}{
 			netID: {
 				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},
 			},
 		},
-		MetaData: fmt.Sprintf("Can connect to nodes in your network `%s` via Remote Access Client. Platform users will have read-only access to the the dashboard.", netID),
+		MetaData: fmt.Sprintf("Can connect to nodes in your network `%s` via Netmaker Desktop App. Platform users will have read-only access to the the dashboard.", netID),
 	}
 	d, _ = json.Marshal(NetworkAdminGroup)
 	database.Insert(NetworkAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
@@ -808,87 +810,7 @@ func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[mode
 }
 
 func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filteredNodes []models.Node) {
-
-	nodesMap := make(map[string]struct{})
-	allNetworkRoles := make(map[models.UserRoleID]struct{})
-	defer func() {
-		filteredNodes = logic.AddStaticNodestoList(filteredNodes)
-	}()
-	if len(user.NetworkRoles) > 0 {
-		for _, netRoles := range user.NetworkRoles {
-			for netRoleI := range netRoles {
-				allNetworkRoles[netRoleI] = struct{}{}
-			}
-		}
-	}
-	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
-		filteredNodes = nodes
-		return
-	}
-	if len(user.UserGroups) > 0 {
-		for userGID := range user.UserGroups {
-			userG, err := GetUserGroup(userGID)
-			if err == nil {
-				if len(userG.NetworkRoles) > 0 {
-					if _, ok := userG.NetworkRoles[models.AllNetworks]; ok {
-						filteredNodes = nodes
-						return
-					}
-					for _, netRoles := range userG.NetworkRoles {
-						for netRoleI := range netRoles {
-							allNetworkRoles[netRoleI] = struct{}{}
-						}
-					}
-				}
-			}
-		}
-	}
-	for networkRoleID := range allNetworkRoles {
-		userPermTemplate, err := logic.GetRole(networkRoleID)
-		if err != nil {
-			continue
-		}
-		networkNodes := logic.GetNetworkNodesMemory(nodes, userPermTemplate.NetworkID.String())
-		if userPermTemplate.FullAccess {
-			for _, node := range networkNodes {
-				if _, ok := nodesMap[node.ID.String()]; ok {
-					continue
-				}
-				nodesMap[node.ID.String()] = struct{}{}
-				filteredNodes = append(filteredNodes, node)
-			}
-
-			continue
-		}
-		if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
-			if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok {
-				for _, node := range networkNodes {
-					if _, ok := nodesMap[node.ID.String()]; ok {
-						continue
-					}
-					if node.IsIngressGateway {
-						nodesMap[node.ID.String()] = struct{}{}
-						filteredNodes = append(filteredNodes, node)
-					}
-				}
-			} else {
-				for gwID, scope := range rsrcPerms {
-					if _, ok := nodesMap[gwID.String()]; ok {
-						continue
-					}
-					if scope.Read {
-						gwNode, err := logic.GetNodeByID(gwID.String())
-						if err == nil && gwNode.IsIngressGateway {
-							nodesMap[gwNode.ID.String()] = struct{}{}
-							filteredNodes = append(filteredNodes, gwNode)
-						}
-					}
-				}
-			}
-		}
-
-	}
-	return
+	return filteredNodes
 }
 
 func FilterNetworksByRole(allnetworks []models.Network, user models.User) []models.Network {
@@ -1209,7 +1131,7 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
 		defaultUserAcl := models.Acl{
 			ID:          fmt.Sprintf("%s.%s-grp", netID, models.NetworkAdmin),
 			Name:        "Network Admin",
-			MetaData:    "This Policy allows all network admins to communicate with all remote access gateways",
+			MetaData:    "This Policy allows all network admins to communicate with all gateways",
 			Default:     true,
 			ServiceType: models.Any,
 			NetworkID:   netID,
@@ -1227,8 +1149,8 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
 			},
 			Dst: []models.AclPolicyTag{
 				{
-					ID:    models.DeviceAclID,
-					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+					ID:    models.NodeTagID,
+					Value: fmt.Sprintf("%s.%s", netID, models.GwTagName),
 				}},
 			AllowedDirection: models.TrafficDirectionUni,
 			Enabled:          true,
@@ -1242,7 +1164,7 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
 		defaultUserAcl := models.Acl{
 			ID:          fmt.Sprintf("%s.%s-grp", netID, models.NetworkUser),
 			Name:        "Network User",
-			MetaData:    "This Policy allows all network users to communicate with all remote access gateways",
+			MetaData:    "This Policy allows all network users to communicate with all gateways",
 			Default:     true,
 			ServiceType: models.Any,
 			NetworkID:   netID,
@@ -1261,8 +1183,8 @@ func CreateDefaultUserPolicies(netID models.NetworkID) {
 
 			Dst: []models.AclPolicyTag{
 				{
-					ID:    models.DeviceAclID,
-					Value: fmt.Sprintf("%s.%s", netID, models.RemoteAccessTagName),
+					ID:    models.NodeTagID,
+					Value: fmt.Sprintf("%s.%s", netID, models.GwTagName),
 				}},
 			AllowedDirection: models.TrafficDirectionUni,
 			Enabled:          true,

+ 11 - 12
release.md

@@ -1,21 +1,20 @@
-# Netmaker v0.30.0
+# Netmaker v0.90.0
 
 ## Whats New ✨
-- Advanced ACL Rules - port, protocol and traffic direction
-- Reduced Firewall Requirements To One Single Port (443 udp/tcp)
-- Option to Turn off STUN or specify custom stun servers
-- Improved Connectivity Status Indicator with real-time troubleshooting help.
+- ACL Rules for Egress Ranges
+- High Availability for Egress Routes
+- Remote Access Gateways and Relays have been merged together into  "Gateways" and are now available on CE.
+- Enchanced Graph Page
+- Ability to Define Additional Nameservers in your network
 
 ## What's Fixed/Improved 🛠
 - Metrics Data
-- Optimised MQ message size
-- FailOver Stability Fixes
-- Scalability Fixes
-- Duplicate Node IP check on update
+- IPv6 DNS Entries Are Not Working.
+- FailOver connection improvements.
+- Optimized Failover peer signaling.
+- Improved Connectivity Status Indicator with real-time troubleshooting help.
 
 ## Known Issues 🐞
 
-- IPv6 DNS Entries Are Not Working.
-- Stale Peer On The Interface, When Forced Removed From Multiple Networks At Once.
-- WireGuard DNS issue on most flavours of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
+- WireGuard DNS issue on Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
 

+ 3 - 1
scripts/netmaker.default.env

@@ -73,6 +73,8 @@ OIDC_ISSUER=
 JWT_VALIDITY_DURATION=43200
 # Auto disable a user's connecteds clients bassed on JWT token expiration
 RAC_AUTO_DISABLE=false
+# Allow a user to connect to multiple networks simultaneously
+RAC_RESTRICT_TO_SINGLE_NETWORK=false
 # if turned on data will be cached on to improve performance significantly (IMPORTANT: If HA set to `false` )
 CACHING_ENABLED=true
 # if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint
@@ -91,7 +93,7 @@ EMAIL_SENDER_PASSWORD=
 # default domain for internal DNS lookup
 DEFAULT_DOMAIN=hosted.nm
 # managed dns setting, set to true to resolve dns entries on netmaker network
-MANAGE_DNS=false
+MANAGE_DNS=true
 # set to true, old acl is supported, otherwise, old acl is disabled
 OLD_ACL_SUPPORT=true
 # if STUN is set to true, hole punch is called

+ 2 - 2
scripts/nm-quick.sh

@@ -253,10 +253,10 @@ save_config() { (
 	fi
 	# copy entries from the previous config
 	local toCopy=("SERVER_HOST" "SERVER_HOST6" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD" "LICENSE_KEY" "NETMAKER_TENANT_ID"
-		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
+		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT" "MANAGE_DNS" "DEFAULT_DOMAIN"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
 		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "ALLOWED_EMAIL_DOMAINS" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
-		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION"
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "RAC_RESTRICT_TO_SINGLE_NETWORK" "CACHING_ENABLED" "ENDPOINT_DETECTION"
 		"SMTP_HOST" "SMTP_PORT" "EMAIL_SENDER_ADDR" "EMAIL_SENDER_USER" "EMAIL_SENDER_PASSWORD")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"

+ 1 - 1
scripts/nm-upgrade.sh

@@ -179,7 +179,7 @@ save_config() { (
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "STUN_PORT" "VERBOSITY"
 		"TURN_PORT" "USE_TURN" "DEBUG_MODE" "TURN_API_PORT" "REST_BACKEND"
 		"DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
-		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE")
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "RAC_RESTRICT_TO_SINGLE_NETWORK")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 	done

+ 6 - 0
servercfg/serverconf.go

@@ -94,6 +94,7 @@ func GetServerConfig() config.ServerConfig {
 	}
 	cfg.JwtValidityDuration = GetJwtValidityDuration()
 	cfg.RacAutoDisable = GetRacAutoDisable()
+	cfg.RacRestrictToSingleNetwork = GetRacRestrictToSingleNetwork()
 	cfg.MetricInterval = GetMetricInterval()
 	cfg.ManageDNS = GetManageDNS()
 	cfg.Stun = IsStunEnabled()
@@ -120,6 +121,11 @@ func GetRacAutoDisable() bool {
 	return os.Getenv("RAC_AUTO_DISABLE") == "true"
 }
 
+// GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
+func GetRacRestrictToSingleNetwork() bool {
+	return os.Getenv("RAC_RESTRICT_TO_SINGLE_NETWORK") == "true"
+}
+
 // GetServerInfo - gets the server config into memory from file or env
 func GetServerInfo() models.ServerConfig {
 	var cfg models.ServerConfig

+ 1 - 1
swagger.yaml

@@ -1466,7 +1466,7 @@ info:
   contact: {}
   description: NetMaker API Docs
   title: NetMaker
-  version: 0.30.0
+  version: 0.90.0
 paths:
   /api/dns:
     get:

+ 16 - 0
utils/utils.go

@@ -3,6 +3,7 @@ package utils
 import (
 	"log/slog"
 	"runtime"
+	"strings"
 	"time"
 )
 
@@ -59,3 +60,18 @@ func TraceCaller() {
 	slog.Debug("Called from function: %s\n", "func-name", funcName)
 	slog.Debug("File: %s, Line: %d\n", "file", file, "line-no", line)
 }
+
+// NoEmptyStringToCsv takes a bunch of strings, filters out empty ones and returns a csv version of the string
+func NoEmptyStringToCsv(strs ...string) string {
+	var sb strings.Builder
+	for _, str := range strs {
+		trimmedStr := strings.TrimSpace(str)
+		if trimmedStr != "" && trimmedStr != "<nil>" {
+			if sb.Len() > 0 {
+				sb.WriteString(", ")
+			}
+			sb.WriteString(str)
+		}
+	}
+	return sb.String()
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов