Переглянути джерело

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

v0.24.1
Abhishek K 1 рік тому
батько
коміт
b2ae18dde3

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

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

+ 1 - 1
README.md

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

+ 34 - 273
auth/auth.go

@@ -3,156 +3,25 @@ package auth
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"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/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/models"
 	"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 ==
 // == consts ==
 const (
 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 (
 var (
 	auth_provider *oauth2.Config
 	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
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
 func IsOauthUser(user *models.User) error {
 	var currentValue, err = FetchPassValue("")
 	var currentValue, err = FetchPassValue("")
@@ -163,81 +32,30 @@ func IsOauthUser(user *models.User) error {
 	return bCryptErr
 	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 {
 func addUser(email string) error {
 	var hasSuperAdmin, err = logic.HasSuperAdmin()
 	var hasSuperAdmin, err = logic.HasSuperAdmin()
@@ -247,7 +65,7 @@ func addUser(email string) error {
 	} // generate random password to adapt to current model
 	} // generate random password to adapt to current model
 	var newPass, fetchErr = FetchPassValue("")
 	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
-		slog.Error("failed to get password", "error", err.Error())
+		slog.Error("failed to get password", "error", fetchErr.Error())
 		return fetchErr
 		return fetchErr
 	}
 	}
 	var newUser = models.User{
 	var newUser = models.User{
@@ -273,77 +91,20 @@ func addUser(email string) error {
 	return nil
 	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 (
 import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"strings"
 	"time"
 	"time"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
@@ -59,12 +58,12 @@ func SessionHandler(conn *websocket.Conn) {
 		logger.Log(0, "Failed to process sso request -", err.Error())
 		logger.Log(0, "Failed to process sso request -", err.Error())
 		return
 		return
 	}
 	}
+	defer netcache.Del(stateStr)
 	// Wait for the user to finish his auth flow...
 	// 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)
 	answer := make(chan netcache.CValue, 1)
 	defer close(answer)
 	defer close(answer)
 	defer close(timeout)
 	defer close(timeout)
-
 	if len(registerMessage.User) > 0 { // handle basic auth
 	if len(registerMessage.User) > 0 { // handle basic auth
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 		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
 	} else { // handle SSO / OAuth
 		if auth_provider == nil {
 		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, ""))
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
 			if err != nil {
 				logger.Log(0, "error during message writing:", err.Error())
 				logger.Log(0, "error during message writing:", err.Error())
@@ -118,29 +121,37 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 			return
 		}
 		}
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "via SSO")
 		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))
 		err = conn.WriteMessage(messageType, []byte(redirectUrl))
 		if err != nil {
 		if err != nil {
 			logger.Log(0, "error during message writing:", err.Error())
 			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() {
 	go func() {
 		for {
 		for {
 			cachedReq, err := netcache.Get(stateStr)
 			cachedReq, err := netcache.Get(stateStr)
 			if err != nil {
 			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 {
 			} else if len(cachedReq.User) > 0 {
 				logger.Log(0, "host SSO process completed for user", cachedReq.User)
 				logger.Log(0, "host SSO process completed for user", cachedReq.User)
 				answer <- *cachedReq
 				answer <- *cachedReq
 				break
 				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)
 		go CheckNetRegAndHostUpdate(netsToAdd[:], &result.Host, uuid.Nil)
 	case <-timeout: // the read from req.answerCh has timed out
 	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
 	// Cleanly close the connection by sending a close message and then
 	// waiting (with timeout) for the server to close the connection.
 	// 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:
 services:
   netclient:
   netclient:
     container_name: netclient
     container_name: netclient
-    image: 'gravitl/netclient:v0.24.0'
+    image: 'gravitl/netclient:v0.24.1'
     hostname: netmaker-1
     hostname: netmaker-1
     network_mode: host
     network_mode: host
     restart: on-failure
     restart: on-failure

+ 1 - 1
controllers/docs.go

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

+ 1 - 1
controllers/enrollmentkeys.go

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

+ 31 - 26
controllers/ext_client.go

@@ -15,13 +15,13 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 
 
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 
 
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/skip2/go-qrcode"
 	"github.com/skip2/go-qrcode"
+	"golang.org/x/exp/slices"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 )
@@ -200,6 +200,24 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		return
 		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
 	addrString := client.Address
 	if addrString != "" {
 	if addrString != "" {
 		addrString += "/32"
 		addrString += "/32"
@@ -215,12 +233,18 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	if network.DefaultKeepalive != 0 {
 	if network.DefaultKeepalive != 0 {
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 		keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
 	}
 	}
+
 	gwendpoint := ""
 	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 {
 	} else {
-		gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
+		gwendpoint = fmt.Sprintf("%s:%d", preferredIp, host.ListenPort)
 	}
 	}
+
 	var newAllowedIPs string
 	var newAllowedIPs string
 	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
 	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
 		egressrange := "0.0.0.0/0"
 		egressrange := "0.0.0.0/0"
@@ -605,36 +629,17 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	err = logic.DeleteExtClient(params["network"], params["clientid"])
+	err = logic.DeleteExtClientAndCleanup(extclient)
 	if err != nil {
 	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"])
 		err = errors.New("Could not delete extclient " + params["clientid"])
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		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() {
 	go func() {
 		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
 		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() {
 		if servercfg.IsDNSMode() {
 			logic.SetDNS()
 			logic.SetDNS()

+ 1 - 1
controllers/migrate.go

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

+ 2 - 16
controllers/network.go

@@ -442,36 +442,22 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 
 
 	// validate address ranges: must be private
 	// validate address ranges: must be private
 	if network.AddressRange != "" {
 	if network.AddressRange != "" {
-		_, ipNet, err := net.ParseCIDR(network.AddressRange)
+		_, _, err := net.ParseCIDR(network.AddressRange)
 		if err != nil {
 		if err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 				err.Error())
 				err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			return
 			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 != "" {
 	if network.AddressRange6 != "" {
-		_, ipNet, err := net.ParseCIDR(network.AddressRange6)
+		_, _, err := net.ParseCIDR(network.AddressRange6)
 		if err != nil {
 		if err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 			logger.Log(0, r.Header.Get("user"), "failed to create network: ",
 				err.Error())
 				err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 			return
 			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)
 	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
 			isnetadmin := issuperadmin || isadmin
-			if errN == nil && (issuperadmin || isadmin) {
+			if issuperadmin || isadmin {
 				nodeID = "mastermac"
 				nodeID = "mastermac"
 				isAuthorized = true
 				isAuthorized = true
 				r.Header.Set("ismasterkey", "yes")
 				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"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("metadata cannot be longer than 255 characters"), "badrequest"))
 		return
 		return
 	}
 	}
+	if !servercfg.IsPro {
+		newData.AdditionalRagIps = []string{}
+	}
 	newNode := newData.ConvertToServerNode(&currentNode)
 	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 != currentNode.IsInternetGateway {
 		if newNode.IsInternetGateway {
 		if newNode.IsInternetGateway {
 			logic.SetInternetGw(newNode, models.InetNodeReq{})
 			logic.SetInternetGw(newNode, models.InetNodeReq{})
 		} else {
 		} else {
 			logic.UnsetInternetGw(newNode)
 			logic.UnsetInternetGw(newNode)
 		}
 		}
-
 	}
 	}
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {
 	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(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/{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/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(getPendingUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
 	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(deletePendingUser))).Methods(http.MethodDelete)
@@ -119,7 +115,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	successJSONResponse, jsonError := json.Marshal(successResponse)
 	successJSONResponse, jsonError := json.Marshal(successResponse)
 	if jsonError != nil {
 	if jsonError != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
-			"error marshalling resp: ", err.Error())
+			"error marshalling resp: ", jsonError.Error())
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 		return
 	}
 	}

+ 6 - 10
go.mod

@@ -4,7 +4,7 @@ go 1.19
 
 
 require (
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.19.0
+	github.com/go-playground/validator/v10 v10.20.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
@@ -15,13 +15,12 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.9.0
 	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
 	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/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
 	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
 	gopkg.in/yaml.v3 v3.0.1
 )
 )
 
 
@@ -46,7 +45,7 @@ require (
 )
 )
 
 
 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/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
@@ -54,17 +53,14 @@ require (
 )
 )
 
 
 require (
 require (
-	cloud.google.com/go/compute v1.20.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.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/hashicorp/go-version v1.6.0
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	golang.org/x/sync v0.1.0 // 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.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 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang/protobuf v1.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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.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 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
 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/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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
@@ -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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -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.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.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.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.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.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.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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 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-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 h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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
       hostNetwork: true
       containers:
       containers:
       - name: netclient
       - name: netclient
-        image: gravitl/netclient:v0.24.0
+        image: gravitl/netclient:v0.24.1
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

+ 33 - 0
logic/extpeers.go

@@ -11,6 +11,7 @@ import (
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
@@ -95,6 +96,35 @@ func DeleteExtClient(network string, clientid string) error {
 	return nil
 	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
 // GetNetworkExtClients - gets the ext clients of given network
 func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 func GetNetworkExtClients(network string) ([]models.ExtClient, error) {
 	var extclients []models.ExtClient
 	var extclients []models.ExtClient
@@ -445,6 +475,9 @@ func getExtpeersExtraRoutes(network string) (egressRoutes []models.EgressNetwork
 		return
 		return
 	}
 	}
 	for _, extPeer := range extPeers {
 	for _, extPeer := range extPeers {
+		if len(extPeer.ExtraAllowedIPs) == 0 {
+			continue
+		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(extPeer)...)
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(extPeer)...)
 	}
 	}
 	return
 	return

+ 0 - 1
logic/hosts.go

@@ -217,7 +217,6 @@ func UpdateHost(newHost, currentHost *models.Host) {
 	newHost.Nodes = currentHost.Nodes
 	newHost.Nodes = currentHost.Nodes
 	newHost.PublicKey = currentHost.PublicKey
 	newHost.PublicKey = currentHost.PublicKey
 	newHost.TrafficKeyPublic = currentHost.TrafficKeyPublic
 	newHost.TrafficKeyPublic = currentHost.TrafficKeyPublic
-	newHost.EndpointIPv6 = currentHost.EndpointIPv6
 	// changeable fields
 	// changeable fields
 	if len(newHost.Version) == 0 {
 	if len(newHost.Version) == 0 {
 		newHost.Version = currentHost.Version
 		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 {
 			if peer.IsEgressGateway {
 				hostPeerUpdate.EgressRoutes = append(hostPeerUpdate.EgressRoutes, models.EgressNetworkRoutes{
 				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 {
 			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
 			//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
 				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{
 			peerConfig.Endpoint = &net.UDPAddr{
@@ -306,6 +307,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					Mask: getCIDRMaskFromAddr(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,
 				EgressGWCfg: node.EgressGatewayRequest,
 			}
 			}
 
 
@@ -323,6 +329,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					IP:   net.ParseIP(node.PrimaryAddress()),
 					Mask: getCIDRMaskFromAddr(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{
 				EgressGWCfg: models.EgressGatewayRequest{
 					NodeID:     fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 					NodeID:     fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 					NetID:      node.Network,
 					NetID:      node.Network,

+ 1 - 1
logic/telemetry.go

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

+ 1 - 1
logic/version.go

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

+ 5 - 5
logic/version_test.go

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

+ 1 - 1
logic/zombie.go

@@ -49,7 +49,7 @@ func CheckZombies(newnode *models.Node) {
 func checkForZombieHosts(h *models.Host) {
 func checkForZombieHosts(h *models.Host) {
 	hosts, err := GetAllHosts()
 	hosts, err := GetAllHosts()
 	if err != nil {
 	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 {
 	for _, existing := range hosts {
 		if existing.ID == h.ID {
 		if existing.ID == h.ID {

+ 1 - 9
main.go

@@ -12,7 +12,6 @@ import (
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
 
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/config"
 	"github.com/gravitl/netmaker/config"
 	controller "github.com/gravitl/netmaker/controllers"
 	controller "github.com/gravitl/netmaker/controllers"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
@@ -28,7 +27,7 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v0.24.0"
+var version = "v0.24.1"
 
 
 // Start DB Connection and start API Request Handler
 // Start DB Connection and start API Request Handler
 func main() {
 func main() {
@@ -91,13 +90,6 @@ func initialize() { // Client Mode Prereq Check
 
 
 	logic.SetJWTSecret()
 	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()
 	err = serverctl.SetDefaults()
 	if err != nil {
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())
 		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 := ApiHost{}
 	a.Debug = h.Debug
 	a.Debug = h.Debug
 	a.EndpointIP = h.EndpointIP.String()
 	a.EndpointIP = h.EndpointIP.String()
+	if a.EndpointIP == "<nil>" {
+		a.EndpointIP = ""
+	}
 	a.EndpointIPv6 = h.EndpointIPv6.String()
 	a.EndpointIPv6 = h.EndpointIPv6.String()
+	if a.EndpointIPv6 == "<nil>" {
+		a.EndpointIPv6 = ""
+	}
 	a.FirewallInUse = h.FirewallInUse
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
 	a.ID = h.ID.String()
 	a.Interfaces = make([]ApiIface, len(h.Interfaces))
 	a.Interfaces = make([]ApiIface, len(h.Interfaces))

+ 14 - 0
models/api_node.go

@@ -5,6 +5,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
+	"golang.org/x/exp/slog"
 )
 )
 
 
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
 // ApiNode is a stripped down Node DTO that exposes only required fields to external systems
@@ -44,6 +45,7 @@ type ApiNode struct {
 	IsInternetGateway bool                `json:"isinternetgateway" yaml:"isinternetgateway"`
 	IsInternetGateway bool                `json:"isinternetgateway" yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
 	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
 // 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.LastPeerUpdate = time.Unix(a.LastPeerUpdate, 0)
 	convertedNode.ExpirationDateTime = time.Unix(a.ExpirationDateTime, 0)
 	convertedNode.ExpirationDateTime = time.Unix(a.ExpirationDateTime, 0)
 	convertedNode.Metadata = a.Metadata
 	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
 	return &convertedNode
 }
 }
 
 
@@ -163,6 +173,10 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailOverPeers = nm.FailOverPeers
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.FailedOverBy = nm.FailedOverBy
 	apiNode.Metadata = nm.Metadata
 	apiNode.Metadata = nm.Metadata
+	apiNode.AdditionalRagIps = []string{}
+	for _, ip := range nm.AdditionalRagIps {
+		apiNode.AdditionalRagIps = append(apiNode.AdditionalRagIps, ip.String())
+	}
 	return &apiNode
 	return &apiNode
 }
 }
 
 

+ 11 - 7
models/mqtt.go

@@ -34,17 +34,21 @@ type IngressInfo struct {
 
 
 // EgressInfo - struct for egress info
 // EgressInfo - struct for egress info
 type EgressInfo struct {
 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
 // EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
 type EgressNetworkRoutes struct {
 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
 // 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"`
 	IsInternetGateway bool                `json:"isinternetgateway" yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req" yaml:"inet_node_req"`
 	InternetGwID      string              `json:"internetgw_node_id" yaml:"internetgw_node_id"`
 	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
 // LegacyNode - legacy struct for node model

+ 6 - 0
models/structs.go

@@ -72,7 +72,9 @@ type UserRemoteGws struct {
 	IsInternetGateway bool      `json:"is_internet_gateway"`
 	IsInternetGateway bool      `json:"is_internet_gateway"`
 	GwClient          ExtClient `json:"gw_client"`
 	GwClient          ExtClient `json:"gw_client"`
 	GwPeerPublicKey   string    `json:"gw_peer_public_key"`
 	GwPeerPublicKey   string    `json:"gw_peer_public_key"`
+	GwListenPort      int       `json:"gw_listen_port"`
 	Metadata          string    `json:"metadata"`
 	Metadata          string    `json:"metadata"`
+	AllowedEndpoints  []string  `json:"allowed_endpoints"`
 }
 }
 
 
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
 // UserRemoteGwsReq - struct to hold user remote acccess gws req
@@ -372,3 +374,7 @@ type LoginReqDto struct {
 const (
 const (
 	ResHeaderKeyStAccessToken = "St-Access-Token"
 	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 {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
+	defer resp.Body.Close()
 	msg, err := io.ReadAll(resp.Body)
 	msg, err := io.ReadAll(resp.Body)
 	if err != nil {
 	if err != nil {
 		return "", err
 		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
 // Keepalive -- periodically pings all nodes to let them know server is still alive and doing well
 func Keepalive(ctx context.Context) {
 func Keepalive(ctx context.Context) {
+	go PublishPeerUpdate(true)
 	for {
 	for {
 		select {
 		select {
 		case <-ctx.Done():
 		case <-ctx.Done():

+ 4 - 1
mq/publishers.go

@@ -195,7 +195,10 @@ func PushMetricsToExporter(metrics models.Metrics) error {
 	if err != nil {
 	if err != nil {
 		return errors.New("failed to marshal metrics: " + err.Error())
 		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
 		var err error
 		if token.Error() == nil {
 		if token.Error() == nil {
 			err = errors.New("connection timeout")
 			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"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"strings"
 
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -58,6 +60,10 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getAzureUserInfo(rState, rCode)
 	var content, err = getAzureUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
 		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)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -96,7 +102,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
@@ -121,7 +127,7 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth 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 {
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 		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>
 </body>
 </html>`
 </html>`
 
 
+const oauthStateInvalid = `<!DOCTYPE html><html>
+<body>
+<h3>Invalid OAuth Session. Please re-try again.</h3>
+</body>
+</html>`
+
 const userNotAllowed = `<!DOCTYPE html><html>
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
 <body>
 <h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
 <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))
 	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) {
 func handleSomethingWentWrong(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusInternalServerError)
 	response.WriteHeader(http.StatusInternalServerError)

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

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"strings"
 
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -58,6 +60,10 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGithubUserInfo(rState, rCode)
 	var content, err = getGithubUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
 		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)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -96,7 +102,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
@@ -121,7 +127,7 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth 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 {
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 		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"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"strings"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -60,6 +62,10 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGoogleUserInfo(rState, rCode)
 	var content, err = getGoogleUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
 		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)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -99,7 +105,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
@@ -124,7 +130,7 @@ func getGoogleUserInfo(state string, code string) (*OAuthUser, error) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 	if (!isValid || state != oauth_state_string) && !isStateCached(state) {
 		return nil, fmt.Errorf("invalid oauth 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 {
 	if err != nil {
 		return nil, fmt.Errorf("code exchange failed: %s", err.Error())
 		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"
 	"fmt"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"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
 	// check if user approval is already pending
 	if logic.IsPendingUser(userClaims.getUserName()) {
 	if logic.IsPendingUser(userClaims.getUserName()) {
-		handleOauthUserNotAllowed(w)
+		handleOauthUserSignUpApprovalPending(w)
 		return
 		return
 	}
 	}
 	user, err := logic.GetUser(userClaims.getUserName())
 	user, err := logic.GetUser(userClaims.getUserName())
@@ -62,7 +63,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		w.Write(response)
 		w.Write(response)
 		return
 		return
 	}
 	}
-	newPass, fetchErr := FetchPassValue("")
+	newPass, fetchErr := auth.FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

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

@@ -4,9 +4,11 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -71,6 +73,10 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getOIDCUserInfo(rState, rCode)
 	var content, err = getOIDCUserInfo(rState, rCode)
 	if err != nil {
 	if err != nil {
 		logger.Log(1, "error when getting user info from callback:", err.Error())
 		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)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
@@ -109,7 +115,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = FetchPassValue("")
+	var newPass, fetchErr = auth.FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
@@ -146,7 +152,7 @@ func getOIDCUserInfo(state string, code string) (u *OAuthUser, e error) {
 	ctx, cancel := context.WithTimeout(context.Background(), OIDC_TIMEOUT)
 	ctx, cancel := context.WithTimeout(context.Background(), OIDC_TIMEOUT)
 	defer cancel()
 	defer cancel()
 
 
-	oauth2Token, err := auth_provider.Exchange(ctx, code)
+	oauth2Token, err := auth_provider.Exchange(ctx, code, oauth2.SetAuthURLParam("prompt", "login"))
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to exchange oauth2 token using code \"%s\"", code)
 		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/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
-	"github.com/gravitl/netmaker/models"
 )
 )
 
 
 var (
 var (
@@ -68,7 +67,18 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) {
 		w.Write(response)
 		w.Write(response)
 		return
 		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())
 	logger.Log(1, "registering host for user:", userClaims.getUserName(), reqKeyIf.Host.Name, reqKeyIf.Host.ID.String())
 
 
 	// Send OK to user in the browser
 	// 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)
 	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/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/pro/auth"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"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/{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/{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/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
 // 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 {
 		for _, extclient := range extclients {
 			if extclient.OwnerID == user.UserName && remoteGwID == extclient.IngressGatewayID {
 			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() {
 		if servercfg.IsDNSMode() {
@@ -212,7 +226,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				})
 				userGws[node.Network] = gws
 				userGws[node.Network] = gws
 				delete(user.RemoteGwIDs, node.ID.String())
 				delete(user.RemoteGwIDs, node.ID.String())
@@ -227,7 +243,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Connected:         true,
 					Connected:         true,
 					IsInternetGateway: node.IsInternetGateway,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				})
 				userGws[node.Network] = gws
 				userGws[node.Network] = gws
 				processedAdminNodeIds[node.ID.String()] = struct{}{}
 				processedAdminNodeIds[node.ID.String()] = struct{}{}
@@ -260,7 +278,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 				Network:           node.Network,
 				Network:           node.Network,
 				IsInternetGateway: node.IsInternetGateway,
 				IsInternetGateway: node.IsInternetGateway,
 				GwPeerPublicKey:   host.PublicKey.String(),
 				GwPeerPublicKey:   host.PublicKey.String(),
+				GwListenPort:      logic.GetPeerListenPort(host),
 				Metadata:          node.Metadata,
 				Metadata:          node.Metadata,
+				AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 			})
 			})
 			userGws[node.Network] = gws
 			userGws[node.Network] = gws
 		}
 		}
@@ -287,7 +307,9 @@ func getUserRemoteAccessGws(w http.ResponseWriter, r *http.Request) {
 					Network:           node.Network,
 					Network:           node.Network,
 					IsInternetGateway: node.IsInternetGateway,
 					IsInternetGateway: node.IsInternetGateway,
 					GwPeerPublicKey:   host.PublicKey.String(),
 					GwPeerPublicKey:   host.PublicKey.String(),
+					GwListenPort:      logic.GetPeerListenPort(host),
 					Metadata:          node.Metadata,
 					Metadata:          node.Metadata,
+					AllowedEndpoints:  getAllowedRagEndpoints(&node, host),
 				})
 				})
 				userGws[node.Network] = gws
 				userGws[node.Network] = gws
 			}
 			}
@@ -338,3 +360,19 @@ func ingressGatewayUsers(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(gwUsers)
 	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/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/pro/auth"
 	proControllers "github.com/gravitl/netmaker/pro/controllers"
 	proControllers "github.com/gravitl/netmaker/pro/controllers"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
@@ -81,6 +82,13 @@ func InitPro() {
 			AddRacHooks()
 			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.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer

+ 1 - 1
pro/logic/nodes.go

@@ -148,7 +148,7 @@ func GetNetworkIngresses(network string) ([]models.Node, error) {
 	return ingresses, nil
 	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 {
 func GetAllowedIpForInetNodeClient(node, peer *models.Node) []net.IPNet {
 	var allowedips = []net.IPNet{}
 	var allowedips = []net.IPNet{}
 
 

+ 12 - 14
release.md

@@ -1,24 +1,22 @@
-# Netmaker v0.24.0
+# Netmaker v0.24.1
 
 
 ## Whats New ✨
 ## 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 🛠
 ## 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 🐞
 ## Known Issues 🐞
 
 
 - Erratic Traffic Data In Metrics
 - 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.
 - `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 - install yq if not present
 install_yq() {
 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
 	fi
 	set +e
 	set +e
 	if ! command -v yq &>/dev/null; then
 	if ! command -v yq &>/dev/null; then
@@ -122,6 +124,7 @@ setup_netclient() {
 	chmod +x netclient
 	chmod +x netclient
 	./netclient install
 	./netclient install
 	echo "Register token: $TOKEN"
 	echo "Register token: $TOKEN"
+	sleep 2
 	netclient register -t $TOKEN
 	netclient register -t $TOKEN
 
 
 	echo "waiting for netclient to become available"
 	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 - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
 configure_netclient() {
-
+	sleep 2
 	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
 	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
 	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
 	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
 		echo "Error obtaining NODE_ID for the new network"
 		echo "Error obtaining NODE_ID for the new network"
@@ -298,7 +301,7 @@ install_dependencies() {
 
 
 	OS=$(uname)
 	OS=$(uname)
 	if [ -f /etc/debian_version ]; then
 	if [ -f /etc/debian_version ]; then
-		dependencies="git wireguard wireguard-tools dnsutils jq docker.io docker-compose 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'
 		update_cmd='apt update'
 		install_cmd='apt-get install -y'
 		install_cmd='apt-get install -y'
 	elif [ -f /etc/alpine-release ]; then
 	elif [ -f /etc/alpine-release ]; then
@@ -306,16 +309,20 @@ install_dependencies() {
 		update_cmd='apk update'
 		update_cmd='apk update'
 		install_cmd='apk --update add'
 		install_cmd='apk --update add'
 	elif [ -f /etc/centos-release ]; then
 	elif [ -f /etc/centos-release ]; then
-		dependencies="git wireguard jq bind-utils docker.io docker-compose 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'
 		install_cmd='yum install -y'
 	elif [ -f /etc/fedora-release ]; then
 	elif [ -f /etc/fedora-release ]; then
-		dependencies="git wireguard bind-utils jq docker.io docker-compose 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'
 		install_cmd='dnf install -y'
 	elif [ -f /etc/redhat-release ]; then
 	elif [ -f /etc/redhat-release ]; then
-		dependencies="git wireguard jq docker.io bind-utils docker-compose 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'
 		install_cmd='yum install -y'
 	elif [ -f /etc/arch-release ]; then
 	elif [ -f /etc/arch-release ]; then
 		dependencies="git wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
 		dependencies="git wireguard-tools dnsutils jq docker.io docker-compose grep gawk"
@@ -326,7 +333,20 @@ install_dependencies() {
 		update_cmd='pkg update'
 		update_cmd='pkg update'
 		install_cmd='pkg install -y'
 		install_cmd='pkg install -y'
 	else
 	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
 	fi
 
 
 	if [ -z "${install_cmd}" ]; then
 	if [ -z "${install_cmd}" ]; then
@@ -345,6 +365,50 @@ install_dependencies() {
     	echo "Unsupported architechure"
     	echo "Unsupported architechure"
     	# exit 1
     	# exit 1
     fi
     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
 	set -- $dependencies
 
 
 	${update_cmd}
 	${update_cmd}
@@ -371,8 +435,10 @@ install_dependencies() {
 		else
 		else
 			if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 			if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 				is_installed=$(opkg list-installed $1 | grep $1)
 				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")
 				is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+			else
+				is_installed=$(yum list installed | grep $1)
 			fi
 			fi
 			if [ "${is_installed}" != "" ]; then
 			if [ "${is_installed}" != "" ]; then
 				echo "    " $1 is installed
 				echo "    " $1 is installed
@@ -382,8 +448,10 @@ install_dependencies() {
 				sleep 5
 				sleep 5
 				if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 				if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 					is_installed=$(opkg list-installed $1 | grep $1)
 					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")
 					is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+				else
+					is_installed=$(yum list installed | grep $1)
 				fi
 				fi
 				if [ "${is_installed}" != "" ]; then
 				if [ "${is_installed}" != "" ]; then
 					echo "    " $1 is installed
 					echo "    " $1 is installed
@@ -398,6 +466,27 @@ install_dependencies() {
 		shift
 		shift
 	done
 	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 "-----------------------------------------------------"
 	echo "dependency check complete"
 	echo "dependency check complete"
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
@@ -582,7 +671,19 @@ install_netmaker() {
 
 
 	# start docker and rebuild containers / networks
 	# start docker and rebuild containers / networks
 	cd "${SCRIPT_DIR}"
 	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 -
 	cd -
 	wait_seconds 2
 	wait_seconds 2
 
 

+ 1 - 1
servercfg/serverconf.go

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

+ 9 - 1
swagger.yml

@@ -61,6 +61,9 @@ definitions:
             endpointip:
             endpointip:
                 type: string
                 type: string
                 x-go-name: EndpointIP
                 x-go-name: EndpointIP
+            endpointipv6:
+                type: string
+                x-go-name: EndpointIPv6
             firewallinuse:
             firewallinuse:
                 type: string
                 type: string
                 x-go-name: FirewallInUse
                 x-go-name: FirewallInUse
@@ -374,6 +377,8 @@ definitions:
                 x-go-name: EgressRanges
                 x-go-name: EgressRanges
             node_addr:
             node_addr:
                 $ref: '#/definitions/IPNet'
                 $ref: '#/definitions/IPNet'
+            node_addr6:
+                $ref: '#/definitions/IPNet'
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
     EnrollmentKey:
     EnrollmentKey:
@@ -522,6 +527,9 @@ definitions:
             endpointip:
             endpointip:
                 type: string
                 type: string
                 x-go-name: EndpointIP
                 x-go-name: EndpointIP
+            endpointipv6:
+                type: string
+                x-go-name: EndpointIPv6
             firewallinuse:
             firewallinuse:
                 type: string
                 type: string
                 x-go-name: FirewallInUse
                 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.
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
     title: Netmaker
-    version: 0.24.0
+    version: 0.24.1
 paths:
 paths:
     /api/dns:
     /api/dns:
         get:
         get: