Selaa lähdekoodia

Merge pull request #2901 from gravitl/release-v0.24.0

v0.24.0
Abhishek K 1 vuosi sitten
vanhempi
commit
5693384ec3
77 muutettua tiedostoa jossa 1553 lisäystä ja 441 poistoa
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 0
      .gitignore
  3. 1 1
      README.md
  4. 32 19
      auth/auth.go
  5. 24 3
      auth/azure-ad.go
  6. 51 1
      auth/error.go
  7. 24 3
      auth/github.go
  8. 25 3
      auth/google.go
  9. 13 8
      auth/headless_callback.go
  10. 9 1
      auth/host_session.go
  11. 24 3
      auth/oidc.go
  12. 20 0
      cli/cmd/failover/disable.go
  13. 20 0
      cli/cmd/failover/enable.go
  14. 28 0
      cli/cmd/failover/root.go
  15. 3 0
      cli/cmd/host/update.go
  16. 3 0
      cli/cmd/network/create.go
  17. 5 4
      cli/cmd/node/create_ingress.go
  18. 5 4
      cli/cmd/node/delete_ingress.go
  19. 1 1
      cli/cmd/node/list.go
  20. 2 0
      cli/cmd/root.go
  21. 1 1
      cli/functions/acl.go
  22. 18 0
      cli/functions/failover.go
  23. 1 1
      compose/docker-compose.netclient.yml
  24. 2 0
      config/config.go
  25. 32 4
      controllers/docs.go
  26. 2 2
      controllers/enrollmentkeys.go
  27. 7 8
      controllers/ext_client.go
  28. 62 28
      controllers/hosts.go
  29. 5 0
      controllers/server.go
  30. 139 0
      controllers/user.go
  31. 3 1
      database/database.go
  32. 7 7
      go.mod
  33. 27 17
      go.sum
  34. 1 1
      k8s/client/netclient-daemonset.yaml
  35. 1 1
      k8s/client/netclient.yaml
  36. 1 1
      k8s/server/netmaker-ui.yaml
  37. 4 2
      logic/acls/common.go
  38. 7 2
      logic/acls/nodeacls/retrieve.go
  39. 33 7
      logic/auth.go
  40. 2 2
      logic/errors.go
  41. 5 0
      logic/gateway.go
  42. 5 0
      logic/hosts.go
  43. 0 1
      logic/jwts.go
  44. 1 1
      logic/networks.go
  45. 15 0
      logic/nodes.go
  46. 26 7
      logic/peers.go
  47. 7 15
      logic/telemetry.go
  48. 44 0
      logic/users.go
  49. 1 1
      logic/util.go
  50. 1 1
      logic/zombie.go
  51. 2 2
      main.go
  52. 2 2
      migrate/migrate.go
  53. 40 23
      models/api_host.go
  54. 1 0
      models/host.go
  55. 17 15
      models/mqtt.go
  56. 1 1
      models/node.go
  57. 12 11
      models/structs.go
  58. 3 138
      mq/emqx_cloud.go
  59. 19 3
      mq/handlers.go
  60. 24 7
      mq/mq.go
  61. 6 1
      pro/controllers/failover.go
  62. 9 0
      pro/controllers/inet_gws.go
  63. 5 2
      pro/controllers/metrics.go
  64. 13 4
      pro/controllers/middleware.go
  65. 1 0
      pro/initialize.go
  66. 6 0
      pro/logic/failover.go
  67. 8 19
      pro/logic/metrics.go
  68. 25 3
      pro/logic/nodes.go
  69. 1 1
      pro/logic/relays.go
  70. 1 1
      pro/trial.go
  71. 1 0
      pro/types.go
  72. 4 0
      pro/util.go
  73. 16 11
      release.md
  74. 4 0
      scripts/netmaker.default.env
  75. 32 4
      scripts/nm-quick.sh
  76. 21 1
      servercfg/serverconf.go
  77. 527 30
      swagger.yml

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

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.24.0
         - v0.23.0
         - v0.22.0
         - v0.21.2

+ 1 - 0
.gitignore

@@ -25,3 +25,4 @@ data/
 netmaker.exe
 netmaker.code-workspace
 dist/
+nmctl

+ 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.23.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.24.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" />

+ 32 - 19
auth/auth.go

@@ -32,7 +32,6 @@ const (
 	github_provider_name   = "github"
 	oidc_provider_name     = "oidc"
 	verify_user            = "verifyuser"
-	auth_key               = "netmaker_auth"
 	user_signin_length     = 16
 	node_signin_length     = 64
 	headless_signin_length = 32
@@ -75,10 +74,10 @@ func InitializeAuthProvider() string {
 	if functions == nil {
 		return ""
 	}
-	var _, err = fetchPassValue(logic.RandomString(64))
+	logger.Log(0, "setting oauth secret")
+	var err = logic.SetAuthSecret(logic.RandomString(64))
 	if err != nil {
-		logger.Log(0, err.Error())
-		return ""
+		logger.FatalLog("failed to set auth_secret", err.Error())
 	}
 	var authInfo = servercfg.GetAuthProviderInfo()
 	var serverConn = servercfg.GetAPIHost()
@@ -156,7 +155,7 @@ func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
-	var currentValue, err = fetchPassValue("")
+	var currentValue, err = FetchPassValue("")
 	if err != nil {
 		return err
 	}
@@ -246,8 +245,9 @@ func addUser(email string) error {
 		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
 		return err
 	} // generate random password to adapt to current model
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
+		slog.Error("failed to get password", "error", err.Error())
 		return fetchErr
 	}
 	var newUser = models.User{
@@ -255,6 +255,7 @@ func addUser(email string) error {
 		Password: newPass,
 	}
 	if !hasSuperAdmin { // must be first attempt, create a superadmin
+		logger.Log(0, "creating superadmin")
 		if err = logic.CreateSuperAdmin(&newUser); err != nil {
 			slog.Error("error creating super admin from user", "email", email, "error", err)
 		} else {
@@ -264,7 +265,7 @@ func addUser(email string) error {
 		// TODO: add ability to add users with preemptive permissions
 		newUser.IsAdmin = false
 		if err = logic.CreateUser(&newUser); err != nil {
-			logger.Log(1, "error creating user,", email, "; user not added")
+			logger.Log(0, "error creating user,", email, "; user not added", "error", err.Error())
 		} else {
 			logger.Log(0, "user created from ", email)
 		}
@@ -272,25 +273,17 @@ func addUser(email string) error {
 	return nil
 }
 
-func fetchPassValue(newValue string) (string, error) {
+func FetchPassValue(newValue string) (string, error) {
 
 	type valueHolder struct {
 		Value string `json:"value" bson:"value"`
 	}
-	var b64NewValue = base64.StdEncoding.EncodeToString([]byte(newValue))
-	var newValueHolder = &valueHolder{
-		Value: b64NewValue,
-	}
-	var data, marshalErr = json.Marshal(newValueHolder)
-	if marshalErr != nil {
-		return "", marshalErr
-	}
-
-	var currentValue, err = logic.FetchAuthSecret(auth_key, string(data))
+	newValueHolder := valueHolder{}
+	var currentValue, err = logic.FetchAuthSecret()
 	if err != nil {
 		return "", err
 	}
-	var unmarshErr = json.Unmarshal([]byte(currentValue), newValueHolder)
+	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
 	if unmarshErr != nil {
 		return "", unmarshErr
 	}
@@ -334,3 +327,23 @@ func isStateCached(state string) bool {
 	_, err := netcache.Get(state)
 	return err == nil || strings.Contains(err.Error(), "expired")
 }
+
+// isEmailAllowed - checks if email is allowed to signup
+func isEmailAllowed(email string) bool {
+	allowedDomains := servercfg.GetAllowedEmailDomains()
+	domains := strings.Split(allowedDomains, ",")
+	if len(domains) == 1 && domains[0] == "*" {
+		return true
+	}
+	emailParts := strings.Split(email, "@")
+	if len(emailParts) < 2 {
+		return false
+	}
+	baseDomainOfEmail := emailParts[1]
+	for _, domain := range domains {
+		if domain == baseDomainOfEmail {
+			return true
+		}
+	}
+	return false
+}

+ 24 - 3
auth/azure-ad.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -60,9 +61,29 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
+	if !isEmailAllowed(content.UserPrincipalName) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.UserPrincipalName) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.UserPrincipalName)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.UserPrincipalName); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.UserPrincipalName,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 		}
 	}
@@ -75,7 +96,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 51 - 1
auth/error.go

@@ -12,17 +12,44 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
-<h3>Only Admins are allowed to access Dashboard.</h3>
+<h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
 <p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
 </body>
 </html>
 `
+
+const userFirstTimeSignUp = `<!DOCTYPE html><html>
+<body>
+<h3>Thank you for signing up. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
+const userSignUpApprovalPending = `<!DOCTYPE html><html>
+<body>
+<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
 const userNotFound = `<!DOCTYPE html><html>
 <body>
 <h3>User Not Found.</h3>
 </body>
 </html>`
 
+const somethingwentwrong = `<!DOCTYPE html><html>
+<body>
+<h3>Something went wrong. Contact Admin.</h3>
+</body>
+</html>`
+
+const notallowedtosignup = `<!DOCTYPE html><html>
+<body>
+<h3>Your email is not allowed. Please contact your administrator.</h3>
+</body>
+</html>`
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
@@ -34,6 +61,23 @@ func handleOauthUserNotAllowed(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusForbidden)
 	response.Write([]byte(userNotAllowed))
 }
+func handleFirstTimeOauthUserSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userFirstTimeSignUp))
+}
+
+func handleOauthUserSignUpApprovalPending(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userSignUpApprovalPending))
+}
+
+func handleOauthUserNotAllowedToSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(notallowedtosignup))
+}
 
 // handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
 func handleOauthNotConfigured(response http.ResponseWriter) {
@@ -41,3 +85,9 @@ func handleOauthNotConfigured(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusInternalServerError)
 	response.Write([]byte(oauthNotConfigured))
 }
+
+func handleSomethingWentWrong(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusInternalServerError)
+	response.Write([]byte(somethingwentwrong))
+}

+ 24 - 3
auth/github.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -60,9 +61,29 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
+	if !isEmailAllowed(content.Login) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Login) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Login)
-	if err != nil { // user must not exist, so try to make one
-		if err = addUser(content.Login); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Login,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 		}
 	}
@@ -75,7 +96,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 25 - 3
auth/google.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -62,14 +63,35 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 		}
 	}
 	user, err := logic.GetUser(content.Email)
 	if err != nil {
+		logger.Log(0, "error fetching user: ", err.Error())
 		handleOauthUserNotFound(w)
 		return
 	}
@@ -77,7 +99,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 13 - 8
auth/headless_callback.go

@@ -50,19 +50,24 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	_, err = logic.GetUser(userClaims.getUserName())
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(userClaims.getUserName()); err != nil {
-			logger.Log(1, "could not create new user: ", userClaims.getUserName())
-			return
-		}
+	// check if user approval is already pending
+	if logic.IsPendingUser(userClaims.getUserName()) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
+	user, err := logic.GetUser(userClaims.getUserName())
+	if err != nil {
+		response := returnErrTemplate("", "user not found", state, reqKeyIf)
+		w.WriteHeader(http.StatusForbidden)
+		w.Write(response)
+		return
 	}
-	newPass, fetchErr := fetchPassValue("")
+	newPass, fetchErr := FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
-		UserName: userClaims.getUserName(),
+		UserName: user.UserName,
 		Password: newPass,
 	})
 	if jwtErr != nil {

+ 9 - 1
auth/host_session.go

@@ -248,12 +248,20 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 				// check if relay node exists and acting as relay
 				relaynode, err := logic.GetNodeByID(relayNodeId.String())
 				if err == nil && relaynode.IsRelay {
+					slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
 					newNode.IsRelayed = true
 					newNode.RelayedBy = relayNodeId.String()
-					slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
+					updatedRelayNode := relaynode
+					updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
+					logic.UpdateRelayed(&relaynode, &updatedRelayNode)
+					if err := logic.UpsertNode(&updatedRelayNode); err != nil {
+						slog.Error("failed to update node", "nodeid", relayNodeId.String())
+					}
 					if err := logic.UpsertNode(newNode); err != nil {
 						slog.Error("failed to update node", "nodeid", relayNodeId.String())
 					}
+				} else {
+					slog.Error("failed to relay node. maybe specified relay node is actually not a relay?", "err", err)
 				}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)

+ 24 - 3
auth/oidc.go

@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -73,9 +74,29 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 		}
 	}
@@ -88,7 +109,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 20 - 0
cli/cmd/failover/disable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var disableFailoverCmd = &cobra.Command{
+	Use:   "disable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Disable failover for a given Node",
+	Long:  `Disable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DisableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(disableFailoverCmd)
+}

+ 20 - 0
cli/cmd/failover/enable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var enableFailoverCmd = &cobra.Command{
+	Use:   "enable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Enable failover for a given Node",
+	Long:  `Enable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.EnableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(enableFailoverCmd)
+}

+ 28 - 0
cli/cmd/failover/root.go

@@ -0,0 +1,28 @@
+package failover
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "failover",
+	Short: "Enable/Disable failover for a node associated with a network",
+	Long:  `Enable/Disable failover for a node associated with a network`,
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}

+ 3 - 0
cli/cmd/host/update.go

@@ -14,6 +14,7 @@ import (
 var (
 	apiHostFilePath string
 	endpoint        string
+	endpoint6       string
 	name            string
 	listenPort      int
 	mtu             int
@@ -40,6 +41,7 @@ var hostUpdateCmd = &cobra.Command{
 		} else {
 			apiHost.ID = args[0]
 			apiHost.EndpointIP = endpoint
+			apiHost.EndpointIPv6 = endpoint6
 			apiHost.Name = name
 			apiHost.ListenPort = listenPort
 			apiHost.MTU = mtu
@@ -54,6 +56,7 @@ var hostUpdateCmd = &cobra.Command{
 func init() {
 	hostUpdateCmd.Flags().StringVar(&apiHostFilePath, "file", "", "Path to host_definition.json")
 	hostUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the Host")
+	hostUpdateCmd.Flags().StringVar(&endpoint6, "endpoint6", "", "IPv6 Endpoint of the Host")
 	hostUpdateCmd.Flags().StringVar(&name, "name", "", "Host name")
 	hostUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Listen port of the host")
 	hostUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "Host MTU size")

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

@@ -32,6 +32,9 @@ var networkCreateCmd = &cobra.Command{
 				network.AddressRange6 = address6
 				network.IsIPv6 = "yes"
 			}
+			if address == "" {
+				network.IsIPv4 = "no"
+			}
 			if udpHolePunch {
 				network.DefaultUDPHolePunch = "yes"
 			}

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

@@ -6,10 +6,11 @@ import (
 )
 
 var nodeCreateIngressCmd = &cobra.Command{
-	Use:   "create_ingress [NETWORK NAME] [NODE ID]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Turn a Node into a Ingress",
-	Long:  `Turn a Node into a Ingress`,
+	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"},
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
 	},

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

@@ -6,10 +6,11 @@ import (
 )
 
 var nodeDeleteIngressCmd = &cobra.Command{
-	Use:   "delete_ingress [NETWORK NAME] [NODE ID]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Delete Ingress role from a Node",
-	Long:  `Delete Ingress role from a Node`,
+	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"},
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
 	},

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

@@ -29,7 +29,7 @@ var nodeListCmd = &cobra.Command{
 			functions.PrettyPrint(data)
 		default:
 			table := tablewriter.NewWriter(os.Stdout)
-			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Ingress", "Relay"})
+			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Remote Access Gateway", "Relay"})
 			for _, d := range data {
 				addresses := ""
 				if d.Address != "" {

+ 2 - 0
cli/cmd/root.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
+	"github.com/gravitl/netmaker/cli/cmd/failover"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
@@ -53,4 +54,5 @@ func init() {
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
+	rootCmd.AddCommand(failover.GetRoot())
 }

+ 1 - 1
cli/functions/acl.go

@@ -14,5 +14,5 @@ func GetACL(networkName string) *acls.ACLContainer {
 
 // UpdateACL - update an ACL
 func UpdateACL(networkName string, payload *acls.ACLContainer) *acls.ACLContainer {
-	return request[acls.ACLContainer](http.MethodPut, fmt.Sprintf("/api/networks/%s/acls", networkName), payload)
+	return request[acls.ACLContainer](http.MethodPut, fmt.Sprintf("/api/networks/%s/acls/v2", networkName), payload)
 }

+ 18 - 0
cli/functions/failover.go

@@ -0,0 +1,18 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// EnableNodeFailover - Enable failover for a given Node
+func EnableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodPost, fmt.Sprintf("/api/v1/node/%s/failover", nodeID), nil)
+}
+
+// DisableNodeFailover - Disable failover for a given Node
+func DisableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/node/%s/failover", 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.23.0'
+    image: 'gravitl/netclient:v0.24.0'
     hostname: netmaker-1
     network_mode: host
     restart: on-failure

+ 2 - 0
config/config.go

@@ -92,6 +92,8 @@ type ServerConfig struct {
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
+	EndpointDetection          bool          `json:"endpoint_detection"`
+	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 }
 
 // SQLConfig - Generic SQL Config

+ 32 - 4
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.23.0
+//	Version: 0.24.0
 //	Host: api.demo.netmaker.io
 //
 //	Consumes:
@@ -49,6 +49,12 @@ type hasAdmin struct {
 	Admin bool
 }
 
+// swagger:response apiHostSliceResponse
+type apiHostSliceResponse struct {
+	// in: body
+	Host []models.ApiHost
+}
+
 // swagger:response apiHostResponse
 type apiHostResponse struct {
 	// in: body
@@ -251,7 +257,7 @@ type networkBodyResponse struct {
 	Network models.Network `json:"network"`
 }
 
-// swagger:parameters updateNetworkACL getNetworkACL
+// swagger:parameters updateNetworkACL
 type aclContainerBodyParam struct {
 	// ACL Container
 	// in: body
@@ -269,7 +275,7 @@ type aclContainerResponse struct {
 type nodeSliceResponse struct {
 	// Nodes
 	// in: body
-	Nodes []models.LegacyNode `json:"nodes"`
+	Nodes []models.ApiNode `json:"nodes"`
 }
 
 // swagger:response nodeResponse
@@ -348,8 +354,26 @@ type HostFromNetworkParams struct {
 	Network string `json:"network"`
 }
 
+// swagger:parameters createEnrollmentKey
+type createEnrollmentKeyParams struct {
+	// APIEnrollmentKey
+	// in: body
+	Body models.APIEnrollmentKey `json:"body"`
+}
+
+// swagger:parameters updateEnrollmentKey
+type updateEnrollmentKeyParams struct {
+	// KeyID
+	// in: path
+	KeyID string `json:"keyid"`
+
+	// APIEnrollmentKey
+	// in: body
+	Body models.APIEnrollmentKey `json:"body"`
+}
+
 // swagger:parameters deleteEnrollmentKey
-type DeleteEnrollmentKeyParam struct {
+type deleteEnrollmentKeyParam struct {
 	// in: path
 	KeyID string `json:"keyid"`
 }
@@ -435,6 +459,7 @@ func useUnused() bool {
 	_ = userAuthBodyParam{}
 	_ = usernamePathParam{}
 	_ = hasAdmin{}
+	_ = apiHostSliceResponse{}
 	_ = apiHostResponse{}
 	_ = fileResponse{}
 	_ = extClientConfParams{}
@@ -443,5 +468,8 @@ func useUnused() bool {
 	_ = signal{}
 	_ = filenameToGet{}
 	_ = dnsNetworkPathParam{}
+	_ = createEnrollmentKeyParams{}
+	_ = updateEnrollmentKeyParams{}
+	_ = deleteEnrollmentKeyParam{}
 	return false
 }

+ 2 - 2
controllers/enrollmentkeys.go

@@ -180,7 +180,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 }
 
-// swagger:route PUT /api/v1/enrollment-keys/:id enrollmentKeys updateEnrollmentKey
+// swagger:route PUT /api/v1/enrollment-keys/{keyid} enrollmentKeys updateEnrollmentKey
 //
 // Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
 //
@@ -308,7 +308,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	if !hostExists {
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		// register host
-		logic.CheckHostPorts(&newHost)
+		//logic.CheckHostPorts(&newHost)
 		// create EMQX credentials and ACLs for host
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {

+ 7 - 8
controllers/ext_client.go

@@ -394,9 +394,9 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		}
 		for _, extclient := range extclients {
 			if extclient.RemoteAccessClientID != "" &&
-				extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID && nodeid == extclient.IngressGatewayID {
+				extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID && extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
 				// extclient on the gw already exists for the remote access client
-				err = errors.New("remote client config already exists on the gateway. it may have been created by another user with this same remote client machine")
+				err = errors.New("remote client config already exists on the gateway")
 				slog.Error("failed to create extclient", "user", userName, "error", err)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				return
@@ -436,15 +436,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := logic.SetClientDefaultACLs(&extclient); err != nil {
-		slog.Error("failed to set default acls for extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
 	slog.Info("created extclient", "user", r.Header.Get("user"), "network", node.Network, "clientid", extclient.ClientID)
 	w.WriteHeader(http.StatusOK)
 	go func() {
+		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
+			slog.Error("failed to set default acls for extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
 		if err := mq.PublishPeerUpdate(false); err != nil {
 			logger.Log(1, "error setting ext peers on "+nodeid+": "+err.Error())
 		}

+ 62 - 28
controllers/hosts.go

@@ -32,6 +32,7 @@ func hostHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
+	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 
@@ -61,7 +62,7 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 //	  		oauth
 //
 //			Responses:
-//				200: apiHostResponse
+//				200: apiHostSliceResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
 	currentHosts, err := logic.GetAllHosts()
 	if err != nil {
@@ -133,17 +134,18 @@ func pull(w http.ResponseWriter, r *http.Request) {
 
 	serverConf.TrafficKey = key
 	response := models.HostPull{
-		Host:            *host,
-		Nodes:           logic.GetHostNodes(host),
-		ServerConfig:    serverConf,
-		Peers:           hPU.Peers,
-		PeerIDs:         hPU.PeerIDs,
-		HostNetworkInfo: hPU.HostNetworkInfo,
-		EgressRoutes:    hPU.EgressRoutes,
-		FwUpdate:        hPU.FwUpdate,
-		ChangeDefaultGw: hPU.ChangeDefaultGw,
-		DefaultGwIp:     hPU.DefaultGwIp,
-		IsInternetGw:    hPU.IsInternetGw,
+		Host:              *host,
+		Nodes:             logic.GetHostNodes(host),
+		ServerConfig:      serverConf,
+		Peers:             hPU.Peers,
+		PeerIDs:           hPU.PeerIDs,
+		HostNetworkInfo:   hPU.HostNetworkInfo,
+		EgressRoutes:      hPU.EgressRoutes,
+		FwUpdate:          hPU.FwUpdate,
+		ChangeDefaultGw:   hPU.ChangeDefaultGw,
+		DefaultGwIp:       hPU.DefaultGwIp,
+		IsInternetGw:      hPU.IsInternetGw,
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 
 	logger.Log(1, hostID, "completed a pull")
@@ -552,26 +554,27 @@ func authenticateHost(response http.ResponseWriter, request *http.Request) {
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 	}
-
-	// Create EMQX creds and ACLs if not found
-	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
-		if err := mq.GetEmqxHandler().CreateEmqxUser(host.ID.String(), authRequest.Password); err != nil {
-			slog.Error("failed to create host credentials for EMQX: ", err.Error())
-		} else {
-			if err := mq.GetEmqxHandler().CreateHostACL(host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
-				slog.Error("failed to add host ACL rules to EMQX: ", err.Error())
-			}
-			for _, nodeID := range host.Nodes {
-				if node, err := logic.GetNodeByID(nodeID); err == nil {
-					if err = mq.GetEmqxHandler().AppendNodeUpdateACL(host.ID.String(), node.Network, node.ID.String(), servercfg.GetServer()); err != nil {
-						slog.Error("failed to add ACLs for EMQX node", "error", err)
+	go func() {
+		// Create EMQX creds and ACLs if not found
+		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+			if err := mq.GetEmqxHandler().CreateEmqxUser(host.ID.String(), authRequest.Password); err != nil {
+				slog.Error("failed to create host credentials for EMQX: ", err.Error())
+			} else {
+				if err := mq.GetEmqxHandler().CreateHostACL(host.ID.String(), servercfg.GetServerInfo().Server); err != nil {
+					slog.Error("failed to add host ACL rules to EMQX: ", err.Error())
+				}
+				for _, nodeID := range host.Nodes {
+					if node, err := logic.GetNodeByID(nodeID); err == nil {
+						if err = mq.GetEmqxHandler().AppendNodeUpdateACL(host.ID.String(), node.Network, node.ID.String(), servercfg.GetServer()); err != nil {
+							slog.Error("failed to add ACLs for EMQX node", "error", err)
+						}
+					} else {
+						slog.Error("failed to get node", "nodeid", nodeID, "error", err)
 					}
-				} else {
-					slog.Error("failed to get node", "nodeid", nodeID, "error", err)
 				}
 			}
 		}
-	}
+	}()
 
 	response.WriteHeader(http.StatusOK)
 	response.Header().Set("Content-Type", "application/json")
@@ -749,3 +752,34 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
 	w.WriteHeader(http.StatusOK)
 }
+
+// swagger:route DELETE /api/emqx/hosts hosts delEmqxHosts
+//
+// Lists all hosts.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: apiHostResponse
+func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
+	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
+	}
+	for _, host := range currentHosts {
+		// delete EMQX credentials for host
+		if err := mq.GetEmqxHandler().DeleteEmqxUser(host.ID.String()); err != nil {
+			slog.Error("failed to remove host credentials from EMQX", "id", host.ID, "error", err)
+		}
+	}
+	err = mq.GetEmqxHandler().DeleteEmqxUser(servercfg.GetMqUserName())
+	if err != nil {
+		slog.Error("failed to remove server credentials from EMQX", "user", servercfg.GetMqUserName(), "error", err)
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
+}

+ 5 - 0
controllers/server.go

@@ -55,6 +55,7 @@ func getUsage(w http.ResponseWriter, _ *http.Request) {
 		Egresses         int `json:"egresses"`
 		Relays           int `json:"relays"`
 		InternetGateways int `json:"internet_gateways"`
+		FailOvers        int `json:"fail_overs"`
 	}
 	var serverUsage usage
 	hosts, err := logic.GetAllHosts()
@@ -90,6 +91,10 @@ func getUsage(w http.ResponseWriter, _ *http.Request) {
 	if err == nil {
 		serverUsage.InternetGateways = len(gateways)
 	}
+	failOvers, err := logic.GetAllFailOvers()
+	if err == nil {
+		serverUsage.FailOvers = len(failOvers)
+	}
 	w.Header().Set("Content-Type", "application/json")
 	json.NewEncoder(w).Encode(models.SuccessResponse{
 		Code:     http.StatusOK,

+ 139 - 0
controllers/user.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/auth"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -35,6 +36,11 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO)
 	r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost)
+
 }
 
 // swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
@@ -583,3 +589,136 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	go auth.SessionHandler(conn)
 }
+
+// swagger:route GET /api/users_pending user getPendingUsers
+//
+// Get all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func getPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+
+	users, err := logic.ListPendingUsers()
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	logic.SortUsers(users[:])
+	logger.Log(2, r.Header.Get("user"), "fetched pending users")
+	json.NewEncoder(w).Encode(users)
+}
+
+// swagger:route POST /api/users_pending/user/{username} user approvePendingUser
+//
+// approve pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func approvePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			var newPass, fetchErr = auth.FetchPassValue("")
+			if fetchErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
+				return
+			}
+			if err = logic.CreateUser(&models.User{
+				UserName: user.UserName,
+				Password: newPass,
+			}); err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
+				return
+			}
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "approved "+username)
+}
+
+// swagger:route DELETE /api/users_pending/user/{username} user deletePendingUser
+//
+// delete pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deletePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
+}
+
+// swagger:route DELETE /api/users_pending/{username}/pending user deleteAllPendingUsers
+//
+// delete all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
+}

+ 3 - 1
database/database.go

@@ -61,7 +61,8 @@ const (
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
-
+	// PENDING_USERS_TABLE_NAME - table name for pending users
+	PENDING_USERS_TABLE_NAME = "pending_users"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -144,6 +145,7 @@ func createTables() {
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
+	CreateTable(PENDING_USERS_TABLE_NAME)
 }
 
 func CreateTable(tableName string) error {

+ 7 - 7
go.mod

@@ -4,7 +4,7 @@ go 1.19
 
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.18.0
+	github.com/go-playground/validator/v10 v10.19.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -13,12 +13,12 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.22
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.19.0
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/oauth2 v0.17.0
-	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/crypto v0.22.0
+	golang.org/x/net v0.22.0 // indirect
+	golang.org/x/oauth2 v0.18.0
+	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	google.golang.org/protobuf v1.31.0 // indirect
@@ -38,7 +38,7 @@ require (
 )
 
 require (
-	github.com/go-jose/go-jose/v3 v3.0.1
+	github.com/go-jose/go-jose/v3 v3.0.3
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5

+ 27 - 17
go.sum

@@ -20,24 +20,24 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
-github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
-github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
+github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -85,9 +85,9 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -95,44 +95,54 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
-golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.23.0
+        image: gravitl/netclient:v0.24.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.23.0
+        image: gravitl/netclient:v0.24.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.23.0
+        image: gravitl/netmaker-ui:v0.24.0
         ports:
         - containerPort: 443
         env:

+ 4 - 2
logic/acls/common.go

@@ -64,9 +64,9 @@ func (acl ACL) Save(containerID ContainerID, ID AclID) (ACL, error) {
 
 // ACL.IsAllowed - sees if ID is allowed in referring ACL
 func (acl ACL) IsAllowed(ID AclID) (allowed bool) {
-	AclMutex.RLock()
+	AclMutex.Lock()
 	allowed = acl[ID] == Allowed
-	AclMutex.RUnlock()
+	AclMutex.Unlock()
 	return
 }
 
@@ -88,6 +88,8 @@ func (aclContainer ACLContainer) RemoveACL(ID AclID) ACLContainer {
 
 // ACLContainer.ChangeAccess - changes the relationship between two nodes in memory
 func (networkACL ACLContainer) ChangeAccess(ID1, ID2 AclID, value byte) {
+	AclMutex.Lock()
+	defer AclMutex.Unlock()
 	if _, ok := networkACL[ID1]; !ok {
 		slog.Error("ACL missing for ", "id", ID1)
 		return

+ 7 - 2
logic/acls/nodeacls/retrieve.go

@@ -3,21 +3,26 @@ package nodeacls
 import (
 	"encoding/json"
 	"fmt"
+	"sync"
 
 	"github.com/gravitl/netmaker/logic/acls"
 )
 
+var NodesAllowedACLMutex = &sync.Mutex{}
+
 // AreNodesAllowed - checks if nodes are allowed to communicate in their network ACL
 func AreNodesAllowed(networkID NetworkID, node1, node2 NodeID) bool {
+	NodesAllowedACLMutex.Lock()
+	defer NodesAllowedACLMutex.Unlock()
 	var currentNetworkACL, err = FetchAllACLs(networkID)
 	if err != nil {
 		return false
 	}
 	var allowed bool
-	acls.AclMutex.RLock()
+	acls.AclMutex.Lock()
 	currNetworkACLNode1 := currentNetworkACL[acls.AclID(node1)]
 	currNetworkACLNode2 := currentNetworkACL[acls.AclID(node2)]
-	acls.AclMutex.RUnlock()
+	acls.AclMutex.Unlock()
 	allowed = currNetworkACLNode1.IsAllowed(acls.AclID(node2)) && currNetworkACLNode2.IsAllowed(acls.AclID(node1))
 	return allowed
 }

+ 33 - 7
logic/auth.go

@@ -1,6 +1,7 @@
 package logic
 
 import (
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -15,6 +16,10 @@ import (
 	"github.com/gravitl/netmaker/models"
 )
 
+const (
+	auth_key = "netmaker_auth"
+)
+
 // HasSuperAdmin - checks if server has an superadmin/owner
 func HasSuperAdmin() (bool, error) {
 
@@ -96,12 +101,14 @@ func CreateUser(user *models.User) error {
 	}
 	var err = ValidateUser(user)
 	if err != nil {
+		logger.Log(0, "failed to validate user", err.Error())
 		return err
 	}
 
 	// encrypt that password so we never see it again
 	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
 	if err != nil {
+		logger.Log(0, "error encrypting pass", err.Error())
 		return err
 	}
 	// set password to encrypted password
@@ -109,6 +116,7 @@ func CreateUser(user *models.User) error {
 
 	tokenString, _ := CreateUserJWT(user.UserName, user.IsSuperAdmin, user.IsAdmin)
 	if tokenString == "" {
+		logger.Log(0, "failed to generate token", err.Error())
 		return err
 	}
 
@@ -117,10 +125,12 @@ func CreateUser(user *models.User) error {
 	// connect db
 	data, err := json.Marshal(user)
 	if err != nil {
+		logger.Log(0, "failed to marshal", err.Error())
 		return err
 	}
 	err = database.Insert(user.UserName, string(data), database.USERS_TABLE_NAME)
 	if err != nil {
+		logger.Log(0, "failed to insert user", err.Error())
 		return err
 	}
 
@@ -279,15 +289,31 @@ func DeleteUser(user string) (bool, error) {
 	return true, nil
 }
 
+func SetAuthSecret(secret string) error {
+	type valueHolder struct {
+		Value string `json:"value" bson:"value"`
+	}
+	record, err := FetchAuthSecret()
+	if err == nil {
+		v := valueHolder{}
+		json.Unmarshal([]byte(record), &v)
+		if v.Value != "" {
+			return nil
+		}
+	}
+	var b64NewValue = base64.StdEncoding.EncodeToString([]byte(secret))
+	newValueHolder := valueHolder{
+		Value: b64NewValue,
+	}
+	d, _ := json.Marshal(newValueHolder)
+	return database.Insert(auth_key, string(d), database.GENERATED_TABLE_NAME)
+}
+
 // FetchAuthSecret - manages secrets for oauth
-func FetchAuthSecret(key string, secret string) (string, error) {
-	var record, err = database.FetchRecord(database.GENERATED_TABLE_NAME, key)
+func FetchAuthSecret() (string, error) {
+	var record, err = database.FetchRecord(database.GENERATED_TABLE_NAME, auth_key)
 	if err != nil {
-		if err = database.Insert(key, secret, database.GENERATED_TABLE_NAME); err != nil {
-			return "", err
-		} else {
-			return secret, nil
-		}
+		return "", err
 	}
 	return record, nil
 }

+ 2 - 2
logic/errors.go

@@ -4,8 +4,8 @@ import (
 	"encoding/json"
 	"net/http"
 
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"golang.org/x/exp/slog"
 )
 
 // FormatError - takes ErrorResponse and uses correct code
@@ -62,7 +62,7 @@ func ReturnErrorResponse(response http.ResponseWriter, request *http.Request, er
 	if err != nil {
 		panic(err)
 	}
-	logger.Log(1, "processed request error:", errorMessage.Message)
+	slog.Debug("processed request error", "err", errorMessage.Message)
 	response.Header().Set("Content-Type", "application/json")
 	response.WriteHeader(errorMessage.Code)
 	response.Write(jsonResponse)

+ 5 - 0
logic/gateway.go

@@ -164,6 +164,11 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
+	if servercfg.IsPro {
+		if _, exists := FailOverExists(node.Network); exists {
+			ResetFailedOverPeer(&node)
+		}
+	}
 	node.SetLastModified()
 	if node.Metadata == "" {
 		node.Metadata = "This host can be used for remote access"

+ 5 - 0
logic/hosts.go

@@ -217,6 +217,7 @@ func UpdateHost(newHost, currentHost *models.Host) {
 	newHost.Nodes = currentHost.Nodes
 	newHost.PublicKey = currentHost.PublicKey
 	newHost.TrafficKeyPublic = currentHost.TrafficKeyPublic
+	newHost.EndpointIPv6 = currentHost.EndpointIPv6
 	// changeable fields
 	if len(newHost.Version) == 0 {
 		newHost.Version = currentHost.Version
@@ -258,6 +259,10 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		currHost.EndpointIP = newHost.EndpointIP
 		sendPeerUpdate = true
 	}
+	if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() {
+		currHost.EndpointIPv6 = newHost.EndpointIPv6
+		sendPeerUpdate = true
+	}
 	currHost.DaemonInstalled = newHost.DaemonInstalled
 	currHost.Debug = newHost.Debug
 	currHost.Verbosity = newHost.Verbosity

+ 0 - 1
logic/jwts.go

@@ -106,7 +106,6 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 		if err != nil {
 			return "", false, false, err
 		}
-
 		if user.UserName != "" {
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 		}

+ 1 - 1
logic/networks.go

@@ -138,7 +138,7 @@ func GetParentNetwork(networkname string) (models.Network, error) {
 	return network, nil
 }
 
-// GetParentNetwork - get parent network
+// GetNetworkSettings - get parent network
 func GetNetworkSettings(networkname string) (models.Network, error) {
 
 	var network models.Network

+ 15 - 0
logic/nodes.go

@@ -625,3 +625,18 @@ func ValidateParams(nodeid, netid string) (models.Node, error) {
 	}
 	return node, nil
 }
+
+// GetAllFailOvers - gets all the nodes that are failovers
+func GetAllFailOvers() ([]models.Node, error) {
+	nodes, err := GetAllNodes()
+	if err != nil {
+		return nil, err
+	}
+	igs := make([]models.Node, 0)
+	for _, node := range nodes {
+		if node.IsFailOver {
+			igs = append(igs, node)
+		}
+	}
+	return igs, nil
+}

+ 26 - 7
logic/peers.go

@@ -25,6 +25,10 @@ var (
 	ResetFailedOverPeer = func(failedOverNode *models.Node) error {
 		return nil
 	}
+	// FailOverExists - check if failover node existed or not
+	FailOverExists = func(network string) (failOverNode models.Node, exists bool) {
+		return failOverNode, exists
+	}
 	// GetFailOverPeerIps - gets failover peerips
 	GetFailOverPeerIps = func(peer, node *models.Node) []net.IPNet {
 		return []net.IPNet{}
@@ -72,10 +76,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		FwUpdate: models.FwUpdate{
 			EgressInfo: make(map[string]models.EgressInfo),
 		},
-		PeerIDs:         make(models.PeerMap, 0),
-		Peers:           []wgtypes.PeerConfig{},
-		NodePeers:       []wgtypes.PeerConfig{},
-		HostNetworkInfo: models.HostInfoMap{},
+		PeerIDs:           make(models.PeerMap, 0),
+		Peers:             []wgtypes.PeerConfig{},
+		NodePeers:         []wgtypes.PeerConfig{},
+		HostNetworkInfo:   models.HostInfoMap{},
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 
 	slog.Debug("peer update for host", "hostId", host.ID.String())
@@ -168,7 +173,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 			if peer.IsEgressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
-					NodeAddr:     node.PrimaryAddressIPNet(),
+					NodeAddr:     node.Address,
+					NodeAddr6:    node.Address6,
 					EgressRanges: peer.EgressGatewayRanges,
 				})
 			}
@@ -206,8 +212,21 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					uselocal = false
 				}
 			}
+
+			//if host is ipv4 only or ipv4+ipv6, set the peer endpoint to ipv4 address, if host is ipv6 only, set the peer endpoint to ipv6 address
+			peerEndpoint := peerHost.EndpointIP
+			if ipv4 := host.EndpointIP.To4(); ipv4 != nil {
+				peerEndpoint = peerHost.EndpointIP
+			} else {
+				//if peer host's ipv6 address is empty, it means that peer is an IPv4 only host
+				//IPv4 only host could not communicate with IPv6 only host
+				if peerHost.EndpointIPv6 != nil && peerHost.EndpointIPv6.String() != "" {
+					peerEndpoint = peerHost.EndpointIPv6
+				}
+			}
+
 			peerConfig.Endpoint = &net.UDPAddr{
-				IP:   peerHost.EndpointIP,
+				IP:   peerEndpoint,
 				Port: GetPeerListenPort(peerHost),
 			}
 
@@ -371,6 +390,7 @@ func GetPeerListenPort(host *models.Host) int {
 // 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
+	allowedips = getNodeAllowedIPs(peer, node)
 	if peer.IsInternetGateway && node.InternetGwID == peer.ID.String() {
 		allowedips = append(allowedips, GetAllowedIpForInetNodeClient(node, peer)...)
 		return allowedips
@@ -381,7 +401,6 @@ func GetAllowedIPs(node, peer *models.Node, metrics *models.Metrics) []net.IPNet
 			return allowedips
 		}
 	}
-	allowedips = append(allowedips, getNodeAllowedIPs(peer, node)...)
 
 	// handle ingress gateway peers
 	if peer.IsIngressGateway {

+ 7 - 15
logic/telemetry.go

@@ -39,10 +39,7 @@ func sendTelemetry() error {
 		return err
 	}
 	// get telemetry data
-	d, err := FetchTelemetryData()
-	if err != nil {
-		slog.Error("error fetching telemetry data", "error", err)
-	}
+	d := FetchTelemetryData()
 	// get tenant admin email
 	adminEmail := os.Getenv("NM_EMAIL")
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
@@ -82,7 +79,7 @@ func sendTelemetry() error {
 }
 
 // FetchTelemetryData - fetches telemetry data: count of various object types in DB
-func FetchTelemetryData() (telemetryData, error) {
+func FetchTelemetryData() telemetryData {
 	var data telemetryData
 
 	data.IsPro = servercfg.IsPro
@@ -92,21 +89,16 @@ func FetchTelemetryData() (telemetryData, error) {
 	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
 	data.Servers = getServerCount()
-	nodes, err := GetAllNodes()
-	if err == nil {
-		data.Nodes = len(nodes)
-		data.Count = getClientCount(nodes)
-	}
-	endDate, err := GetTrialEndDate()
-	if err != nil {
-		logger.Log(0, "error getting trial end date", err.Error())
-	}
+	nodes, _ := GetAllNodes()
+	data.Nodes = len(nodes)
+	data.Count = getClientCount(nodes)
+	endDate, _ := GetTrialEndDate()
 	data.ProTrialEndDate = endDate
 	if endDate.After(time.Now()) {
 		data.IsProTrial = true
 	}
 	data.IsSaasTenant = servercfg.DeployedByOperator()
-	return data, err
+	return data
 }
 
 // getServerCount returns number of servers from database

+ 44 - 0
logic/users.go

@@ -75,3 +75,47 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 	}
 	return models.ReturnUser{}, errors.New("superadmin not found")
 }
+
+func InsertPendingUser(u *models.User) error {
+	data, err := json.Marshal(u)
+	if err != nil {
+		return err
+	}
+	return database.Insert(u.UserName, string(data), database.PENDING_USERS_TABLE_NAME)
+}
+
+func DeletePendingUser(username string) error {
+	return database.DeleteRecord(database.PENDING_USERS_TABLE_NAME, username)
+}
+
+func IsPendingUser(username string) bool {
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		return false
+
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err := json.Unmarshal([]byte(record), &u)
+		if err == nil && u.UserName == username {
+			return true
+		}
+	}
+	return false
+}
+
+func ListPendingUsers() ([]models.ReturnUser, error) {
+	pendingUsers := []models.ReturnUser{}
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return pendingUsers, err
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			pendingUsers = append(pendingUsers, u)
+		}
+	}
+	return pendingUsers, nil
+}

+ 1 - 1
logic/util.go

@@ -89,7 +89,7 @@ func StringSliceContains(slice []string, item string) bool {
 	return false
 }
 
-// NormalCIDR - returns the first address of CIDR
+// NormalizeCIDR - returns the first address of CIDR
 func NormalizeCIDR(address string) (string, error) {
 	ip, IPNet, err := net.ParseCIDR(address)
 	if err != nil {

+ 1 - 1
logic/zombie.go

@@ -76,7 +76,7 @@ func checkForZombieHosts(h *models.Host) {
 // ManageZombies - goroutine which adds/removes/deletes nodes from the zombie node quarantine list
 func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 	logger.Log(2, "Zombie management started")
-	InitializeZombies()
+	go InitializeZombies()
 
 	// Zombie Nodes Cleanup Four Times a Day
 	ticker := time.NewTicker(time.Hour * ZOMBIE_TIMEOUT)

+ 2 - 2
main.go

@@ -28,7 +28,7 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.23.0"
+var version = "v0.24.0"
 
 // Start DB Connection and start API Request Handler
 func main() {
@@ -155,7 +155,7 @@ func runMessageQueue(wg *sync.WaitGroup, ctx context.Context) {
 	defer wg.Done()
 	brokerHost, _ := servercfg.GetMessageQueueEndpoint()
 	logger.Log(0, "connecting to mq broker at", brokerHost)
-	mq.SetupMQTT()
+	mq.SetupMQTT(true)
 	if mq.IsConnected() {
 		logger.Log(0, "connected to MQ Broker")
 	} else {

+ 2 - 2
migrate/migrate.go

@@ -177,7 +177,7 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 func updateAcls() {
 	// get all networks
 	networks, err := logic.GetNetworks()
-	if err != nil {
+	if err != nil && !database.IsEmptyRecord(err) {
 		slog.Error("acls migration failed. error getting networks", "error", err)
 		return
 	}
@@ -287,7 +287,7 @@ func updateAcls() {
 		}
 
 		// save new acls
-		slog.Info(fmt.Sprintf("(migration) saving new acls for network: %s", network.NetID), "networkAcl", networkAcl)
+		slog.Debug(fmt.Sprintf("(migration) saving new acls for network: %s", network.NetID), "networkAcl", networkAcl)
 		if _, err := networkAcl.Save(acls.ContainerID(network.NetID)); err != nil {
 			slog.Error(fmt.Sprintf("error during acls migration. error saving new acls for network: %s", network.NetID), "error", err)
 			continue

+ 40 - 23
models/api_host.go

@@ -8,27 +8,35 @@ import (
 
 // ApiHost - the host struct for API usage
 type ApiHost struct {
-	ID                  string   `json:"id"`
-	Verbosity           int      `json:"verbosity"`
-	FirewallInUse       string   `json:"firewallinuse"`
-	Version             string   `json:"version"`
-	Name                string   `json:"name"`
-	OS                  string   `json:"os"`
-	Debug               bool     `json:"debug"`
-	IsStatic            bool     `json:"isstatic"`
-	ListenPort          int      `json:"listenport"`
-	WgPublicListenPort  int      `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
-	MTU                 int      `json:"mtu"                   yaml:"mtu"`
-	Interfaces          []Iface  `json:"interfaces"            yaml:"interfaces"`
-	DefaultInterface    string   `json:"defaultinterface"      yaml:"defautlinterface"`
-	EndpointIP          string   `json:"endpointip"            yaml:"endpointip"`
-	PublicKey           string   `json:"publickey"`
-	MacAddress          string   `json:"macaddress"`
-	Nodes               []string `json:"nodes"`
-	IsDefault           bool     `json:"isdefault"             yaml:"isdefault"`
-	NatType             string   `json:"nat_type"              yaml:"nat_type"`
-	PersistentKeepalive int      `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
-	AutoUpdate          bool     `json:"autoupdate"              yaml:"autoupdate"`
+	ID                  string     `json:"id"`
+	Verbosity           int        `json:"verbosity"`
+	FirewallInUse       string     `json:"firewallinuse"`
+	Version             string     `json:"version"`
+	Name                string     `json:"name"`
+	OS                  string     `json:"os"`
+	Debug               bool       `json:"debug"`
+	IsStatic            bool       `json:"isstatic"`
+	ListenPort          int        `json:"listenport"`
+	WgPublicListenPort  int        `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
+	MTU                 int        `json:"mtu"                   yaml:"mtu"`
+	Interfaces          []ApiIface `json:"interfaces"            yaml:"interfaces"`
+	DefaultInterface    string     `json:"defaultinterface"      yaml:"defautlinterface"`
+	EndpointIP          string     `json:"endpointip"            yaml:"endpointip"`
+	EndpointIPv6        string     `json:"endpointipv6"            yaml:"endpointipv6"`
+	PublicKey           string     `json:"publickey"`
+	MacAddress          string     `json:"macaddress"`
+	Nodes               []string   `json:"nodes"`
+	IsDefault           bool       `json:"isdefault"             yaml:"isdefault"`
+	NatType             string     `json:"nat_type"              yaml:"nat_type"`
+	PersistentKeepalive int        `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
+	AutoUpdate          bool       `json:"autoupdate"              yaml:"autoupdate"`
+}
+
+// ApiIface - the interface struct for API usage
+// The original Iface struct contains a net.Address, which does not get marshalled correctly
+type ApiIface struct {
+	Name          string `json:"name"`
+	AddressString string `json:"addressString"`
 }
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -36,11 +44,15 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a := ApiHost{}
 	a.Debug = h.Debug
 	a.EndpointIP = h.EndpointIP.String()
+	a.EndpointIPv6 = h.EndpointIPv6.String()
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
-	a.Interfaces = h.Interfaces
+	a.Interfaces = make([]ApiIface, len(h.Interfaces))
 	for i := range a.Interfaces {
-		a.Interfaces[i].AddressString = a.Interfaces[i].Address.String()
+		a.Interfaces[i] = ApiIface{
+			Name:          h.Interfaces[i].Name,
+			AddressString: h.Interfaces[i].Address.String(),
+		}
 	}
 	a.DefaultInterface = h.DefaultInterface
 	a.IsStatic = h.IsStatic
@@ -73,6 +85,11 @@ func (a *ApiHost) ConvertAPIHostToNMHost(currentHost *Host) *Host {
 	} else {
 		h.EndpointIP = net.ParseIP(a.EndpointIP)
 	}
+	if len(a.EndpointIPv6) == 0 || strings.Contains(a.EndpointIPv6, "nil") {
+		h.EndpointIPv6 = currentHost.EndpointIPv6
+	} else {
+		h.EndpointIPv6 = net.ParseIP(a.EndpointIPv6)
+	}
 	h.Debug = a.Debug
 	h.FirewallInUse = a.FirewallInUse
 	h.IPForwarding = currentHost.IPForwarding

+ 1 - 0
models/host.go

@@ -63,6 +63,7 @@ type Host struct {
 	Interfaces          []Iface          `json:"interfaces"              yaml:"interfaces"`
 	DefaultInterface    string           `json:"defaultinterface"        yaml:"defaultinterface"`
 	EndpointIP          net.IP           `json:"endpointip"              yaml:"endpointip"`
+	EndpointIPv6        net.IP           `json:"endpointipv6"            yaml:"endpointipv6"`
 	IsDocker            bool             `json:"isdocker"                yaml:"isdocker"`
 	IsK8S               bool             `json:"isk8s"                   yaml:"isk8s"`
 	IsStatic            bool             `json:"isstatic"                yaml:"isstatic"`

+ 17 - 15
models/mqtt.go

@@ -8,21 +8,22 @@ import (
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host            Host                 `json:"host" bson:"host" yaml:"host"`
-	ChangeDefaultGw bool                 `json:"change_default_gw"`
-	DefaultGwIp     net.IP               `json:"default_gw_ip"`
-	IsInternetGw    bool                 `json:"is_inet_gw"`
-	NodeAddrs       []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
-	Server          string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers           []wgtypes.PeerConfig
-	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ReplacePeers    bool                  `json:"replace_peers"`
+	Host              Host                 `json:"host" bson:"host" yaml:"host"`
+	ChangeDefaultGw   bool                 `json:"change_default_gw"`
+	DefaultGwIp       net.IP               `json:"default_gw_ip"`
+	IsInternetGw      bool                 `json:"is_inet_gw"`
+	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
+	Server            string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers             []wgtypes.PeerConfig
+	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ReplacePeers      bool                  `json:"replace_peers"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 
 // IngressInfo - struct for ingress info
@@ -42,6 +43,7 @@ type EgressInfo struct {
 // EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
 type EgressNetworkRoutes struct {
 	NodeAddr     net.IPNet `json:"node_addr"`
+	NodeAddr6    net.IPNet `json:"node_addr6"`
 	EgressRanges []string  `json:"egress_ranges"`
 }
 

+ 1 - 1
models/node.go

@@ -205,7 +205,7 @@ func (extPeer *ExtClient) AddressIPNet4() net.IPNet {
 // ExtClient.AddressIPNet6 - return ipv6 IPNet format
 func (extPeer *ExtClient) AddressIPNet6() net.IPNet {
 	return net.IPNet{
-		IP:   net.ParseIP(extPeer.Address),
+		IP:   net.ParseIP(extPeer.Address6),
 		Mask: net.CIDRMask(128, 128),
 	}
 }

+ 12 - 11
models/structs.go

@@ -232,17 +232,18 @@ type TrafficKeys struct {
 
 // HostPull - response of a host's pull
 type HostPull struct {
-	Host            Host                  `json:"host" yaml:"host"`
-	Nodes           []Node                `json:"nodes" yaml:"nodes"`
-	Peers           []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
-	ServerConfig    ServerConfig          `json:"server_config" yaml:"server_config"`
-	PeerIDs         PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ChangeDefaultGw bool                  `json:"change_default_gw"`
-	DefaultGwIp     net.IP                `json:"default_gw_ip"`
-	IsInternetGw    bool                  `json:"is_inet_gw"`
+	Host              Host                  `json:"host" yaml:"host"`
+	Nodes             []Node                `json:"nodes" yaml:"nodes"`
+	Peers             []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
+	ServerConfig      ServerConfig          `json:"server_config" yaml:"server_config"`
+	PeerIDs           PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ChangeDefaultGw   bool                  `json:"change_default_gw"`
+	DefaultGwIp       net.IP                `json:"default_gw_ip"`
+	IsInternetGw      bool                  `json:"is_inet_gw"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 
 type DefaultGwInfo struct {

+ 3 - 138
mq/emqx_cloud.go

@@ -22,13 +22,6 @@ type userCreateReq struct {
 	Password string `json:"password"`
 }
 
-type cloudAcl struct {
-	UserName string `json:"username"`
-	Topic    string `json:"topic"`
-	Action   string `json:"action"`
-	Access   string `json:"access"`
-}
-
 func (e *EmqxCloud) GetType() servercfg.Emqxdeploy { return servercfg.EmqxCloudDeploy }
 
 func (e *EmqxCloud) CreateEmqxUser(username, pass string) error {
@@ -89,54 +82,7 @@ func (e *EmqxCloud) CreateEmqxUserforServer() error {
 	if res.StatusCode != http.StatusOK {
 		return errors.New("request failed " + string(body))
 	}
-	// add acls
-	acls := []cloudAcl{
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    fmt.Sprintf("update/%s/#", servercfg.GetServer()),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    fmt.Sprintf("host/serverupdate/%s/#", servercfg.GetServer()),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    fmt.Sprintf("signal/%s/#", servercfg.GetServer()),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    fmt.Sprintf("metrics/%s/#", servercfg.GetServer()),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    "peers/host/#",
-			Access:   "allow",
-			Action:   "pub",
-		},
-		{
-			UserName: servercfg.GetMqUserName(),
-			Topic:    "node/update/#",
-			Access:   "allow",
-			Action:   "pub",
-		},
-		{
-
-			UserName: servercfg.GetMqUserName(),
-			Topic:    "host/update/#",
-			Access:   "allow",
-			Action:   "pub",
-		},
-	}
-
-	return e.createacls(acls)
+	return nil
 }
 
 func (e *EmqxCloud) CreateEmqxDefaultAuthenticator() error { return nil } // ignore
@@ -147,94 +93,13 @@ func (e *EmqxCloud) CreateDefaultDenyRule() error {
 	return nil
 }
 
-func (e *EmqxCloud) createacls(acls []cloudAcl) error {
-	payload, err := json.Marshal(acls)
-	if err != nil {
-		return err
-	}
-	client := &http.Client{}
-	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/acl", e.URL), strings.NewReader(string(payload)))
-	if err != nil {
-		return err
-	}
-	req.Header.Add("Content-Type", "application/json")
-	req.SetBasicAuth(e.AppID, e.AppSecret)
-	res, err := client.Do(req)
-	if err != nil {
-		return err
-	}
-	defer res.Body.Close()
-
-	body, err := io.ReadAll(res.Body)
-	if err != nil {
-		return err
-	}
-	if res.StatusCode != http.StatusOK {
-		return errors.New("request failed " + string(body))
-	}
-	return nil
-}
-
 func (e *EmqxCloud) CreateHostACL(hostID, serverName string) error {
-	acls := []cloudAcl{
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("peers/host/%s/%s", hostID, serverName),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("host/update/%s/%s", hostID, serverName),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("host/serverupdate/%s/%s", serverName, hostID),
-			Access:   "allow",
-			Action:   "pub",
-		},
-	}
-
-	return e.createacls(acls)
+	return nil
 }
 
 func (e *EmqxCloud) AppendNodeUpdateACL(hostID, nodeNetwork, nodeID, serverName string) error {
-	acls := []cloudAcl{
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("node/update/%s/%s", nodeNetwork, nodeID),
-			Access:   "allow",
-			Action:   "sub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("ping/%s/%s", serverName, nodeID),
-			Access:   "allow",
-			Action:   "pubsub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("update/%s/%s", serverName, nodeID),
-			Access:   "allow",
-			Action:   "pubsub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("signal/%s/%s", serverName, nodeID),
-			Access:   "allow",
-			Action:   "pubsub",
-		},
-		{
-			UserName: hostID,
-			Topic:    fmt.Sprintf("metrics/%s/%s", serverName, nodeID),
-			Access:   "allow",
-			Action:   "pubsub",
-		},
-	}
+	return nil
 
-	return e.createacls(acls)
 }
 
 func (e *EmqxCloud) GetUserACL(username string) (*aclObject, error) { return nil, nil } // ununsed on cloud since it doesn't overwrite acls list

+ 19 - 3
mq/handlers.go

@@ -92,7 +92,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 	}
 	decrypted, decryptErr := decryptMsgWithHost(currentHost, msg.Payload())
 	if decryptErr != nil {
-		slog.Error("failed to decrypt message for host", "id", id, "error", decryptErr)
+		slog.Error("failed to decrypt message for host", "id", id, "name", currentHost.Name, "error", decryptErr)
 		return
 	}
 	var hostUpdate models.HostUpdate
@@ -198,7 +198,21 @@ func signalPeer(signal models.Signal) {
 	signal.IsPro = servercfg.IsPro
 	peerHost, err := logic.GetHost(signal.ToHostID)
 	if err != nil {
-		slog.Error("failed to signal, peer not found", "error", err)
+		slog.Error("failed to signal, peer host not found", "error", err)
+		return
+	}
+	peerNode, err := logic.GetNodeByID(signal.ToNodeID)
+	if err != nil {
+		slog.Error("failed to signal, node not found", "error", err)
+		return
+	}
+	node, err := logic.GetNodeByID(signal.FromNodeID)
+	if err != nil {
+		slog.Error("failed to signal, peer node not found", "error", err)
+		return
+	}
+	if peerNode.IsIngressGateway || node.IsIngressGateway || peerNode.IsInternetGateway || node.IsInternetGateway {
+		signal.Action = ""
 		return
 	}
 	err = HostUpdate(&models.HostUpdate{
@@ -282,9 +296,11 @@ func HandleHostCheckin(h, currentHost *models.Host) bool {
 		!h.EndpointIP.Equal(currentHost.EndpointIP) ||
 		(len(h.NatType) > 0 && h.NatType != currentHost.NatType) ||
 		h.DefaultInterface != currentHost.DefaultInterface ||
-		(h.ListenPort != 0 && h.ListenPort != currentHost.ListenPort) || (h.WgPublicListenPort != 0 && h.WgPublicListenPort != currentHost.WgPublicListenPort)
+		(h.ListenPort != 0 && h.ListenPort != currentHost.ListenPort) ||
+		(h.WgPublicListenPort != 0 && h.WgPublicListenPort != currentHost.WgPublicListenPort) || (!h.EndpointIPv6.Equal(currentHost.EndpointIPv6))
 	if ifaceDelta { // only save if something changes
 		currentHost.EndpointIP = h.EndpointIP
+		currentHost.EndpointIPv6 = h.EndpointIPv6
 		currentHost.Interfaces = h.Interfaces
 		currentHost.DefaultInterface = h.DefaultInterface
 		currentHost.NatType = h.NatType

+ 24 - 7
mq/mq.go

@@ -10,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
 )
 
 // KEEPALIVE_TIMEOUT - time in seconds for timeout
@@ -27,19 +28,20 @@ var mqclient mqtt.Client
 func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 	broker, _ := servercfg.GetMessageQueueEndpoint()
 	opts.AddBroker(broker)
-	id := logic.RandomString(23)
-	opts.ClientID = id
+	opts.ClientID = logic.RandomString(23)
 	opts.SetUsername(user)
 	opts.SetPassword(password)
 	opts.SetAutoReconnect(true)
 	opts.SetConnectRetry(true)
-	opts.SetConnectRetryInterval(time.Second << 2)
+	opts.SetCleanSession(true)
+	opts.SetConnectRetryInterval(time.Second * 4)
 	opts.SetKeepAlive(time.Minute)
+	opts.SetCleanSession(true)
 	opts.SetWriteTimeout(time.Minute)
 }
 
 // SetupMQTT creates a connection to broker and return client
-func SetupMQTT() {
+func SetupMQTT(fatal bool) {
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		if emqx.GetType() == servercfg.EmqxOnPremDeploy {
 			time.Sleep(10 * time.Second) // wait for the REST endpoint to be ready
@@ -69,6 +71,7 @@ func SetupMQTT() {
 
 	opts := mqtt.NewClientOptions()
 	setMqOptions(servercfg.GetMqUserName(), servercfg.GetMqPassword(), opts)
+	logger.Log(0, "Mq Client Connecting with Random ID: ", opts.ClientID)
 	opts.SetOnConnectHandler(func(client mqtt.Client) {
 		serverName := servercfg.GetServer()
 		if token := client.Subscribe(fmt.Sprintf("update/%s/#", serverName), 0, mqtt.MessageHandler(UpdateNode)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
@@ -91,6 +94,13 @@ func SetupMQTT() {
 		opts.SetOrderMatters(false)
 		opts.SetResumeSubs(true)
 	})
+	opts.SetConnectionLostHandler(func(c mqtt.Client, e error) {
+		slog.Warn("detected broker connection lost", "err", e.Error())
+		c.Disconnect(250)
+		slog.Info("re-initiating MQ connection")
+		SetupMQTT(false)
+
+	})
 	mqclient = mqtt.NewClient(opts)
 	tperiod := time.Now().Add(10 * time.Second)
 	for {
@@ -98,9 +108,16 @@ func SetupMQTT() {
 			logger.Log(2, "unable to connect to broker, retrying ...")
 			if time.Now().After(tperiod) {
 				if token.Error() == nil {
-					logger.FatalLog("could not connect to broker, token timeout, exiting ...")
+					if fatal {
+						logger.FatalLog("could not connect to broker, token timeout, exiting ...")
+					}
+					logger.Log(0, "could not connect to broker, token timeout, exiting ...")
+
 				} else {
-					logger.FatalLog("could not connect to broker, exiting ...", token.Error().Error())
+					if fatal {
+						logger.FatalLog("could not connect to broker, exiting ...", token.Error().Error())
+					}
+					logger.Log(0, "could not connect to broker, exiting ...", token.Error().Error())
 				}
 			}
 		} else {
@@ -124,7 +141,7 @@ func Keepalive(ctx context.Context) {
 
 // IsConnected - function for determining if the mqclient is connected or not
 func IsConnected() bool {
-	return mqclient != nil && mqclient.IsConnected()
+	return mqclient != nil && mqclient.IsConnectionOpen()
 }
 
 // CloseClient - function to close the mq connection from server

+ 6 - 1
pro/controllers/failover.go

@@ -134,10 +134,15 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 
 	failOverNode, exists := proLogic.FailOverExists(node.Network)
 	if !exists {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failover node doesn't exist in the network"), "badrequest"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("req-from: %s, failover node doesn't exist in the network", host.Name), "badrequest"))
 		return
 	}
 	var failOverReq models.FailOverMeReq

+ 9 - 0
pro/controllers/inet_gws.go

@@ -11,6 +11,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"github.com/gravitl/netmaker/servercfg"
 )
 
 // InetHandlers - handlers for internet gw
@@ -66,6 +67,14 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	proLogic.SetInternetGw(&node, request)
+	if servercfg.IsPro {
+		if _, exists := proLogic.FailOverExists(node.Network); exists {
+			go func() {
+				proLogic.ResetFailedOverPeer(&node)
+				mq.PublishPeerUpdate(false)
+			}()
+		}
+	}
 	err = logic.UpsertNode(&node)
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))

+ 5 - 2
pro/controllers/metrics.go

@@ -2,9 +2,11 @@ package controllers
 
 import (
 	"encoding/json"
-	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"net/http"
 
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"golang.org/x/exp/slog"
+
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
@@ -122,12 +124,13 @@ func getNetworkExtMetrics(w http.ResponseWriter, r *http.Request) {
 				continue
 			}
 			// if metrics for that client have been reported, append them
-			if len(ingressMetrics.Connectivity[clients[j].ClientID].NodeName) > 0 {
+			if _, ok := ingressMetrics.Connectivity[clients[j].ClientID]; ok {
 				networkMetrics.Connectivity[clients[j].ClientID] = ingressMetrics.Connectivity[clients[j].ClientID]
 			}
 		}
 	}
 
+	slog.Debug("sending collected client metrics", "metrics", networkMetrics.Connectivity)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)

+ 13 - 4
pro/controllers/middleware.go

@@ -1,16 +1,25 @@
 package controllers
 
 import (
+	"net/http"
+
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/servercfg"
-	"net/http"
 )
 
+var limitedApis = map[string]struct{}{
+	"/api/server/status":          {},
+	"/api/emqx/hosts":             {},
+	"/api/users/adm/authenticate": {},
+}
+
 func OnlyServerAPIWhenUnlicensedMiddleware(handler http.Handler) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
-		if servercfg.ErrLicenseValidation != nil && request.URL.Path != "/api/server/status" {
-			logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
-			return
+		if servercfg.ErrLicenseValidation != nil {
+			if _, ok := limitedApis[request.URL.Path]; !ok {
+				logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
+				return
+			}
 		}
 		handler.ServeHTTP(writer, request)
 	})

+ 1 - 0
pro/initialize.go

@@ -84,6 +84,7 @@ func InitPro() {
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
+	logic.FailOverExists = proLogic.FailOverExists
 	logic.CreateFailOver = proLogic.CreateFailOver
 	logic.GetFailOverPeerIps = proLogic.GetFailOverPeerIps
 	logic.DenyClientNodeAccess = proLogic.DenyClientNode

+ 6 - 0
pro/logic/failover.go

@@ -11,6 +11,9 @@ import (
 )
 
 func SetFailOverCtx(failOverNode, victimNode, peerNode models.Node) error {
+	if victimNode.IsIngressGateway || peerNode.IsIngressGateway || victimNode.IsInternetGateway || peerNode.IsInternetGateway {
+		return nil
+	}
 	if peerNode.FailOverPeers == nil {
 		peerNode.FailOverPeers = make(map[string]struct{})
 	}
@@ -119,6 +122,9 @@ func GetFailOverPeerIps(peer, node *models.Node) []net.IPNet {
 				}
 				allowedips = append(allowedips, allowed)
 			}
+			if failOverpeer.IsEgressGateway {
+				allowedips = append(allowedips, logic.GetEgressIPs(&failOverpeer)...)
+			}
 
 		}
 	}

+ 8 - 19
pro/logic/metrics.go

@@ -104,7 +104,6 @@ func MQUpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 }
 
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
-
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	if err != nil {
 		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
@@ -121,21 +120,13 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 	if newMetrics.Connectivity == nil {
 		newMetrics.Connectivity = make(map[string]models.Metric)
 	}
-	if len(attachedClients) > 0 {
-		// associate ext clients with IDs
-		for i := range attachedClients {
-			extMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
-			if len(extMetric.NodeName) == 0 &&
-				len(newMetrics.Connectivity[attachedClients[i].ClientID].NodeName) > 0 { // cover server clients
-				extMetric = newMetrics.Connectivity[attachedClients[i].ClientID]
-				if extMetric.TotalReceived > 0 && extMetric.TotalSent > 0 {
-					extMetric.Connected = true
-				}
-			}
-			extMetric.NodeName = attachedClients[i].ClientID
-			delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
-			newMetrics.Connectivity[attachedClients[i].ClientID] = extMetric
-		}
+	for i := range attachedClients {
+		slog.Debug("[metrics] processing attached client", "client", attachedClients[i].ClientID, "public key", attachedClients[i].PublicKey)
+		clientMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
+		clientMetric.NodeName = attachedClients[i].ClientID
+		newMetrics.Connectivity[attachedClients[i].ClientID] = clientMetric
+		delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
+		slog.Debug("[metrics] attached client metric", "metric", clientMetric)
 	}
 
 	// run through metrics for each peer
@@ -168,7 +159,5 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 
 	}
 
-	for k := range oldMetrics.Connectivity { // cleanup any left over data, self healing
-		delete(newMetrics.Connectivity, k)
-	}
+	slog.Debug("[metrics] node metrics data", "node ID", currentNode.ID, "metrics", newMetrics)
 }

+ 25 - 3
pro/logic/nodes.go

@@ -10,6 +10,11 @@ import (
 	"golang.org/x/exp/slog"
 )
 
+const (
+	IPv4Network = "0.0.0.0/0"
+	IPv6Network = "::/0"
+)
+
 func ValidateInetGwReq(inetNode models.Node, req models.InetNodeReq, update bool) error {
 	inetHost, err := logic.GetHost(inetNode.HostID.String())
 	if err != nil {
@@ -104,6 +109,9 @@ func SetDefaultGwForRelayedUpdate(relayed, relay models.Node, peerUpdate models.
 	if relay.InternetGwID != "" {
 		peerUpdate.ChangeDefaultGw = true
 		peerUpdate.DefaultGwIp = relay.Address.IP
+		if peerUpdate.DefaultGwIp == nil {
+			peerUpdate.DefaultGwIp = relay.Address6.IP
+		}
 
 	}
 	return peerUpdate
@@ -118,7 +126,9 @@ func SetDefaultGw(node models.Node, peerUpdate models.HostPeerUpdate) models.Hos
 		}
 		peerUpdate.ChangeDefaultGw = true
 		peerUpdate.DefaultGwIp = inetNode.Address.IP
-
+		if peerUpdate.DefaultGwIp == nil {
+			peerUpdate.DefaultGwIp = inetNode.Address6.IP
+		}
 	}
 	return peerUpdate
 }
@@ -140,6 +150,18 @@ func GetNetworkIngresses(network string) ([]models.Node, error) {
 
 // GetAllowedIpsForInet - get inet cidr for node using a inet gw
 func GetAllowedIpForInetNodeClient(node, peer *models.Node) []net.IPNet {
-	_, ipnet, _ := net.ParseCIDR("0.0.0.0/0")
-	return []net.IPNet{*ipnet}
+	var allowedips = []net.IPNet{}
+
+	if peer.Address.IP != nil {
+		_, ipnet, _ := net.ParseCIDR(IPv4Network)
+		allowedips = append(allowedips, *ipnet)
+		return allowedips
+	}
+
+	if peer.Address6.IP != nil {
+		_, ipnet, _ := net.ParseCIDR(IPv6Network)
+		allowedips = append(allowedips, *ipnet)
+	}
+
+	return allowedips
 }

+ 1 - 1
pro/logic/relays.go

@@ -238,7 +238,7 @@ func getRelayedAddresses(id string) []net.IPNet {
 		addrs = append(addrs, node.Address)
 	}
 	if node.Address6.IP != nil {
-		node.Address.Mask = net.CIDRMask(128, 128)
+		node.Address6.Mask = net.CIDRMask(128, 128)
 		addrs = append(addrs, node.Address6)
 	}
 	return addrs

+ 1 - 1
pro/trial.go

@@ -42,7 +42,7 @@ const trial_data_key = "trialdata"
 
 // stores trial end date
 func initTrial() error {
-	telData, _ := logic.FetchTelemetryData()
+	telData := logic.FetchTelemetryData()
 	if telData.Hosts > 0 || telData.Networks > 0 || telData.Users > 0 {
 		return nil // database is already populated, so skip creating trial
 	}

+ 1 - 0
pro/types.go

@@ -63,6 +63,7 @@ type Usage struct {
 	Egresses         int `json:"egresses"`
 	Relays           int `json:"relays"`
 	InternetGateways int `json:"internet_gateways"`
+	FailOvers        int `json:"fail_overs"`
 }
 
 // Usage.SetDefaults - sets the default values for usage

+ 4 - 0
pro/util.go

@@ -59,5 +59,9 @@ func getCurrentServerUsage() (limits Usage) {
 	if err == nil {
 		limits.InternetGateways = len(gateways)
 	}
+	failovers, err := logic.GetAllFailOvers()
+	if err == nil {
+		limits.FailOvers = len(failovers)
+	}
 	return
 }

+ 16 - 11
release.md

@@ -1,19 +1,24 @@
-# Netmaker v0.23.0
+# Netmaker v0.24.0
 
 ## Whats New ✨
 
-- Revamped Internet Gateways: hosts and clients can now use internet gateways! More info [here](https://docs.netmaker.io/pro/internet-gateways.html)
-  On community edition, internet gateways for clients can be accessed via the Remote Access tab.
-- PostUp and PostDown commands for clients
-- EMQX cloud support
-- Metadata for Remote Access Gateways
+- IPv6 and Dual Stack Networks Support Across Platform
+- Endpoint Detection Can Now Be Turned Off By Setting `ENDPOINT_DETECTION=false` On Server Config
+- New SignUp Flow For Oauth Users, With Admin Approval Process.
+- Added Failover Commands to nmctl
 
 ## What's Fixed/Improved 🛠
 
-- Allow creation of gateways, relays and egress without clients, relayed hosts and external ranges respectively
-- Make default host a remote access gateway and a failover host on joining a new network
-- Stability fixes with ACLs
-- Fixed issues with install/upgrade scripts
-- Fixed issues with CoreDNS
+- Scalability Fixes around Mq connection, ACLs
+- Fixed Zombie Node Logic To Avoid Choking On the Channel
+- Fixed Egress Routes In Dual Stack Netmaker Overlay Networks
+- Fixed Client Connectivity Metrics Data
+- Fixed auto-relay with enrollment key
+- Imporved Logic Around Oauth Sceret Management
+- Improved Oauth Message Templates
 
 ## Known Issues 🐞
+
+- Erratic Traffic Data In Metrics
+- `netclient server leave` Leaves a Stale Node Record In At Least One Network When Part Of Multiple Networks, But Can Be Deleted From The UI.
+- On Darwin Stale Egress Route Entries Remain On The Machine After Removing Egress Range Or Removing The Egress Server

+ 4 - 0
scripts/netmaker.default.env

@@ -53,6 +53,8 @@ TELEMETRY=on
 # OAuth section
 #
 ###
+# only mentioned domains will be allowded to signup using oauth, by default all domains are allowed
+ALLOWED_EMAIL_DOMAINS=*
 # "<azure-ad|github|google|oidc>"
 AUTH_PROVIDER=
 # "<client id of your oauth provider>"
@@ -71,3 +73,5 @@ JWT_VALIDITY_DURATION=43200
 RAC_AUTO_DISABLE=true
 # 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
+ENDPOINT_DETECTION=true

+ 32 - 4
scripts/nm-quick.sh

@@ -248,8 +248,8 @@ save_config() { (
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
-		"DEBUG_MODE"  "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" "CACHING_ENABLED")
+		"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")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 	done
@@ -694,7 +694,12 @@ upgrade() {
 	unset IMAGE_TAG
 	unset BUILD_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
-	BUILD_TAG=$UI_IMAGE_TAG
+	semver=$(chsv_check_version_ex "$UI_IMAGE_TAG")
+  	if [[ ! "$semver" ]]; then
+		BUILD_TAG=$LATEST
+	else
+		BUILD_TAG=$UI_IMAGE_TAG
+  	fi
 	echo "-----------------------------------------------------"
 	echo "Provide Details for pro installation:"
 	echo "    1. Log into https://app.netmaker.io"
@@ -720,7 +725,13 @@ downgrade () {
 	unset IMAGE_TAG
 	unset BUILD_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
-	BUILD_TAG=$UI_IMAGE_TAG
+
+	semver=$(chsv_check_version_ex "$UI_IMAGE_TAG")
+  	if [[ ! "$semver" ]]; then
+		BUILD_TAG=$LATEST
+	else
+		BUILD_TAG=$UI_IMAGE_TAG
+  	fi
 	save_config
 	if [ -a "$SCRIPT_DIR"/docker-compose.override.yml ]; then
 		rm -f "$SCRIPT_DIR"/docker-compose.override.yml
@@ -730,6 +741,23 @@ downgrade () {
 	install_netmaker
 }
 
+function chsv_check_version() {
+  if [[ $1 =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then
+    echo "$1"
+  else
+    echo ""
+  fi
+}
+
+function chsv_check_version_ex() {
+  if [[ $1 =~ ^v.+$ ]]; then
+    chsv_check_version "${1:1}"
+  else
+    chsv_check_version "${1}"
+  fi
+}
+
+
 
 main (){
 

+ 21 - 1
servercfg/serverconf.go

@@ -441,7 +441,7 @@ func GetPublicIP() (string, error) {
 	endpoint := ""
 	var err error
 
-	iplist := []string{"https://ip.server.gravitl.com", "https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
+	iplist := []string{"https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
 	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
 	if publicIpService != "" {
 		// prepend the user-specified service so it's checked first
@@ -674,6 +674,15 @@ func DeployedByOperator() bool {
 	return config.Config.Server.DeployedByOperator
 }
 
+// IsEndpointDetectionEnabled - returns true if endpoint detection enabled
+func IsEndpointDetectionEnabled() bool {
+	var enabled = true //default
+	if os.Getenv("ENDPOINT_DETECTION") != "" {
+		enabled = os.Getenv("ENDPOINT_DETECTION") == "true"
+	}
+	return enabled
+}
+
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)
 func GetEnvironment() string {
 	if env := os.Getenv("ENVIRONMENT"); env != "" {
@@ -703,3 +712,14 @@ func GetEmqxAppID() string {
 func GetEmqxAppSecret() string {
 	return os.Getenv("EMQX_APP_SECRET")
 }
+
+// GetAllowedEmailDomains - gets the allowed email domains for oauth signup
+func GetAllowedEmailDomains() string {
+	allowedDomains := "*"
+	if os.Getenv("ALLOWED_EMAIL_DOMAINS") != "" {
+		allowedDomains = os.Getenv("ALLOWED_EMAIL_DOMAINS")
+	} else if config.Config.Server.AllowedEmailDomains != "" {
+		allowedDomains = config.Config.Server.AllowedEmailDomains
+	}
+	return allowedDomains
+}

+ 527 - 30
swagger.yml

@@ -15,9 +15,43 @@ definitions:
         description: ACLContainer - the total list of all node's ACL in a given network
         type: object
         x-go-package: github.com/gravitl/netmaker/logic/acls
+    APIEnrollmentKey:
+        description: APIEnrollmentKey - used to create enrollment keys via API
+        properties:
+            expiration:
+                format: int64
+                type: integer
+                x-go-name: Expiration
+            networks:
+                items:
+                    type: string
+                type: array
+                x-go-name: Networks
+            relay:
+                type: string
+                x-go-name: Relay
+            tags:
+                items:
+                    type: string
+                type: array
+                x-go-name: Tags
+            type:
+                $ref: '#/definitions/KeyType'
+            unlimited:
+                type: boolean
+                x-go-name: Unlimited
+            uses_remaining:
+                format: int64
+                type: integer
+                x-go-name: UsesRemaining
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     ApiHost:
         description: ApiHost - the host struct for API usage
         properties:
+            autoupdate:
+                type: boolean
+                x-go-name: AutoUpdate
             debug:
                 type: boolean
                 x-go-name: Debug
@@ -35,18 +69,12 @@ definitions:
                 x-go-name: ID
             interfaces:
                 items:
-                    $ref: '#/definitions/Iface'
+                    $ref: '#/definitions/ApiIface'
                 type: array
                 x-go-name: Interfaces
             isdefault:
                 type: boolean
                 x-go-name: IsDefault
-            isrelay:
-                type: boolean
-                x-go-name: IsRelay
-            isrelayed:
-                type: boolean
-                x-go-name: IsRelayed
             isstatic:
                 type: boolean
                 x-go-name: IsStatic
@@ -75,17 +103,13 @@ definitions:
             os:
                 type: string
                 x-go-name: OS
+            persistentkeepalive:
+                format: int64
+                type: integer
+                x-go-name: PersistentKeepalive
             publickey:
                 type: string
                 x-go-name: PublicKey
-            relay_hosts:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayedHosts
-            relayed_by:
-                type: string
-                x-go-name: RelayedBy
             verbosity:
                 format: int64
                 type: integer
@@ -99,6 +123,139 @@ definitions:
                 x-go-name: WgPublicListenPort
         type: object
         x-go-package: github.com/gravitl/netmaker/models
+    ApiIface:
+        description: |-
+            ApiIface - the interface struct for API usage
+            The original Iface struct contains a net.Address, which does not get marshalled correctly
+        properties:
+            addressString:
+                type: string
+                x-go-name: AddressString
+            name:
+                type: string
+                x-go-name: Name
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    ApiNode:
+        description: ApiNode is a stripped down Node DTO that exposes only required fields to external systems
+        properties:
+            address:
+                type: string
+                x-go-name: Address
+            address6:
+                type: string
+                x-go-name: Address6
+            allowedips:
+                items:
+                    type: string
+                type: array
+                x-go-name: AllowedIPs
+            connected:
+                type: boolean
+                x-go-name: Connected
+            defaultacl:
+                description: == PRO ==
+                type: string
+                x-go-name: DefaultACL
+            dnson:
+                type: boolean
+                x-go-name: DNSOn
+            egressgatewaynatenabled:
+                type: boolean
+                x-go-name: EgressGatewayNatEnabled
+            egressgatewayranges:
+                items:
+                    type: string
+                type: array
+                x-go-name: EgressGatewayRanges
+            expdatetime:
+                format: int64
+                type: integer
+                x-go-name: ExpirationDateTime
+            fail_over_peers:
+                additionalProperties:
+                    type: object
+                type: object
+                x-go-name: FailOverPeers
+            failed_over_by:
+                format: uuid
+                type: string
+                x-go-name: FailedOverBy
+            hostid:
+                type: string
+                x-go-name: HostID
+            id:
+                type: string
+                x-go-name: ID
+            inet_node_req:
+                $ref: '#/definitions/InetNodeReq'
+            ingressdns:
+                type: string
+                x-go-name: IngressDns
+            internetgw_node_id:
+                type: string
+                x-go-name: InternetGwID
+            is_fail_over:
+                type: boolean
+                x-go-name: IsFailOver
+            isegressgateway:
+                type: boolean
+                x-go-name: IsEgressGateway
+            isingressgateway:
+                type: boolean
+                x-go-name: IsIngressGateway
+            isinternetgateway:
+                type: boolean
+                x-go-name: IsInternetGateway
+            isrelay:
+                type: boolean
+                x-go-name: IsRelay
+            isrelayed:
+                type: boolean
+                x-go-name: IsRelayed
+            lastcheckin:
+                format: int64
+                type: integer
+                x-go-name: LastCheckIn
+            lastmodified:
+                format: int64
+                type: integer
+                x-go-name: LastModified
+            lastpeerupdate:
+                format: int64
+                type: integer
+                x-go-name: LastPeerUpdate
+            localaddress:
+                type: string
+                x-go-name: LocalAddress
+            metadata:
+                type: string
+                x-go-name: Metadata
+            network:
+                type: string
+                x-go-name: Network
+            networkrange:
+                type: string
+                x-go-name: NetworkRange
+            networkrange6:
+                type: string
+                x-go-name: NetworkRange6
+            pendingdelete:
+                type: boolean
+                x-go-name: PendingDelete
+            relayedby:
+                type: string
+                x-go-name: RelayedBy
+            relaynodes:
+                items:
+                    type: string
+                type: array
+                x-go-name: RelayedNodes
+            server:
+                type: string
+                x-go-name: Server
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     AuthParams:
         description: AuthParams - struct for auth params
         properties:
@@ -135,6 +292,12 @@ definitions:
                     type: string
                 type: array
                 x-go-name: ExtraAllowedIPs
+            postdown:
+                type: string
+                x-go-name: PostDown
+            postup:
+                type: string
+                x-go-name: PostUp
             publickey:
                 type: string
                 x-go-name: PublicKey
@@ -187,6 +350,32 @@ definitions:
                 x-go-name: Ranges
         type: object
         x-go-package: github.com/gravitl/netmaker/models
+    EgressInfo:
+        description: EgressInfo - struct for egress info
+        properties:
+            egress_gateway_cfg:
+                $ref: '#/definitions/EgressGatewayRequest'
+            egress_gw_addr:
+                $ref: '#/definitions/IPNet'
+            egress_id:
+                type: string
+                x-go-name: EgressID
+            network:
+                $ref: '#/definitions/IPNet'
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    EgressNetworkRoutes:
+        description: EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
+        properties:
+            egress_ranges:
+                items:
+                    type: string
+                type: array
+                x-go-name: EgressRanges
+            node_addr:
+                $ref: '#/definitions/IPNet'
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     EnrollmentKey:
         description: EnrollmentKey - the key used to register hosts and join them to specific networks
         properties:
@@ -199,6 +388,10 @@ definitions:
                     type: string
                 type: array
                 x-go-name: Networks
+            relay:
+                format: uuid
+                type: string
+                x-go-name: Relay
             tags:
                 items:
                     type: string
@@ -230,6 +423,11 @@ definitions:
             address6:
                 type: string
                 x-go-name: Address6
+            allowed_ips:
+                items:
+                    type: string
+                type: array
+                x-go-name: AllowedIPs
             clientid:
                 type: string
                 x-go-name: ClientID
@@ -265,6 +463,12 @@ definitions:
             ownerid:
                 type: string
                 x-go-name: OwnerID
+            postdown:
+                type: string
+                x-go-name: PostDown
+            postup:
+                type: string
+                x-go-name: PostUp
             privatekey:
                 type: string
                 x-go-name: PrivateKey
@@ -280,6 +484,19 @@ definitions:
         title: File represents an open file descriptor.
         type: object
         x-go-package: os
+    FwUpdate:
+        description: FwUpdate - struct for firewall updates
+        properties:
+            egress_info:
+                additionalProperties:
+                    $ref: '#/definitions/EgressInfo'
+                type: object
+                x-go-name: EgressInfo
+            is_egress_gw:
+                type: boolean
+                x-go-name: IsEgressGw
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     HardwareAddr:
         items:
             format: uint8
@@ -362,6 +579,8 @@ definitions:
             os:
                 type: string
                 x-go-name: OS
+            persistentkeepalive:
+                $ref: '#/definitions/Duration'
             publickey:
                 $ref: '#/definitions/Key'
             traffickeypublic:
@@ -386,11 +605,55 @@ definitions:
                 x-go-name: WgPublicListenPort
         type: object
         x-go-package: github.com/gravitl/netmaker/models
+    HostInfoMap:
+        additionalProperties:
+            $ref: '#/definitions/HostNetworkInfo'
+        description: HostInfoMap - map of host public keys to host networking info
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    HostNetworkInfo:
+        description: HostNetworkInfo - holds info related to host networking (used for client side peer calculations)
+        properties:
+            interfaces:
+                items:
+                    $ref: '#/definitions/Iface'
+                type: array
+                x-go-name: Interfaces
+            is_static:
+                type: boolean
+                x-go-name: IsStatic
+            listen_port:
+                format: int64
+                type: integer
+                x-go-name: ListenPort
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     HostPull:
         description: HostPull - response of a host's pull
         properties:
+            change_default_gw:
+                type: boolean
+                x-go-name: ChangeDefaultGw
+            default_gw_ip:
+                type: string
+                x-go-name: DefaultGwIp
+            egress_network_routes:
+                items:
+                    $ref: '#/definitions/EgressNetworkRoutes'
+                type: array
+                x-go-name: EgressRoutes
+            endpoint_detection:
+                type: boolean
+                x-go-name: EndpointDetection
+            fw_update:
+                $ref: '#/definitions/FwUpdate'
             host:
                 $ref: '#/definitions/Host'
+            host_network_info:
+                $ref: '#/definitions/HostInfoMap'
+            is_inet_gw:
+                type: boolean
+                x-go-name: IsInternetGw
             nodes:
                 items:
                     $ref: '#/definitions/Node'
@@ -413,6 +676,9 @@ definitions:
             address:
                 type: string
                 x-go-name: Address
+            host_id:
+                type: string
+                x-go-name: HostID
             id:
                 type: string
                 x-go-name: ID
@@ -466,6 +732,16 @@ definitions:
                 x-go-name: Name
         type: object
         x-go-package: github.com/gravitl/netmaker/models
+    InetNodeReq:
+        description: InetNodeReq - exit node request struct
+        properties:
+            inet_node_client_ids:
+                items:
+                    type: string
+                type: array
+                x-go-name: InetNodeClientIDs
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     Key:
         description: |-
             A Key is a public, private, or pre-shared secret key.  The Key constructor
@@ -758,13 +1034,15 @@ definitions:
                 format: date-time
                 type: string
                 x-go-name: ExpirationDateTime
-            failover:
-                type: boolean
-                x-go-name: Failover
-            failovernode:
+            fail_over_peers:
+                additionalProperties:
+                    type: object
+                type: object
+                x-go-name: FailOverPeers
+            failed_over_by:
                 format: uuid
                 type: string
-                x-go-name: FailoverNode
+                x-go-name: FailedOverBy
             hostid:
                 format: uuid
                 type: string
@@ -773,6 +1051,8 @@ definitions:
                 format: uuid
                 type: string
                 x-go-name: ID
+            inet_node_req:
+                $ref: '#/definitions/InetNodeReq'
             ingressdns:
                 type: string
                 x-go-name: IngressDNS
@@ -782,14 +1062,21 @@ definitions:
             ingressgatewayrange6:
                 type: string
                 x-go-name: IngressGatewayRange6
-            internetgateway:
-                $ref: '#/definitions/UDPAddr'
+            internetgw_node_id:
+                type: string
+                x-go-name: InternetGwID
+            is_fail_over:
+                type: boolean
+                x-go-name: IsFailOver
             isegressgateway:
                 type: boolean
                 x-go-name: IsEgressGateway
             isingressgateway:
                 type: boolean
                 x-go-name: IsIngressGateway
+            isinternetgateway:
+                type: boolean
+                x-go-name: IsInternetGateway
             isrelay:
                 type: boolean
                 x-go-name: IsRelay
@@ -810,6 +1097,9 @@ definitions:
                 x-go-name: LastPeerUpdate
             localaddress:
                 $ref: '#/definitions/IPNet'
+            metadata:
+                type: string
+                x-go-name: Metadata
             network:
                 type: string
                 x-go-name: Network
@@ -823,8 +1113,6 @@ definitions:
             pendingdelete:
                 type: boolean
                 x-go-name: PendingDelete
-            persistentkeepalive:
-                $ref: '#/definitions/Duration'
             relayedby:
                 type: string
                 x-go-name: RelayedBy
@@ -918,6 +1206,8 @@ definitions:
                 type: string
             APIPort:
                 type: string
+            AllowedEmailDomains:
+                type: string
             AllowedOrigin:
                 type: string
             AuthProvider:
@@ -930,6 +1220,8 @@ definitions:
                 type: string
             BrokerType:
                 type: string
+            CacheEnabled:
+                type: string
             ClientID:
                 type: string
             ClientSecret:
@@ -965,6 +1257,8 @@ definitions:
             IsEE:
                 type: string
                 x-go-name: IsPro
+            JwtValidityDuration:
+                $ref: '#/definitions/Duration'
             LicenseValue:
                 type: string
             MQPassword:
@@ -997,6 +1291,8 @@ definitions:
                 type: string
             PublicIPService:
                 type: string
+            RacAutoDisable:
+                type: boolean
             RestBackend:
                 type: string
             SQLConn:
@@ -1033,6 +1329,9 @@ definitions:
                 type: integer
             Version:
                 type: string
+            endpoint_detection:
+                type: boolean
+                x-go-name: EndpointDetection
         type: object
         x-go-package: github.com/gravitl/netmaker/config
     Signal:
@@ -1040,9 +1339,18 @@ definitions:
         properties:
             action:
                 $ref: '#/definitions/SignalAction'
+            from_host_id:
+                type: string
+                x-go-name: FromHostID
             from_host_pubkey:
                 type: string
                 x-go-name: FromHostPubKey
+            from_node_id:
+                type: string
+                x-go-name: FromNodeID
+            is_pro:
+                type: boolean
+                x-go-name: IsPro
             reply:
                 type: boolean
                 x-go-name: Reply
@@ -1053,12 +1361,15 @@ definitions:
                 format: int64
                 type: integer
                 x-go-name: TimeStamp
+            to_host_id:
+                type: string
+                x-go-name: ToHostID
             to_host_pubkey:
                 type: string
                 x-go-name: ToHostPubKey
-            turn_relay_addr:
+            to_node_id:
                 type: string
-                x-go-name: TurnRelayEndpoint
+                x-go-name: ToNodeID
         type: object
         x-go-package: github.com/gravitl/netmaker/models
     SignalAction:
@@ -1114,6 +1425,10 @@ definitions:
             issuperadmin:
                 type: boolean
                 x-go-name: IsSuperAdmin
+            last_login_time:
+                format: date-time
+                type: string
+                x-go-name: LastLoginTime
             password:
                 type: string
                 x-go-name: Password
@@ -1149,7 +1464,7 @@ info:
 
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
-    version: 0.23.0
+    version: 0.24.0
 paths:
     /api/dns:
         get:
@@ -1277,6 +1592,17 @@ paths:
             summary: Push DNS entries to nameserver.
             tags:
                 - dns
+    /api/emqx/hosts:
+        delete:
+            operationId: delEmqxHosts
+            responses:
+                "200":
+                    $ref: '#/responses/apiHostResponse'
+            schemes:
+                - https
+            summary: Lists all hosts.
+            tags:
+                - hosts
     /api/extclients:
         get:
             operationId: getAllExtClients
@@ -1468,7 +1794,7 @@ paths:
             operationId: getHosts
             responses:
                 "200":
-                    $ref: '#/responses/apiHostResponse'
+                    $ref: '#/responses/apiHostSliceResponse'
             schemes:
                 - https
             summary: Lists all hosts.
@@ -1721,6 +2047,23 @@ paths:
     /api/networks/{networkname}/acls:
         get:
             operationId: getNetworkACL
+            parameters:
+                - description: 'name: network name'
+                  in: path
+                  name: networkname
+                  required: true
+                  type: string
+                  x-go-name: Networkname
+            responses:
+                "200":
+                    $ref: '#/responses/aclContainerResponse'
+            schemes:
+                - https
+            summary: Get a network ACL (Access Control List).
+            tags:
+                - networks
+        put:
+            operationId: updateNetworkACL
             parameters:
                 - description: 'name: network name'
                   in: path
@@ -1739,9 +2082,10 @@ paths:
                     $ref: '#/responses/aclContainerResponse'
             schemes:
                 - https
-            summary: Get a network ACL (Access Control List).
+            summary: Update a network ACL (Access Control List).
             tags:
                 - networks
+    /api/networks/{networkname}/acls/v2:
         put:
             operationId: updateNetworkACL
             parameters:
@@ -1765,6 +2109,17 @@ paths:
             summary: Update a network ACL (Access Control List).
             tags:
                 - networks
+    /api/node/{nodeid}/failOverME:
+        post:
+            operationId: failOver_me
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
     /api/nodes:
         get:
             operationId: getAllNodes
@@ -2014,6 +2369,37 @@ paths:
             summary: Remove a relay.
             tags:
                 - nodes
+    /api/nodes/{network}/{nodeid}/inet_gw:
+        delete:
+            operationId: deleteInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Delete an internet gw.
+            tags:
+                - nodes
+        post:
+            operationId: createInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create an inet node.
+            tags:
+                - nodes
+        put:
+            operationId: updateInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: update an inet node.
+            tags:
+                - nodes
     /api/nodes/{network}/{nodeid}/ingress/users:
         get:
             operationId: ingressGatewayUsers
@@ -2287,6 +2673,49 @@ paths:
             summary: Transfers superadmin role to an admin user.
             tags:
                 - user
+    /api/users_pending:
+        get:
+            operationId: getPendingUsers
+            responses:
+                "200":
+                    $ref: '#/responses/userBodyResponse'
+            schemes:
+                - https
+            summary: Get all pending users.
+            tags:
+                - user
+    /api/users_pending/{username}/pending:
+        delete:
+            operationId: deleteAllPendingUsers
+            responses:
+                "200":
+                    $ref: '#/responses/userBodyResponse'
+            schemes:
+                - https
+            summary: delete all pending users.
+            tags:
+                - user
+    /api/users_pending/user/{username}:
+        delete:
+            operationId: deletePendingUser
+            responses:
+                "200":
+                    $ref: '#/responses/userBodyResponse'
+            schemes:
+                - https
+            summary: delete pending user.
+            tags:
+                - user
+        post:
+            operationId: approvePendingUser
+            responses:
+                "200":
+                    $ref: '#/responses/userBodyResponse'
+            schemes:
+                - https
+            summary: approve pending user.
+            tags:
+                - user
     /api/v1/enrollment-keys:
         get:
             operationId: getEnrollmentKeys
@@ -2300,6 +2729,13 @@ paths:
                 - enrollmentKeys
         post:
             operationId: createEnrollmentKey
+            parameters:
+                - description: APIEnrollmentKey
+                  in: body
+                  name: body
+                  schema:
+                    $ref: '#/definitions/APIEnrollmentKey'
+                  x-go-name: Body
             responses:
                 "200":
                     $ref: '#/responses/EnrollmentKey'
@@ -2325,6 +2761,29 @@ paths:
             summary: Deletes an EnrollmentKey from Netmaker server.
             tags:
                 - enrollmentKeys
+        put:
+            operationId: updateEnrollmentKey
+            parameters:
+                - description: KeyID
+                  in: path
+                  name: keyid
+                  required: true
+                  type: string
+                  x-go-name: KeyID
+                - description: APIEnrollmentKey
+                  in: body
+                  name: body
+                  schema:
+                    $ref: '#/definitions/APIEnrollmentKey'
+                  x-go-name: Body
+            responses:
+                "200":
+                    $ref: '#/responses/EnrollmentKey'
+            schemes:
+                - https
+            summary: Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
+            tags:
+                - enrollmentKeys
     /api/v1/enrollment-keys/{token}:
         post:
             operationId: handleHostRegister
@@ -2347,6 +2806,17 @@ paths:
             summary: Handles a Netclient registration with server and add nodes accordingly.
             tags:
                 - enrollmentKeys
+    /api/v1/fallback/host/{hostid}:
+        put:
+            operationId: hostUpdateFallback
+            responses:
+                "200":
+                    $ref: '#/responses/apiHostResponse'
+            schemes:
+                - https
+            summary: Updates a Netclient host on Netmaker server.
+            tags:
+                - hosts
     /api/v1/host:
         get:
             description: Used by clients for "pull" command
@@ -2369,6 +2839,27 @@ paths:
             summary: Delete all legacy nodes from DB.
             tags:
                 - nodes
+    /api/v1/node/failover:
+        delete:
+            operationId: deletefailOver
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
+        post:
+            operationId: createfailOver
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
     /api/v1/nodes/migrate:
         put:
             operationId: migrateData
@@ -2423,6 +2914,12 @@ responses:
         description: ""
         schema:
             $ref: '#/definitions/ApiHost'
+    apiHostSliceResponse:
+        description: ""
+        schema:
+            items:
+                $ref: '#/definitions/ApiHost'
+            type: array
     byteArrayResponse:
         description: ""
         schema:
@@ -2474,7 +2971,7 @@ responses:
         description: ""
         schema:
             items:
-                $ref: '#/definitions/LegacyNode'
+                $ref: '#/definitions/ApiNode'
             type: array
     okResponse:
         description: ""