Просмотр исходного кода

Merge pull request #2936 from gravitl/release-v0.24.1

v0.24.1
Abhishek K 1 год назад
Родитель
Сommit
b2ae18dde3

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

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

+ 1 - 1
README.md

@@ -16,7 +16,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.24.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.24.1-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" />

+ 34 - 273
auth/auth.go

@@ -3,156 +3,25 @@ package auth
 import (
 	"encoding/base64"
 	"encoding/json"
-	"errors"
 	"fmt"
-	"net/http"
-	"strings"
-	"time"
 
-	"golang.org/x/crypto/bcrypt"
-	"golang.org/x/exp/slog"
-	"golang.org/x/oauth2"
-
-	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/exp/slog"
+	"golang.org/x/oauth2"
 )
 
 // == consts ==
 const (
-	init_provider          = "initprovider"
-	get_user_info          = "getuserinfo"
-	handle_callback        = "handlecallback"
-	handle_login           = "handlelogin"
-	google_provider_name   = "google"
-	azure_ad_provider_name = "azure-ad"
-	github_provider_name   = "github"
-	oidc_provider_name     = "oidc"
-	verify_user            = "verifyuser"
-	user_signin_length     = 16
-	node_signin_length     = 64
-	headless_signin_length = 32
+	node_signin_length = 64
 )
 
-// OAuthUser - generic OAuth strategy user
-type OAuthUser struct {
-	Name              string `json:"name" bson:"name"`
-	Email             string `json:"email" bson:"email"`
-	Login             string `json:"login" bson:"login"`
-	UserPrincipalName string `json:"userPrincipalName" bson:"userPrincipalName"`
-	AccessToken       string `json:"accesstoken" bson:"accesstoken"`
-}
-
 var (
 	auth_provider *oauth2.Config
-	upgrader      = websocket.Upgrader{}
 )
 
-func getCurrentAuthFunctions() map[string]interface{} {
-	var authInfo = servercfg.GetAuthProviderInfo()
-	var authProvider = authInfo[0]
-	switch authProvider {
-	case google_provider_name:
-		return google_functions
-	case azure_ad_provider_name:
-		return azure_ad_functions
-	case github_provider_name:
-		return github_functions
-	case oidc_provider_name:
-		return oidc_functions
-	default:
-		return nil
-	}
-}
-
-// InitializeAuthProvider - initializes the auth provider if any is present
-func InitializeAuthProvider() string {
-	var functions = getCurrentAuthFunctions()
-	if functions == nil {
-		return ""
-	}
-	logger.Log(0, "setting oauth secret")
-	var err = logic.SetAuthSecret(logic.RandomString(64))
-	if err != nil {
-		logger.FatalLog("failed to set auth_secret", err.Error())
-	}
-	var authInfo = servercfg.GetAuthProviderInfo()
-	var serverConn = servercfg.GetAPIHost()
-	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
-		serverConn = "http://" + serverConn
-		logger.Log(1, "localhost OAuth detected, proceeding with insecure http redirect: (", serverConn, ")")
-	} else {
-		serverConn = "https://" + serverConn
-		logger.Log(1, "external OAuth detected, proceeding with https redirect: ("+serverConn+")")
-	}
-
-	if authInfo[0] == "oidc" {
-		functions[init_provider].(func(string, string, string, string))(serverConn+"/api/oauth/callback", authInfo[1], authInfo[2], authInfo[3])
-		return authInfo[0]
-	}
-
-	functions[init_provider].(func(string, string, string))(serverConn+"/api/oauth/callback", authInfo[1], authInfo[2])
-	return authInfo[0]
-}
-
-// HandleAuthCallback - handles oauth callback
-// Note: not included in API reference as part of the OAuth process itself.
-func HandleAuthCallback(w http.ResponseWriter, r *http.Request) {
-	if auth_provider == nil {
-		handleOauthNotConfigured(w)
-		return
-	}
-	var functions = getCurrentAuthFunctions()
-	if functions == nil {
-		return
-	}
-	state, _ := getStateAndCode(r)
-	_, err := netcache.Get(state) // if in netcache proceeed with node registration login
-	if err == nil || errors.Is(err, netcache.ErrExpired) {
-		switch len(state) {
-		case node_signin_length:
-			logger.Log(1, "proceeding with host SSO callback")
-			HandleHostSSOCallback(w, r)
-		case headless_signin_length:
-			logger.Log(1, "proceeding with headless SSO callback")
-			HandleHeadlessSSOCallback(w, r)
-		default:
-			logger.Log(1, "invalid state length: ", fmt.Sprintf("%d", len(state)))
-		}
-	} else { // handle normal login
-		functions[handle_callback].(func(http.ResponseWriter, *http.Request))(w, r)
-	}
-}
-
-// swagger:route GET /api/oauth/login nodes HandleAuthLogin
-//
-// Handles OAuth login.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//			Responses:
-//			200:  okResponse
-func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
-	if auth_provider == nil {
-		handleOauthNotConfigured(w)
-		return
-	}
-	var functions = getCurrentAuthFunctions()
-	if functions == nil {
-		return
-	}
-	if servercfg.GetFrontendURL() == "" {
-		handleOauthNotConfigured(w)
-		return
-	}
-	functions[handle_login].(func(http.ResponseWriter, *http.Request))(w, r)
-}
-
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
 	var currentValue, err = FetchPassValue("")
@@ -163,81 +32,30 @@ func IsOauthUser(user *models.User) error {
 	return bCryptErr
 }
 
-// HandleHeadlessSSO - handles the OAuth login flow for headless interfaces such as Netmaker CLI via websocket
-func HandleHeadlessSSO(w http.ResponseWriter, r *http.Request) {
-	conn, err := upgrader.Upgrade(w, r, nil)
-	if err != nil {
-		logger.Log(0, "error during connection upgrade for headless sign-in:", err.Error())
-		return
-	}
-	if conn == nil {
-		logger.Log(0, "failed to establish web-socket connection during headless sign-in")
-		return
-	}
-	defer conn.Close()
+func FetchPassValue(newValue string) (string, error) {
 
-	req := &netcache.CValue{User: "", Pass: ""}
-	stateStr := logic.RandomString(headless_signin_length)
-	if err = netcache.Set(stateStr, req); err != nil {
-		logger.Log(0, "Failed to process sso request -", err.Error())
-		return
+	type valueHolder struct {
+		Value string `json:"value" bson:"value"`
 	}
-
-	timeout := make(chan bool, 1)
-	answer := make(chan string, 1)
-	defer close(answer)
-	defer close(timeout)
-
-	if auth_provider == nil {
-		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
-			logger.Log(0, "error during message writing:", err.Error())
-		}
-		return
+	newValueHolder := valueHolder{}
+	var currentValue, err = logic.FetchAuthSecret()
+	if err != nil {
+		return "", err
 	}
-	redirectUrl = fmt.Sprintf("https://%s/api/oauth/register/%s", servercfg.GetAPIConnString(), stateStr)
-	if err = conn.WriteMessage(websocket.TextMessage, []byte(redirectUrl)); err != nil {
-		logger.Log(0, "error during message writing:", err.Error())
+	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
+	if unmarshErr != nil {
+		return "", unmarshErr
 	}
 
-	go func() {
-		for {
-			cachedReq, err := netcache.Get(stateStr)
-			if err != nil {
-				if strings.Contains(err.Error(), "expired") {
-					logger.Log(0, "timeout occurred while waiting for SSO")
-					timeout <- true
-					break
-				}
-				continue
-			} else if cachedReq.Pass != "" {
-				logger.Log(0, "SSO process completed for user ", cachedReq.User)
-				answer <- cachedReq.Pass
-				break
-			}
-			time.Sleep(500) // try it 2 times per second to see if auth is completed
-		}
-	}()
-
-	select {
-	case result := <-answer:
-		if err = conn.WriteMessage(websocket.TextMessage, []byte(result)); err != nil {
-			logger.Log(0, "Error during message writing:", err.Error())
-		}
-	case <-timeout:
-		logger.Log(0, "Authentication server time out for headless SSO login")
-		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
-			logger.Log(0, "Error during message writing:", err.Error())
-		}
-	}
-	if err = netcache.Del(stateStr); err != nil {
-		logger.Log(0, "failed to remove SSO cache entry", err.Error())
-	}
-	if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
-		logger.Log(0, "write close:", err.Error())
+	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
+	if b64Err != nil {
+		logger.Log(0, "could not decode pass")
+		return "", nil
 	}
+	return string(b64CurrentValue), nil
 }
 
-// == private methods ==
+// == private ==
 
 func addUser(email string) error {
 	var hasSuperAdmin, err = logic.HasSuperAdmin()
@@ -247,7 +65,7 @@ func addUser(email string) error {
 	} // generate random password to adapt to current model
 	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
-		slog.Error("failed to get password", "error", err.Error())
+		slog.Error("failed to get password", "error", fetchErr.Error())
 		return fetchErr
 	}
 	var newUser = models.User{
@@ -273,77 +91,20 @@ func addUser(email string) error {
 	return nil
 }
 
-func FetchPassValue(newValue string) (string, error) {
-
-	type valueHolder struct {
-		Value string `json:"value" bson:"value"`
-	}
-	newValueHolder := valueHolder{}
-	var currentValue, err = logic.FetchAuthSecret()
-	if err != nil {
-		return "", err
-	}
-	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
-	if unmarshErr != nil {
-		return "", unmarshErr
-	}
-
-	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
-	if b64Err != nil {
-		logger.Log(0, "could not decode pass")
-		return "", nil
-	}
-	return string(b64CurrentValue), nil
-}
-
-func getStateAndCode(r *http.Request) (string, string) {
-	var state, code string
-	if r.FormValue("state") != "" && r.FormValue("code") != "" {
-		state = r.FormValue("state")
-		code = r.FormValue("code")
-	} else if r.URL.Query().Get("state") != "" && r.URL.Query().Get("code") != "" {
-		state = r.URL.Query().Get("state")
-		code = r.URL.Query().Get("code")
-	}
-
-	return state, code
-}
-
-func (user *OAuthUser) getUserName() string {
-	var userName string
-	if user.Email != "" {
-		userName = user.Email
-	} else if user.Login != "" {
-		userName = user.Login
-	} else if user.UserPrincipalName != "" {
-		userName = user.UserPrincipalName
-	} else if user.Name != "" {
-		userName = user.Name
-	}
-	return userName
-}
+func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) {
 
-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
+	user, err := logic.GetUser(username)
+	if err != nil && shouldAddUser { // user must not exist, so try to make one
+		if err = addUser(username); err != nil {
+			logger.Log(0, "failed to add user", username, "during a node SSO network join on network", network)
+			// response := returnErrTemplate(user.UserName, "failed to add user", state, reqKeyIf)
+			// w.WriteHeader(http.StatusInternalServerError)
+			// w.Write(response)
+			return nil, fmt.Errorf("failed to add user to system")
 		}
+		logger.Log(0, "user", username, "was added during a node SSO network join on network", network)
+		user, _ = logic.GetUser(username)
 	}
-	return false
+
+	return user, nil
 }

+ 24 - 18
auth/host_session.go

@@ -3,7 +3,6 @@ package auth
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/google/uuid"
@@ -59,12 +58,12 @@ func SessionHandler(conn *websocket.Conn) {
 		logger.Log(0, "Failed to process sso request -", err.Error())
 		return
 	}
+	defer netcache.Del(stateStr)
 	// Wait for the user to finish his auth flow...
-	timeout := make(chan bool, 1)
+	timeout := make(chan bool, 2)
 	answer := make(chan netcache.CValue, 1)
 	defer close(answer)
 	defer close(timeout)
-
 	if len(registerMessage.User) > 0 { // handle basic auth
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 
@@ -111,6 +110,10 @@ func SessionHandler(conn *websocket.Conn) {
 		}
 	} else { // handle SSO / OAuth
 		if auth_provider == nil {
+			err = conn.WriteMessage(messageType, []byte("Oauth not configured"))
+			if err != nil {
+				logger.Log(0, "error during message writing:", err.Error())
+			}
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
 				logger.Log(0, "error during message writing:", err.Error())
@@ -118,29 +121,37 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 		}
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "via SSO")
-		redirectUrl = fmt.Sprintf("https://%s/api/oauth/register/%s", servercfg.GetAPIConnString(), stateStr)
+		redirectUrl := fmt.Sprintf("https://%s/api/oauth/register/%s", servercfg.GetAPIConnString(), stateStr)
 		err = conn.WriteMessage(messageType, []byte(redirectUrl))
 		if err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 		}
 	}
 
+	go func() {
+		for {
+			msgType, _, err := conn.ReadMessage()
+			if err != nil || msgType == websocket.CloseMessage {
+				netcache.Del(stateStr)
+				return
+			}
+		}
+	}()
+
 	go func() {
 		for {
 			cachedReq, err := netcache.Get(stateStr)
 			if err != nil {
-				if strings.Contains(err.Error(), "expired") {
-					logger.Log(1, "timeout occurred while waiting for SSO registration")
-					timeout <- true
-					break
-				}
-				continue
+				logger.Log(0, "oauth state has been deleted ", err.Error())
+				timeout <- true
+				break
+
 			} else if len(cachedReq.User) > 0 {
 				logger.Log(0, "host SSO process completed for user", cachedReq.User)
 				answer <- *cachedReq
 				break
 			}
-			time.Sleep(500) // try it 2 times per second to see if auth is completed
+			time.Sleep(time.Second)
 		}
 	}()
 
@@ -217,13 +228,8 @@ func SessionHandler(conn *websocket.Conn) {
 		}
 		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil)
 	case <-timeout: // the read from req.answerCh has timed out
-		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
-			logger.Log(0, "error during timeout message writing:", err.Error())
-		}
-	}
-	// The entry is not needed anymore, but we will let the producer to close it to avoid panic cases
-	if err = netcache.Del(stateStr); err != nil {
-		logger.Log(0, "failed to remove node SSO cache entry", err.Error())
+		logger.Log(0, "timeout signal recv,exiting oauth socket conn")
+		break
 	}
 	// Cleanly close the connection by sending a close message and then
 	// waiting (with timeout) for the server to close the connection.

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

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

+ 1 - 1
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.24.0
+//	Version: 0.24.1
 //	Host: api.demo.netmaker.io
 //
 //	Consumes:

+ 1 - 1
controllers/enrollmentkeys.go

@@ -279,7 +279,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// version check
-	if !logic.IsVersionComptatible(newHost.Version) {
+	if !logic.IsVersionCompatible(newHost.Version) {
 		err := fmt.Errorf("bad client version on register: %s", newHost.Version)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return

+ 31 - 26
controllers/ext_client.go

@@ -15,13 +15,13 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/servercfg"
 
 	"github.com/gravitl/netmaker/models"
 
 	"github.com/gravitl/netmaker/mq"
 	"github.com/skip2/go-qrcode"
+	"golang.org/x/exp/slices"
 	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
@@ -200,6 +200,24 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	preferredIp := strings.TrimSpace(r.URL.Query().Get("preferredip"))
+	if preferredIp != "" {
+		allowedPreferredIps := []string{}
+		for i := range gwnode.AdditionalRagIps {
+			allowedPreferredIps = append(allowedPreferredIps, gwnode.AdditionalRagIps[i].String())
+		}
+		allowedPreferredIps = append(allowedPreferredIps, host.EndpointIP.String())
+		allowedPreferredIps = append(allowedPreferredIps, host.EndpointIPv6.String())
+		if !slices.Contains(allowedPreferredIps, preferredIp) {
+			slog.Warn("preferred endpoint ip is not associated with the RAG. proceeding with preferred ip", "preferred ip", preferredIp)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("preferred endpoint ip is not associated with the RAG"), "badrequest"))
+			return
+		}
+		if net.ParseIP(preferredIp).To4() == nil {
+			preferredIp = fmt.Sprintf("[%s]", preferredIp)
+		}
+	}
+
 	addrString := client.Address
 	if addrString != "" {
 		addrString += "/32"
@@ -215,12 +233,18 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if network.DefaultKeepalive != 0 {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 	}
+
 	gwendpoint := ""
-	if host.EndpointIP.To4() == nil {
-		gwendpoint = fmt.Sprintf("[%s]:%d", host.EndpointIP.String(), host.ListenPort)
+	if preferredIp == "" {
+		if host.EndpointIP.To4() == nil {
+			gwendpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), host.ListenPort)
+		} else {
+			gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
+		}
 	} else {
-		gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
+		gwendpoint = fmt.Sprintf("%s:%d", preferredIp, host.ListenPort)
 	}
+
 	var newAllowedIPs string
 	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
@@ -605,36 +629,17 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = logic.DeleteExtClient(params["network"], params["clientid"])
+	err = logic.DeleteExtClientAndCleanup(extclient)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to delete extclient [%s],network [%s]: %v", clientid, network, err))
+		slog.Error("deleteExtClient: ", "Error", err.Error())
 		err = errors.New("Could not delete extclient " + params["clientid"])
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 
-	// delete client acls
-	var networkAcls acls.ACLContainer
-	networkAcls, err = networkAcls.Get(acls.ContainerID(network))
-	if err != nil {
-		slog.Error("failed to get network acls", "err", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	for objId := range networkAcls {
-		delete(networkAcls[objId], acls.AclID(clientid))
-	}
-	delete(networkAcls, acls.AclID(clientid))
-	if _, err = networkAcls.Save(acls.ContainerID(network)); err != nil {
-		slog.Error("failed to update network acls", "err", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
 	go func() {
 		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
-			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())
+			slog.Error("error setting ext peers on " + ingressnode.ID.String() + ": " + err.Error())
 		}
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()

+ 1 - 1
controllers/migrate.go

@@ -51,7 +51,7 @@ func migrate(w http.ResponseWriter, r *http.Request) {
 		}
 		var legacyNode models.LegacyNode
 		if err = json.Unmarshal([]byte(record), &legacyNode); err != nil {
-			slog.Error("decoding legacy node", "errror", err)
+			slog.Error("decoding legacy node", "error", err)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("decode legacy node %w", err), "badrequest"))
 			return
 		}

+ 2 - 16
controllers/network.go

@@ -442,36 +442,22 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 
 	// validate address ranges: must be private
 	if network.AddressRange != "" {
-		_, ipNet, err := net.ParseCIDR(network.AddressRange)
+		_, _, err := net.ParseCIDR(network.AddressRange)
 		if err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 				err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			return
 		}
-		if !ipNet.IP.IsPrivate() {
-			err := errors.New("address range must be private")
-			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
-				err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-			return
-		}
 	}
 	if network.AddressRange6 != "" {
-		_, ipNet, err := net.ParseCIDR(network.AddressRange6)
+		_, _, err := net.ParseCIDR(network.AddressRange6)
 		if err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 				err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			return
 		}
-		if !ipNet.IP.IsPrivate() {
-			err := errors.New("address range must be private")
-			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
-				err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-			return
-		}
 	}
 
 	network, err = logic.CreateNetwork(network)

+ 8 - 2
controllers/node.go

@@ -202,7 +202,7 @@ func Authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Han
 			}
 
 			isnetadmin := issuperadmin || isadmin
-			if errN == nil && (issuperadmin || isadmin) {
+			if issuperadmin || isadmin {
 				nodeID = "mastermac"
 				isAuthorized = true
 				r.Header.Set("ismasterkey", "yes")
@@ -635,14 +635,20 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("metadata cannot be longer than 255 characters"), "badrequest"))
 		return
 	}
+	if !servercfg.IsPro {
+		newData.AdditionalRagIps = []string{}
+	}
 	newNode := newData.ConvertToServerNode(&currentNode)
+	if newNode == nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("error converting node"), "badrequest"))
+		return
+	}
 	if newNode.IsInternetGateway != currentNode.IsInternetGateway {
 		if newNode.IsInternetGateway {
 			logic.SetInternetGw(newNode, models.InetNodeReq{})
 		} else {
 			logic.UnsetInternetGw(newNode)
 		}
-
 	}
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {

+ 1 - 5
controllers/user.go

@@ -32,10 +32,6 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
-	r.HandleFunc("/api/oauth/login", auth.HandleAuthLogin).Methods(http.MethodGet)
-	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)
@@ -119,7 +115,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	successJSONResponse, jsonError := json.Marshal(successResponse)
 	if jsonError != nil {
 		logger.Log(0, username,
-			"error marshalling resp: ", err.Error())
+			"error marshalling resp: ", jsonError.Error())
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 	}

+ 6 - 10
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.19.0
+	github.com/go-playground/validator/v10 v10.20.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -15,13 +15,12 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.22.0
+	golang.org/x/crypto v0.23.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.org/x/oauth2 v0.20.0
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
-	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -46,7 +45,7 @@ require (
 )
 
 require (
-	cloud.google.com/go/compute/metadata v0.2.3 // indirect
+	cloud.google.com/go/compute/metadata v0.3.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
@@ -54,17 +53,14 @@ require (
 )
 
 require (
-	cloud.google.com/go/compute v1.20.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/hashicorp/go-version v1.6.0
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	google.golang.org/appengine v1.6.8 // indirect
 )

+ 12 - 26
go.sum

@@ -1,7 +1,5 @@
-cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
-cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -27,15 +25,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/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.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
-github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.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.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=
@@ -97,8 +90,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.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/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 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=
@@ -110,8 +103,8 @@ 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/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@@ -124,8 +117,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.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=
@@ -134,25 +127,18 @@ 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/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.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=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

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

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

+ 33 - 0
logic/extpeers.go

@@ -11,6 +11,7 @@ import (
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
@@ -95,6 +96,35 @@ func DeleteExtClient(network string, clientid string) error {
 	return nil
 }
 
+// DeleteExtClientAndCleanup - deletes an existing ext client and update ACLs
+func DeleteExtClientAndCleanup(extClient models.ExtClient) error {
+
+	//delete extClient record
+	err := DeleteExtClient(extClient.Network, extClient.ClientID)
+	if err != nil {
+		slog.Error("DeleteExtClientAndCleanup-remove extClient record: ", "Error", err.Error())
+		return err
+	}
+
+	//update ACLs
+	var networkAcls acls.ACLContainer
+	networkAcls, err = networkAcls.Get(acls.ContainerID(extClient.Network))
+	if err != nil {
+		slog.Error("DeleteExtClientAndCleanup-update network acls: ", "Error", err.Error())
+		return err
+	}
+	for objId := range networkAcls {
+		delete(networkAcls[objId], acls.AclID(extClient.ClientID))
+	}
+	delete(networkAcls, acls.AclID(extClient.ClientID))
+	if _, err = networkAcls.Save(acls.ContainerID(extClient.Network)); err != nil {
+		slog.Error("DeleteExtClientAndCleanup-update network acls:", "Error", err.Error())
+		return err
+	}
+
+	return nil
+}
+
 // GetNetworkExtClients - gets the ext clients of given network
 func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 	var extclients []models.ExtClient
@@ -445,6 +475,9 @@ func getExtpeersExtraRoutes(network string) (egressRoutes []models.EgressNetwork
 		return
 	}
 	for _, extPeer := range extPeers {
+		if len(extPeer.ExtraAllowedIPs) == 0 {
+			continue
+		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(extPeer)...)
 	}
 	return

+ 0 - 1
logic/hosts.go

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

+ 22 - 11
logic/peers.go

@@ -173,9 +173,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			}
 			if peer.IsEgressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
-					NodeAddr:     node.Address,
-					NodeAddr6:    node.Address6,
-					EgressRanges: peer.EgressGatewayRanges,
+					EgressGwAddr:  peer.Address,
+					EgressGwAddr6: peer.Address6,
+					NodeAddr:      node.Address,
+					NodeAddr6:     node.Address6,
+					EgressRanges:  peer.EgressGatewayRanges,
 				})
 			}
 			if peer.IsIngressGateway {
@@ -213,16 +215,15 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				}
 			}
 
+			//1. check currHost has ipv4 endpoint and peerhost has ipv4 then set ipv4 endpoint for peer
+			// 2. check currHost has ipv6 endpoint and peerhost has ipv6 then set ipv6 endpoint for peer
+
 			//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 {
+			var peerEndpoint net.IP
+			if host.EndpointIP != nil && peerHost.EndpointIP != 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
-				}
+			} else if host.EndpointIPv6 != nil && peerHost.EndpointIPv6 != nil {
+				peerEndpoint = peerHost.EndpointIPv6
 			}
 
 			peerConfig.Endpoint = &net.UDPAddr{
@@ -306,6 +307,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
 				},
+				Network6: node.NetworkRange6,
+				EgressGwAddr6: net.IPNet{
+					IP:   node.Address6.IP,
+					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
+				},
 				EgressGWCfg: node.EgressGatewayRequest,
 			}
 
@@ -323,6 +329,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					Mask: getCIDRMaskFromAddr(node.PrimaryAddress()),
 				},
+				Network6: node.NetworkRange6,
+				EgressGwAddr6: net.IPNet{
+					IP:   node.Address6.IP,
+					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
+				},
 				EgressGWCfg: models.EgressGatewayRequest{
 					NodeID:     fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 					NetID:      node.Network,

+ 1 - 1
logic/telemetry.go

@@ -106,7 +106,7 @@ func FetchTelemetryData() telemetryData {
 func getServerCount() int {
 	data, err := database.FetchRecords(database.SERVER_UUID_TABLE_NAME)
 	if err != nil {
-		logger.Log(0, "errror retrieving server data", err.Error())
+		logger.Log(0, "error retrieving server data", err.Error())
 	}
 	return len(data)
 }

+ 1 - 1
logic/version.go

@@ -10,7 +10,7 @@ import (
 const MinVersion = "v0.17.0"
 
 // IsVersionCompatible checks that the version passed is compabtible (>=) with MinVersion
-func IsVersionComptatible(ver string) bool {
+func IsVersionCompatible(ver string) bool {
 	// during dev, assume developers know what they are doing
 	if ver == "dev" {
 		return true

+ 5 - 5
logic/version_test.go

@@ -9,27 +9,27 @@ import (
 func TestVersion(t *testing.T) {
 	t.Run("valid version", func(t *testing.T) {
 		is := is.New(t)
-		valid := IsVersionComptatible("v0.17.1-testing")
+		valid := IsVersionCompatible("v0.17.1-testing")
 		is.Equal(valid, true)
 	})
 	t.Run("dev version", func(t *testing.T) {
 		is := is.New(t)
-		valid := IsVersionComptatible("dev")
+		valid := IsVersionCompatible("dev")
 		is.Equal(valid, true)
 	})
 	t.Run("invalid version", func(t *testing.T) {
 		is := is.New(t)
-		valid := IsVersionComptatible("v0.14.2-refactor")
+		valid := IsVersionCompatible("v0.14.2-refactor")
 		is.Equal(valid, false)
 	})
 	t.Run("no version", func(t *testing.T) {
 		is := is.New(t)
-		valid := IsVersionComptatible("testing")
+		valid := IsVersionCompatible("testing")
 		is.Equal(valid, false)
 	})
 	t.Run("incomplete version", func(t *testing.T) {
 		is := is.New(t)
-		valid := IsVersionComptatible("0.18")
+		valid := IsVersionCompatible("0.18")
 		is.Equal(valid, true)
 	})
 }

+ 1 - 1
logic/zombie.go

@@ -49,7 +49,7 @@ func CheckZombies(newnode *models.Node) {
 func checkForZombieHosts(h *models.Host) {
 	hosts, err := GetAllHosts()
 	if err != nil {
-		logger.Log(3, "errror retrieving all hosts", err.Error())
+		logger.Log(3, "error retrieving all hosts", err.Error())
 	}
 	for _, existing := range hosts {
 		if existing.ID == h.ID {

+ 1 - 9
main.go

@@ -12,7 +12,6 @@ import (
 	"sync"
 	"syscall"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/config"
 	controller "github.com/gravitl/netmaker/controllers"
 	"github.com/gravitl/netmaker/database"
@@ -28,7 +27,7 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.24.0"
+var version = "v0.24.1"
 
 // Start DB Connection and start API Request Handler
 func main() {
@@ -91,13 +90,6 @@ func initialize() { // Client Mode Prereq Check
 
 	logic.SetJWTSecret()
 
-	var authProvider = auth.InitializeAuthProvider()
-	if authProvider != "" {
-		logger.Log(0, "OAuth provider,", authProvider+",", "initialized")
-	} else {
-		logger.Log(0, "no OAuth provider found or not configured, continuing without OAuth")
-	}
-
 	err = serverctl.SetDefaults()
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())

+ 6 - 0
models/api_host.go

@@ -44,7 +44,13 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a := ApiHost{}
 	a.Debug = h.Debug
 	a.EndpointIP = h.EndpointIP.String()
+	if a.EndpointIP == "<nil>" {
+		a.EndpointIP = ""
+	}
 	a.EndpointIPv6 = h.EndpointIPv6.String()
+	if a.EndpointIPv6 == "<nil>" {
+		a.EndpointIPv6 = ""
+	}
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
 	a.Interfaces = make([]ApiIface, len(h.Interfaces))

+ 14 - 0
models/api_node.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/google/uuid"
+	"golang.org/x/exp/slog"
 )
 
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
@@ -44,6 +45,7 @@ type ApiNode struct {
 	IsInternetGateway bool                `json:"isinternetgateway" yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
+	AdditionalRagIps  []string            `json:"additional_rag_ips" yaml:"additional_rag_ips"`
 }
 
 // ApiNode.ConvertToServerNode - converts an api node to a server node
@@ -109,6 +111,14 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.LastPeerUpdate = time.Unix(a.LastPeerUpdate, 0)
 	convertedNode.ExpirationDateTime = time.Unix(a.ExpirationDateTime, 0)
 	convertedNode.Metadata = a.Metadata
+	for _, ip := range a.AdditionalRagIps {
+		ragIp := net.ParseIP(ip)
+		if ragIp == nil {
+			slog.Error("error parsing additional rag ip", "error", err, "ip", ip)
+			return nil
+		}
+		convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp)
+	}
 	return &convertedNode
 }
 
@@ -163,6 +173,10 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata
+	apiNode.AdditionalRagIps = []string{}
+	for _, ip := range nm.AdditionalRagIps {
+		apiNode.AdditionalRagIps = append(apiNode.AdditionalRagIps, ip.String())
+	}
 	return &apiNode
 }
 

+ 11 - 7
models/mqtt.go

@@ -34,17 +34,21 @@ type IngressInfo struct {
 
 // EgressInfo - struct for egress info
 type EgressInfo struct {
-	EgressID     string               `json:"egress_id" yaml:"egress_id"`
-	Network      net.IPNet            `json:"network" yaml:"network"`
-	EgressGwAddr net.IPNet            `json:"egress_gw_addr" yaml:"egress_gw_addr"`
-	EgressGWCfg  EgressGatewayRequest `json:"egress_gateway_cfg" yaml:"egress_gateway_cfg"`
+	EgressID      string               `json:"egress_id" yaml:"egress_id"`
+	Network       net.IPNet            `json:"network" yaml:"network"`
+	EgressGwAddr  net.IPNet            `json:"egress_gw_addr" yaml:"egress_gw_addr"`
+	Network6      net.IPNet            `json:"network6" yaml:"network6"`
+	EgressGwAddr6 net.IPNet            `json:"egress_gw_addr6" yaml:"egress_gw_addr6"`
+	EgressGWCfg   EgressGatewayRequest `json:"egress_gateway_cfg" yaml:"egress_gateway_cfg"`
 }
 
 // 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"`
+	EgressGwAddr  net.IPNet `json:"egress_gw_addr" yaml:"egress_gw_addr"`
+	EgressGwAddr6 net.IPNet `json:"egress_gw_addr6" yaml:"egress_gw_addr6"`
+	NodeAddr      net.IPNet `json:"node_addr"`
+	NodeAddr6     net.IPNet `json:"node_addr6"`
+	EgressRanges  []string  `json:"egress_ranges"`
 }
 
 // PeerRouteInfo - struct for peer info for an ext. client

+ 1 - 0
models/node.go

@@ -96,6 +96,7 @@ type Node struct {
 	IsInternetGateway bool                `json:"isinternetgateway" yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
+	AdditionalRagIps  []net.IP            `json:"additional_rag_ips" yaml:"additional_rag_ips"`
 }
 
 // LegacyNode - legacy struct for node model

+ 6 - 0
models/structs.go

@@ -72,7 +72,9 @@ type UserRemoteGws struct {
 	IsInternetGateway bool      `json:"is_internet_gateway"`
 	GwClient          ExtClient `json:"gw_client"`
 	GwPeerPublicKey   string    `json:"gw_peer_public_key"`
+	GwListenPort      int       `json:"gw_listen_port"`
 	Metadata          string    `json:"metadata"`
+	AllowedEndpoints  []string  `json:"allowed_endpoints"`
 }
 
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
@@ -372,3 +374,7 @@ type LoginReqDto struct {
 const (
 	ResHeaderKeyStAccessToken = "St-Access-Token"
 )
+
+type GetClientConfReqDto struct {
+	PreferredIp string `json:"preferred_ip"`
+}

+ 1 - 0
mq/migrate.go

@@ -57,6 +57,7 @@ func getEmqxAuthTokenOld() (string, error) {
 	if err != nil {
 		return "", err
 	}
+	defer resp.Body.Close()
 	msg, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return "", err

+ 1 - 0
mq/mq.go

@@ -129,6 +129,7 @@ func SetupMQTT(fatal bool) {
 
 // Keepalive -- periodically pings all nodes to let them know server is still alive and doing well
 func Keepalive(ctx context.Context) {
+	go PublishPeerUpdate(true)
 	for {
 		select {
 		case <-ctx.Done():

+ 4 - 1
mq/publishers.go

@@ -195,7 +195,10 @@ func PushMetricsToExporter(metrics models.Metrics) error {
 	if err != nil {
 		return errors.New("failed to marshal metrics: " + err.Error())
 	}
-	if token := mqclient.Publish("metrics_exporter", 2, true, data); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil {
+	if mqclient == nil || !mqclient.IsConnectionOpen() {
+		return errors.New("cannot publish ... mqclient not connected")
+	}
+	if token := mqclient.Publish("metrics_exporter", 0, true, data); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil {
 		var err error
 		if token.Error() == nil {
 			err = errors.New("connection timeout")

+ 276 - 0
pro/auth/auth.go

@@ -0,0 +1,276 @@
+package auth
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gorilla/websocket"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/pro/netcache"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/oauth2"
+)
+
+// == consts ==
+const (
+	init_provider          = "initprovider"
+	get_user_info          = "getuserinfo"
+	handle_callback        = "handlecallback"
+	handle_login           = "handlelogin"
+	google_provider_name   = "google"
+	azure_ad_provider_name = "azure-ad"
+	github_provider_name   = "github"
+	oidc_provider_name     = "oidc"
+	verify_user            = "verifyuser"
+	user_signin_length     = 16
+	node_signin_length     = 64
+	headless_signin_length = 32
+)
+
+// OAuthUser - generic OAuth strategy user
+type OAuthUser struct {
+	Name              string `json:"name" bson:"name"`
+	Email             string `json:"email" bson:"email"`
+	Login             string `json:"login" bson:"login"`
+	UserPrincipalName string `json:"userPrincipalName" bson:"userPrincipalName"`
+	AccessToken       string `json:"accesstoken" bson:"accesstoken"`
+}
+
+var (
+	auth_provider *oauth2.Config
+	upgrader      = websocket.Upgrader{}
+)
+
+func getCurrentAuthFunctions() map[string]interface{} {
+	var authInfo = servercfg.GetAuthProviderInfo()
+	var authProvider = authInfo[0]
+	switch authProvider {
+	case google_provider_name:
+		return google_functions
+	case azure_ad_provider_name:
+		return azure_ad_functions
+	case github_provider_name:
+		return github_functions
+	case oidc_provider_name:
+		return oidc_functions
+	default:
+		return nil
+	}
+}
+
+// InitializeAuthProvider - initializes the auth provider if any is present
+func InitializeAuthProvider() string {
+	var functions = getCurrentAuthFunctions()
+	if functions == nil {
+		return ""
+	}
+	logger.Log(0, "setting oauth secret")
+	var err = logic.SetAuthSecret(logic.RandomString(64))
+	if err != nil {
+		logger.FatalLog("failed to set auth_secret", err.Error())
+	}
+	var authInfo = servercfg.GetAuthProviderInfo()
+	var serverConn = servercfg.GetAPIHost()
+	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
+		serverConn = "http://" + serverConn
+		logger.Log(1, "localhost OAuth detected, proceeding with insecure http redirect: (", serverConn, ")")
+	} else {
+		serverConn = "https://" + serverConn
+		logger.Log(1, "external OAuth detected, proceeding with https redirect: ("+serverConn+")")
+	}
+
+	if authInfo[0] == "oidc" {
+		functions[init_provider].(func(string, string, string, string))(serverConn+"/api/oauth/callback", authInfo[1], authInfo[2], authInfo[3])
+		return authInfo[0]
+	}
+
+	functions[init_provider].(func(string, string, string))(serverConn+"/api/oauth/callback", authInfo[1], authInfo[2])
+	return authInfo[0]
+}
+
+// HandleAuthCallback - handles oauth callback
+// Note: not included in API reference as part of the OAuth process itself.
+func HandleAuthCallback(w http.ResponseWriter, r *http.Request) {
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
+		return
+	}
+	var functions = getCurrentAuthFunctions()
+	if functions == nil {
+		return
+	}
+	state, _ := getStateAndCode(r)
+	_, err := netcache.Get(state) // if in netcache proceeed with node registration login
+	if err == nil || errors.Is(err, netcache.ErrExpired) {
+		switch len(state) {
+		case node_signin_length:
+			logger.Log(1, "proceeding with host SSO callback")
+			HandleHostSSOCallback(w, r)
+		case headless_signin_length:
+			logger.Log(1, "proceeding with headless SSO callback")
+			HandleHeadlessSSOCallback(w, r)
+		default:
+			logger.Log(1, "invalid state length: ", fmt.Sprintf("%d", len(state)))
+		}
+	} else { // handle normal login
+		functions[handle_callback].(func(http.ResponseWriter, *http.Request))(w, r)
+	}
+}
+
+// swagger:route GET /api/oauth/login nodes HandleAuthLogin
+//
+// Handles OAuth login.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//			Responses:
+//			200:  okResponse
+func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
+		return
+	}
+	var functions = getCurrentAuthFunctions()
+	if functions == nil {
+		return
+	}
+	if servercfg.GetFrontendURL() == "" {
+		handleOauthNotConfigured(w)
+		return
+	}
+	functions[handle_login].(func(http.ResponseWriter, *http.Request))(w, r)
+}
+
+// HandleHeadlessSSO - handles the OAuth login flow for headless interfaces such as Netmaker CLI via websocket
+func HandleHeadlessSSO(w http.ResponseWriter, r *http.Request) {
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		logger.Log(0, "error during connection upgrade for headless sign-in:", err.Error())
+		return
+	}
+	if conn == nil {
+		logger.Log(0, "failed to establish web-socket connection during headless sign-in")
+		return
+	}
+	defer conn.Close()
+
+	req := &netcache.CValue{User: "", Pass: ""}
+	stateStr := logic.RandomString(headless_signin_length)
+	if err = netcache.Set(stateStr, req); err != nil {
+		logger.Log(0, "Failed to process sso request -", err.Error())
+		return
+	}
+
+	timeout := make(chan bool, 1)
+	answer := make(chan string, 1)
+	defer close(answer)
+	defer close(timeout)
+
+	if auth_provider == nil {
+		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+			logger.Log(0, "error during message writing:", err.Error())
+		}
+		return
+	}
+	redirectUrl = fmt.Sprintf("https://%s/api/oauth/register/%s", servercfg.GetAPIConnString(), stateStr)
+	if err = conn.WriteMessage(websocket.TextMessage, []byte(redirectUrl)); err != nil {
+		logger.Log(0, "error during message writing:", err.Error())
+	}
+
+	go func() {
+		for {
+			cachedReq, err := netcache.Get(stateStr)
+			if err != nil {
+				if strings.Contains(err.Error(), "expired") {
+					logger.Log(0, "timeout occurred while waiting for SSO")
+					timeout <- true
+					break
+				}
+				continue
+			} else if cachedReq.Pass != "" {
+				logger.Log(0, "SSO process completed for user ", cachedReq.User)
+				answer <- cachedReq.Pass
+				break
+			}
+			time.Sleep(500) // try it 2 times per second to see if auth is completed
+		}
+	}()
+
+	select {
+	case result := <-answer:
+		if err = conn.WriteMessage(websocket.TextMessage, []byte(result)); err != nil {
+			logger.Log(0, "Error during message writing:", err.Error())
+		}
+	case <-timeout:
+		logger.Log(0, "Authentication server time out for headless SSO login")
+		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+			logger.Log(0, "Error during message writing:", err.Error())
+		}
+	}
+	if err = netcache.Del(stateStr); err != nil {
+		logger.Log(0, "failed to remove SSO cache entry", err.Error())
+	}
+	if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+		logger.Log(0, "write close:", err.Error())
+	}
+}
+
+// == private methods ==
+
+func getStateAndCode(r *http.Request) (string, string) {
+	var state, code string
+	if r.FormValue("state") != "" && r.FormValue("code") != "" {
+		state = r.FormValue("state")
+		code = r.FormValue("code")
+	} else if r.URL.Query().Get("state") != "" && r.URL.Query().Get("code") != "" {
+		state = r.URL.Query().Get("state")
+		code = r.URL.Query().Get("code")
+	}
+
+	return state, code
+}
+
+func (user *OAuthUser) getUserName() string {
+	var userName string
+	if user.Email != "" {
+		userName = user.Email
+	} else if user.Login != "" {
+		userName = user.Login
+	} else if user.UserPrincipalName != "" {
+		userName = user.UserPrincipalName
+	} else if user.Name != "" {
+		userName = user.Name
+	}
+	return userName
+}
+
+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
+}

+ 8 - 2
auth/azure-ad.go → pro/auth/azure-ad.go

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -58,6 +60,10 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getAzureUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
+		if strings.Contains(err.Error(), "invalid oauth state") {
+			handleOauthNotValid(w)
+			return
+		}
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -96,7 +102,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -121,7 +127,7 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth state")
 	}
-	var token, err = auth_provider.Exchange(context.Background(), code)
+	var token, err = auth_provider.Exchange(context.Background(), code, oauth2.SetAuthURLParam("prompt", "login"))
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 	}

+ 12 - 0
auth/error.go → pro/auth/error.go

@@ -10,6 +10,12 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 </body>
 </html>`
 
+const oauthStateInvalid = `<!DOCTYPE html><html>
+<body>
+<h3>Invalid OAuth Session. Please re-try again.</h3>
+</body>
+</html>`
+
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
 <h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
@@ -86,6 +92,12 @@ func handleOauthNotConfigured(response http.ResponseWriter) {
 	response.Write([]byte(oauthNotConfigured))
 }
 
+func handleOauthNotValid(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusBadRequest)
+	response.Write([]byte(oauthStateInvalid))
+}
+
 func handleSomethingWentWrong(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusInternalServerError)

+ 8 - 2
auth/github.go → pro/auth/github.go

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -58,6 +60,10 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGithubUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
+		if strings.Contains(err.Error(), "invalid oauth state") {
+			handleOauthNotValid(w)
+			return
+		}
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -96,7 +102,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -121,7 +127,7 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth state")
 	}
-	var token, err = auth_provider.Exchange(context.Background(), code)
+	var token, err = auth_provider.Exchange(context.Background(), code, oauth2.SetAuthURLParam("prompt", "login"))
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 	}

+ 8 - 2
auth/google.go → pro/auth/google.go

@@ -6,8 +6,10 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 	"time"
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -60,6 +62,10 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGoogleUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
+		if strings.Contains(err.Error(), "invalid oauth state") {
+			handleOauthNotValid(w)
+			return
+		}
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -99,7 +105,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -124,7 +130,7 @@ func getGoogleUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth state")
 	}
-	var token, err = auth_provider.Exchange(context.Background(), code)
+	var token, err = auth_provider.Exchange(context.Background(), code, oauth2.SetAuthURLParam("prompt", "login"))
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 	}

+ 3 - 2
auth/headless_callback.go → pro/auth/headless_callback.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
@@ -52,7 +53,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 
 	// check if user approval is already pending
 	if logic.IsPendingUser(userClaims.getUserName()) {
-		handleOauthUserNotAllowed(w)
+		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	user, err := logic.GetUser(userClaims.getUserName())
@@ -62,7 +63,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		w.Write(response)
 		return
 	}
-	newPass, fetchErr := FetchPassValue("")
+	newPass, fetchErr := auth.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 8 - 2
auth/oidc.go → pro/auth/oidc.go

@@ -4,9 +4,11 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -71,6 +73,10 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getOIDCUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from callback:", err.Error())
+		if strings.Contains(err.Error(), "invalid oauth state") {
+			handleOauthNotValid(w)
+			return
+		}
 		handleOauthNotConfigured(w)
 		return
 	}
@@ -109,7 +115,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -146,7 +152,7 @@ func getOIDCUserInfo(state string, code string) (u *OAuthUser, e error) {
 	ctx, cancel := context.WithTimeout(context.Background(), OIDC_TIMEOUT)
 	defer cancel()
 
-	oauth2Token, err := auth_provider.Exchange(ctx, code)
+	oauth2Token, err := auth_provider.Exchange(ctx, code, oauth2.SetAuthURLParam("prompt", "login"))
 	if err != nil {
 		return nil, fmt.Errorf("failed to exchange oauth2 token using code \"%s\"", code)
 	}

+ 12 - 22
auth/register_callback.go → pro/auth/register_callback.go

@@ -10,7 +10,6 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
-	"github.com/gravitl/netmaker/models"
 )
 
 var (
@@ -68,7 +67,18 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) {
 		w.Write(response)
 		return
 	}
-
+	// check if user exists
+	user, err := logic.GetUser(userClaims.getUserName())
+	if err != nil {
+		handleOauthUserNotFound(w)
+		return
+	}
+	if !user.IsAdmin && !user.IsSuperAdmin {
+		response := returnErrTemplate(userClaims.getUserName(), "only admin users can register using SSO", state, reqKeyIf)
+		w.WriteHeader(http.StatusForbidden)
+		w.Write(response)
+		return
+	}
 	logger.Log(1, "registering host for user:", userClaims.getUserName(), reqKeyIf.Host.Name, reqKeyIf.Host.ID.String())
 
 	// Send OK to user in the browser
@@ -145,23 +155,3 @@ func RegisterHostSSO(w http.ResponseWriter, r *http.Request) {
 
 	http.Redirect(w, r, auth_provider.AuthCodeURL(machineKeyStr), http.StatusSeeOther)
 }
-
-// == private ==
-
-func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) {
-
-	user, err := logic.GetUser(username)
-	if err != nil && shouldAddUser { // user must not exist, so try to make one
-		if err = addUser(username); err != nil {
-			logger.Log(0, "failed to add user", username, "during a node SSO network join on network", network)
-			// response := returnErrTemplate(user.UserName, "failed to add user", state, reqKeyIf)
-			// w.WriteHeader(http.StatusInternalServerError)
-			// w.Write(response)
-			return nil, fmt.Errorf("failed to add user to system")
-		}
-		logger.Log(0, "user", username, "was added during a node SSO network join on network", network)
-		user, _ = logic.GetUser(username)
-	}
-
-	return user, nil
-}

+ 0 - 0
auth/templates.go → pro/auth/templates.go


+ 39 - 1
pro/controllers/users.go

@@ -10,6 +10,8 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/pro/auth"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 )
@@ -19,6 +21,10 @@ func UserHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGws)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
+	r.HandleFunc("/api/oauth/login", auth.HandleAuthLogin).Methods(http.MethodGet)
+	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)
 }
 
 // swagger:route POST /api/users/{username}/remote_access_gw user attachUserToRemoteAccessGateway
@@ -114,7 +120,15 @@ func removeUserFromRemoteAccessGW(w http.ResponseWriter, r *http.Request) {
 		}
 		for _, extclient := range extclients {
 			if extclient.OwnerID == user.UserName && remoteGwID == extclient.IngressGatewayID {
-				logic.DeleteExtClient(extclient.Network, extclient.ClientID)
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
 			}
 		}
 		if servercfg.IsDNSMode() {
@@ -212,7 +226,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				userGws[node.Network] = gws
 				delete(user.RemoteGwIDs, node.ID.String())
@@ -227,7 +243,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				userGws[node.Network] = gws
 				processedAdminNodeIds[node.ID.String()] = struct{}{}
@@ -260,7 +278,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 				Network:           node.Network,
 				IsInternetGateway: node.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
+				GwListenPort:      logic.GetPeerListenPort(host),
 				Metadata:          node.Metadata,
+				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			})
 			userGws[node.Network] = gws
 		}
@@ -287,7 +307,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Network:           node.Network,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				userGws[node.Network] = gws
 			}
@@ -338,3 +360,19 @@ func ingressGatewayUsers(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(gwUsers)
 }
+
+func getAllowedRagEndpoints(ragNode *models.Node, ragHost *models.Host) []string {
+	endpoints := []string{}
+	if len(ragHost.EndpointIP) > 0 {
+		endpoints = append(endpoints, ragHost.EndpointIP.String())
+	}
+	if len(ragHost.EndpointIPv6) > 0 {
+		endpoints = append(endpoints, ragHost.EndpointIPv6.String())
+	}
+	if servercfg.IsPro {
+		for _, ip := range ragNode.AdditionalRagIps {
+			endpoints = append(endpoints, ip.String())
+		}
+	}
+	return endpoints
+}

+ 8 - 0
pro/initialize.go

@@ -11,6 +11,7 @@ import (
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/pro/auth"
 	proControllers "github.com/gravitl/netmaker/pro/controllers"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
@@ -81,6 +82,13 @@ func InitPro() {
 			AddRacHooks()
 		}
 
+		var authProvider = auth.InitializeAuthProvider()
+		if authProvider != "" {
+			slog.Info("OAuth provider,", authProvider+",", "initialized")
+		} else {
+			slog.Error("no OAuth provider found or not configured, continuing without OAuth")
+		}
+
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer

+ 1 - 1
pro/logic/nodes.go

@@ -148,7 +148,7 @@ func GetNetworkIngresses(network string) ([]models.Node, error) {
 	return ingresses, nil
 }
 
-// GetAllowedIpsForInet - get inet cidr for node using a inet gw
+// GetAllowedIpForInetNodeClient - get inet cidr for node using a inet gw
 func GetAllowedIpForInetNodeClient(node, peer *models.Node) []net.IPNet {
 	var allowedips = []net.IPNet{}
 

+ 12 - 14
release.md

@@ -1,24 +1,22 @@
-# Netmaker v0.24.0
+# Netmaker v0.24.1
 
 ## Whats New ✨
-
-- 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
+- Users Can define Multiple Endpoints On The Remote Access Gateway To Choose From While Establishing a Connection.
+- OAUTH Code Moved From CE To Pro.
+- nm-quick.sh Enhancement To Install The Latest Docker To Enable Support Of the Latest Distros.
+- IPv6 Enhancements.
 
 ## What's Fixed/Improved 🛠
 
-- 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
+- Egress Enhancement In Multiple Networks
+- Fix armv5-v7 Upgrade Download Link
+- Fix Windows Interface Issue In Multiple Networks
+- SSO network join Improvements.
+- Remove Egress Routes After Egress Gateway Removed
+- Remote Access Gateway Connection Handling Improvements.
 
 ## 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
+- IPv6 internet traffic does not route to the InetGw in Dual Stack Network

+ 116 - 15
scripts/nm-quick.sh

@@ -91,9 +91,11 @@ set_buildinfo() {
 
 # install_yq - install yq if not present
 install_yq() {
-	if ! command -v yq &>/dev/null; then
-		wget -qO /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
-		chmod +x /usr/bin/yq
+	if [ -f /etc/debian_version ]; then
+		if ! command -v yq &>/dev/null; then
+			wget -qO /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+			chmod +x /usr/bin/yq
+		fi
 	fi
 	set +e
 	if ! command -v yq &>/dev/null; then
@@ -122,6 +124,7 @@ setup_netclient() {
 	chmod +x netclient
 	./netclient install
 	echo "Register token: $TOKEN"
+	sleep 2
 	netclient register -t $TOKEN
 
 	echo "waiting for netclient to become available"
@@ -143,7 +146,7 @@ setup_netclient() {
 
 # configure_netclient - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
-
+	sleep 2
 	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
 	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
 		echo "Error obtaining NODE_ID for the new network"
@@ -298,7 +301,7 @@ install_dependencies() {
 
 	OS=$(uname)
 	if [ -f /etc/debian_version ]; then
-		dependencies="git wireguard wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
+		dependencies="git wireguard-tools dnsutils jq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin grep gawk"
 		update_cmd='apt update'
 		install_cmd='apt-get install -y'
 	elif [ -f /etc/alpine-release ]; then
@@ -306,16 +309,20 @@ install_dependencies() {
 		update_cmd='apk update'
 		install_cmd='apk --update add'
 	elif [ -f /etc/centos-release ]; then
-		dependencies="git wireguard jq bind-utils docker.io docker-compose grep gawk"
-		update_cmd='yum update'
+		dependencies="wget git wireguard-tools jq bind-utils docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin grep gawk"
+		update_cmd='yum updateinfo'
+		install_cmd='yum install -y'
+	elif [ -f /etc/amazon-linux-release ]; then
+		dependencies="git wireguard-tools bind-utils jq docker grep gawk"
+		update_cmd='yum updateinfo'
 		install_cmd='yum install -y'
 	elif [ -f /etc/fedora-release ]; then
-		dependencies="git wireguard bind-utils jq docker.io docker-compose grep gawk"
-		update_cmd='dnf update'
+		dependencies="wget git wireguard-tools bind-utils jq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin grep gawk"
+		update_cmd='dnf updateinfo'
 		install_cmd='dnf install -y'
 	elif [ -f /etc/redhat-release ]; then
-		dependencies="git wireguard jq docker.io bind-utils docker-compose grep gawk"
-		update_cmd='yum update'
+		dependencies="wget git wireguard-tools jq bind-utils docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin grep gawk"
+		update_cmd='yum updateinfo'
 		install_cmd='yum install -y'
 	elif [ -f /etc/arch-release ]; then
 		dependencies="git wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
@@ -326,7 +333,20 @@ install_dependencies() {
 		update_cmd='pkg update'
 		install_cmd='pkg install -y'
 	else
-		install_cmd=''
+		echo "-----------------------nm-quick.sh----------------------------------------------"
+		echo "OS supported and tested include:"
+		echo "   Debian"
+		echo "   Ubuntu"
+		echo "   Fedora"
+		echo "   Centos"
+		echo "   Redhat"
+		echo "   Amazon Linux"
+		echo "   Rocky Linux"
+		echo "   AlmaLinux"
+
+		echo "Your OS system is not in the support list, please chanage to an OS in the list"
+		echo "--------------------------------------------------------------------------------"
+		exit 1
 	fi
 
 	if [ -z "${install_cmd}" ]; then
@@ -345,6 +365,50 @@ install_dependencies() {
     	echo "Unsupported architechure"
     	# exit 1
     fi
+
+	# setup docker repository
+	if [ "$(cat /etc/*-release |grep ubuntu |wc -l)" -gt 0 ]; then
+		apt update
+		apt install -y ca-certificates curl
+		install -m 0755 -d /etc/apt/keyrings
+		curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
+		chmod a+r /etc/apt/keyrings/docker.asc
+		echo \
+		  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
+		  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
+		  tee /etc/apt/sources.list.d/docker.list > /dev/null
+		apt update
+	elif [ "$(cat /etc/*-release |grep debian |wc -l)" -gt 0 ]; then
+		apt update
+		apt install -y ca-certificates curl
+		install -m 0755 -d /etc/apt/keyrings
+		curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
+		chmod a+r /etc/apt/keyrings/docker.asc
+		echo \
+		  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
+		  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
+		  tee /etc/apt/sources.list.d/docker.list > /dev/null
+		apt update
+	elif [ -f /etc/fedora-release ]; then
+		dnf -y install dnf-plugins-core
+		dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
+	elif [ -f /etc/centos-release ]; then
+		yum install -y yum-utils
+		yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
+		if [ "$(cat /etc/*-release |grep 'release 8' |wc -l)" -gt 0 ]; then
+			yum install -y elrepo-release epel-release
+		elif [ "$(cat /etc/*-release |grep 'release 7' |wc -l)" -gt 0 ]; then
+			yum install -y elrepo-release epel-release
+			yum install -y yum-plugin-elrepo
+		fi
+	elif [ -f /etc/redhat-release ]; then
+		yum install -y yum-utils
+		yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
+		if [ "$(cat /etc/*-release |grep 'release 8' |wc -l)" -gt 0 ]; then
+			yum install -y elrepo-release epel-release
+		fi
+	fi
+
 	set -- $dependencies
 
 	${update_cmd}
@@ -371,8 +435,10 @@ install_dependencies() {
 		else
 			if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 				is_installed=$(opkg list-installed $1 | grep $1)
-			else
+			elif [ -f /etc/debian_version ]; then
 				is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+			else
+				is_installed=$(yum list installed | grep $1)
 			fi
 			if [ "${is_installed}" != "" ]; then
 				echo "    " $1 is installed
@@ -382,8 +448,10 @@ install_dependencies() {
 				sleep 5
 				if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 					is_installed=$(opkg list-installed $1 | grep $1)
-				else
+				elif [ -f /etc/debian_version ]; then
 					is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+				else
+					is_installed=$(yum list installed | grep $1)
 				fi
 				if [ "${is_installed}" != "" ]; then
 					echo "    " $1 is installed
@@ -398,6 +466,27 @@ install_dependencies() {
 		shift
 	done
 
+	# Startup docker daemon for OS which does not start it automatically
+	if [ -f /etc/fedora-release ]; then
+		systemctl start docker
+		systemctl enable docker
+	elif [ -f /etc/amazon-linux-release ]; then
+		systemctl start docker
+		systemctl enable docker
+		usermod -a -G docker ec2-user
+		mkdir -p /usr/local/lib/docker/cli-plugins
+		curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) \
+		-o /usr/local/lib/docker/cli-plugins/docker-compose
+		chown root:root /usr/local/lib/docker/cli-plugins/docker-compose
+		chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
+	elif [ -f /etc/centos-release ]; then
+		systemctl start docker
+		systemctl enable docker
+	elif [ -f /etc/redhat-release ]; then
+		systemctl start docker
+		systemctl enable docker
+	fi
+
 	echo "-----------------------------------------------------"
 	echo "dependency check complete"
 	echo "-----------------------------------------------------"
@@ -582,7 +671,19 @@ install_netmaker() {
 
 	# start docker and rebuild containers / networks
 	cd "${SCRIPT_DIR}"
-	docker-compose up -d --force-recreate
+	if [ -f /etc/debian_version ]; then
+		docker compose up -d --force-recreate
+	elif [ -f /etc/fedora-release ]; then
+		docker compose up -d --force-recreate
+	elif [ -f /etc/amazon-linux-release ]; then
+		docker compose up -d --force-recreate
+	elif [ -f /etc/centos-release ]; then
+		docker compose up -d --force-recreate
+	elif [ -f /etc/redhat-release ]; then
+		docker compose up -d --force-recreate
+	else
+		docker-compose up -d --force-recreate
+	fi
 	cd -
 	wait_seconds 2
 

+ 1 - 1
servercfg/serverconf.go

@@ -620,7 +620,7 @@ func GetNetmakerTenantID() string {
 	return netmakerTenantID
 }
 
-// GetNetworkLimit - fetches free tier limits on users
+// GetUserLimit - fetches free tier limits on users
 func GetUserLimit() int {
 	var userslimit int
 	if os.Getenv("USERS_LIMIT") != "" {

+ 9 - 1
swagger.yml

@@ -61,6 +61,9 @@ definitions:
             endpointip:
                 type: string
                 x-go-name: EndpointIP
+            endpointipv6:
+                type: string
+                x-go-name: EndpointIPv6
             firewallinuse:
                 type: string
                 x-go-name: FirewallInUse
@@ -374,6 +377,8 @@ definitions:
                 x-go-name: EgressRanges
             node_addr:
                 $ref: '#/definitions/IPNet'
+            node_addr6:
+                $ref: '#/definitions/IPNet'
         type: object
         x-go-package: github.com/gravitl/netmaker/models
     EnrollmentKey:
@@ -522,6 +527,9 @@ definitions:
             endpointip:
                 type: string
                 x-go-name: EndpointIP
+            endpointipv6:
+                type: string
+                x-go-name: EndpointIPv6
             firewallinuse:
                 type: string
                 x-go-name: FirewallInUse
@@ -1464,7 +1472,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.24.0
+    version: 0.24.1
 paths:
     /api/dns:
         get: