Browse Source

Merge pull request #1490 from gravitl/release_v0.15.0

Release v0.15.0
Alex Feiszli 2 years ago
parent
commit
d2aa528d0f
64 changed files with 1367 additions and 559 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 1
      README.md
  3. 0 1
      auth/auth.go
  4. 9 2
      auth/azure-ad.go
  5. 9 2
      auth/github.go
  6. 9 2
      auth/google.go
  7. 9 2
      auth/oidc.go
  8. 2 2
      compose/docker-compose.reference.yml
  9. 2 2
      compose/docker-compose.yml
  10. 1 0
      config/config.go
  11. 1 0
      config/environments/dev.yaml
  12. 0 1
      controllers/config/dnsconfig/netmaker.hosts
  13. 1 0
      controllers/controller.go
  14. 69 0
      controllers/ipservice.go
  15. 2 4
      controllers/network.go
  16. 15 18
      controllers/node.go
  17. 4 0
      database/database.go
  18. 7 4
      go.mod
  19. 8 6
      go.sum
  20. 1 1
      k8s/client/netclient-daemonset.yaml
  21. 1 1
      k8s/client/netclient.yaml
  22. 1 1
      k8s/server/netmaker-server.yaml
  23. 1 1
      k8s/server/netmaker-ui.yaml
  24. 0 3
      logger/util.go
  25. 50 0
      logic/auth.go
  26. 149 36
      logic/gateway.go
  27. 0 30
      logic/networks.go
  28. 3 2
      logic/nodes.go
  29. 45 4
      logic/peers.go
  30. 0 6
      logic/relay.go
  31. 3 32
      logic/server.go
  32. 3 200
      logic/wireguard.go
  33. 22 18
      logic/zombie.go
  34. 6 0
      main.go
  35. 65 49
      models/node.go
  36. 17 0
      models/ssocache.go
  37. 1 0
      mq/handlers.go
  38. 7 0
      netclient/cli_options/flags.go
  39. 14 12
      netclient/config/config.go
  40. 1 0
      netclient/daemon/common.go
  41. 39 24
      netclient/functions/common.go
  42. 9 0
      netclient/functions/daemon.go
  43. 22 4
      netclient/functions/join.go
  44. 21 2
      netclient/functions/mqhandlers.go
  45. 13 1
      netclient/functions/mqpublish.go
  46. 1 0
      netclient/functions/upgrades/upgrades.go
  47. 6 0
      netclient/global_settings/globalsettings.go
  48. 92 0
      netclient/local/routes.go
  49. 40 3
      netclient/local/routes_darwin.go
  50. 35 2
      netclient/local/routes_freebsd.go
  51. 30 0
      netclient/local/routes_linux.go
  52. 45 0
      netclient/local/routes_windows.go
  53. 5 0
      netclient/main.go
  54. 4 0
      netclient/ncutils/iface.go
  55. 53 7
      netclient/ncutils/netclientutils.go
  56. 1 1
      netclient/netclient.exe.manifest.xml
  57. 5 5
      netclient/versioninfo.json
  58. 41 15
      netclient/wireguard/common.go
  59. 1 0
      netclient/wireguard/mac.go
  60. 0 35
      scripts/netmaker-server.sh
  61. 302 0
      scripts/nm-quick-interactive.sh
  62. 33 4
      scripts/nm-quick.sh
  63. 18 13
      scripts/openwrt-daemon.sh
  64. 11 0
      servercfg/serverconf.go

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

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.15.0
         - v0.14.6
         - v0.14.5
         - v0.14.4

+ 1 - 1
README.md

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

+ 0 - 1
auth/auth.go

@@ -29,7 +29,6 @@ const (
 	auth_key               = "netmaker_auth"
 )
 
-var oauth_state_string = "netmaker-oauth-state" // should be set randomly each provider login
 var auth_provider *oauth2.Config
 
 func getCurrentAuthFunctions() map[string]interface{} {

+ 9 - 2
auth/azure-ad.go

@@ -41,7 +41,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
-	oauth_state_string = logic.RandomString(16)
+	var oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
 		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
@@ -49,6 +49,12 @@ func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
 		return
 	}
+
+	if err := logic.SetState(oauth_state_string); err != nil {
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		return
+	}
+
 	var url = auth_provider.AuthCodeURL(oauth_state_string)
 	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
 }
@@ -88,7 +94,8 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 }
 
 func getAzureUserInfo(state string, code string) (*azureOauthUser, error) {
-	if state != oauth_state_string {
+	oauth_state_string, isValid := logic.IsStateValid(state)
+	if !isValid || state != oauth_state_string {
 		return nil, fmt.Errorf("invalid oauth state")
 	}
 	var token, err = auth_provider.Exchange(context.Background(), code)

+ 9 - 2
auth/github.go

@@ -41,7 +41,7 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
-	oauth_state_string = logic.RandomString(16)
+	var oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
 		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
@@ -49,6 +49,12 @@ func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
 		return
 	}
+
+	if err := logic.SetState(oauth_state_string); err != nil {
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		return
+	}
+
 	var url = auth_provider.AuthCodeURL(oauth_state_string)
 	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
 }
@@ -88,7 +94,8 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 }
 
 func getGithubUserInfo(state string, code string) (*githubOauthUser, error) {
-	if state != oauth_state_string {
+	oauth_state_string, isValid := logic.IsStateValid(state)
+	if !isValid || state != oauth_state_string {
 		return nil, fmt.Errorf("invalid OAuth state")
 	}
 	var token, err = auth_provider.Exchange(context.Background(), code)

+ 9 - 2
auth/google.go

@@ -42,7 +42,7 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) {
 }
 
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
-	oauth_state_string = logic.RandomString(16)
+	var oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
 		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
@@ -50,6 +50,12 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
 		return
 	}
+
+	if err := logic.SetState(oauth_state_string); err != nil {
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		return
+	}
+
 	var url = auth_provider.AuthCodeURL(oauth_state_string)
 	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
 }
@@ -89,7 +95,8 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 }
 
 func getGoogleUserInfo(state string, code string) (*googleOauthUser, error) {
-	if state != oauth_state_string {
+	oauth_state_string, isValid := logic.IsStateValid(state)
+	if !isValid || state != oauth_state_string {
 		return nil, fmt.Errorf("invalid OAuth state")
 	}
 	var token, err = auth_provider.Exchange(context.Background(), code)

+ 9 - 2
auth/oidc.go

@@ -54,7 +54,7 @@ func initOIDC(redirectURL string, clientID string, clientSecret string, issuer s
 }
 
 func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
-	oauth_state_string = logic.RandomString(16)
+	var oauth_state_string = logic.RandomString(16)
 	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
 		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
 		return
@@ -62,6 +62,12 @@ func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
 		return
 	}
+
+	if err := logic.SetState(oauth_state_string); err != nil {
+		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		return
+	}
+
 	var url = auth_provider.AuthCodeURL(oauth_state_string)
 	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
 }
@@ -101,7 +107,8 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 }
 
 func getOIDCUserInfo(state string, code string) (u *OIDCUser, e error) {
-	if state != oauth_state_string {
+	oauth_state_string, isValid := logic.IsStateValid(state)
+	if !isValid || state != oauth_state_string {
 		return nil, fmt.Errorf("invalid OAuth state")
 	}
 

+ 2 - 2
compose/docker-compose.reference.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker: # The Primary Server for running Netmaker
     container_name: netmaker
-    image: gravitl/netmaker:v0.14.6
+    image: gravitl/netmaker:v0.15.0
     cap_add: 
       - NET_ADMIN
       - NET_RAW
@@ -62,7 +62,7 @@ services:
       - traefik.http.services.netmaker-api.loadbalancer.server.port=8081
   netmaker-ui:  # The Netmaker UI Component
     container_name: netmaker-ui
-    image: gravitl/netmaker-ui:v0.14.6
+    image: gravitl/netmaker-ui:v0.15.0
     depends_on:
       - netmaker
     links:

+ 2 - 2
compose/docker-compose.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.14.6
+    image: gravitl/netmaker:v0.15.0
     cap_add: 
       - NET_ADMIN
       - NET_RAW
@@ -51,7 +51,7 @@ services:
       - traefik.http.services.netmaker-api.loadbalancer.server.port=8081
   netmaker-ui:
     container_name: netmaker-ui
-    image: gravitl/netmaker-ui:v0.14.6
+    image: gravitl/netmaker-ui:v0.15.0
     depends_on:
       - netmaker
     links:

+ 1 - 0
config/config.go

@@ -69,6 +69,7 @@ type ServerConfig struct {
 	MQPort                string `yaml:"mqport"`
 	MQServerPort          string `yaml:"mqserverport"`
 	Server                string `yaml:"server"`
+	PublicIPService       string `yaml:"publicipservice"`
 }
 
 // SQLConfig - Generic SQL Config

+ 1 - 0
config/environments/dev.yaml

@@ -11,3 +11,4 @@ server:
   disableremoteipcheck: "" # defaults to "false" or DISABLE_REMOTE_IP_CHECK (if set)
   version: "" # version of server
   rce: "" # defaults to "off"
+  publicipservice: "" # defaults to "" or PUBLIC_IP_SERVICE (if set)

+ 0 - 1
controllers/config/dnsconfig/netmaker.hosts

@@ -1 +0,0 @@
-10.0.0.2         testnode.skynet myhost.skynet

+ 1 - 0
controllers/controller.go

@@ -24,6 +24,7 @@ var HttpHandlers = []interface{}{
 	fileHandlers,
 	serverHandlers,
 	extClientHandlers,
+	ipHandlers,
 }
 
 // HandleRESTRequests - handles the rest requests

+ 69 - 0
controllers/ipservice.go

@@ -0,0 +1,69 @@
+package controller
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/netclient/ncutils"
+)
+
+func ipHandlers(r *mux.Router) {
+	r.HandleFunc("/api/getip", http.HandlerFunc(getPublicIP)).Methods("GET")
+}
+
+func getPublicIP(w http.ResponseWriter, r *http.Request) {
+	r.Header.Set("Connection", "close")
+	ip, err := parseIP(r)
+	if err != nil {
+		w.WriteHeader(400)
+		if ip != "" {
+			w.Write([]byte("ip is invalid: " + ip))
+			return
+		} else {
+			w.Write([]byte("no ip found"))
+			return
+		}
+	} else {
+		if err != nil {
+			fmt.Println(err)
+		}
+	}
+	w.WriteHeader(200)
+	w.Write([]byte(ip))
+}
+
+func parseIP(r *http.Request) (string, error) {
+	// Get Public IP from header
+	ip := r.Header.Get("X-REAL-IP")
+	ipnet := net.ParseIP(ip)
+	if ipnet != nil && !ncutils.IpIsPrivate(ipnet) {
+		return ip, nil
+	}
+
+	// If above fails, get Public IP from other header instead
+	forwardips := r.Header.Get("X-FORWARDED-FOR")
+	iplist := strings.Split(forwardips, ",")
+	for _, ip := range iplist {
+		ipnet := net.ParseIP(ip)
+		if ipnet != nil && !ncutils.IpIsPrivate(ipnet) {
+			return ip, nil
+		}
+	}
+
+	// If above also fails, get Public IP from Remote Address of request
+	ip, _, err := net.SplitHostPort(r.RemoteAddr)
+	if err != nil {
+		return "", err
+	}
+	ipnet = net.ParseIP(ip)
+	if ipnet != nil {
+		if ncutils.IpIsPrivate(ipnet) {
+			return ip, fmt.Errorf("ip is a private address")
+		}
+		return ip, nil
+	}
+	return "", fmt.Errorf("no ip found")
+}

+ 2 - 4
controllers/network.go

@@ -39,7 +39,7 @@ func networkHandlers(r *mux.Router) {
 	r.HandleFunc("/api/networks/{networkname}/acls", securityCheck(true, http.HandlerFunc(getNetworkACL))).Methods("GET")
 }
 
-//simple get all networks function
+// simple get all networks function
 func getNetworks(w http.ResponseWriter, r *http.Request) {
 
 	headerNetworks := r.Header.Get("networks")
@@ -216,9 +216,7 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		for _, node := range nodes {
-			if err = mq.NodeUpdate(&node); err != nil {
-				logger.Log(1, "failed to send update to node during a network wide update", node.Name, node.ID, err.Error())
-			}
+			runUpdates(&node, true)
 		}
 	}
 

+ 15 - 18
controllers/node.go

@@ -171,13 +171,13 @@ func nodeauth(next http.Handler) http.HandlerFunc {
 	}
 }
 
-//The middleware for most requests to the API
-//They all pass  through here first
-//This will validate the JWT (or check for master token)
-//This will also check against the authNetwork and make sure the node should be accessing that endpoint,
-//even if it's technically ok
-//This is kind of a poor man's RBAC. There's probably a better/smarter way.
-//TODO: Consider better RBAC implementations
+// The middleware for most requests to the API
+// They all pass  through here first
+// This will validate the JWT (or check for master token)
+// This will also check against the authNetwork and make sure the node should be accessing that endpoint,
+// even if it's technically ok
+// This is kind of a poor man's RBAC. There's probably a better/smarter way.
+// TODO: Consider better RBAC implementations
 func authorize(nodesAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
@@ -302,7 +302,7 @@ func authorize(nodesAllowed, networkCheck bool, authNetwork string, next http.Ha
 	}
 }
 
-//Gets all nodes associated with network, including pending nodes
+// Gets all nodes associated with network, including pending nodes
 func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -325,8 +325,8 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(nodes)
 }
 
-//A separate function to get all nodes, not just nodes for a particular network.
-//Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
+// A separate function to get all nodes, not just nodes for a particular network.
+// Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
 func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	user, err := logic.GetUser(r.Header.Get("user"))
@@ -372,7 +372,7 @@ func getUsersNodes(user models.User) ([]models.Node, error) {
 	return nodes, err
 }
 
-//Get an individual node. Nothin fancy here folks.
+// Get an individual node. Nothin fancy here folks.
 func getNode(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -406,10 +406,10 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(response)
 }
 
-//Get the time that a network of nodes was last modified.
-//TODO: This needs to be refactored
-//Potential way to do this: On UpdateNode, set a new field for "LastModified"
-//If we go with the existing way, we need to at least set network.NodesLastModified on UpdateNode
+// Get the time that a network of nodes was last modified.
+// TODO: This needs to be refactored
+// Potential way to do this: On UpdateNode, set a new field for "LastModified"
+// If we go with the existing way, we need to at least set network.NodesLastModified on UpdateNode
 func getLastModified(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -736,9 +736,6 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	}
 	if relayupdate {
 		updatenodes := logic.UpdateRelay(node.Network, node.RelayAddrs, newNode.RelayAddrs)
-		if err = logic.NetworkNodesUpdatePullChanges(node.Network); err != nil {
-			logger.Log(1, "error setting relay updates:", err.Error())
-		}
 		if len(updatenodes) > 0 {
 			for _, relayedNode := range updatenodes {
 				runUpdates(&relayedNode, false)

+ 4 - 0
database/database.go

@@ -56,6 +56,9 @@ const GENERATED_TABLE_NAME = "generated"
 // NODE_ACLS_TABLE_NAME - stores the node ACL rules
 const NODE_ACLS_TABLE_NAME = "nodeacls"
 
+// SSO_STATE_CACHE - holds sso session information for OAuth2 sign-ins
+const SSO_STATE_CACHE = "ssostatecache"
+
 // == ERROR CONSTS ==
 
 // NO_RECORD - no singular result found
@@ -135,6 +138,7 @@ func createTables() {
 	createTable(SERVER_UUID_TABLE_NAME)
 	createTable(GENERATED_TABLE_NAME)
 	createTable(NODE_ACLS_TABLE_NAME)
+	createTable(SSO_STATE_CACHE)
 }
 
 func createTable(tableName string) error {

+ 7 - 4
go.mod

@@ -15,13 +15,13 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.0
 	github.com/txn2/txeh v1.3.0
-	github.com/urfave/cli/v2 v2.11.1
+	github.com/urfave/cli/v2 v2.11.2
 	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
 	golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
 	golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220324164955-056925b7df31
 	google.golang.org/protobuf v1.28.0 // indirect
-	gopkg.in/ini.v1 v1.66.6
+	gopkg.in/ini.v1 v1.67.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -35,7 +35,10 @@ require (
 	github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0
 )
 
-require github.com/coreos/go-oidc/v3 v3.2.0
+require (
+	github.com/coreos/go-oidc/v3 v3.2.0
+	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
+)
 
 require (
 	cloud.google.com/go v0.81.0 // indirect
@@ -61,7 +64,7 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/google/go-cmp v0.5.7 // indirect
+	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
 	github.com/gorilla/websocket v1.4.2 // indirect
 	github.com/josharian/native v1.0.0 // indirect

+ 8 - 6
go.sum

@@ -207,8 +207,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -445,8 +446,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
-github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE=
-github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
+github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA=
+github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@@ -503,6 +504,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20220601225756-64ec528b34cd h1:9NbNcTg//wfC5JskFW4Z3sqwVnjmJKHxLAol1bW2qgw=
@@ -740,7 +743,6 @@ golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg=
 golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
@@ -864,8 +866,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/gookit/color.v1 v1.1.6/go.mod h1:IcEkFGaveVShJ+j8ew+jwe9epHyGpJ9IrptHmW3laVY=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
-gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

@@ -83,7 +83,7 @@ spec:
           value: "Kubernetes"
         - name: VERBOSITY
           value: "3"
-        image: gravitl/netmaker:v0.14.6
+        image: gravitl/netmaker:v0.15.0
         imagePullPolicy: Always
         name: netmaker
         ports:

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

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

+ 0 - 3
logger/util.go

@@ -2,8 +2,6 @@ package logger
 
 import (
 	"strings"
-
-	"github.com/gravitl/netmaker/servercfg"
 )
 
 // Verbosity - current logging verbosity level (optionally set)
@@ -25,6 +23,5 @@ func getVerbose() int32 {
 	if Verbosity >= 1 && Verbosity <= 4 {
 		return int32(Verbosity)
 	}
-	Verbosity = int(servercfg.GetVerbosity())
 	return int32(Verbosity)
 }

+ 50 - 0
logic/auth.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"time"
 
 	"github.com/go-playground/validator/v10"
 	"github.com/gravitl/netmaker/database"
@@ -270,3 +271,52 @@ func FetchAuthSecret(key string, secret string) (string, error) {
 	}
 	return record, nil
 }
+
+// GetState - gets an SsoState from DB, if expired returns error
+func GetState(state string) (*models.SsoState, error) {
+	var s models.SsoState
+	record, err := database.FetchRecord(database.SSO_STATE_CACHE, state)
+	if err != nil {
+		return &s, err
+	}
+
+	if err = json.Unmarshal([]byte(record), &s); err != nil {
+		return &s, err
+	}
+
+	if s.IsExpired() {
+		return &s, fmt.Errorf("state expired")
+	}
+
+	return &s, nil
+}
+
+// SetState - sets a state with new expiration
+func SetState(state string) error {
+	s := models.SsoState{
+		Value:      state,
+		Expiration: time.Now().Add(models.DefaultExpDuration),
+	}
+
+	data, err := json.Marshal(&s)
+	if err != nil {
+		return err
+	}
+
+	return database.Insert(state, string(data), database.SSO_STATE_CACHE)
+}
+
+// IsStateValid - checks if given state is valid or not
+// deletes state after call is made to clean up, should only be called once per sign-in
+func IsStateValid(state string) (string, bool) {
+	s, err := GetState(state)
+	if s.Value != "" {
+		delState(state)
+	}
+	return s.Value, err == nil
+}
+
+// delState - removes a state from cache/db
+func delState(state string) error {
+	return database.DeleteRecord(database.SSO_STATE_CACHE, state)
+}

+ 149 - 36
logic/gateway.go

@@ -3,6 +3,7 @@ package logic
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"strings"
 	"time"
 
@@ -20,6 +21,9 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if node.OS != "linux" && node.OS != "freebsd" { // add in darwin later
 		return models.Node{}, errors.New(node.OS + " is unsupported for egress gateways")
 	}
+	if node.OS == "linux" && node.FirewallInUse == models.FIREWALL_NONE {
+		return models.Node{}, errors.New("firewall is not supported for egress gateways")
+	}
 	if gateway.NatEnabled == "" {
 		gateway.NatEnabled = "yes"
 	}
@@ -30,20 +34,29 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	node.IsEgressGateway = "yes"
 	node.EgressGatewayRanges = gateway.Ranges
 	node.EgressGatewayNatEnabled = gateway.NatEnabled
+	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	postUpCmd := ""
 	postDownCmd := ""
+	logger.Log(3, "creating egress gateway firewall in use is '", node.FirewallInUse, "'")
 	if node.OS == "linux" {
-		postUpCmd = "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT; "
-		postUpCmd += "iptables -A FORWARD -o " + node.Interface + " -j ACCEPT"
-		postDownCmd = "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT; "
-		postDownCmd += "iptables -D FORWARD -o " + node.Interface + " -j ACCEPT"
-
-		if node.EgressGatewayNatEnabled == "yes" {
-			postUpCmd += "; iptables -t nat -A POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
-			postDownCmd += "; iptables -t nat -D POSTROUTING -o " + gateway.Interface + " -j MASQUERADE"
+		switch node.FirewallInUse {
+		case models.FIREWALL_NFTABLES:
+			// nftables only supported on Linux
+			// assumes chains eg FORWARD and POSTROUTING already exist
+			logger.Log(3, "creating egress gateway nftables is present")
+			// down commands don't remove as removal of the rules leaves an empty chain while
+			// removing the chain with rules in it would remove all rules in that section (not safe
+			// if there are remaining rules on the host that need to stay).  In practice the chain is removed
+			// when non-empty even though the removal of a non-empty chain should not be possible per nftables wiki.
+			postUpCmd, postDownCmd = firewallNFTCommandsCreateEgress(node.Interface, gateway.Interface, gateway.Ranges, node.EgressGatewayNatEnabled)
+
+		default: // iptables assumed
+			logger.Log(3, "creating egress gateway nftables is not present")
+			postUpCmd, postDownCmd = firewallIPTablesCommandsCreateEgress(node.Interface, gateway.Interface, node.EgressGatewayNatEnabled)
 		}
 	}
 	if node.OS == "freebsd" {
+		// spacing around ; is important for later parsing of postup/postdown in wireguard/common.go
 		postUpCmd = "kldload ipfw ipfw_nat ; "
 		postUpCmd += "ipfw disable one_pass ; "
 		postUpCmd += "ipfw nat 1 config if " + gateway.Interface + " same_ports unreg_only reset ; "
@@ -65,12 +78,12 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	}
 	if node.PostUp != "" {
 		if !strings.Contains(node.PostUp, postUpCmd) {
-			postUpCmd = node.PostUp + "; " + postUpCmd
+			postUpCmd = node.PostUp + " ; " + postUpCmd
 		}
 	}
 	if node.PostDown != "" {
 		if !strings.Contains(node.PostDown, postDownCmd) {
-			postDownCmd = node.PostDown + "; " + postDownCmd
+			postDownCmd = node.PostDown + " ; " + postDownCmd
 		}
 	}
 
@@ -84,9 +97,6 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if err = database.Insert(node.ID, string(nodeData), database.NODES_TABLE_NAME); err != nil {
 		return models.Node{}, err
 	}
-	if err = NetworkNodesUpdatePullChanges(node.Network); err != nil {
-		return models.Node{}, err
-	}
 	return node, nil
 }
 
@@ -115,23 +125,27 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 
 	node.IsEgressGateway = "no"
 	node.EgressGatewayRanges = []string{}
+	node.EgressGatewayRequest = models.EgressGatewayRequest{} // remove preserved request as the egress gateway is gone
+	// needed in case we don't preserve a gateway (i.e., no ingress to preserve)
 	node.PostUp = ""
 	node.PostDown = ""
+
+	logger.Log(3, "deleting egress gateway firewall in use is '", node.FirewallInUse, "'")
 	if node.IsIngressGateway == "yes" { // check if node is still an ingress gateway before completely deleting postdown/up rules
+		// still have an ingress gateway so preserve it
 		if node.OS == "linux" {
-			node.PostUp = "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT ; "
-			node.PostUp += "iptables -A FORWARD -o " + node.Interface + " -j ACCEPT ; "
-			node.PostUp += "iptables -t nat -A POSTROUTING -o " + node.Interface + " -j MASQUERADE"
-			node.PostDown = "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT ; "
-			node.PostDown += "iptables -D FORWARD -o " + node.Interface + " -j ACCEPT ; "
-			node.PostDown += "iptables -t nat -D POSTROUTING -o " + node.Interface + " -j MASQUERADE"
-		}
-		if node.OS == "freebsd" {
-			node.PostUp = ""
-			node.PostDown = "ipfw delete 64000 ; "
-			node.PostDown += "ipfw delete 65534 ; "
-			node.PostDown += "kldunload ipfw_nat ipfw"
+			switch node.FirewallInUse {
+			case models.FIREWALL_NFTABLES:
+				// nftables only supported on Linux
+				// assumes chains eg FORWARD and POSTROUTING already exist
+				logger.Log(3, "deleting egress gateway nftables is present")
+				node.PostUp, node.PostDown = firewallNFTCommandsCreateIngress(node.Interface)
+			default:
+				logger.Log(3, "deleting egress gateway nftables is not present")
+				node.PostUp, node.PostDown = firewallIPTablesCommandsCreateIngress(node.Interface)
+			}
 		}
+		// no need to preserve ingress gateway on FreeBSD as ingress is not supported on that OS
 	}
 	node.SetLastModified()
 
@@ -142,19 +156,20 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 	if err = database.Insert(node.ID, string(data), database.NODES_TABLE_NAME); err != nil {
 		return models.Node{}, err
 	}
-	if err = NetworkNodesUpdatePullChanges(network); err != nil {
-		return models.Node{}, err
-	}
 	return node, nil
 }
 
 // CreateIngressGateway - creates an ingress gateway
 func CreateIngressGateway(netid string, nodeid string) (models.Node, error) {
 
+	var postUpCmd, postDownCmd string
 	node, err := GetNodeByID(nodeid)
 	if node.OS != "linux" { // add in darwin later
 		return models.Node{}, errors.New(node.OS + " is unsupported for ingress gateways")
 	}
+	if node.OS == "linux" && node.FirewallInUse == models.FIREWALL_NONE {
+		return models.Node{}, errors.New("firewall is not supported for ingress gateways")
+	}
 
 	if err != nil {
 		return models.Node{}, err
@@ -166,20 +181,26 @@ func CreateIngressGateway(netid string, nodeid string) (models.Node, error) {
 	}
 	node.IsIngressGateway = "yes"
 	node.IngressGatewayRange = network.AddressRange
-	postUpCmd := "iptables -A FORWARD -i " + node.Interface + " -j ACCEPT ; "
-	postUpCmd += "iptables -A FORWARD -o " + node.Interface + " -j ACCEPT ; "
-	postUpCmd += "iptables -t nat -A POSTROUTING -o " + node.Interface + " -j MASQUERADE"
-	postDownCmd := "iptables -D FORWARD -i " + node.Interface + " -j ACCEPT ; "
-	postDownCmd += "iptables -D FORWARD -o " + node.Interface + " -j ACCEPT ; "
-	postDownCmd += "iptables -t nat -D POSTROUTING -o " + node.Interface + " -j MASQUERADE"
+	logger.Log(3, "creating ingress gateway firewall in use is '", node.FirewallInUse, "'")
+	switch node.FirewallInUse {
+	case models.FIREWALL_NFTABLES:
+		// nftables only supported on Linux
+		// assumes chains eg FORWARD and POSTROUTING already exist
+		logger.Log(3, "creating ingress gateway nftables is present")
+		postUpCmd, postDownCmd = firewallNFTCommandsCreateIngress(node.Interface)
+	default:
+		logger.Log(3, "creating ingress gateway using nftables is not present")
+		postUpCmd, postDownCmd = firewallIPTablesCommandsCreateIngress(node.Interface)
+	}
+
 	if node.PostUp != "" {
 		if !strings.Contains(node.PostUp, postUpCmd) {
-			postUpCmd = node.PostUp + "; " + postUpCmd
+			postUpCmd = node.PostUp + " ; " + postUpCmd
 		}
 	}
 	if node.PostDown != "" {
 		if !strings.Contains(node.PostDown, postDownCmd) {
-			postDownCmd = node.PostDown + "; " + postDownCmd
+			postDownCmd = node.PostDown + " ; " + postDownCmd
 		}
 	}
 	node.SetLastModified()
@@ -214,12 +235,26 @@ func DeleteIngressGateway(networkName string, nodeid string) (models.Node, error
 	if err = DeleteGatewayExtClients(node.ID, networkName); err != nil {
 		return models.Node{}, err
 	}
+	logger.Log(3, "deleting ingress gateway")
 
 	node.UDPHolePunch = network.DefaultUDPHolePunch
 	node.LastModified = time.Now().Unix()
 	node.IsIngressGateway = "no"
 	node.IngressGatewayRange = ""
 
+	// default to removing postup and postdown
+	node.PostUp = ""
+	node.PostDown = ""
+
+	logger.Log(3, "deleting ingress gateway firewall in use is '", node.FirewallInUse, "' and isEgressGateway is", node.IsEgressGateway)
+	if node.EgressGatewayRequest.NodeID != "" {
+		_, err := CreateEgressGateway(node.EgressGatewayRequest)
+		if err != nil {
+			logger.Log(0, fmt.Sprintf("failed to create egress gateway on node [%s] on network [%s]: %v",
+				node.EgressGatewayRequest.NodeID, node.EgressGatewayRequest.NetID, err))
+		}
+	}
+
 	data, err := json.Marshal(&node)
 	if err != nil {
 		return models.Node{}, err
@@ -248,3 +283,81 @@ func DeleteGatewayExtClients(gatewayID string, networkName string) error {
 	}
 	return nil
 }
+
+// firewallNFTCommandsCreateIngress - used to centralize firewall command maintenance for creating an ingress gateway using the nftables firewall.
+func firewallNFTCommandsCreateIngress(networkInterface string) (string, string) {
+	// spacing around ; is important for later parsing of postup/postdown in wireguard/common.go
+	postUp := "nft add table ip filter ; "
+	postUp += "nft add chain ip filter FORWARD ; "
+	postUp += "nft add rule ip filter FORWARD iifname " + networkInterface + " counter accept ; "
+	postUp += "nft add rule ip filter FORWARD oifname " + networkInterface + " counter accept ; "
+	postUp += "nft add table nat ; "
+	postUp += "nft add chain nat POSTROUTING ; "
+	postUp += "nft add rule ip nat POSTROUTING oifname " + networkInterface + " counter masquerade"
+
+	// doesn't remove potentially empty tables or chains
+	postDown := "nft flush table filter ; "
+	postDown += "nft flush table nat ; "
+
+	return postUp, postDown
+}
+
+// firewallNFTCommandsCreateEgress - used to centralize firewall command maintenance for creating an egress gateway using the nftables firewall.
+func firewallNFTCommandsCreateEgress(networkInterface string, gatewayInterface string, gatewayranges []string, egressNatEnabled string) (string, string) {
+	// spacing around ; is important for later parsing of postup/postdown in wireguard/common.go
+	postUp := "nft add table ip filter ; "
+	postUp += "nft add chain ip filter forward ; "
+	postUp += "nft add rule filter forward ct state related,established accept ; "
+	postUp += "nft add rule ip filter forward iifname " + networkInterface + " accept ; "
+	postUp += "nft add rule ip filter forward oifname " + networkInterface + " accept ; "
+	postUp += "nft add table nat ; "
+	postUp += "nft 'add chain ip nat prerouting { type nat hook prerouting priority 0 ;}' ; "
+	postUp += "nft 'add chain ip nat postrouting { type nat hook postrouting priority 0 ;}' ; "
+	for _, networkCIDR := range gatewayranges {
+		postUp += "nft add rule nat postrouting iifname " + networkInterface + " oifname " + gatewayInterface + " ip saddr " + networkCIDR + " masquerade ; "
+	}
+
+	postDown := "nft flush table filter ; "
+
+	if egressNatEnabled == "yes" {
+		postUp += "nft add table nat ; "
+		postUp += "nft add chain nat POSTROUTING ; "
+		postUp += "nft add rule ip nat POSTROUTING oifname " + gatewayInterface + " counter masquerade ; "
+
+		postDown += "nft flush table nat ; "
+	}
+
+	return postUp, postDown
+}
+
+// firewallIPTablesCommandsCreateIngress - used to centralize firewall command maintenance for creating an ingress gateway using the iptables firewall.
+func firewallIPTablesCommandsCreateIngress(networkInterface string) (string, string) {
+	// spacing around ; is important for later parsing of postup/postdown in wireguard/common.go
+	postUp := "iptables -A FORWARD -i " + networkInterface + " -j ACCEPT ; "
+	postUp += "iptables -A FORWARD -o " + networkInterface + " -j ACCEPT ; "
+	postUp += "iptables -t nat -A POSTROUTING -o " + networkInterface + " -j MASQUERADE"
+
+	// doesn't remove potentially empty tables or chains
+	postDown := "iptables -D FORWARD -i " + networkInterface + " -j ACCEPT ; "
+	postDown += "iptables -D FORWARD -o " + networkInterface + " -j ACCEPT ; "
+	postDown += "iptables -t nat -D POSTROUTING -o " + networkInterface + " -j MASQUERADE"
+
+	return postUp, postDown
+}
+
+// firewallIPTablesCommandsCreateEgress - used to centralize firewall command maintenance for creating an egress gateway using the iptables firewall.
+func firewallIPTablesCommandsCreateEgress(networkInterface string, gatewayInterface string, egressNatEnabled string) (string, string) {
+	// spacing around ; is important for later parsing of postup/postdown in wireguard/common.go
+	postUp := "iptables -A FORWARD -i " + networkInterface + " -j ACCEPT ; "
+	postUp += "iptables -A FORWARD -o " + networkInterface + " -j ACCEPT"
+	postDown := "iptables -D FORWARD -i " + networkInterface + " -j ACCEPT ; "
+	postDown += "iptables -D FORWARD -o " + networkInterface + " -j ACCEPT"
+
+	if egressNatEnabled == "yes" {
+		postUp += " ; iptables -t nat -A POSTROUTING -o " + gatewayInterface + " -j MASQUERADE"
+		postDown += " ; iptables -t nat -D POSTROUTING -o " + gatewayInterface + " -j MASQUERADE"
+	}
+
+	return postUp, postDown
+
+}

+ 0 - 30
logic/networks.go

@@ -91,36 +91,6 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 	return network, nil
 }
 
-// NetworkNodesUpdatePullChanges - tells nodes on network to pull
-func NetworkNodesUpdatePullChanges(networkName string) error {
-
-	collections, err := database.FetchRecords(database.NODES_TABLE_NAME)
-	if err != nil {
-		if database.IsEmptyRecord(err) {
-			return nil
-		}
-		return err
-	}
-
-	for _, value := range collections {
-		var node models.Node
-		err := json.Unmarshal([]byte(value), &node)
-		if err != nil {
-			fmt.Println("error in node address assignment!")
-			return err
-		}
-		if node.Network == networkName {
-			data, err := json.Marshal(&node)
-			if err != nil {
-				return err
-			}
-			database.Insert(node.ID, string(data), database.NODES_TABLE_NAME)
-		}
-	}
-
-	return nil
-}
-
 // GetNetworkNonServerNodeCount - get number of network non server nodes
 func GetNetworkNonServerNodeCount(networkName string) (int, error) {
 

+ 3 - 2
logic/nodes.go

@@ -185,7 +185,7 @@ func DeleteNodeByID(node *models.Node, exterminate bool) error {
 		// ignoring for now, could hit a nil pointer if delete called twice
 		logger.Log(2, "attempted to remove node ACL for node", node.Name, node.ID)
 	}
-	removeZombie <- node.ID
+	// removeZombie <- node.ID
 	if node.IsServer == "yes" {
 		return removeLocalServer(node)
 	}
@@ -288,7 +288,7 @@ func CreateNode(node *models.Node) error {
 	if err != nil {
 		return err
 	}
-	CheckZombies(node)
+	// CheckZombies(node)
 
 	nodebytes, err := json.Marshal(&node)
 	if err != nil {
@@ -427,6 +427,7 @@ func SetNodeDefaults(node *models.Node) {
 	node.SetDefaultIngressGateway()
 	node.SetDefaulIsPending()
 	node.SetDefaultMTU()
+	node.SetDefaultNFTablesPresent()
 	node.SetDefaultIsRelayed()
 	node.SetDefaultIsRelay()
 	node.SetDefaultIsDocker()

+ 45 - 4
logic/peers.go

@@ -16,6 +16,7 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slices"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
@@ -268,6 +269,14 @@ func GetAllowedIPs(node, peer *models.Node) []net.IPNet {
 	if peer.IsEgressGateway == "yes" {
 		//hasGateway = true
 		egressIPs := getEgressIPs(node, peer)
+		// remove internet gateway if server
+		if node.IsServer == "yes" {
+			for i := len(egressIPs) - 1; i >= 0; i-- {
+				if egressIPs[i].IP.String() == "0.0.0.0/0" || egressIPs[i].IP.String() == "::/0" {
+					egressIPs = append(egressIPs[:i], egressIPs[i+1:]...)
+				}
+			}
+		}
 		allowedips = append(allowedips, egressIPs...)
 	}
 
@@ -324,6 +333,16 @@ func GetAllowedIPs(node, peer *models.Node) []net.IPNet {
 				extAllowedIPs := getEgressIPs(node, relayedNode)
 				allowedips = append(allowedips, extAllowedIPs...)
 			}
+			if relayedNode.IsIngressGateway == "yes" {
+				extPeers, err := getExtPeers(relayedNode)
+				if err == nil {
+					for _, extPeer := range extPeers {
+						allowedips = append(allowedips, extPeer.AllowedIPs...)
+					}
+				} else {
+					logger.Log(0, "failed to retrieve extclients from relayed ingress", err.Error())
+				}
+			}
 		}
 	}
 	return allowedips
@@ -408,7 +427,15 @@ func GetPeerUpdateForRelayedNode(node *models.Node, udppeers map[string]string)
 	//delete egressrange from allowedip if we are egress gateway
 	if node.IsEgressGateway == "yes" {
 		for i := len(allowedips) - 1; i >= 0; i-- {
-			if StringSliceContains(node.EgressGatewayRanges, allowedips[i].IP.String()) {
+			if StringSliceContains(node.EgressGatewayRanges, allowedips[i].String()) {
+				allowedips = append(allowedips[:i], allowedips[i+1:]...)
+			}
+		}
+	}
+	//delete extclients from allowedip if we are ingress gateway
+	if node.IsIngressGateway == "yes" {
+		for i := len(allowedips) - 1; i >= 0; i-- {
+			if strings.Contains(node.IngressGatewayRange, allowedips[i].IP.String()) {
 				allowedips = append(allowedips[:i], allowedips[i+1:]...)
 			}
 		}
@@ -458,6 +485,15 @@ func GetPeerUpdateForRelayedNode(node *models.Node, udppeers map[string]string)
 	if relay.IsServer == "yes" {
 		serverNodeAddresses = append(serverNodeAddresses, models.ServerAddr{IsLeader: IsLeader(relay), Address: relay.Address})
 	}
+	//if ingress add extclients
+	if node.IsIngressGateway == "yes" {
+		extPeers, err := getExtPeers(node)
+		if err == nil {
+			peers = append(peers, extPeers...)
+		} else {
+			logger.Log(2, "could not retrieve ext peers for ", node.Name, err.Error())
+		}
+	}
 	peerUpdate.Network = node.Network
 	peerUpdate.ServerVersion = servercfg.Version
 	peerUpdate.Peers = peers
@@ -467,6 +503,11 @@ func GetPeerUpdateForRelayedNode(node *models.Node, udppeers map[string]string)
 }
 
 func getEgressIPs(node, peer *models.Node) []net.IPNet {
+	//check for internet gateway
+	internetGateway := false
+	if slices.Contains(peer.EgressGatewayRanges, "0.0.0.0/0") || slices.Contains(peer.EgressGatewayRanges, "::0") {
+		internetGateway = true
+	}
 	allowedips := []net.IPNet{}
 	for _, iprange := range peer.EgressGatewayRanges { // go through each cidr for egress gateway
 		_, ipnet, err := net.ParseCIDR(iprange) // confirming it's valid cidr
@@ -474,13 +515,13 @@ func getEgressIPs(node, peer *models.Node) []net.IPNet {
 			logger.Log(1, "could not parse gateway IP range. Not adding ", iprange)
 			continue // if can't parse CIDR
 		}
-		nodeEndpointArr := strings.Split(peer.Endpoint, ":") // getting the public ip of node
-		if ipnet.Contains(net.ParseIP(nodeEndpointArr[0])) { // ensuring egress gateway range does not contain endpoint of node
+		nodeEndpointArr := strings.Split(peer.Endpoint, ":")                     // getting the public ip of node
+		if ipnet.Contains(net.ParseIP(nodeEndpointArr[0])) && !internetGateway { // ensuring egress gateway range does not contain endpoint of node
 			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", node.Endpoint, ", omitting")
 			continue // skip adding egress range if overlaps with node's ip
 		}
 		// TODO: Could put in a lot of great logic to avoid conflicts / bad routes
-		if ipnet.Contains(net.ParseIP(node.LocalAddress)) { // ensuring egress gateway range does not contain public ip of node
+		if ipnet.Contains(net.ParseIP(node.LocalAddress)) && !internetGateway { // ensuring egress gateway range does not contain public ip of node
 			logger.Log(2, "egress IP range of ", iprange, " overlaps with ", node.LocalAddress, ", omitting")
 			continue // skip adding egress range if overlaps with node's local ip
 		}

+ 0 - 6
logic/relay.go

@@ -41,9 +41,6 @@ func CreateRelay(relay models.RelayRequest) ([]models.Node, models.Node, error)
 	if err != nil {
 		return returnnodes, node, err
 	}
-	if err = NetworkNodesUpdatePullChanges(node.Network); err != nil {
-		return returnnodes, models.Node{}, err
-	}
 	return returnnodes, node, nil
 }
 
@@ -125,8 +122,5 @@ func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
 	if err = database.Insert(nodeid, string(data), database.NODES_TABLE_NAME); err != nil {
 		return returnnodes, models.Node{}, err
 	}
-	if err = NetworkNodesUpdatePullChanges(network); err != nil {
-		return returnnodes, models.Node{}, err
-	}
 	return returnnodes, node, nil
 }

+ 3 - 32
logic/server.go

@@ -11,6 +11,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/ncutils"
+	"github.com/gravitl/netmaker/netclient/wireguard"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
@@ -147,13 +148,13 @@ func ServerJoin(networkSettings *models.Network) (models.Node, error) {
 		return returnNode, err
 	}
 
-	peers, hasGateway, gateways, err := GetServerPeers(node)
+	peers, err := GetPeerUpdate(node)
 	if err != nil && !ncutils.IsEmptyRecord(err) {
 		logger.Log(1, "failed to retrieve peers")
 		return returnNode, err
 	}
 
-	err = initWireguard(node, privateKey, peers[:], hasGateway, gateways[:])
+	err = wireguard.InitWireguard(node, privateKey, peers.Peers, false)
 	if err != nil {
 		return returnNode, err
 	}
@@ -187,36 +188,6 @@ func ServerUpdate(serverNode *models.Node, ifaceDelta bool) error {
 	return serverPush(serverNode)
 }
 
-/**
- * Below function needs major refactor
- *
- */
-
-// GetServerPeers - gets peers of server
-func GetServerPeers(serverNode *models.Node) ([]wgtypes.PeerConfig, bool, []string, error) {
-	update, err := GetPeerUpdate(serverNode)
-	if err != nil {
-		return []wgtypes.PeerConfig{}, false, []string{}, err
-	}
-
-	// this is temporary code, should be removed by 0.14.4
-	// refactor server routing to use client-side routing code
-	var hasGateways = false
-	var gateways = []string{}
-	nodes, err := GetNetworkNodes(serverNode.Network)
-	if err == nil {
-		for _, node := range nodes {
-			if node.IsEgressGateway == "yes" && !IsLocalServer(&node) {
-				gateways = append(gateways, node.EgressGatewayRanges...)
-			}
-		}
-		hasGateways = len(gateways) > 0
-	}
-	// end temporary code
-
-	return update.Peers, hasGateways, gateways, nil
-}
-
 // == Private ==
 
 func isDeleteError(err error) bool {

+ 3 - 200
logic/wireguard.go

@@ -1,18 +1,12 @@
 package logic
 
 import (
-	"errors"
-	"fmt"
-	"net"
 	"os"
 	"os/exec"
-	"strconv"
 	"strings"
-	"time"
 
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/netclient/local"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/wireguard"
 	"golang.zx2c4.com/wireguard/wgctrl"
@@ -123,177 +117,6 @@ func getSystemPeers(node *models.Node) (map[string]string, error) {
 	}
 	return peers, nil
 }
-
-func initWireguard(node *models.Node, privkey string, peers []wgtypes.PeerConfig, hasGateway bool, gateways []string) error {
-
-	key, err := wgtypes.ParseKey(privkey)
-	if err != nil {
-		return err
-	}
-
-	wgclient, err := wgctrl.New()
-	if err != nil {
-		return err
-	}
-	defer wgclient.Close()
-
-	var ifacename string
-	if node.Interface != "" {
-		ifacename = node.Interface
-	} else {
-		logger.Log(2, "no server interface provided to configure")
-	}
-	if node.Address == "" {
-		logger.Log(2, "no server address to provided configure")
-	}
-
-	if ncutils.IsKernel() {
-		logger.Log(2, "setting kernel device", ifacename)
-		network, err := GetNetwork(node.Network)
-		if err != nil {
-			logger.Log(0, "failed to get network"+err.Error())
-			return err
-		}
-		var address4 string
-		var address6 string
-		var mask4 string
-		var mask6 string
-		if network.AddressRange != "" {
-			net := strings.Split(network.AddressRange, "/")
-			mask4 = net[len(net)-1]
-			address4 = node.Address
-		}
-		if network.AddressRange6 != "" {
-			net := strings.Split(network.AddressRange6, "/")
-			mask6 = net[len(net)-1]
-			address6 = node.Address6
-		}
-
-		setKernelDevice(ifacename, address4, mask4, address6, mask6)
-	}
-
-	nodeport := int(node.ListenPort)
-	var conf = wgtypes.Config{
-		PrivateKey:   &key,
-		ListenPort:   &nodeport,
-		ReplacePeers: true,
-		Peers:        peers,
-	}
-
-	if !ncutils.IsKernel() {
-		if err := wireguard.WriteWgConfig(node, key.String(), peers); err != nil {
-			logger.Log(1, "error writing wg conf file: ", err.Error())
-			return err
-		}
-		// spin up userspace + apply the conf file
-		var deviceiface = ifacename
-		confPath := ncutils.GetNetclientPathSpecific() + ifacename + ".conf"
-		d, _ := wgclient.Device(deviceiface)
-		for d != nil && d.Name == deviceiface {
-			_ = RemoveConf(ifacename, false) // remove interface first
-			time.Sleep(time.Second >> 2)
-			d, _ = wgclient.Device(deviceiface)
-		}
-		time.Sleep(time.Second >> 2)
-		err = applyWGQuickConf(confPath)
-		if err != nil {
-			logger.Log(1, "failed to create wireguard interface")
-			return err
-		}
-	} else {
-		ipExec, err := exec.LookPath("ip")
-		if err != nil {
-			return err
-		}
-
-		_, err = wgclient.Device(ifacename)
-		if err != nil {
-			if os.IsNotExist(err) {
-				fmt.Println("Device does not exist: ")
-				fmt.Println(err)
-			} else {
-				return errors.New("Unknown config error: " + err.Error())
-			}
-		}
-
-		err = wgclient.ConfigureDevice(ifacename, conf)
-		if err != nil {
-			if os.IsNotExist(err) {
-				fmt.Println("Device does not exist: ")
-				fmt.Println(err)
-			} else {
-				fmt.Printf("This is inconvenient: %v", err)
-			}
-		}
-
-		if _, err := ncutils.RunCmd(ipExec+" link set down dev "+ifacename, false); err != nil {
-			logger.Log(2, "attempted to remove interface before editing")
-			return err
-		}
-
-		if node.PostDown != "" {
-			runcmds := strings.Split(node.PostDown, "; ")
-			_ = ncutils.RunCmds(runcmds, false)
-		}
-		// set MTU of node interface
-		if _, err := ncutils.RunCmd(ipExec+" link set mtu "+strconv.Itoa(int(node.MTU))+" up dev "+ifacename, true); err != nil {
-			logger.Log(2, "failed to create interface with mtu", strconv.Itoa(int(node.MTU)), "-", ifacename)
-			return err
-		}
-
-		if node.PostUp != "" {
-			runcmds := strings.Split(node.PostUp, "; ")
-			_ = ncutils.RunCmds(runcmds, true)
-		}
-		if hasGateway {
-			for _, gateway := range gateways {
-				_, _ = ncutils.RunCmd(ipExec+" -4 route add "+gateway+" dev "+ifacename, true)
-			}
-		}
-		if node.Address != "" {
-			logger.Log(1, "adding address:", node.Address)
-			_, _ = ncutils.RunCmd(ipExec+" address add dev "+ifacename+" "+node.Address+"/32", true)
-		}
-		if node.Address6 != "" {
-			logger.Log(1, "adding address6:", node.Address6)
-			_, _ = ncutils.RunCmd(ipExec+" address add dev "+ifacename+" "+node.Address6+"/128", true)
-		}
-		wireguard.SetPeers(ifacename, node, peers)
-	}
-
-	if node.IsServer == "yes" {
-		setServerRoutes(node.Interface, node.Network)
-	}
-
-	return err
-}
-
-func setKernelDevice(ifacename, address4, mask4, address6, mask6 string) error {
-	ipExec, err := exec.LookPath("ip")
-	if err != nil {
-		return err
-	}
-
-	// == best effort ==
-	ncutils.RunCmd("ip link delete dev "+ifacename, false)
-	ncutils.RunCmd(ipExec+" link add dev "+ifacename+" type wireguard", true)
-	if address4 != "" {
-		ncutils.RunCmd(ipExec+" address add dev "+ifacename+" "+address4+"/"+mask4, true)
-	}
-	if address6 != "" {
-		ncutils.RunCmd(ipExec+" address add dev "+ifacename+" "+address6+"/"+mask6, true)
-	}
-
-	return nil
-}
-
-func applyWGQuickConf(confPath string) error {
-	if _, err := ncutils.RunCmd("wg-quick up "+confPath, true); err != nil {
-		return err
-	}
-	return nil
-}
-
 func removeWGQuickConf(confPath string, printlog bool) error {
 	if _, err := ncutils.RunCmd("wg-quick down "+confPath, printlog); err != nil {
 		return err
@@ -302,8 +125,7 @@ func removeWGQuickConf(confPath string, printlog bool) error {
 }
 
 func setWGConfig(node *models.Node, peerupdate bool) error {
-
-	peers, hasGateway, gateways, err := GetServerPeers(node)
+	peers, err := GetPeerUpdate(node)
 	if err != nil {
 		return err
 	}
@@ -312,15 +134,14 @@ func setWGConfig(node *models.Node, peerupdate bool) error {
 		return err
 	}
 	if peerupdate {
-		if err := wireguard.SetPeers(node.Interface, node, peers); err != nil {
+		if err := wireguard.SetPeers(node.Interface, node, peers.Peers); err != nil {
 			logger.Log(0, "error updating peers", err.Error())
 		}
 		logger.Log(2, "updated peers on server", node.Name)
 	} else {
-		err = initWireguard(node, privkey, peers[:], hasGateway, gateways[:])
+		err = wireguard.InitWireguard(node, privkey, peers.Peers, false)
 		logger.Log(3, "finished setting wg config on server", node.Name)
 	}
-	peers = nil
 	return err
 }
 
@@ -392,21 +213,3 @@ func removeLocalServer(node *models.Node) error {
 	}
 	return err
 }
-
-func setServerRoutes(iface, network string) {
-	parentNetwork, err := GetParentNetwork(network)
-	if err == nil {
-		if parentNetwork.AddressRange != "" {
-			ip, cidr, err := net.ParseCIDR(parentNetwork.AddressRange)
-			if err == nil {
-				local.SetCIDRRoute(iface, ip.String(), cidr)
-			}
-		}
-		if parentNetwork.AddressRange6 != "" {
-			ip, cidr, err := net.ParseCIDR(parentNetwork.AddressRange6)
-			if err == nil {
-				local.SetCIDRRoute(iface, ip.String(), cidr)
-			}
-		}
-	}
-}

+ 22 - 18
logic/zombie.go

@@ -17,8 +17,8 @@ const (
 
 var (
 	zombies      []string
-	removeZombie chan string = make(chan (string))
-	newZombie    chan string = make(chan (string))
+	removeZombie chan string = make(chan (string), 10)
+	newZombie    chan string = make(chan (string), 10)
 )
 
 // CheckZombies - checks if new node has same macaddress as existing node
@@ -47,30 +47,34 @@ func ManageZombies(ctx context.Context) {
 			zombies = append(zombies, id)
 		case id := <-removeZombie:
 			found := false
-			for i, zombie := range zombies {
-				if zombie == id {
-					logger.Log(1, "removing zombie from quaratine list", zombie)
-					zombies = append(zombies[:i], zombies[i+1:]...)
-					found = true
+			if len(zombies) > 0 {
+				for i := len(zombies) - 1; i >= 0; i-- {
+					if zombies[i] == id {
+						logger.Log(1, "removing zombie from quaratine list", zombies[i])
+						zombies = append(zombies[:i], zombies[i+1:]...)
+						found = true
+					}
 				}
 			}
 			if !found {
 				logger.Log(3, "no zombies found")
 			}
 		case <-time.After(time.Second * ZOMBIE_TIMEOUT):
-			for i, zombie := range zombies {
-				node, err := GetNodeByID(zombie)
-				if err != nil {
-					logger.Log(1, "error retrieving zombie node", zombie, err.Error())
-					continue
-				}
-				if time.Since(time.Unix(node.LastCheckIn, 0)) > time.Minute*ZOMBIE_DELETE_TIME {
-					if err := DeleteNodeByID(&node, true); err != nil {
-						logger.Log(1, "error deleting zombie node", zombie, err.Error())
+			if len(zombies) > 0 {
+				for i := len(zombies) - 1; i >= 0; i-- {
+					node, err := GetNodeByID(zombies[i])
+					if err != nil {
+						logger.Log(1, "error retrieving zombie node", zombies[i], err.Error())
 						continue
 					}
-					logger.Log(1, "deleting zombie node", node.Name)
-					zombies = append(zombies[:i], zombies[i+1:]...)
+					if time.Since(time.Unix(node.LastCheckIn, 0)) > time.Minute*ZOMBIE_DELETE_TIME {
+						if err := DeleteNodeByID(&node, true); err != nil {
+							logger.Log(1, "error deleting zombie node", zombies[i], err.Error())
+							continue
+						}
+						logger.Log(1, "deleting zombie node", node.Name)
+						zombies = append(zombies[:i], zombies[i+1:]...)
+					}
 				}
 			}
 		}

+ 6 - 0
main.go

@@ -42,6 +42,7 @@ func main() {
 	fmt.Println(models.RetrieveLogo()) // print the logo
 	initialize()                       // initial db and acls; gen cert if required
 	setGarbageCollection()
+	setVerbosity()
 	defer database.CloseDB()
 	startControllers() // start the api endpoint and mq
 }
@@ -182,6 +183,11 @@ func runMessageQueue(wg *sync.WaitGroup) {
 	client.Disconnect(250)
 }
 
+func setVerbosity() {
+	verbose := int(servercfg.GetVerbosity())
+	logger.Verbosity = verbose
+}
+
 func setGarbageCollection() {
 	_, gcset := os.LookupEnv("GOGC")
 	if !gcset {

+ 65 - 49
models/node.go

@@ -28,6 +28,12 @@ const (
 	NODE_NOOP = "noop"
 	// NODE_FORCE_UPDATE - indicates a node should pull all changes
 	NODE_FORCE_UPDATE = "force"
+	// FIREWALL_IPTABLES - indicates that iptables is the firewall in use
+	FIREWALL_IPTABLES = "iptables"
+	// FIREWALL_NFTABLES - indicates nftables is in use (Linux only)
+	FIREWALL_NFTABLES = "nftables"
+	// FIREWALL_NONE - indicates that no supported firewall in use
+	FIREWALL_NONE = "none"
 )
 
 var seededRand *rand.Rand = rand.New(
@@ -35,55 +41,58 @@ var seededRand *rand.Rand = rand.New(
 
 // Node - struct for node model
 type Node struct {
-	ID                      string   `json:"id,omitempty" bson:"id,omitempty" yaml:"id,omitempty" validate:"required,min=5" validate:"id_unique`
-	Address                 string   `json:"address" bson:"address" yaml:"address" validate:"omitempty,ipv4"`
-	Address6                string   `json:"address6" bson:"address6" yaml:"address6" validate:"omitempty,ipv6"`
-	LocalAddress            string   `json:"localaddress" bson:"localaddress" yaml:"localaddress" validate:"omitempty,ip"`
-	Name                    string   `json:"name" bson:"name" yaml:"name" validate:"omitempty,max=62,in_charset"`
-	NetworkSettings         Network  `json:"networksettings" bson:"networksettings" yaml:"networksettings" validate:"-"`
-	ListenPort              int32    `json:"listenport" bson:"listenport" yaml:"listenport" validate:"omitempty,numeric,min=1024,max=65535"`
-	LocalListenPort         int32    `json:"locallistenport" bson:"locallistenport" yaml:"locallistenport" validate:"numeric,min=0,max=65535"`
-	PublicKey               string   `json:"publickey" bson:"publickey" yaml:"publickey" validate:"required,base64"`
-	Endpoint                string   `json:"endpoint" bson:"endpoint" yaml:"endpoint" validate:"required,ip"`
-	PostUp                  string   `json:"postup" bson:"postup" yaml:"postup"`
-	PostDown                string   `json:"postdown" bson:"postdown" yaml:"postdown"`
-	AllowedIPs              []string `json:"allowedips" bson:"allowedips" yaml:"allowedips"`
-	PersistentKeepalive     int32    `json:"persistentkeepalive" bson:"persistentkeepalive" yaml:"persistentkeepalive" validate:"omitempty,numeric,max=1000"`
-	IsHub                   string   `json:"ishub" bson:"ishub" yaml:"ishub" validate:"checkyesorno"`
-	AccessKey               string   `json:"accesskey" bson:"accesskey" yaml:"accesskey"`
-	Interface               string   `json:"interface" bson:"interface" yaml:"interface"`
-	LastModified            int64    `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"`
-	ExpirationDateTime      int64    `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"`
-	LastPeerUpdate          int64    `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"`
-	LastCheckIn             int64    `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"`
-	MacAddress              string   `json:"macaddress" bson:"macaddress" yaml:"macaddress"`
-	Password                string   `json:"password" bson:"password" yaml:"password" validate:"required,min=6"`
-	Network                 string   `json:"network" bson:"network" yaml:"network" validate:"network_exists"`
-	IsRelayed               string   `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
-	IsPending               string   `json:"ispending" bson:"ispending" yaml:"ispending"`
-	IsRelay                 string   `json:"isrelay" bson:"isrelay" yaml:"isrelay" validate:"checkyesorno"`
-	IsDocker                string   `json:"isdocker" bson:"isdocker" yaml:"isdocker" validate:"checkyesorno"`
-	IsK8S                   string   `json:"isk8s" bson:"isk8s" yaml:"isk8s" validate:"checkyesorno"`
-	IsEgressGateway         string   `json:"isegressgateway" bson:"isegressgateway" yaml:"isegressgateway"`
-	IsIngressGateway        string   `json:"isingressgateway" bson:"isingressgateway" yaml:"isingressgateway"`
-	EgressGatewayRanges     []string `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
-	EgressGatewayNatEnabled string   `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	RelayAddrs              []string `json:"relayaddrs" bson:"relayaddrs" yaml:"relayaddrs"`
-	IngressGatewayRange     string   `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
+	ID                      string               `json:"id,omitempty" bson:"id,omitempty" yaml:"id,omitempty" validate:"required,min=5,id_unique"`
+	Address                 string               `json:"address" bson:"address" yaml:"address" validate:"omitempty,ipv4"`
+	Address6                string               `json:"address6" bson:"address6" yaml:"address6" validate:"omitempty,ipv6"`
+	LocalAddress            string               `json:"localaddress" bson:"localaddress" yaml:"localaddress" validate:"omitempty,ip"`
+	Name                    string               `json:"name" bson:"name" yaml:"name" validate:"omitempty,max=62,in_charset"`
+	NetworkSettings         Network              `json:"networksettings" bson:"networksettings" yaml:"networksettings" validate:"-"`
+	ListenPort              int32                `json:"listenport" bson:"listenport" yaml:"listenport" validate:"omitempty,numeric,min=1024,max=65535"`
+	LocalListenPort         int32                `json:"locallistenport" bson:"locallistenport" yaml:"locallistenport" validate:"numeric,min=0,max=65535"`
+	PublicKey               string               `json:"publickey" bson:"publickey" yaml:"publickey" validate:"required,base64"`
+	Endpoint                string               `json:"endpoint" bson:"endpoint" yaml:"endpoint" validate:"required,ip"`
+	PostUp                  string               `json:"postup" bson:"postup" yaml:"postup"`
+	PostDown                string               `json:"postdown" bson:"postdown" yaml:"postdown"`
+	AllowedIPs              []string             `json:"allowedips" bson:"allowedips" yaml:"allowedips"`
+	PersistentKeepalive     int32                `json:"persistentkeepalive" bson:"persistentkeepalive" yaml:"persistentkeepalive" validate:"omitempty,numeric,max=1000"`
+	IsHub                   string               `json:"ishub" bson:"ishub" yaml:"ishub" validate:"checkyesorno"`
+	AccessKey               string               `json:"accesskey" bson:"accesskey" yaml:"accesskey"`
+	Interface               string               `json:"interface" bson:"interface" yaml:"interface"`
+	LastModified            int64                `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"`
+	ExpirationDateTime      int64                `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"`
+	LastPeerUpdate          int64                `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"`
+	LastCheckIn             int64                `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"`
+	MacAddress              string               `json:"macaddress" bson:"macaddress" yaml:"macaddress"`
+	Password                string               `json:"password" bson:"password" yaml:"password" validate:"required,min=6"`
+	Network                 string               `json:"network" bson:"network" yaml:"network" validate:"network_exists"`
+	IsRelayed               string               `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
+	IsPending               string               `json:"ispending" bson:"ispending" yaml:"ispending"`
+	IsRelay                 string               `json:"isrelay" bson:"isrelay" yaml:"isrelay" validate:"checkyesorno"`
+	IsDocker                string               `json:"isdocker" bson:"isdocker" yaml:"isdocker" validate:"checkyesorno"`
+	IsK8S                   string               `json:"isk8s" bson:"isk8s" yaml:"isk8s" validate:"checkyesorno"`
+	IsEgressGateway         string               `json:"isegressgateway" bson:"isegressgateway" yaml:"isegressgateway"`
+	IsIngressGateway        string               `json:"isingressgateway" bson:"isingressgateway" yaml:"isingressgateway"`
+	EgressGatewayRanges     []string             `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
+	EgressGatewayNatEnabled string               `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
+	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
+	RelayAddrs              []string             `json:"relayaddrs" bson:"relayaddrs" yaml:"relayaddrs"`
+	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
 	// IsStatic - refers to if the Endpoint is set manually or dynamically
-	IsStatic     string      `json:"isstatic" bson:"isstatic" yaml:"isstatic" validate:"checkyesorno"`
-	UDPHolePunch string      `json:"udpholepunch" bson:"udpholepunch" yaml:"udpholepunch" validate:"checkyesorno"`
-	DNSOn        string      `json:"dnson" bson:"dnson" yaml:"dnson" validate:"checkyesorno"`
-	IsServer     string      `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
-	Action       string      `json:"action" bson:"action" yaml:"action"`
-	IsLocal      string      `json:"islocal" bson:"islocal" yaml:"islocal" validate:"checkyesorno"`
-	LocalRange   string      `json:"localrange" bson:"localrange" yaml:"localrange"`
-	IPForwarding string      `json:"ipforwarding" bson:"ipforwarding" yaml:"ipforwarding" validate:"checkyesorno"`
-	OS           string      `json:"os" bson:"os" yaml:"os"`
-	MTU          int32       `json:"mtu" bson:"mtu" yaml:"mtu"`
-	Version      string      `json:"version" bson:"version" yaml:"version"`
-	Server       string      `json:"server" bson:"server" yaml:"server"`
-	TrafficKeys  TrafficKeys `json:"traffickeys" bson:"traffickeys" yaml:"traffickeys"`
+	IsStatic        string      `json:"isstatic" bson:"isstatic" yaml:"isstatic" validate:"checkyesorno"`
+	UDPHolePunch    string      `json:"udpholepunch" bson:"udpholepunch" yaml:"udpholepunch" validate:"checkyesorno"`
+	DNSOn           string      `json:"dnson" bson:"dnson" yaml:"dnson" validate:"checkyesorno"`
+	IsServer        string      `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
+	Action          string      `json:"action" bson:"action" yaml:"action"`
+	IsLocal         string      `json:"islocal" bson:"islocal" yaml:"islocal" validate:"checkyesorno"`
+	LocalRange      string      `json:"localrange" bson:"localrange" yaml:"localrange"`
+	IPForwarding    string      `json:"ipforwarding" bson:"ipforwarding" yaml:"ipforwarding" validate:"checkyesorno"`
+	OS              string      `json:"os" bson:"os" yaml:"os"`
+	MTU             int32       `json:"mtu" bson:"mtu" yaml:"mtu"`
+	Version         string      `json:"version" bson:"version" yaml:"version"`
+	Server          string      `json:"server" bson:"server" yaml:"server"`
+	TrafficKeys     TrafficKeys `json:"traffickeys" bson:"traffickeys" yaml:"traffickeys"`
+	FirewallInUse   string      `json:"firewallinuse" bson:"firewallinuse" yaml:"firewallinuse"`
+	InternetGateway string      `json:"internetgateway" bson:"internetgateway" yaml:"internetgateway"`
 }
 
 // NodesArray - used for node sorting
@@ -119,6 +128,13 @@ func (node *Node) SetDefaultMTU() {
 	}
 }
 
+// Node.SetDefaultNFTablesPresent - sets default for nftables check
+func (node *Node) SetDefaultNFTablesPresent() {
+	if node.FirewallInUse == "" {
+		node.FirewallInUse = FIREWALL_IPTABLES // default to iptables
+	}
+}
+
 // Node.SetDefaulIsPending - sets ispending default
 func (node *Node) SetDefaulIsPending() {
 	if node.IsPending == "" {
@@ -254,7 +270,7 @@ func (node *Node) SetDefaultName() {
 }
 
 // Node.Fill - fills other node data into calling node data if not set on calling node
-func (newNode *Node) Fill(currentNode *Node) {
+func (newNode *Node) Fill(currentNode *Node) { // TODO add new field for nftables present
 	newNode.ID = currentNode.ID
 
 	if newNode.Address == "" {

+ 17 - 0
models/ssocache.go

@@ -0,0 +1,17 @@
+package models
+
+import "time"
+
+// DefaultExpDuration - the default expiration time of SsoState
+const DefaultExpDuration = time.Minute * 5
+
+// SsoState - holds SSO sign-in session data
+type SsoState struct {
+	Value      string    `json:"value"`
+	Expiration time.Time `json:"expiration"`
+}
+
+// SsoState.IsExpired - tells if an SsoState is expired or not
+func (s *SsoState) IsExpired() bool {
+	return time.Now().After(s.Expiration)
+}

+ 1 - 0
mq/handlers.go

@@ -77,6 +77,7 @@ func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 			logger.Log(1, "error unmarshaling payload ", err.Error())
 			return
 		}
+		newNode.SetLastCheckIn()
 		if err := logic.UpdateNode(&currentNode, &newNode); err != nil {
 			logger.Log(1, "error saving node", err.Error())
 			return

+ 7 - 0
netclient/cli_options/flags.go

@@ -66,6 +66,13 @@ func GetFlags(hostname string) []cli.Flag {
 			Value:   "",
 			Usage:   "Identifiable name for machine within Netmaker network.",
 		},
+		&cli.StringFlag{
+			Name:    "publicipservice",
+			Aliases: []string{"ip-service"},
+			EnvVars: []string{"NETCLIENT_IP_SERVICE"},
+			Value:   "",
+			Usage:   "The service to call to obtain the public IP of the machine that is running netclient.",
+		},
 		&cli.StringFlag{
 			Name:    "name",
 			EnvVars: []string{"NETCLIENT_NAME"},

+ 14 - 12
netclient/config/config.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/netclient/global_settings"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/urfave/cli/v2"
 	"gopkg.in/yaml.v3"
@@ -30,6 +31,7 @@ type ClientConfig struct {
 	Daemon          string              `yaml:"daemon"`
 	OperatingSystem string              `yaml:"operatingsystem"`
 	AccessKey       string              `yaml:"accesskey"`
+	PublicIPService string              `yaml:"publicipservice"`
 }
 
 // RegisterRequest - struct for registation with netmaker server
@@ -231,6 +233,10 @@ func GetCLIConfig(c *cli.Context) (ClientConfig, string, error) {
 		cfg.Server.CoreDNSAddr = c.String("corednsaddr")
 		cfg.Server.API = c.String("apiserver")
 	}
+	cfg.PublicIPService = c.String("publicipservice")
+	// populate the map as we're not running as a daemon so won't be building the map otherwise
+	// (and the map will be used by GetPublicIP()).
+	global_settings.PublicIPServices[cfg.Network] = cfg.PublicIPService
 	cfg.Node.Name = c.String("name")
 	cfg.Node.Interface = c.String("interface")
 	cfg.Node.Password = c.String("password")
@@ -264,32 +270,28 @@ func ReadConfig(network string) (*ClientConfig, error) {
 		err := errors.New("no network provided - exiting")
 		return nil, err
 	}
-	nofile := false
 	home := ncutils.GetNetclientPathSpecific()
 	file := fmt.Sprintf(home + "netconfig-" + network)
 	f, err := os.Open(file)
-
 	if err != nil {
 		if err = ReplaceWithBackup(network); err != nil {
-			nofile = true
+			return nil, err
 		}
 		f, err = os.Open(file)
 		if err != nil {
-			nofile = true
+			return nil, err
 		}
 	}
 	defer f.Close()
 
 	var cfg ClientConfig
-
-	if !nofile {
-		decoder := yaml.NewDecoder(f)
-		err = decoder.Decode(&cfg)
-		if err != nil {
-			fmt.Println("trouble decoding file")
-			return nil, err
-		}
+	decoder := yaml.NewDecoder(f)
+	err = decoder.Decode(&cfg)
+	if err != nil {
+		logger.Log(2, "trouble decoding file", err.Error())
+		return nil, err
 	}
+
 	return &cfg, err
 }
 

+ 1 - 0
netclient/daemon/common.go

@@ -13,6 +13,7 @@ import (
 
 // InstallDaemon - Calls the correct function to install the netclient as a daemon service on the given operating system.
 func InstallDaemon() error {
+
 	os := runtime.GOOS
 	var err error
 

+ 39 - 24
netclient/functions/common.go

@@ -30,6 +30,16 @@ const LINUX_APP_DATA_PATH = "/etc/netmaker"
 // HTTP_TIMEOUT - timeout in seconds for http requests
 const HTTP_TIMEOUT = 30
 
+// HTTPClient - http client to be reused by all
+var HTTPClient http.Client
+
+// SetHTTPClient -sets http client with sane default
+func SetHTTPClient() {
+	HTTPClient = http.Client{
+		Timeout: HTTP_TIMEOUT * time.Second,
+	}
+}
+
 // ListPorts - lists ports of WireGuard devices
 func ListPorts() error {
 	wgclient, err := wgctrl.New()
@@ -193,7 +203,6 @@ func LeaveNetwork(network string) error {
 			if wgErr == nil && removeIface != "" {
 				removeIface = macIface
 			}
-			wgErr = nil
 		}
 		dev, devErr := wgClient.Device(removeIface)
 		if devErr == nil {
@@ -224,18 +233,24 @@ func DeleteInterface(ifacename string, postdown string) error {
 
 // WipeLocal - wipes local instance
 func WipeLocal(network string) error {
-	cfg, err := config.ReadConfig(network)
-	if err != nil {
-		return err
+	var ifacename string
+
+	if network == "" {
+		return errors.New("no network provided")
 	}
-	nodecfg := cfg.Node
-	ifacename := nodecfg.Interface
-	if ifacename != "" {
-		if err = wireguard.RemoveConf(ifacename, true); err == nil {
-			logger.Log(1, "network:", nodecfg.Network, "removed WireGuard interface: ", ifacename)
-		} else if strings.Contains(err.Error(), "does not exist") {
-			err = nil
+	cfg, err := config.ReadConfig(network)
+	if err == nil {
+		nodecfg := cfg.Node
+		ifacename = nodecfg.Interface
+		if ifacename != "" {
+			if err = wireguard.RemoveConf(ifacename, true); err == nil {
+				logger.Log(1, "network:", nodecfg.Network, "removed WireGuard interface: ", ifacename)
+			} else if strings.Contains(err.Error(), "does not exist") {
+				err = nil
+			}
 		}
+	} else {
+		logger.Log(0, "failed to read "+network+" config: ", err.Error())
 	}
 
 	home := ncutils.GetNetclientPathSpecific()
@@ -281,17 +296,20 @@ func WipeLocal(network string) error {
 			log.Println(err.Error())
 		}
 	}
-	if ncutils.FileExists(home + ifacename + ".conf") {
-		err = os.Remove(home + ifacename + ".conf")
+	if ifacename != "" {
+		if ncutils.FileExists(home + ifacename + ".conf") {
+			err = os.Remove(home + ifacename + ".conf")
+			if err != nil {
+				log.Println("error removing .conf:")
+				log.Println(err.Error())
+			}
+		}
+		err = removeHostDNS(ifacename, ncutils.IsWindows())
 		if err != nil {
-			log.Println("error removing .conf:")
-			log.Println(err.Error())
+			logger.Log(0, "failed to delete dns entries for", ifacename, err.Error())
 		}
 	}
-	err = removeHostDNS(ifacename, ncutils.IsWindows())
-	if err != nil {
-		logger.Log(0, "failed to delete dns entries for", ifacename, err.Error())
-	}
+
 	return err
 }
 
@@ -300,7 +318,7 @@ func GetNetmakerPath() string {
 	return LINUX_APP_DATA_PATH
 }
 
-//API function to interact with netmaker api endpoints. response from endpoint is returned
+// API function to interact with netmaker api endpoints. response from endpoint is returned
 func API(data any, method, url, authorization string) (*http.Response, error) {
 	var request *http.Request
 	var err error
@@ -323,10 +341,7 @@ func API(data any, method, url, authorization string) (*http.Response, error) {
 	if authorization != "" {
 		request.Header.Set("authorization", "Bearer "+authorization)
 	}
-	client := http.Client{
-		Timeout: HTTP_TIMEOUT * time.Second,
-	}
-	return client.Do(request)
+	return HTTPClient.Do(request)
 }
 
 // Authenticate authenticates with api to permit subsequent interactions with the api

+ 9 - 0
netclient/functions/daemon.go

@@ -22,6 +22,7 @@ import (
 	"github.com/gravitl/netmaker/netclient/auth"
 	"github.com/gravitl/netmaker/netclient/config"
 	"github.com/gravitl/netmaker/netclient/daemon"
+	"github.com/gravitl/netmaker/netclient/global_settings"
 	"github.com/gravitl/netmaker/netclient/local"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/wireguard"
@@ -96,11 +97,19 @@ func startGoRoutines(wg *sync.WaitGroup) context.CancelFunc {
 		if err := wireguard.ApplyConf(&cfg.Node, cfg.Node.Interface, ncutils.GetNetclientPathSpecific()+cfg.Node.Interface+".conf"); err != nil {
 			logger.Log(0, "failed to start ", cfg.Node.Interface, "wg interface", err.Error())
 		}
+		if cfg.PublicIPService != "" {
+			global_settings.PublicIPServices[network] = cfg.PublicIPService
+		}
+
 		server := cfg.Server.Server
 		if !serverSet[server] {
 			// == subscribe to all nodes for each on machine ==
 			serverSet[server] = true
 			logger.Log(1, "started daemon for server ", server)
+			err := local.SetNetmakerDomainRoute(cfg.Server.API)
+			if err != nil {
+				logger.Log(0, "error setting route for netmaker: "+err.Error())
+			}
 			wg.Add(1)
 			go messageQueue(ctx, wg, &cfg)
 		}

+ 22 - 4
netclient/functions/join.go

@@ -85,7 +85,7 @@ func JoinNetwork(cfg *config.ClientConfig, privateKey string) error {
 		if cfg.Node.IsLocal == "yes" && cfg.Node.LocalAddress != "" {
 			cfg.Node.Endpoint = cfg.Node.LocalAddress
 		} else {
-			cfg.Node.Endpoint, err = ncutils.GetPublicIP()
+			cfg.Node.Endpoint, err = ncutils.GetPublicIP(cfg.Server.API)
 		}
 		if err != nil || cfg.Node.Endpoint == "" {
 			logger.Log(0, "network:", cfg.Network, "error setting cfg.Node.Endpoint.")
@@ -114,7 +114,19 @@ func JoinNetwork(cfg *config.ClientConfig, privateKey string) error {
 
 	if ncutils.IsFreeBSD() {
 		cfg.Node.UDPHolePunch = "no"
+		cfg.Node.FirewallInUse = models.FIREWALL_IPTABLES // nftables not supported by FreeBSD
 	}
+
+	if cfg.Node.FirewallInUse == "" {
+		if ncutils.IsNFTablesPresent() {
+			cfg.Node.FirewallInUse = models.FIREWALL_NFTABLES
+		} else if ncutils.IsIPTablesPresent() {
+			cfg.Node.FirewallInUse = models.FIREWALL_IPTABLES
+		} else {
+			cfg.Node.FirewallInUse = models.FIREWALL_NONE
+		}
+	}
+
 	// make sure name is appropriate, if not, give blank name
 	cfg.Node.Name = formatName(cfg.Node)
 	cfg.Node.OS = runtime.GOOS
@@ -188,15 +200,21 @@ func JoinNetwork(cfg *config.ClientConfig, privateKey string) error {
 	if err = config.SaveBackup(node.Network); err != nil {
 		logger.Log(0, "network:", node.Network, "failed to make backup, node will not auto restore if config is corrupted")
 	}
-	logger.Log(0, "starting wireguard")
-	err = wireguard.InitWireguard(&node, privateKey, nodeGET.Peers[:], false)
+
+	err = local.SetNetmakerDomainRoute(cfg.Server.API)
 	if err != nil {
-		return err
+		logger.Log(0, "error setting route for netmaker: "+err.Error())
 	}
 	cfg.Node = node
 	if err := Register(cfg); err != nil {
 		return err
 	}
+
+	logger.Log(0, "starting wireguard")
+	err = wireguard.InitWireguard(&node, privateKey, nodeGET.Peers[:], false)
+	if err != nil {
+		return err
+	}
 	if cfg.Server.Server == "" {
 		return errors.New("did not receive broker address from registration")
 	}

+ 21 - 2
netclient/functions/mqhandlers.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 	"os"
 	"runtime"
 	"strings"
@@ -206,13 +207,31 @@ func UpdatePeers(client mqtt.Client, msg mqtt.Message) {
 		cfg.Server.Version = peerUpdate.ServerVersion
 		config.Write(&cfg, cfg.Network)
 	}
-
 	file := ncutils.GetNetclientPathSpecific() + cfg.Node.Interface + ".conf"
-	err = wireguard.UpdateWgPeers(file, peerUpdate.Peers)
+	internetGateway, err := wireguard.UpdateWgPeers(file, peerUpdate.Peers)
 	if err != nil {
 		logger.Log(0, "error updating wireguard peers"+err.Error())
 		return
 	}
+	//check if internet gateway has changed
+	oldGateway, err := net.ResolveUDPAddr("udp", cfg.Node.InternetGateway)
+	if err != nil || (oldGateway == &net.UDPAddr{}) {
+		oldGateway = nil
+	}
+	if (internetGateway == nil && oldGateway != nil) || (internetGateway != nil && internetGateway != oldGateway) {
+		cfg.Node.InternetGateway = internetGateway.String()
+		if err := config.ModNodeConfig(&cfg.Node); err != nil {
+			logger.Log(0, "failed to save internet gateway", err.Error())
+		}
+		if ncutils.IsWindows() {
+			wireguard.RemoveConfGraceful(cfg.Node.Interface)
+		}
+		if err := wireguard.ApplyConf(&cfg.Node, cfg.Node.Interface, file); err != nil {
+			logger.Log(0, "error applying internet gateway", err.Error())
+		}
+		UpdateLocalListenPort(&cfg)
+		return
+	}
 	queryAddr := cfg.Node.PrimaryAddress()
 
 	//err = wireguard.SyncWGQuickConf(cfg.Node.Interface, file)

+ 13 - 1
netclient/functions/mqpublish.go

@@ -13,6 +13,7 @@ import (
 
 	"github.com/cloverstd/tcping/ping"
 	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/auth"
 	"github.com/gravitl/netmaker/netclient/config"
 	"github.com/gravitl/netmaker/netclient/ncutils"
@@ -43,8 +44,19 @@ func checkin() {
 		var nodeCfg config.ClientConfig
 		nodeCfg.Network = network
 		nodeCfg.ReadConfig()
+		// check for nftables present if on Linux
+		if ncutils.IsLinux() {
+			if ncutils.IsNFTablesPresent() {
+				nodeCfg.Node.FirewallInUse = models.FIREWALL_NFTABLES
+			} else {
+				nodeCfg.Node.FirewallInUse = models.FIREWALL_IPTABLES
+			}
+		} else {
+			// defaults to iptables for now, may need another default for non-Linux OSes
+			nodeCfg.Node.FirewallInUse = models.FIREWALL_IPTABLES
+		}
 		if nodeCfg.Node.IsStatic != "yes" {
-			extIP, err := ncutils.GetPublicIP()
+			extIP, err := ncutils.GetPublicIP(nodeCfg.Server.API)
 			if err != nil {
 				logger.Log(1, "error encountered checking public ip addresses: ", err.Error())
 			}

+ 1 - 0
netclient/functions/upgrades/upgrades.go

@@ -3,6 +3,7 @@ package upgrades
 func init() {
 	addUpgrades([]UpgradeInfo{
 		upgrade0145,
+		upgrade0146,
 	})
 }
 

+ 6 - 0
netclient/global_settings/globalsettings.go

@@ -0,0 +1,6 @@
+package global_settings
+
+// globalsettings - settings that are global in nature.  Avoids circular dependencies between config loading and usage.
+
+// PublicIPServices - the list of user-specified IP services to use to obtain the node's public IP
+var PublicIPServices map[string]string = make(map[string]string)

+ 92 - 0
netclient/local/routes.go

@@ -1,7 +1,9 @@
 package local
 
 import (
+	"fmt"
 	"net"
+	"strings"
 
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/netclient/ncutils"
@@ -12,6 +14,17 @@ import (
 
 // SetPeerRoutes - sets/removes ip routes for each peer on a network
 func SetPeerRoutes(iface string, oldPeers map[string]bool, newPeers []wgtypes.PeerConfig) {
+
+	// get the default route
+	var hasRoute bool
+	gwIP, gwIface, err := GetDefaultRoute()
+	if err != nil {
+		logger.Log(0, "error getting default route:", err.Error())
+	}
+	if gwIP != "" && gwIface != "" && err == nil {
+		hasRoute = true
+	}
+
 	// traverse through all recieved peers
 	for _, peer := range newPeers {
 		for _, allowedIP := range peer.AllowedIPs {
@@ -23,6 +36,16 @@ func SetPeerRoutes(iface string, oldPeers map[string]bool, newPeers []wgtypes.Pe
 				delete(oldPeers, allowedIP.String())
 			}
 		}
+		if peer.Endpoint == nil {
+			continue
+		}
+		if hasRoute && !ncutils.IpIsPrivate(peer.Endpoint.IP) {
+			ipNet, err := ncutils.GetIPNetFromString(peer.Endpoint.IP.String())
+			if err != nil {
+				logger.Log(0, "error parsing ip:", err.Error())
+			}
+			SetExplicitRoute(gwIface, &ipNet, gwIP)
+		}
 	}
 	// traverse through all remaining existing peers
 	for i := range oldPeers {
@@ -37,19 +60,59 @@ func SetPeerRoutes(iface string, oldPeers map[string]bool, newPeers []wgtypes.Pe
 
 // SetCurrentPeerRoutes - sets all the current peers
 func SetCurrentPeerRoutes(iface, currentAddr string, peers []wgtypes.PeerConfig) {
+
+	// get the default route
+	var hasRoute bool
+	gwIP, gwIface, err := GetDefaultRoute()
+	if err != nil {
+		logger.Log(0, "error getting default route:", err.Error())
+	}
+	if gwIP != "" && gwIface != "" && err == nil {
+		hasRoute = true
+	}
+
+	// traverse through all recieved peers
 	for _, peer := range peers {
 		for _, allowedIP := range peer.AllowedIPs {
 			setRoute(iface, &allowedIP, currentAddr)
 		}
+		if peer.Endpoint == nil {
+			continue
+		}
+		if hasRoute && !ncutils.IpIsPrivate(peer.Endpoint.IP) {
+			ipNet, err := ncutils.GetIPNetFromString(peer.Endpoint.IP.String())
+			if err != nil {
+				logger.Log(0, "error parsing ip:", err.Error())
+			}
+			SetExplicitRoute(gwIface, &ipNet, gwIP)
+		}
 	}
+
 }
 
 // FlushPeerRoutes - removes all current peer routes
 func FlushPeerRoutes(iface, currentAddr string, peers []wgtypes.Peer) {
+	// get the default route
+	var hasRoute bool
+	gwIP, gwIface, err := GetDefaultRoute()
+	if err != nil {
+		logger.Log(0, "error getting default route:", err.Error())
+	}
+	if gwIP != "" && gwIface != "" && err == nil {
+		hasRoute = true
+	}
+
 	for _, peer := range peers {
 		for _, allowedIP := range peer.AllowedIPs {
 			deleteRoute(iface, &allowedIP, currentAddr)
 		}
+		if hasRoute && !ncutils.IpIsPrivate(peer.Endpoint.IP) {
+			ipNet, err := ncutils.GetIPNetFromString(peer.Endpoint.IP.String())
+			if err != nil {
+				logger.Log(0, "error parsing ip:", err.Error())
+			}
+			deleteRoute(gwIface, &ipNet, gwIP)
+		}
 	}
 }
 
@@ -62,3 +125,32 @@ func SetCIDRRoute(iface, currentAddr string, cidr *net.IPNet) {
 func RemoveCIDRRoute(iface, currentAddr string, cidr *net.IPNet) {
 	removeCidr(iface, cidr, currentAddr)
 }
+
+// SetNetmakerDomainRoute - sets explicit route over Gateway for a given DNS name
+func SetNetmakerDomainRoute(domainRaw string) error {
+	parts := strings.Split(domainRaw, ":")
+	hostname := parts[0]
+	var address net.IPNet
+
+	gwIP, gwIface, err := GetDefaultRoute()
+	if err != nil {
+		return fmt.Errorf("error getting default route: %w", err)
+	}
+
+	ips, err := net.LookupIP(hostname)
+	if err != nil {
+		return err
+	}
+	for _, ip := range ips {
+		if ipv4 := ip.To4(); ipv4 != nil {
+			address, err = ncutils.GetIPNetFromString(ipv4.String())
+			if err == nil {
+				break
+			}
+		}
+	}
+	if err != nil || address.IP == nil {
+		return fmt.Errorf("address not found")
+	}
+	return SetExplicitRoute(gwIface, &address, gwIP)
+}

+ 40 - 3
netclient/local/routes_darwin.go

@@ -1,14 +1,46 @@
 package local
 
 import (
-	"net"
-	"strings"
-
+	"fmt"
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/netclient/ncutils"
+	"net"
+	"regexp"
+	"strings"
 )
 
+// GetDefaultRoute - Gets the default route (ip and interface) on a mac machine
+func GetDefaultRoute() (string, string, error) {
+	var ipaddr string
+	var iface string
+	var err error
+	var outLine string
+	output, err := ncutils.RunCmd("netstat -nr", false)
+	for _, line := range strings.Split(strings.TrimSuffix(output, "\n"), "\n") {
+		if strings.Contains(line, "default") {
+			outLine = line
+			break
+		}
+	}
+	space := regexp.MustCompile(`\s+`)
+	outFormatted := space.ReplaceAllString(outLine, " ")
+	if err != nil {
+		return ipaddr, iface, err
+	}
+	outputSlice := strings.Split(string(outFormatted), " ")
+	if !strings.Contains(outputSlice[0], "default") {
+		return ipaddr, iface, fmt.Errorf("could not find default gateway")
+	}
+	ipaddr = outputSlice[1]
+	if err = ncutils.CheckIPAddress(ipaddr); err != nil {
+		return ipaddr, iface, err
+	}
+	iface = outputSlice[3]
+
+	return ipaddr, iface, err
+}
+
 // route -n add -net 10.0.0.0/8 192.168.0.254
 // networksetup -setadditionalroutes Ethernet 192.168.1.0 255.255.255.0 10.0.0.2 persistent
 func setRoute(iface string, addr *net.IPNet, address string) error {
@@ -28,6 +60,11 @@ func setRoute(iface string, addr *net.IPNet, address string) error {
 	return err
 }
 
+// SetExplicitRoute - sets route via explicit ip address
+func SetExplicitRoute(iface string, destination *net.IPNet, gateway string) error {
+	return setRoute(iface, destination, gateway)
+}
+
 func deleteRoute(iface string, addr *net.IPNet, address string) error {
 	var err error
 	_, err = ncutils.RunCmd("route -q -n delete "+addr.String(), false)

+ 35 - 2
netclient/local/routes_freebsd.go

@@ -1,16 +1,49 @@
 package local
 
 import (
+	"fmt"
 	"net"
+	"strings"
 
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 )
 
-func setRoute(iface string, addr *net.IPNet, address string) error {
+// GetDefaultRoute - Gets the default route (ip and interface) on a freebsd machine
+func GetDefaultRoute() (string, string, error) {
+	var ipaddr string
+	var iface string
 	var err error
-	_, _ = ncutils.RunCmd("route add -net "+addr.String()+" -interface "+iface, false)
+
+	output, err := ncutils.RunCmd("route show default", true)
+	if err != nil {
+		return ipaddr, iface, err
+	}
+	outFormatted := strings.ReplaceAll(output, "\n", "")
+	if !strings.Contains(outFormatted, "default") && !strings.Contains(outFormatted, "interface:") {
+		return ipaddr, iface, fmt.Errorf("could not find default gateway")
+	}
+	outputSlice := strings.Split(string(outFormatted), " ")
+	for i, outString := range outputSlice {
+		if outString == "gateway:" {
+			ipaddr = outputSlice[i+1]
+		}
+		if outString == "interface:" {
+			iface = outputSlice[i+1]
+		}
+	}
+	return ipaddr, iface, err
+}
+
+func setRoute(iface string, addr *net.IPNet, address string) error {
+	_, err := ncutils.RunCmd("route add -net "+addr.String()+" -interface "+iface, false)
+	return err
+}
+
+// SetExplicitRoute - sets route via explicit ip address
+func SetExplicitRoute(iface string, destination *net.IPNet, gateway string) error {
+	_, err := ncutils.RunCmd("route add "+destination.String()+" "+gateway, false)
 	return err
 }
 

+ 30 - 0
netclient/local/routes_linux.go

@@ -12,6 +12,30 @@ import (
 	"github.com/gravitl/netmaker/netclient/ncutils"
 )
 
+// GetDefaultRoute - Gets the default route (ip and interface) on a linux machine
+func GetDefaultRoute() (string, string, error) {
+	var ipaddr string
+	var iface string
+	var err error
+	output, err := ncutils.RunCmd("ip route show default", false)
+	if err != nil {
+		return ipaddr, iface, err
+	}
+	outputSlice := strings.Split(output, " ")
+	if !strings.Contains(outputSlice[0], "default") {
+		return ipaddr, iface, fmt.Errorf("could not find default gateway")
+	}
+	for i, outString := range outputSlice {
+		if outString == "via" {
+			ipaddr = outputSlice[i+1]
+		}
+		if outString == "dev" {
+			iface = outputSlice[i+1]
+		}
+	}
+	return ipaddr, iface, err
+}
+
 func setRoute(iface string, addr *net.IPNet, address string) error {
 	out, err := ncutils.RunCmd(fmt.Sprintf("ip route get %s", addr.IP.String()), false)
 	if err != nil || !strings.Contains(out, iface) {
@@ -20,6 +44,12 @@ func setRoute(iface string, addr *net.IPNet, address string) error {
 	return err
 }
 
+// SetExplicitRoute - sets route via explicit ip address
+func SetExplicitRoute(iface string, destination *net.IPNet, gateway string) error {
+	_, err := ncutils.RunCmd(fmt.Sprintf("ip route add %s via %s dev %s", destination.String(), gateway, iface), false)
+	return err
+}
+
 func deleteRoute(iface string, addr *net.IPNet, address string) error {
 	var err error
 	out, _ := ncutils.RunCmd(fmt.Sprintf("ip route get %s", addr.IP.String()), false)

+ 45 - 0
netclient/local/routes_windows.go

@@ -1,12 +1,48 @@
 package local
 
 import (
+	"fmt"
 	"net"
+	"regexp"
+	"strings"
 	"time"
 
 	"github.com/gravitl/netmaker/netclient/ncutils"
 )
 
+// GetDefaultRoute - Gets the default route (ip and interface) on a windows machine
+func GetDefaultRoute() (string, string, error) {
+	var ipaddr string
+	var iface string
+	var err error
+	var outLine string
+	output, err := ncutils.RunCmd("netstat -rn", false)
+	if err != nil {
+		return ipaddr, iface, err
+	}
+	var startLook bool
+	for _, line := range strings.Split(strings.TrimSuffix(output, "\n"), "\n") {
+		if strings.Contains(line, "Active Routes:") {
+			startLook = true
+		}
+		if startLook && strings.Contains(line, "0.0.0.0") {
+			outLine = line
+			break
+		}
+	}
+	if outLine == "" {
+		return ipaddr, iface, fmt.Errorf("could not find default gateway")
+	}
+	space := regexp.MustCompile(`\s+`)
+	outputSlice := strings.Split(strings.TrimSpace(space.ReplaceAllString(outLine, " ")), " ")
+	ipaddr = outputSlice[len(outputSlice)-3]
+	if err = ncutils.CheckIPAddress(ipaddr); err != nil {
+		return ipaddr, iface, fmt.Errorf("invalid output for ip address check: " + err.Error())
+	}
+	iface = "irrelevant"
+	return ipaddr, iface, err
+}
+
 func setRoute(iface string, addr *net.IPNet, address string) error {
 	var err error
 	_, err = ncutils.RunCmd("route ADD "+addr.String()+" "+address, false)
@@ -15,6 +51,15 @@ func setRoute(iface string, addr *net.IPNet, address string) error {
 	return err
 }
 
+// SetExplicitRoute - sets route via explicit ip address
+func SetExplicitRoute(iface string, destination *net.IPNet, gateway string) error {
+	var err error
+	_, err = ncutils.RunCmd("route ADD "+destination.String()+" "+gateway, false)
+	time.Sleep(time.Second >> 2)
+	ncutils.RunCmd("route CHANGE "+destination.IP.String()+" MASK "+destination.Mask.String()+" "+gateway, false)
+	return err
+}
+
 func deleteRoute(iface string, addr *net.IPNet, address string) error {
 	var err error
 	_, err = ncutils.RunCmd("route DELETE "+addr.IP.String()+" MASK "+addr.Mask.String()+" "+address, false)

+ 5 - 0
netclient/main.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/gravitl/netmaker/netclient/cli_options"
 	"github.com/gravitl/netmaker/netclient/config"
+	"github.com/gravitl/netmaker/netclient/functions"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/ncwindows"
 	"github.com/urfave/cli/v2"
@@ -29,12 +30,16 @@ func main() {
 	app.UsageText = "netclient [global options] command [command options] [arguments...]. Adjust verbosity of given command with -v, -vv or -vvv (max)."
 
 	setGarbageCollection()
+	functions.SetHTTPClient()
 
 	if ncutils.IsWindows() {
 		ncwindows.InitWindows()
 	} else {
 		ncutils.CheckUID()
 		ncutils.CheckWG()
+		if ncutils.IsLinux() {
+			ncutils.CheckFirewall()
+		}
 	}
 
 	if len(os.Args) <= 1 && config.GuiActive {

+ 4 - 0
netclient/ncutils/iface.go

@@ -90,3 +90,7 @@ func IfaceExists(ifacename string) bool {
 	}
 	return false
 }
+
+func IpIsPrivate(ipnet net.IP) bool {
+	return ipnet.IsPrivate() || ipnet.IsLoopback()
+}

+ 53 - 7
netclient/ncutils/netclientutils.go

@@ -22,6 +22,7 @@ import (
 	"github.com/c-robinson/iplib"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/netclient/global_settings"
 )
 
 var (
@@ -41,15 +42,12 @@ const NO_DB_RECORDS = "could not find any records"
 // LINUX_APP_DATA_PATH - linux path
 const LINUX_APP_DATA_PATH = "/etc/netclient"
 
-// MAC_APP_DATA_PATH - linux path
+// MAC_APP_DATA_PATH - mac path
 const MAC_APP_DATA_PATH = "/Applications/Netclient"
 
 // WINDOWS_APP_DATA_PATH - windows path
 const WINDOWS_APP_DATA_PATH = "C:\\Program Files (x86)\\Netclient"
 
-// WINDOWS_APP_DATA_PATH - windows path
-//const WINDOWS_WG_DPAPI_PATH = "C:\\Program Files\\WireGuard\\Data\\Configurations"
-
 // WINDOWS_SVC_NAME - service name
 const WINDOWS_SVC_NAME = "netclient"
 
@@ -89,7 +87,7 @@ func IsLinux() bool {
 	return runtime.GOOS == "linux"
 }
 
-// IsLinux - checks if is linux
+// IsFreeBSD - checks if is freebsd
 func IsFreeBSD() bool {
 	return runtime.GOOS == "freebsd"
 }
@@ -109,6 +107,28 @@ func GetWireGuard() string {
 	return "wg"
 }
 
+// IsNFTablesPresent - returns true if nftables is present, false otherwise.
+// Does not consider OS, up to the caller to determine if the OS supports nftables/whether this check is valid.
+func IsNFTablesPresent() bool {
+	found := false
+	_, err := exec.LookPath("nft")
+	if err == nil {
+		found = true
+	}
+	return found
+}
+
+// IsIPTablesPresent - returns true if iptables is present, false otherwise
+// Does not consider OS, up to the caller to determine if the OS supports iptables/whether this check is valid.
+func IsIPTablesPresent() bool {
+	found := false
+	_, err := exec.LookPath("iptables")
+	if err == nil {
+		found = true
+	}
+	return found
+}
+
 // IsKernel - checks if running kernel WireGuard
 func IsKernel() bool {
 	//TODO
@@ -126,9 +146,21 @@ func IsEmptyRecord(err error) bool {
 }
 
 // GetPublicIP - gets public ip
-func GetPublicIP() (string, error) {
+func GetPublicIP(api string) (string, error) {
 
 	iplist := []string{"https://ip.client.gravitl.com", "https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
+
+	for network, ipService := range global_settings.PublicIPServices {
+		logger.Log(3, "User provided public IP service defined for network", network, "is", ipService)
+
+		// prepend the user-specified service so it's checked first
+		iplist = append([]string{ipService}, iplist...)
+	}
+	if api != "" {
+		api = "https://" + api + "/api/getip"
+		iplist = append([]string{api}, iplist...)
+	}
+
 	endpoint := ""
 	var err error
 	for _, ipserver := range iplist {
@@ -218,7 +250,7 @@ func GetLocalIP(localrange string) (string, error) {
 	return local, nil
 }
 
-//GetNetworkIPMask - Pulls the netmask out of the network
+// GetNetworkIPMask - Pulls the netmask out of the network
 func GetNetworkIPMask(networkstring string) (string, string, error) {
 	ip, ipnet, err := net.ParseCIDR(networkstring)
 	if err != nil {
@@ -321,6 +353,13 @@ func GetNetclientPathSpecific() string {
 	}
 }
 
+func CheckIPAddress(ip string) error {
+	if net.ParseIP(ip) == nil {
+		return fmt.Errorf("ip address %s is invalid", ip)
+	}
+	return nil
+}
+
 // GetNewIface - Gets the name of the real interface created on Mac
 func GetNewIface(dir string) (string, error) {
 	files, _ := os.ReadDir(dir)
@@ -492,6 +531,13 @@ func CheckUID() {
 	}
 }
 
+// CheckFirewall - checks if iptables of nft install, if not exit
+func CheckFirewall() {
+	if !IsIPTablesPresent() && !IsNFTablesPresent() {
+		log.Fatal("neither iptables nor nft is installed - please install one or the other and try again")
+	}
+}
+
 // CheckWG - Checks if WireGuard is installed. If not, exit
 func CheckWG() {
 	uspace := GetWireGuard()

+ 1 - 1
netclient/netclient.exe.manifest.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
     <assemblyIdentity
-            version="0.14.6.0"
+            version="0.15.0.0"
             processorArchitecture="*"
             name="netclient.exe"
             type="win32"

+ 5 - 5
netclient/versioninfo.json

@@ -2,14 +2,14 @@
     "FixedFileInfo": {
         "FileVersion": {
             "Major": 0,
-            "Minor": 14,
-            "Patch": 6,
+            "Minor": 15,
+            "Patch": 0,
             "Build": 0
         },
         "ProductVersion": {
             "Major": 0,
-            "Minor": 14,
-            "Patch": 6,
+            "Minor": 15,
+            "Patch": 0,
             "Build": 0
         },
         "FileFlagsMask": "3f",
@@ -29,7 +29,7 @@
         "OriginalFilename": "",
         "PrivateBuild": "",
         "ProductName": "Netclient",
-        "ProductVersion": "v0.14.6.0",
+        "ProductVersion": "v0.15.0.0",
         "SpecialBuild": ""
     },
     "VarFileInfo": {

+ 41 - 15
netclient/wireguard/common.go

@@ -2,7 +2,6 @@ package wireguard
 
 import (
 	"fmt"
-	"log"
 	"net"
 	"runtime"
 	"strconv"
@@ -29,7 +28,6 @@ func SetPeers(iface string, node *models.Node, peers []wgtypes.PeerConfig) error
 	var devicePeers []wgtypes.Peer
 	var keepalive = node.PersistentKeepalive
 	var oldPeerAllowedIps = make(map[string]bool, len(peers))
-
 	var err error
 	devicePeers, err = GetDevicePeers(iface)
 	if err != nil {
@@ -52,7 +50,7 @@ func SetPeers(iface string, node *models.Node, peers []wgtypes.PeerConfig) error
 				currentPeer.PublicKey.String() != peer.PublicKey.String() {
 				_, err := ncutils.RunCmd("wg set "+iface+" peer "+currentPeer.PublicKey.String()+" remove", true)
 				if err != nil {
-					log.Println("error removing peer", peer.Endpoint.String())
+					logger.Log(0, "error removing peer", peer.Endpoint.String())
 				}
 			}
 		}
@@ -82,10 +80,9 @@ func SetPeers(iface string, node *models.Node, peers []wgtypes.PeerConfig) error
 				" allowed-ips "+allowedips, true)
 		}
 		if err != nil {
-			log.Println("error setting peer", peer.PublicKey.String())
+			logger.Log(0, "error setting peer", peer.PublicKey.String())
 		}
 	}
-
 	if len(devicePeers) > 0 {
 		for _, currentPeer := range devicePeers {
 			shouldDelete := true
@@ -104,7 +101,7 @@ func SetPeers(iface string, node *models.Node, peers []wgtypes.PeerConfig) error
 				if shouldDelete {
 					output, err := ncutils.RunCmd("wg set "+iface+" peer "+currentPeer.PublicKey.String()+" remove", true)
 					if err != nil {
-						log.Println(output, "error removing peer", currentPeer.PublicKey.String())
+						logger.Log(0, output, "error removing peer", currentPeer.PublicKey.String())
 					}
 				}
 				for _, ip := range currentPeer.AllowedIPs {
@@ -319,7 +316,6 @@ func ApplyConf(node *models.Node, ifacename string, confPath string) error {
 }
 
 // WriteWgConfig - creates a wireguard config file
-//func WriteWgConfig(cfg *config.ClientConfig, privateKey string, peers []wgtypes.PeerConfig) error {
 func WriteWgConfig(node *models.Node, privateKey string, peers []wgtypes.PeerConfig) error {
 	options := ini.LoadOptions{
 		AllowNonUniqueSections: true,
@@ -342,11 +338,24 @@ func WriteWgConfig(node *models.Node, privateKey string, peers []wgtypes.PeerCon
 	//if node.DNSOn == "yes" {
 	//	wireguard.Section(section_interface).Key("DNS").SetValue(cfg.Server.CoreDNSAddr)
 	//}
+	//need to split postup/postdown because ini lib adds a ` and the ` breaks freebsd
 	if node.PostUp != "" {
-		wireguard.Section(section_interface).Key("PostUp").SetValue(node.PostUp)
+		parts := strings.Split(node.PostUp, " ; ")
+		for i, part := range parts {
+			if i == 0 {
+				wireguard.Section(section_interface).Key("PostUp").SetValue(part)
+			}
+			wireguard.Section(section_interface).Key("PostUp").AddShadow(part)
+		}
 	}
 	if node.PostDown != "" {
-		wireguard.Section(section_interface).Key("PostDown").SetValue(node.PostDown)
+		parts := strings.Split(node.PostDown, " ; ")
+		for i, part := range parts {
+			if i == 0 {
+				wireguard.Section(section_interface).Key("PostDown").SetValue(part)
+			}
+			wireguard.Section(section_interface).Key("PostDown").AddShadow(part)
+		}
 	}
 	if node.MTU != 0 {
 		wireguard.Section(section_interface).Key("MTU").SetValue(strconv.FormatInt(int64(node.MTU), 10))
@@ -382,14 +391,15 @@ func WriteWgConfig(node *models.Node, privateKey string, peers []wgtypes.PeerCon
 }
 
 // UpdateWgPeers - updates the peers of a network
-func UpdateWgPeers(file string, peers []wgtypes.PeerConfig) error {
+func UpdateWgPeers(file string, peers []wgtypes.PeerConfig) (*net.UDPAddr, error) {
+	var internetGateway *net.UDPAddr
 	options := ini.LoadOptions{
 		AllowNonUniqueSections: true,
 		AllowShadows:           true,
 	}
 	wireguard, err := ini.LoadSources(options, file)
 	if err != nil {
-		return err
+		return internetGateway, err
 	}
 	//delete the peers sections as they are going to be replaced
 	wireguard.DeleteSection(section_peers)
@@ -408,6 +418,9 @@ func UpdateWgPeers(file string, peers []wgtypes.PeerConfig) error {
 				}
 			}
 			wireguard.SectionWithIndex(section_peers, i).Key("AllowedIps").SetValue(allowedIPs)
+			if strings.Contains(allowedIPs, "0.0.0.0/0") || strings.Contains(allowedIPs, "::/0") {
+				internetGateway = peer.Endpoint
+			}
 		}
 		if peer.Endpoint != nil {
 			wireguard.SectionWithIndex(section_peers, i).Key("Endpoint").SetValue(peer.Endpoint.String())
@@ -417,9 +430,9 @@ func UpdateWgPeers(file string, peers []wgtypes.PeerConfig) error {
 		}
 	}
 	if err := wireguard.SaveTo(file); err != nil {
-		return err
+		return internetGateway, err
 	}
-	return nil
+	return internetGateway, nil
 }
 
 // UpdateWgInterface - updates the interface section of a wireguard config file
@@ -448,11 +461,24 @@ func UpdateWgInterface(file, privateKey, nameserver string, node models.Node) er
 	//if node.DNSOn == "yes" {
 	//	wireguard.Section(section_interface).Key("DNS").SetValue(nameserver)
 	//}
+	//need to split postup/postdown because ini lib adds a quotes which breaks freebsd
 	if node.PostUp != "" {
-		wireguard.Section(section_interface).Key("PostUp").SetValue(node.PostUp)
+		parts := strings.Split(node.PostUp, " ; ")
+		for i, part := range parts {
+			if i == 0 {
+				wireguard.Section(section_interface).Key("PostUp").SetValue(part)
+			}
+			wireguard.Section(section_interface).Key("PostUp").AddShadow(part)
+		}
 	}
 	if node.PostDown != "" {
-		wireguard.Section(section_interface).Key("PostDown").SetValue(node.PostDown)
+		parts := strings.Split(node.PostDown, " ; ")
+		for i, part := range parts {
+			if i == 0 {
+				wireguard.Section(section_interface).Key("PostDown").SetValue(part)
+			}
+			wireguard.Section(section_interface).Key("PostDown").AddShadow(part)
+		}
 	}
 	if node.MTU != 0 {
 		wireguard.Section(section_interface).Key("MTU").SetValue(strconv.FormatInt(int64(node.MTU), 10))

+ 1 - 0
netclient/wireguard/mac.go

@@ -49,6 +49,7 @@ func WgQuickUpMac(node *models.Node, iface string, confPath string) error {
 		return err
 	}
 	time.Sleep(time.Second / 2)
+
 	err = setConfig(realIface, confPath)
 	if err != nil {
 		logger.Log(1, "error setting config for ", realIface)

+ 0 - 35
scripts/netmaker-server.sh

@@ -1,35 +0,0 @@
-#!/bin/sh
-set -e
-
-mkdir -p /etc/netmaker/config/environments
-wget -O /etc/netmaker/netmaker https://github.com/gravitl/netmaker/releases/download/latest/netmaker
-chmod +x /etc/netmaker/netmaker
-
-cat >/etc/netmaker/config/environments/dev.yaml<<EOL
-server:
-  host:
-  apiport: "8081"
-  masterkey: "secretkey"
-  allowedorigin: "*"
-  restbackend: true            
-  agentbackend: true
-  dnsmode: "on"
-EOL
-
-cat >/etc/systemd/system/netmaker.service<<EOL
-[Unit]
-Description=Netmaker Server
-After=network.target
-
-[Service]
-Type=simple
-Restart=on-failure
-
-WorkingDirectory=/etc/netmaker
-ExecStart=/etc/netmaker/netmaker
-
-[Install]
-WantedBy=multi-user.target
-EOL
-systemctl daemon-reload
-systemctl start netmaker.service

+ 302 - 0
scripts/nm-quick-interactive.sh

@@ -0,0 +1,302 @@
+#!/bin/bash
+
+set -e
+
+cat << "EOF"
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+                                                                                         
+ __   __     ______     ______   __    __     ______     __  __     ______     ______    
+/\ "-.\ \   /\  ___\   /\__  _\ /\ "-./  \   /\  __ \   /\ \/ /    /\  ___\   /\  == \   
+\ \ \-.  \  \ \  __\   \/_/\ \/ \ \ \-./\ \  \ \  __ \  \ \  _"-.  \ \  __\   \ \  __<   
+ \ \_\\"\_\  \ \_____\    \ \_\  \ \_\ \ \_\  \ \_\ \_\  \ \_\ \_\  \ \_____\  \ \_\ \_\ 
+  \/_/ \/_/   \/_____/     \/_/   \/_/  \/_/   \/_/\/_/   \/_/\/_/   \/_____/   \/_/ /_/ 
+                                                                                                                                                                                                 
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+EOF
+
+NETMAKER_BASE_DOMAIN=nm.$(curl -s ifconfig.me | tr . -).nip.io
+COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
+SERVER_PUBLIC_IP=$(curl -s ifconfig.me)
+MASTER_KEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
+
+echo "Default Base Domain: $NETMAKER_BASE_DOMAIN"
+echo "To Override, add a Wildcard (*.netmaker.example.com) DNS record pointing to $SERVER_PUBLIC_IP"
+echo "Or, add three DNS records pointing to $SERVER_PUBLIC_IP for the following (Replacing 'netmaker.example.com' with the domain of your choice):"
+echo "   dashboard.netmaker.example.com"
+echo "         api.netmaker.example.com"
+echo "        grpc.netmaker.example.com"
+echo "-----------------------------------------------------"
+read -p "Domain (Hit 'enter' to use $NETMAKER_BASE_DOMAIN): " domain
+read -p "Email for LetsEncrypt (Hit 'enter' to use $EMAIL): " email
+
+if [ -n "$domain" ]; then
+  NETMAKER_BASE_DOMAIN=$domain
+fi
+if [ -n "$email" ]; then
+  EMAIL=$email
+fi
+
+while true; do
+    read -p 'Configure a default network automatically? [y/n]: ' yn
+    case $yn in
+        [Yy]* ) MESH_SETUP="true"; break;;
+        [Nn]* ) MESH_SETUP="false"; break;;
+        * ) echo "Please answer yes or no.";;
+    esac
+done
+
+while true; do
+    read -p 'Configure a VPN gateway automatically? [y/n]: ' yn
+    case $yn in
+        [Yy]* ) VPN_SETUP="true"; break;;
+        [Nn]* ) VPN_SETUP="false"; break;;
+        * ) echo "Please answer yes or no.";;
+    esac
+done
+
+if [ "${VPN_SETUP}" == "true" ]; then
+while :; do
+    read -ep '# of VPN clients to configure by default: ' num_clients
+    [[ $num_clients =~ ^[[:digit:]]+$ ]] || continue
+    (( ( (num_clients=(10#$num_clients)) <= 200 ) && num_clients >= 0 )) || continue
+    break
+done
+fi
+
+if [ -n "$num_clients" ]; then
+  NUM_CLIENTS=$num_clients
+fi
+
+echo "-----------------------------------------------------------------"
+echo "                SETUP ARGUMENTS"
+echo "-----------------------------------------------------------------"
+echo "        domain: $NETMAKER_BASE_DOMAIN"
+echo "         email: $EMAIL"
+echo "     public ip: $SERVER_PUBLIC_IP"
+echo "   setup mesh?: $MESH_SETUP"
+echo "    setup vpn?: $VPN_SETUP"
+if [ "${VPN_SETUP}" == "true" ]; then
+echo "     # clients: $NUM_CLIENTS"
+fi
+
+while true; do
+    read -p 'Does everything look right? [y/n]: ' yn
+    case $yn in
+        [Yy]* ) override="true"; break;;
+        [Nn]* ) echo "exiting..."; exit;;
+        * ) echo "Please answer yes or no.";;
+    esac
+done
+
+
+echo "Beginning installation in 5 seconds..."
+
+sleep 5
+
+if [ -f "/root/docker-compose.yml" ]; then
+    echo "Using existing docker compose"
+else 
+    echo "Pulling docker compose"
+    wget -q -O /root/docker-compose.yml https://raw.githubusercontent.com/gravitl/netmaker/master/compose/docker-compose.yml
+fi
+
+
+if [ -f "/root/mosquitto.conf" ]; then
+    echo "Using existing mosquitto config"
+else
+    echo "Pulling mosquitto config"
+    wget -q -O /root/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/master/docker/mosquitto.conf
+fi
+
+
+mkdir -p /etc/netmaker
+
+echo "Setting docker-compose..."
+
+sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/docker-compose.yml
+sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
+sed -i "s/REPLACE_MASTER_KEY/$MASTER_KEY/g" /root/docker-compose.yml
+sed -i "s/YOUR_EMAIL/$EMAIL/g" /root/docker-compose.yml
+
+echo "Starting containers..."
+
+docker-compose -f /root/docker-compose.yml up -d
+
+sleep 2
+
+test_connection() {
+
+echo "Testing Traefik setup (please be patient, this may take 1-2 minutes)"
+for i in 1 2 3 4 5 6
+do
+curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
+
+if [[ "$i" == 6 ]]; then
+  echo "    Traefik is having an issue setting up certificates, please investigate (docker logs traefik)"
+  echo "    Exiting..."
+  exit 1
+elif [[ "$curlresponse" == *"failed to verify the legitimacy of the server"* ]]; then
+  echo "    Certificates not yet configured, retrying..."
+
+elif [[ "$curlresponse" == *"left intact"* ]]; then
+  echo "    Certificates ok"
+  break
+else
+  secs=$(($i*5+10))
+  echo "    Issue establishing connection...retrying in $secs seconds..."       
+fi
+sleep $secs
+done
+}
+
+
+setup_mesh() {( set -e
+sleep 5
+echo "Creating netmaker network (10.101.0.0/16)"
+
+curl -s -o /dev/null -d '{"addressrange":"10.101.0.0/16","netid":"netmaker"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks
+
+sleep 5
+
+echo "Creating netmaker access key"
+
+curlresponse=$(curl -s -d '{"uses":99999,"name":"netmaker-key"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks/netmaker/keys)
+ACCESS_TOKEN=$(jq -r '.accessstring' <<< ${curlresponse})
+
+sleep 5
+
+echo "Configuring netmaker server as ingress gateway"
+
+curlresponse=$(curl -s -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/netmaker)
+SERVER_ID=$(jq -r '.[0].id' <<< ${curlresponse})
+
+curl -o /dev/null -s -X POST -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/netmaker/$SERVER_ID/createingress
+
+sleep 5
+)}
+
+mesh_connect_logs() {
+sleep 5
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+echo "DEFAULT NETWORK CLIENT INSTALL INSTRUCTIONS:"
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+sleep 5
+echo "For Linux and Mac clients, install with the following command:"
+echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+echo "curl -sfL https://raw.githubusercontent.com/gravitl/netmaker/develop/scripts/netclient-install.sh | sudo KEY=$VPN_ACCESS_TOKEN sh -"
+sleep 5
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+echo "For Windows clients, perform the following from powershell, as administrator:"
+echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+echo "1. Make sure WireGuardNT is installed - https://download.wireguard.com/windows-client/wireguard-installer.exe"
+echo "2. Download netclient.exe - wget https://github.com/gravitl/netmaker/releases/download/latest/netclient.exe"
+echo "3. Install Netclient - powershell.exe .\\netclient.exe join -t $VPN_ACCESS_TOKEN"
+echo "4. Whitelist C:\ProgramData\Netclient in Windows Defender"
+sleep 5
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+echo "For Android and iOS clients, perform the following steps:"
+echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+echo "1. Log into UI at dashboard.$NETMAKER_BASE_DOMAIN"
+echo "2. Navigate to \"EXTERNAL CLIENTS\" tab"
+echo "3. Select the gateway and create clients"
+echo "4. Scan the QR Code from WireGuard app in iOS or Android"
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+sleep 5
+}
+
+setup_vpn() {( set -e
+
+echo "Creating vpn network (10.201.0.0/16)"
+
+sleep 5
+curl -s -o /dev/null -d '{"addressrange":"10.201.0.0/16","netid":"vpn","defaultextclientdns":"8.8.8.8"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks
+
+sleep 5
+
+echo "Configuring netmaker server as vpn inlet..."
+
+curlresponse=$(curl -s -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/vpn)
+SERVER_ID=$(jq -r '.[0].id' <<< ${curlresponse})
+
+curl -s -o /dev/null -X POST -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/vpn/$SERVER_ID/createingress
+
+echo "Waiting 10 seconds for server to apply configuration..."
+
+sleep 10
+
+
+echo "Configuring netmaker server vpn gateway..."
+
+[ -z "$GATEWAY_IFACE" ] && GATEWAY_IFACE=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)')
+
+echo "Gateway iface: $GATEWAY_IFACE"
+
+curlresponse=$(curl -s -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/vpn)
+SERVER_ID=$(jq -r '.[0].id' <<< ${curlresponse})
+
+EGRESS_JSON=$( jq -n \
+                  --arg gw "$GATEWAY_IFACE" \
+                  '{ranges: ["0.0.0.0/5","8.0.0.0/7","11.0.0.0/8","12.0.0.0/6","16.0.0.0/4","32.0.0.0/3","64.0.0.0/2","128.0.0.0/3","160.0.0.0/5","168.0.0.0/6","172.0.0.0/12","172.32.0.0/11","172.64.0.0/10","172.128.0.0/9","173.0.0.0/8","174.0.0.0/7","176.0.0.0/4","192.0.0.0/9","192.128.0.0/11","192.160.0.0/13","192.169.0.0/16","192.170.0.0/15","192.172.0.0/14","192.176.0.0/12","192.192.0.0/10","193.0.0.0/8","194.0.0.0/7","196.0.0.0/6","200.0.0.0/5","208.0.0.0/4"], interface: $gw}' )
+
+echo "Egress json: $EGRESS_JSON"
+curl -s -o /dev/null -X POST -d "$EGRESS_JSON" -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/nodes/vpn/$SERVER_ID/creategateway
+
+echo "Creating client configs..."
+
+for ((a=1; a <= $NUM_CLIENTS; a++))
+do
+        CLIENT_JSON=$( jq -n \
+                  --arg clientid "vpnclient-$a" \
+                  '{clientid: $clientid}' )
+
+        curl -s -o /dev/null -d "$CLIENT_JSON" -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/extclients/vpn/$SERVER_ID
+done
+sleep 5
+)}
+
+vpn_connect_logs() {
+sleep 5
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+echo "VPN GATEWAY CLIENT INSTALL INSTRUCTIONS:"
+echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+echo "1. log into dashboard.$NETMAKER_BASE_DOMAIN"
+echo "2. Navigate to \"EXTERNAL CLIENTS\" tab"
+echo "3. Download or scan a client config (vpnclient-x) to the appropriate device"
+echo "4. Follow the steps for your system to configure WireGuard on the appropriate device"
+echo "5. Create and delete clients as necessary. Changes to netmaker server settings require regenerating ext clients."
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+sleep 5
+}
+
+set +e
+test_connection
+
+if [ "${MESH_SETUP}" != "false" ]; then
+        setup_mesh
+fi
+
+if [ "${VPN_SETUP}" == "true" ]; then
+        setup_vpn
+fi
+
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+echo "Netmaker setup is now complete. You are ready to begin using Netmaker."
+echo "Visit dashboard.$NETMAKER_BASE_DOMAIN to log in"
+echo "-----------------------------------------------------------------"
+echo "-----------------------------------------------------------------"
+
+# cp -f /etc/skel/.bashrc /root/.bashrc

+ 33 - 4
scripts/nm-quick.sh

@@ -144,6 +144,35 @@ echo "starting containers..."
 
 docker-compose -f /root/docker-compose.yml up -d
 
+test_connection() {
+
+echo "testing Traefik setup (please be patient, this may take 1-2 minutes)"
+for i in 1 2 3 4 5 6
+do
+curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
+
+if [[ "$i" == 6 ]]; then
+  echo "    Traefik is having an issue setting up certificates, please investigate (docker logs traefik)"
+  echo "    exiting..."
+  exit 1
+elif [[ "$curlresponse" == *"failed to verify the legitimacy of the server"* ]]; then
+  echo "    certificates not yet configured, retrying..."
+
+elif [[ "$curlresponse" == *"left intact"* ]]; then
+  echo "    certificates ok"
+  break
+else
+  secs=$(($i*5+10))
+  echo "    issue establishing connection...retrying in $secs seconds..."       
+fi
+sleep $secs
+done
+}
+
+set +e
+test_connection
+
+
 cat << "EOF"
 
                                                                                          
@@ -160,7 +189,7 @@ EOF
 echo "visit https://dashboard.$NETMAKER_BASE_DOMAIN to log in"
 sleep 7
 
-setup_mesh() {
+setup_mesh() {( set -e
 echo "creating netmaker network (10.101.0.0/16)"
 
 curl -s -o /dev/null -d '{"addressrange":"10.101.0.0/16","netid":"netmaker"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks
@@ -196,9 +225,9 @@ echo "        3. Select the gateway and create clients"
 echo "        4. Scan the QR Code from WireGuard app in iOS or Android"
 echo ""
 echo "Netmaker setup is now complete. You are ready to begin using Netmaker."
-}
+)}
 
-setup_vpn() {
+setup_vpn() {( set -e
 echo "creating vpn network (10.201.0.0/16)"
 
 curl -s -o /dev/null -d '{"addressrange":"10.201.0.0/16","netid":"vpn","defaultextclientdns":"8.8.8.8"}' -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' https://api.${NETMAKER_BASE_DOMAIN}/api/networks
@@ -253,7 +282,7 @@ echo "        3. Download or scan a client config (vpnclient-x) to the appropria
 echo "        4. Follow the steps for your system to configure WireGuard on the appropriate device"
 echo "        5. Create and delete clients as necessary. Changes to netmaker server settings require regenerating ext clients."
 
-}
+)}
 
 if [ "${MESH_SETUP}" != "false" ]; then
         setup_mesh

+ 18 - 13
scripts/openwrt-daemon.sh

@@ -1,38 +1,43 @@
-#!/bin/bash /etc/rc.common
+#!/bin/sh /etc/rc.common
 #Created by oycol<[email protected]>
 
 EXTRA_COMMANDS="status"
-EXTRA_HELP="        status  	Check service is running"
+EXTRA_HELP="        status      Check service is running"
 START=99
 
 LOG_FILE="/tmp/netclient.logs"
 
 start() {
+  mkdir -p /etc/netclient/config
+  mkdir -p /etc/systemd/system
+
   if [ ! -f "${LOG_FILE}" ];then
       touch "${LOG_FILE}"
   fi
-  local PIDS=($(ps -e|grep "netclient checkin -n all"|grep -v grep|awk '{print $1}'))
-  if [ ${PIDS} ];then
+
+  local PID=$(ps|grep "netclient daemon"|grep -v grep|awk '{print $1}')
+
+  if [ "${PID}" ];then
     echo "service is running"
     return
   fi
-  bash -c "while [ 1 ]; do /etc/netclient/netclient checkin -n all >> ${LOG_FILE} 2>&1;sleep 15;\
-           if [ $(ls -l ${LOG_FILE}|awk '{print $5}') -gt 10240000 ];then tar zcf "${LOG_FILE}.tar" -C / "tmp/netclient.logs"  && > ${LOG_FILE};fi;done &"
+  /bin/sh -c "while [ 1 ]; do netclient daemon >> ${LOG_FILE} 2>&1;sleep 15;\
+           if [ $(ls -l ${LOG_FILE}|awk '{print $5}') -gt 10240000 ];then tar zcf "${LOG_FILE}.tar" -C / "tmp/netclient.logs"  && > $LOG_FILE;fi;done &"
   echo "start"
 }
 
 stop() {
-  local PIDS=($(ps -e|grep "netclient checkin -n all"|grep -v grep|awk '{print $1}'))
-  for i in "${PIDS[@]}"; do
-    kill $i
-  done
+  local PID=$(ps|grep "netclient daemon"|grep -v grep|awk '{print $1}')
+  if [ "${PID}" ];then
+    kill ${PID}
+  fi
   echo "stop"
 }
 
 status() {
-  local PIDS=($(ps -e|grep "netclient checkin -n all"|grep -v grep|awk '{print $1}'))
-  if [ ${PIDS} ];then
-    echo -e "netclient[${PIDS}] is running \n"
+  local PID=$(ps|grep "netclient daemon"|grep -v grep|awk '{print $1}')
+  if [ "${PID}" ];then
+    echo -e "netclient[${PID}] is running \n"
   else
     echo -e "netclient is not running \n"
   fi

+ 11 - 0
servercfg/serverconf.go

@@ -427,6 +427,17 @@ func GetPublicIP() (string, error) {
 	var err error
 
 	iplist := []string{"https://ip.server.gravitl.com", "https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
+	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
+	if publicIpService != "" {
+		// prepend the user-specified service so it's checked first
+		iplist = append([]string{publicIpService}, iplist...)
+	} else if config.Config.Server.PublicIPService != "" {
+		publicIpService = config.Config.Server.PublicIPService
+
+		// prepend the user-specified service so it's checked first
+		iplist = append([]string{publicIpService}, iplist...)
+	}
+
 	for _, ipserver := range iplist {
 		client := &http.Client{
 			Timeout: time.Second * 10,