Browse Source

merge develop

0xdcarns 2 years ago
parent
commit
5fb3100c72

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

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.18.4
         - v0.18.3
         - v0.18.2
         - v0.18.1

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

+ 3 - 2
compose/docker-compose-emqx.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
   netmaker:
     container_name: netmaker
-    image: gravitl/netmaker:v0.18.2
+    image: gravitl/netmaker:v0.18.4
     restart: always
     volumes:
       - dnsconfig:/root/config/dnsconfig
@@ -30,11 +30,12 @@ services:
       VERBOSITY: "1"
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
+      DEFAULT_PROXY_MODE: "auto"
     ports:
       - "3478:3478/udp"
   netmaker-ui:
     container_name: netmaker-ui
-    image: gravitl/netmaker-ui:v0.18.2
+    image: gravitl/netmaker-ui:v0.18.4
     depends_on:
       - netmaker
     links:

+ 1 - 0
compose/docker-compose.ee.yml

@@ -33,6 +33,7 @@ services:
       METRICS_EXPORTER: "on"
       LICENSE_KEY: "YOUR_LICENSE_KEY"
       NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID"
+      DEFAULT_PROXY_MODE: "auto"
     ports:
       - "3478:3478/udp"
   netmaker-ui:

+ 17 - 0
compose/docker-compose.netclient.yml

@@ -0,0 +1,17 @@
+version: "3.4"
+
+services:
+  netclient:
+    container_name: netclient
+    image: 'gravitl/netclient:v0.18.4'
+    hostname: netmaker-1
+    network_mode: host
+    restart: always
+    environment:
+      TOKEN: "TOKEN_VALUE"
+    volumes:
+      - /etc/netclient:/etc/netclient
+    cap_add:
+      - NET_ADMIN
+      - NET_RAW
+      - SYS_MODULE

+ 1 - 0
compose/docker-compose.reference.yml

@@ -38,6 +38,7 @@ services:
       FRONTEND_URL: "" # "https://dashboard.<netmaker base domain>"
       AZURE_TENANT: "" # "<only for azure, you may optionally specify the tenant for the OAuth>"
       OIDC_ISSUER: "" # https://oidc.yourprovider.com - URL of oidc provider
+      DEFAULT_PROXY_MODE: "auto" # if ON, all new clients will enable proxy by default if OFF, all new clients will disable proxy by default, if AUTO, stick with the existing logic for NAT detection
     ports:
       - "3478:3478/udp" # the stun port
   netmaker-ui:  # The Netmaker UI Component

+ 1 - 0
compose/docker-compose.yml

@@ -28,6 +28,7 @@ services:
       MQ_PASSWORD: "REPLACE_MQ_PASSWORD"
       MQ_USERNAME: "REPLACE_MQ_USERNAME"
       STUN_PORT: "3478"
+      DEFAULT_PROXY_MODE: "auto"
     ports:
       - "3478:3478/udp"
   netmaker-ui:

+ 49 - 42
config/config.go

@@ -32,48 +32,55 @@ type EnvironmentConfig struct {
 
 // ServerConfig - server conf struct
 type ServerConfig struct {
-	CoreDNSAddr          string `yaml:"corednsaddr"`
-	APIConnString        string `yaml:"apiconn"`
-	APIHost              string `yaml:"apihost"`
-	APIPort              string `yaml:"apiport"`
-	Broker               string `yam:"broker"`
-	ServerBrokerEndpoint string `yaml:"serverbrokerendpoint"`
-	BrokerType           string `yaml:"brokertype"`
-	EmqxRestEndpoint     string `yaml:"emqxrestendpoint"`
-	MasterKey            string `yaml:"masterkey"`
-	DNSKey               string `yaml:"dnskey"`
-	AllowedOrigin        string `yaml:"allowedorigin"`
-	NodeID               string `yaml:"nodeid"`
-	RestBackend          string `yaml:"restbackend"`
-	MessageQueueBackend  string `yaml:"messagequeuebackend"`
-	DNSMode              string `yaml:"dnsmode"`
-	DisableRemoteIPCheck string `yaml:"disableremoteipcheck"`
-	Version              string `yaml:"version"`
-	SQLConn              string `yaml:"sqlconn"`
-	Platform             string `yaml:"platform"`
-	Database             string `yaml:"database"`
-	Verbosity            int32  `yaml:"verbosity"`
-	AuthProvider         string `yaml:"authprovider"`
-	OIDCIssuer           string `yaml:"oidcissuer"`
-	ClientID             string `yaml:"clientid"`
-	ClientSecret         string `yaml:"clientsecret"`
-	FrontendURL          string `yaml:"frontendurl"`
-	DisplayKeys          string `yaml:"displaykeys"`
-	AzureTenant          string `yaml:"azuretenant"`
-	Telemetry            string `yaml:"telemetry"`
-	HostNetwork          string `yaml:"hostnetwork"`
-	Server               string `yaml:"server"`
-	PublicIPService      string `yaml:"publicipservice"`
-	MQPassword           string `yaml:"mqpassword"`
-	MQUserName           string `yaml:"mqusername"`
-	MetricsExporter      string `yaml:"metrics_exporter"`
-	BasicAuth            string `yaml:"basic_auth"`
-	LicenseValue         string `yaml:"license_value"`
-	NetmakerAccountID    string `yaml:"netmaker_account_id"`
-	IsEE                 string `yaml:"is_ee"`
-	StunPort             int    `yaml:"stun_port"`
-	StunList             string `yaml:"stun_list"`
-	Proxy                string `yaml:"proxy"`
+	CoreDNSAddr          string    `yaml:"corednsaddr"`
+	APIConnString        string    `yaml:"apiconn"`
+	APIHost              string    `yaml:"apihost"`
+	APIPort              string    `yaml:"apiport"`
+	Broker               string    `yam:"broker"`
+	ServerBrokerEndpoint string    `yaml:"serverbrokerendpoint"`
+	BrokerType           string    `yaml:"brokertype"`
+	EmqxRestEndpoint     string    `yaml:"emqxrestendpoint"`
+	MasterKey            string    `yaml:"masterkey"`
+	DNSKey               string    `yaml:"dnskey"`
+	AllowedOrigin        string    `yaml:"allowedorigin"`
+	NodeID               string    `yaml:"nodeid"`
+	RestBackend          string    `yaml:"restbackend"`
+	MessageQueueBackend  string    `yaml:"messagequeuebackend"`
+	DNSMode              string    `yaml:"dnsmode"`
+	DisableRemoteIPCheck string    `yaml:"disableremoteipcheck"`
+	Version              string    `yaml:"version"`
+	SQLConn              string    `yaml:"sqlconn"`
+	Platform             string    `yaml:"platform"`
+	Database             string    `yaml:"database"`
+	Verbosity            int32     `yaml:"verbosity"`
+	AuthProvider         string    `yaml:"authprovider"`
+	OIDCIssuer           string    `yaml:"oidcissuer"`
+	ClientID             string    `yaml:"clientid"`
+	ClientSecret         string    `yaml:"clientsecret"`
+	FrontendURL          string    `yaml:"frontendurl"`
+	DisplayKeys          string    `yaml:"displaykeys"`
+	AzureTenant          string    `yaml:"azuretenant"`
+	Telemetry            string    `yaml:"telemetry"`
+	HostNetwork          string    `yaml:"hostnetwork"`
+	Server               string    `yaml:"server"`
+	PublicIPService      string    `yaml:"publicipservice"`
+	MQPassword           string    `yaml:"mqpassword"`
+	MQUserName           string    `yaml:"mqusername"`
+	MetricsExporter      string    `yaml:"metrics_exporter"`
+	BasicAuth            string    `yaml:"basic_auth"`
+	LicenseValue         string    `yaml:"license_value"`
+	NetmakerAccountID    string    `yaml:"netmaker_account_id"`
+	IsEE                 string    `yaml:"is_ee"`
+	StunPort             int       `yaml:"stun_port"`
+	StunList             string    `yaml:"stun_list"`
+	Proxy                string    `yaml:"proxy"`
+	DefaultProxyMode     ProxyMode `yaml:"defaultproxymode"`
+}
+
+// ProxyMode - default proxy mode for server
+type ProxyMode struct {
+	Set   bool
+	Value bool
 }
 
 // SQLConfig - Generic SQL Config

+ 1 - 1
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //	Schemes: https
 //	BasePath: /
-//	Version: 0.18.3
+//	Version: 0.18.4
 //	Host: netmaker.io
 //
 //	Consumes:

+ 7 - 2
controllers/enrollmentkeys.go

@@ -160,8 +160,13 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// version check
-	if !logic.IsVersionComptatible(newHost.Version) || newHost.TrafficKeyPublic == nil {
-		err := fmt.Errorf("incompatible netclient")
+	if !logic.IsVersionComptatible(newHost.Version) {
+		err := fmt.Errorf("bad client version on register: %s", newHost.Version)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if newHost.TrafficKeyPublic == nil && newHost.OS != models.OS_Types.IoT {
+		err := fmt.Errorf("missing traffic key")
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}

+ 12 - 4
controllers/ext_client.go

@@ -363,6 +363,13 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		extclient.Enabled = parentNetwork.DefaultACL == "yes"
 	}
 
+	if err := logic.SetClientDefaultACLs(&extclient); err != nil {
+		logger.Log(0, r.Header.Get("user"),
+			fmt.Sprintf("failed to assign ACLs to new ext client on network [%s]: %v", networkName, err))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
 	if err = logic.CreateExtClient(&extclient); err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to create new ext client on network [%s]: %v", networkName, err))
@@ -384,7 +391,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 				logger.Log(0, "failed to associate client", extclient.ClientID, "to user", userID)
 			}
 			extclient.OwnerID = userID
-			if _, err := logic.UpdateExtClient(extclient.ClientID, extclient.Network, extclient.Enabled, &extclient); err != nil {
+			if _, err := logic.UpdateExtClient(extclient.ClientID, extclient.Network, extclient.Enabled, &extclient, extclient.ACLs); err != nil {
 				logger.Log(0, "failed to add owner id", userID, "to client", extclient.ClientID)
 			}
 		}
@@ -477,10 +484,11 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	// == END PRO ==
 
-	var changedEnabled = newExtClient.Enabled != oldExtClient.Enabled // indicates there was a change in enablement
+	var changedEnabled = (newExtClient.Enabled != oldExtClient.Enabled) || // indicates there was a change in enablement
+		len(newExtClient.ACLs) != len(oldExtClient.ACLs)
 	// extra var need as logic.Update changes oldExtClient
 	currentClient := oldExtClient
-	newclient, err := logic.UpdateExtClient(newExtClient.ClientID, params["network"], newExtClient.Enabled, &oldExtClient)
+	newclient, err := logic.UpdateExtClient(newExtClient.ClientID, params["network"], newExtClient.Enabled, &oldExtClient, newExtClient.ACLs)
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to update ext client [%s], network [%s]: %v",
@@ -570,7 +578,7 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	go func() {
-		if err := mq.PublishPeerUpdate(); err != nil {
+		if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
 			logger.Log(1, "error setting ext peers on "+ingressnode.ID.String()+": "+err.Error())
 		}
 		if err = mq.PublishDeleteExtClientDNS(&extclient); err != nil {

+ 68 - 76
controllers/migrate.go

@@ -1,10 +1,18 @@
 package controller
 
 import (
+	"encoding/json"
 	"net/http"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/crypto/bcrypt"
 )
 
-// swagger:route PUT /api/nodes/{network}/{nodeid}/migrate nodes migrateNode
+// swagger:route PUT /api/v1/nodes/migrate nodes migrateNode
 //
 // Used to migrate a legacy node.
 //
@@ -16,79 +24,63 @@ import (
 //			Responses:
 //				200: nodeJoinResponse
 func migrate(w http.ResponseWriter, r *http.Request) {
-	// TODO adapt with enrollment-keys or re-think how this works
-	// we decode our body request params
-	// data := models.MigrationData{}
-	// err := json.NewDecoder(r.Body).Decode(&data)
-	// if err != nil {
-	// 	logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// params := mux.Vars(r)
-	// //check authorization
-	// record, err := database.FetchRecord(database.NODES_TABLE_NAME, data.LegacyNodeID)
-	// if err != nil {
-	// 	logger.Log(0, "no record for legacy node", data.LegacyNodeID, err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// var legacyNode models.LegacyNode
-	// if err = json.Unmarshal([]byte(record), &legacyNode); err != nil {
-	// 	logger.Log(0, "error decoding legacy node", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// if err := bcrypt.CompareHashAndPassword([]byte(legacyNode.Password), []byte(data.Password)); err != nil {
-	// 	logger.Log(0, "error decoding legacy password", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
-	// 	return
-	// }
-	// network, err := logic.GetNetwork(params["network"])
-	// if err != nil {
-	// 	logger.Log(0, "error retrieving network:  ", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// key, err := logic.CreateAccessKey(models.AccessKey{}, network)
-	// if err != nil {
-	// 	logger.Log(0, "error creating key:  ", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// data.JoinData.Key = key.Value
-	// payload, err := json.Marshal(data.JoinData)
-	// if err != nil {
-	// 	logger.Log(0, "error encoding data:  ", err.Error())
-	// 	logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-	// 	return
-	// }
-	// r.Body = io.NopCloser(strings.NewReader(string(payload)))
-	// r.ContentLength = int64(len(string(payload)))
-	// logger.Log(3, "deleteing legacy node", data.LegacyNodeID, legacyNode.ID, legacyNode.Name)
-	// if err := database.DeleteRecord(database.NODES_TABLE_NAME, data.LegacyNodeID); err != nil {
-	// 	logger.Log(0, "error deleting legacy node", legacyNode.Name, err.Error())
-	// }
-	// // createNode(w, r) should not have been tied to another handler func
-	// //newly created node has same node id as legacy node allowing using legacyNode.ID in gateway creation
-	// logger.Log(3, "re-creating legacy gateways")
-	// if legacyNode.IsIngressGateway == "yes" {
-	// 	if _, err := logic.CreateIngressGateway(legacyNode.Network, legacyNode.ID, false); err != nil {
-	// 		logger.Log(0, "error creating ingress gateway during migration", err.Error())
-	// 	}
-	// }
-	// if legacyNode.IsEgressGateway == "yes" {
-	// 	if _, err := logic.CreateEgressGateway(legacyNode.EgressGatewayRequest); err != nil {
-	// 		logger.Log(0, "error creating egress gateway during migration", err.Error())
-	// 	}
-	// }
-	// if legacyNode.IsRelay == "yes" {
-	// 	if _, _, err := logic.CreateRelay(models.RelayRequest{
-	// 		NodeID:     legacyNode.ID,
-	// 		NetID:      legacyNode.Network,
-	// 		RelayAddrs: legacyNode.RelayAddrs,
-	// 	}); err != nil {
-	// 		logger.Log(0, "error creating relay during migration", err.Error())
-	// 	}
-	// }
+	data := models.MigrationData{}
+	err := json.NewDecoder(r.Body).Decode(&data)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	var networksToAdd = []string{}
+	for i := range data.LegacyNodes {
+		legacyNode := data.LegacyNodes[i]
+		record, err := database.FetchRecord(database.NODES_TABLE_NAME, legacyNode.ID)
+		if err != nil {
+			logger.Log(0, "no record for legacy node", legacyNode.ID, err.Error())
+			continue
+		} else {
+			var oldLegacyNode models.LegacyNode
+			if err = json.Unmarshal([]byte(record), &oldLegacyNode); err != nil {
+				logger.Log(0, "error decoding legacy node", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+				continue
+			}
+			if err := bcrypt.CompareHashAndPassword([]byte(oldLegacyNode.Password), []byte(legacyNode.Password)); err != nil {
+				logger.Log(0, "error decoding legacy password", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+				continue
+			}
+			networksToAdd = append(networksToAdd, oldLegacyNode.Network)
+			_ = database.DeleteRecord(database.NODES_TABLE_NAME, oldLegacyNode.ID)
+		}
+	}
+	if len(networksToAdd) == 0 {
+		logger.Log(0, "no valid networks to migrate for host", data.NewHost.Name)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if !logic.HostExists(&data.NewHost) {
+		if err = logic.CreateHost(&data.NewHost); err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	}
+	key, keyErr := logic.RetrievePublicTrafficKey()
+	if keyErr != nil {
+		logger.Log(0, "error retrieving key:", keyErr.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	server := servercfg.GetServerInfo()
+	server.TrafficKey = key
+	response := models.RegisterResponse{
+		ServerConf:    server,
+		RequestedHost: data.NewHost,
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(&response)
+	logger.Log(0, "successfully migrated host", data.NewHost.Name, data.NewHost.ID.String())
+	// notify host of changes, peer and node updates
+	go checkNetRegAndHostUpdate(networksToAdd, &data.NewHost)
 }

+ 3 - 4
controllers/network_test.go

@@ -40,6 +40,8 @@ func TestMain(m *testing.M) {
 			logger.Log(3, "received node update", update.Action)
 		}
 	}()
+	os.Exit(m.Run())
+
 }
 
 func TestCreateNetwork(t *testing.T) {
@@ -213,10 +215,7 @@ func TestIpv6Network(t *testing.T) {
 
 func deleteAllNetworks() {
 	deleteAllNodes()
-	nets, _ := logic.GetNetworks()
-	for _, net := range nets {
-		logic.DeleteNetwork(net.NetID)
-	}
+	database.DeleteAllRecords(database.NETWORKS_TABLE_NAME)
 }
 
 func createNet() {

+ 2 - 2
controllers/node.go

@@ -25,7 +25,6 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/nodes/{network}", authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(false, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPut)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/migrate", migrate).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", authorize(false, true, "user", http.HandlerFunc(createRelay))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", authorize(false, true, "user", http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
@@ -35,6 +34,7 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleteingress", logic.SecurityCheck(false, http.HandlerFunc(deleteIngressGateway))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", authorize(true, true, "node", http.HandlerFunc(updateNode))).Methods(http.MethodPost)
 	r.HandleFunc("/api/nodes/adm/{network}/authenticate", authenticate).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
 
 // swagger:route POST /api/nodes/adm/{network}/authenticate nodes authenticate
@@ -386,7 +386,7 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	hostPeerUpdate, err := logic.GetPeerUpdateForHost(context.Background(), node.Network, host, nil)
+	hostPeerUpdate, err := logic.GetPeerUpdateForHost(context.Background(), node.Network, host, nil, nil)
 	if err != nil && !database.IsEmptyRecord(err) {
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", host.ID.String(), err))

+ 2 - 2
controllers/node_test.go

@@ -113,12 +113,12 @@ func TestGetNetworkNodes(t *testing.T) {
 	t.Run("BadNet", func(t *testing.T) {
 		node, err := logic.GetNetworkNodes("badnet")
 		assert.Nil(t, err)
-		assert.Nil(t, node)
+		assert.Equal(t, []models.Node{}, node)
 	})
 	t.Run("NoNodes", func(t *testing.T) {
 		node, err := logic.GetNetworkNodes("skynet")
 		assert.Nil(t, err)
-		assert.Nil(t, node)
+		assert.Equal(t, []models.Node{}, node)
 	})
 	t.Run("Success", func(t *testing.T) {
 		createTestNode()

+ 4 - 0
controllers/user_test.go

@@ -186,6 +186,7 @@ func TestGetUsers(t *testing.T) {
 		assert.Equal(t, []models.ReturnUser(nil), admin)
 	})
 	t.Run("UserExisits", func(t *testing.T) {
+		user.UserName = "anotheruser"
 		if err := logic.CreateUser(&adminUser); err != nil {
 			t.Error(err)
 		}
@@ -281,6 +282,9 @@ func TestVerifyAuthRequest(t *testing.T) {
 		assert.EqualError(t, err, "error retrieving user from db: could not find any records")
 	})
 	t.Run("Non-Admin", func(t *testing.T) {
+		user.IsAdmin = false
+		user.Password = "somepass"
+		user.UserName = "nonadmin"
 		if err := logic.CreateUser(&user); err != nil {
 			t.Error(err)
 		}

+ 3 - 0
ee/initialize.go

@@ -40,6 +40,9 @@ func InitEE() {
 	logic.EnterpriseFailoverFunc = eelogic.SetFailover
 	logic.EnterpriseResetFailoverFunc = eelogic.ResetFailover
 	logic.EnterpriseResetAllPeersFailovers = eelogic.WipeAffectedFailoversOnly
+	logic.DenyClientNodeAccess = eelogic.DenyClientNode
+	logic.IsClientNodeAllowed = eelogic.IsClientNodeAllowed
+	logic.AllowClientNodeAccess = eelogic.RemoveDeniedNodeFromClient
 }
 
 func setControllerLimits() {

+ 41 - 0
ee/logic/ext_acls.go

@@ -0,0 +1,41 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+// DenyClientNode - add a denied node to an ext client's list
+func DenyClientNode(ec *models.ExtClient, clientOrNodeID string) (ok bool) {
+	if ec == nil || len(clientOrNodeID) == 0 {
+		return
+	}
+	if ec.ACLs == nil {
+		ec.ACLs = map[string]struct{}{}
+	}
+	ok = true
+	ec.ACLs[clientOrNodeID] = struct{}{}
+	return
+}
+
+// IsClientNodeAllowed - checks if given ext client and node are allowed to communicate
+func IsClientNodeAllowed(ec *models.ExtClient, clientOrNodeID string) bool {
+	if ec == nil || len(clientOrNodeID) == 0 {
+		return false
+	}
+	if ec.ACLs == nil {
+		return true
+	}
+	_, ok := ec.ACLs[clientOrNodeID]
+	return ok
+}
+
+// RemoveDeniedNodeFromClient - removes a node id from set of denied nodes
+func RemoveDeniedNodeFromClient(ec *models.ExtClient, clientOrNodeID string) bool {
+	if ec.ACLs == nil {
+		return true
+	}
+	_, ok := ec.ACLs[clientOrNodeID]
+	if !ok {
+		return false
+	}
+	delete(ec.ACLs, clientOrNodeID)
+	return true
+}

+ 12 - 21
functions/helpers_test.go

@@ -3,12 +3,14 @@ package functions
 import (
 	"context"
 	"encoding/json"
+	"os"
 	"testing"
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	"github.com/stretchr/testify/assert"
 )
 
 var (
@@ -39,40 +41,29 @@ func TestMain(m *testing.M) {
 			logger.Log(3, "received node update", update.Action)
 		}
 	}()
+	os.Exit(m.Run())
+
 }
 
 func TestNetworkExists(t *testing.T) {
 	database.DeleteRecord(database.NETWORKS_TABLE_NAME, testNetwork.NetID)
-	defer database.CloseDB()
 	exists, err := logic.NetworkExists(testNetwork.NetID)
-	if err == nil {
-		t.Fatalf("expected error, received nil")
-	}
-	if exists {
-		t.Fatalf("expected false")
-	}
+	assert.NotNil(t, err)
+	assert.False(t, exists)
 
 	err = logic.SaveNetwork(testNetwork)
-	if err != nil {
-		t.Fatalf("failed to save test network in databse: %s", err)
-	}
+	assert.Nil(t, err)
 	exists, err = logic.NetworkExists(testNetwork.NetID)
-	if err != nil {
-		t.Fatalf("expected nil, received err: %s", err)
-	}
-	if !exists {
-		t.Fatalf("expected network to exist in database")
-	}
+	assert.Nil(t, err)
+	assert.True(t, exists)
 
 	err = database.DeleteRecord(database.NETWORKS_TABLE_NAME, testNetwork.NetID)
-	if err != nil {
-		t.Fatalf("expected nil, failed to delete test network: %s", err)
-	}
+	assert.Nil(t, err)
 }
 
 func TestGetAllExtClients(t *testing.T) {
-	defer database.CloseDB()
-	database.DeleteRecord(database.EXT_CLIENT_TABLE_NAME, testExternalClient.ClientID)
+	err := database.DeleteRecord(database.EXT_CLIENT_TABLE_NAME, testExternalClient.ClientID)
+	assert.Nil(t, err)
 
 	extClients, err := GetAllExtClients()
 	if err == nil {

+ 7 - 9
go.mod

@@ -15,11 +15,11 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/stretchr/testify v1.8.2
 	github.com/txn2/txeh v1.3.0
-	golang.org/x/crypto v0.6.0
-	golang.org/x/net v0.6.0 // indirect
-	golang.org/x/oauth2 v0.5.0
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/crypto v0.7.0
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/oauth2 v0.6.0
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220324164955-056925b7df31
 	google.golang.org/protobuf v1.28.1 // indirect
@@ -42,8 +42,7 @@ require (
 
 require (
 	github.com/guumaster/tablewriter v0.0.10
-	github.com/kr/pretty v0.3.0
-	github.com/matryer/is v1.4.0
+	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/cobra v1.6.1
 )
@@ -52,9 +51,8 @@ require (
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
-	github.com/kr/text v0.2.0 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
-	github.com/rogpeppe/go-internal v1.8.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 )
 

+ 16 - 18
go.sum

@@ -69,8 +69,8 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
 github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -80,8 +80,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
 github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
-github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
@@ -111,9 +111,8 @@ github.com/posthog/posthog-go v0.0.0-20211028072449-93c17c49e2b0/go.mod h1:oa2sA
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f h1:BSnJgAfHzEp7o8PYJ7YfwAVHhqu7BYUTggcn/LGlUWY=
 github.com/rqlite/gorqlite v0.0.0-20210514125552-08ff1e76b22f/go.mod h1:UW/gxgQwSePTvL1KA8QEHsXeYHP4xkoXgbDdN781p34=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
@@ -156,8 +155,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -173,11 +172,11 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
-golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
-golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -199,8 +198,8 @@ golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@@ -210,8 +209,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -235,7 +234,6 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

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

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       containers:
       - name: netclient
-        image: gravitl/netclient:v0.18.3
+        image: gravitl/netclient:v0.18.4
         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.18.3
+        image: gravitl/netclient:v0.18.4
         env:
         - name: TOKEN
           value: "TOKEN_VALUE"

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

@@ -79,7 +79,7 @@ spec:
           value: "Kubernetes"
         - name: VERBOSITY
           value: "3"
-        image: gravitl/netmaker:v0.18.3
+        image: gravitl/netmaker:v0.18.4
         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.18.3
+        image: gravitl/netmaker-ui:v0.18.4
         ports:
         - containerPort: 443
         env:

+ 53 - 0
logic/clients.go

@@ -0,0 +1,53 @@
+package logic
+
+import "github.com/gravitl/netmaker/models"
+
+// functions defined here, handle client ACLs, should be set on ee
+
+var (
+	// DenyClientNodeAccess - function to handle adding a node to an ext client's denied node set
+	DenyClientNodeAccess = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+	// IsClientNodeAllowed - function to check if an ext client's denied node set contains a node ID
+	IsClientNodeAllowed = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+	// AllowClientNodeAccess - function to handle removing a node ID from ext client's denied nodes, thus allowing it
+	AllowClientNodeAccess = func(ec *models.ExtClient, clientOrNodeID string) bool { return true }
+)
+
+// SetClientDefaultACLs - set's a client's default ACLs based on network and nodes in network
+func SetClientDefaultACLs(ec *models.ExtClient) error {
+	if !isEE {
+		return nil
+	}
+	networkNodes, err := GetNetworkNodes(ec.Network)
+	if err != nil {
+		return err
+	}
+	network, err := GetNetwork(ec.Network)
+	if err != nil {
+		return err
+	}
+	for i := range networkNodes {
+		currNode := networkNodes[i]
+		if network.DefaultACL == "no" || currNode.DefaultACL == "no" {
+			DenyClientNodeAccess(ec, currNode.ID.String())
+		}
+	}
+	return nil
+}
+
+// SetClientACLs - overwrites an ext client's ACL
+func SetClientACLs(ec *models.ExtClient, newACLs map[string]struct{}) {
+	if ec == nil || newACLs == nil || !isEE {
+		return
+	}
+	ec.ACLs = newACLs
+}
+
+// IsClientNodeAllowedByID - checks if a given ext client ID + nodeID are allowed
+func IsClientNodeAllowedByID(clientID, networkName, clientOrNodeID string) bool {
+	client, err := GetExtClient(clientID, networkName)
+	if err != nil {
+		return false
+	}
+	return IsClientNodeAllowed(&client, clientOrNodeID)
+}

+ 22 - 3
logic/extpeers.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"encoding/json"
+	"fmt"
 	"time"
 
 	"github.com/gravitl/netmaker/database"
@@ -114,6 +115,22 @@ func GetExtClient(clientid string, network string) (models.ExtClient, error) {
 	return extclient, err
 }
 
+// GetExtClient - gets a single ext client on a network
+func GetExtClientByPubKey(publicKey string, network string) (*models.ExtClient, error) {
+	netClients, err := GetNetworkExtClients(network)
+	if err != nil {
+		return nil, err
+	}
+	for i := range netClients {
+		ec := netClients[i]
+		if ec.PublicKey == publicKey {
+			return &ec, nil
+		}
+	}
+
+	return nil, fmt.Errorf("no client found")
+}
+
 // CreateExtClient - creates an extclient
 func CreateExtClient(extclient *models.ExtClient) error {
 
@@ -172,15 +189,17 @@ func CreateExtClient(extclient *models.ExtClient) error {
 }
 
 // UpdateExtClient - only supports name changes right now
-func UpdateExtClient(newclientid string, network string, enabled bool, client *models.ExtClient) (*models.ExtClient, error) {
-
+func UpdateExtClient(newclientid string, network string, enabled bool, client *models.ExtClient, newACLs map[string]struct{}) (*models.ExtClient, error) {
 	err := DeleteExtClient(network, client.ClientID)
 	if err != nil {
 		return client, err
 	}
 	client.ClientID = newclientid
 	client.Enabled = enabled
-	CreateExtClient(client)
+	SetClientACLs(client, newACLs)
+	if err = CreateExtClient(client); err != nil {
+		return client, err
+	}
 	return client, err
 }
 

+ 20 - 1
logic/host_test.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"os"
 	"testing"
 
 	"github.com/google/uuid"
@@ -12,7 +13,7 @@ import (
 	"github.com/matryer/is"
 )
 
-func TestCheckPorts(t *testing.T) {
+func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	peerUpdate := make(chan *models.Node)
@@ -24,6 +25,10 @@ func TestCheckPorts(t *testing.T) {
 		}
 	}()
 
+	os.Exit(m.Run())
+}
+
+func TestCheckPorts(t *testing.T) {
 	h := models.Host{
 		ID:              uuid.New(),
 		EndpointIP:      net.ParseIP("192.168.1.1"),
@@ -36,10 +41,16 @@ func TestCheckPorts(t *testing.T) {
 		ListenPort:      51830,
 		ProxyListenPort: 51730,
 	}
+	//not sure why this initialization is required but without it
+	// RemoveHost returns database is closed
+	database.InitializeDatabase()
+	RemoveHost(&h)
 	CreateHost(&h)
 	t.Run("no change", func(t *testing.T) {
 		is := is.New(t)
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51830)
 		is.Equal(testHost.ProxyListenPort, 51730)
 	})
@@ -47,6 +58,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ListenPort = 51821
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51822)
 		is.Equal(testHost.ProxyListenPort, 51730)
 	})
@@ -54,6 +67,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ProxyListenPort = 65535
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, 51822)
 		is.Equal(testHost.ProxyListenPort, minPort)
 	})
@@ -61,6 +76,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ListenPort = maxPort
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, minPort)
 		is.Equal(testHost.ProxyListenPort, minPort+1)
 	})
@@ -68,6 +85,8 @@ func TestCheckPorts(t *testing.T) {
 		is := is.New(t)
 		testHost.ProxyListenPort = 51821
 		CheckHostPorts(&testHost)
+		t.Log(testHost.ListenPort, testHost.ProxyListenPort)
+		t.Log(h.ListenPort, h.ProxyListenPort)
 		is.Equal(testHost.ListenPort, minPort)
 		is.Equal(testHost.ProxyListenPort, 51822)
 	})

+ 10 - 0
logic/hosts.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
@@ -96,6 +97,15 @@ func CreateHost(h *models.Host) error {
 		return err
 	}
 	h.HostPass = string(hash)
+	// if another server has already updated proxyenabled, leave it alone
+	if !h.ProxyEnabledSet {
+		log.Println("checking default proxy", servercfg.GetServerConfig().DefaultProxyMode)
+		if servercfg.GetServerConfig().DefaultProxyMode.Set {
+			h.ProxyEnabledSet = true
+			h.ProxyEnabled = servercfg.GetServerConfig().DefaultProxyMode.Value
+			log.Println("set proxy enabled to ", h.ProxyEnabled)
+		}
+	}
 	checkForZombieHosts(h)
 	return UpsertHost(h)
 }

+ 2 - 1
logic/metrics/metrics.go

@@ -10,7 +10,7 @@ import (
 )
 
 // Collect - collects metrics
-func Collect(iface, server, network string, peerMap models.PeerMap) (*models.Metrics, error) {
+func Collect(iface, server, network string, peerMap models.PeerMap, proxy bool) (*models.Metrics, error) {
 	var metrics models.Metrics
 	metrics.Connectivity = make(map[string]models.Metric)
 	var wgclient, err = wgctrl.New()
@@ -45,6 +45,7 @@ func Collect(iface, server, network string, peerMap models.PeerMap) (*models.Met
 		newMetric.TotalSent = int64(proxyMetrics.TrafficSent)
 		newMetric.Latency = int64(proxyMetrics.LastRecordedLatency)
 		newMetric.Connected = proxyMetrics.NodeConnectionStatus[id]
+		newMetric.CollectedByProxy = proxy
 		if newMetric.Connected {
 			newMetric.Uptime = 1
 		}

+ 84 - 16
logic/peers.go

@@ -47,7 +47,7 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 		relayPeersMap := make(map[string]models.RelayedConf)
 		for _, relayedHost := range relayedHosts {
 			relayedHost := relayedHost
-			payload, err := GetPeerUpdateForHost(ctx, "", &relayedHost, nil)
+			payload, err := GetPeerUpdateForHost(ctx, "", &relayedHost, nil, nil)
 			if err == nil {
 				relayedEndpoint, udpErr := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", relayedHost.EndpointIP, GetPeerListenPort(&relayedHost)))
 				if udpErr == nil {
@@ -71,6 +71,9 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 		if err != nil {
 			continue
 		}
+		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
+			continue
+		}
 		currentPeers, err := GetNetworkNodes(node.Network)
 		if err != nil {
 			continue
@@ -80,6 +83,9 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 				//skip yourself
 				continue
 			}
+			if !peer.Connected || peer.PendingDelete || peer.Action == models.NODE_DELETE {
+				continue
+			}
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
 				continue
@@ -90,6 +96,7 @@ func GetProxyUpdateForHost(ctx context.Context, host *models.Host) (models.Proxy
 				currPeerConf = models.PeerConf{
 					Proxy:            peerHost.ProxyEnabled,
 					PublicListenPort: int32(GetPeerListenPort(peerHost)),
+					ProxyListenPort:  GetProxyListenPort(peerHost),
 				}
 			}
 
@@ -133,7 +140,7 @@ func ResetPeerUpdateContext() {
 }
 
 // GetPeerUpdateForHost - gets the consolidated peer update for the host from all networks
-func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host, deletedNode *models.Node) (models.HostPeerUpdate, error) {
+func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host, deletedNode *models.Node, deletedClient *models.ExtClient) (models.HostPeerUpdate, error) {
 	if host == nil {
 		return models.HostPeerUpdate{}, errors.New("host is nil")
 	}
@@ -152,10 +159,11 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 		IngressInfo: models.IngressInfo{
 			ExtPeers: make(map[string]models.ExtClientInfo),
 		},
-		EgressInfo: make(map[string]models.EgressInfo),
-		PeerIDs:    make(models.PeerMap, 0),
-		Peers:      []wgtypes.PeerConfig{},
-		NodePeers:  []wgtypes.PeerConfig{},
+		EgressInfo:      make(map[string]models.EgressInfo),
+		PeerIDs:         make(models.PeerMap, 0),
+		Peers:           []wgtypes.PeerConfig{},
+		NodePeers:       []wgtypes.PeerConfig{},
+		HostNetworkInfo: models.HostInfoMap{},
 	}
 
 	logger.Log(1, "peer update for host", host.ID.String())
@@ -250,6 +258,7 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 									},
 									PeerKey: extPeerIdAndAddr.ID,
 									Allow:   true,
+									ID:      extPeerIdAndAddr.ID,
 								}
 							}
 						}
@@ -265,19 +274,26 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 						},
 						PeerKey: peerHost.PublicKey.String(),
 						Allow:   true,
+						ID:      peer.ID.String(),
 					}
 				}
 
+				peerProxyPort := GetProxyListenPort(peerHost)
 				var nodePeer wgtypes.PeerConfig
 				if _, ok := hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()]; !ok {
 					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()] = make(map[string]models.IDandAddr)
 					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, peerConfig)
 					peerIndexMap[peerHost.PublicKey.String()] = len(hostPeerUpdate.Peers) - 1
 					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
-						ID:      peer.ID.String(),
-						Address: peer.PrimaryAddress(),
-						Name:    peerHost.Name,
-						Network: peer.Network,
+						ID:              peer.ID.String(),
+						Address:         peer.PrimaryAddress(),
+						Name:            peerHost.Name,
+						Network:         peer.Network,
+						ProxyListenPort: peerProxyPort,
+					}
+					hostPeerUpdate.HostNetworkInfo[peerHost.PublicKey.String()] = models.HostNetworkInfo{
+						Interfaces:      peerHost.Interfaces,
+						ProxyListenPort: peerProxyPort,
 					}
 					nodePeer = peerConfig
 				} else {
@@ -285,10 +301,15 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 					peerAllowedIPs = append(peerAllowedIPs, allowedips...)
 					hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]].AllowedIPs = peerAllowedIPs
 					hostPeerUpdate.HostPeerIDs[peerHost.PublicKey.String()][peer.ID.String()] = models.IDandAddr{
-						ID:      peer.ID.String(),
-						Address: peer.PrimaryAddress(),
-						Name:    peerHost.Name,
-						Network: peer.Network,
+						ID:              peer.ID.String(),
+						Address:         peer.PrimaryAddress(),
+						Name:            peerHost.Name,
+						Network:         peer.Network,
+						ProxyListenPort: GetProxyListenPort(peerHost),
+					}
+					hostPeerUpdate.HostNetworkInfo[peerHost.PublicKey.String()] = models.HostNetworkInfo{
+						Interfaces:      peerHost.Interfaces,
+						ProxyListenPort: peerProxyPort,
 					}
 					nodePeer = hostPeerUpdate.Peers[peerIndexMap[peerHost.PublicKey.String()]]
 				}
@@ -299,7 +320,6 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 						Address:         peer.PrimaryAddress(),
 						Name:            peerHost.Name,
 						Network:         peer.Network,
-						Interfaces:      peerHost.Interfaces,
 						ProxyListenPort: peerHost.ProxyListenPort,
 					}
 					hostPeerUpdate.NodePeers = append(hostPeerUpdate.NodePeers, nodePeer)
@@ -319,6 +339,7 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 							},
 							PeerKey: extPeerIdAndAddr.ID,
 							Allow:   true,
+							ID:      extPeerIdAndAddr.ID,
 						}
 					}
 					hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, extPeers...)
@@ -331,6 +352,7 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 							Name:    extPeerIdAndAddr.Name,
 							Network: node.Network,
 						}
+
 						hostPeerUpdate.IngressInfo.ExtPeers[extPeerIdAndAddr.ID] = models.ExtClientInfo{
 							Masquerade: true,
 							IngGwAddr: net.IPNet{
@@ -343,7 +365,7 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 								Mask: getCIDRMaskFromAddr(extPeerIdAndAddr.Address),
 							},
 							ExtPeerKey: extPeerIdAndAddr.ID,
-							Peers:      nodePeerMap,
+							Peers:      filterNodeMapForClientACLs(extPeerIdAndAddr.ID, node.Network, nodePeerMap),
 						}
 						if node.Network == network {
 							hostPeerUpdate.PeerIDs[extPeerIdAndAddr.ID] = extPeerIdAndAddr
@@ -386,6 +408,16 @@ func GetPeerUpdateForHost(ctx context.Context, network string, host *models.Host
 		hostPeerUpdate.NodePeers[i] = peer
 	}
 
+	if deletedClient != nil {
+		key, err := wgtypes.ParseKey(deletedClient.PublicKey)
+		if err == nil {
+			hostPeerUpdate.Peers = append(hostPeerUpdate.Peers, wgtypes.PeerConfig{
+				PublicKey: key,
+				Remove:    true,
+			})
+		}
+	}
+
 	return hostPeerUpdate, nil
 }
 
@@ -402,6 +434,15 @@ func GetPeerListenPort(host *models.Host) int {
 	return peerPort
 }
 
+// GetProxyListenPort - fetches the proxy listen port
+func GetProxyListenPort(host *models.Host) int {
+	proxyPort := host.ProxyListenPort
+	if host.PublicListenPort != 0 {
+		proxyPort = host.PublicListenPort
+	}
+	return proxyPort
+}
+
 func getExtPeers(node *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, error) {
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
@@ -414,6 +455,7 @@ func getExtPeers(node *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, e
 		return peers, idsAndAddr, err
 	}
 	for _, extPeer := range extPeers {
+		extPeer := extPeer
 		pubkey, err := wgtypes.ParseKey(extPeer.PublicKey)
 		if err != nil {
 			logger.Log(1, "error parsing ext pub key:", err.Error())
@@ -645,3 +687,29 @@ func getCIDRMaskFromAddr(addr string) net.IPMask {
 	}
 	return cidr
 }
+
+// accounts for ext client ACLs
+func filterNodeMapForClientACLs(publicKey, network string, nodePeerMap map[string]models.PeerRouteInfo) map[string]models.PeerRouteInfo {
+	if !isEE {
+		return nodePeerMap
+	}
+	if nodePeerMap == nil {
+		return map[string]models.PeerRouteInfo{}
+	}
+
+	if len(publicKey) == 0 || len(network) == 0 {
+		return nodePeerMap
+	}
+
+	client, err := GetExtClientByPubKey(publicKey, network)
+	if err != nil {
+		return nodePeerMap
+	}
+	for k := range nodePeerMap {
+		currNodePeer := nodePeerMap[k]
+		if _, ok := client.ACLs[currNodePeer.ID]; ok {
+			delete(nodePeerMap, k)
+		}
+	}
+	return nodePeerMap
+}

+ 3 - 1
logic/pro/networkuser_test.go

@@ -1,6 +1,7 @@
 package pro
 
 import (
+	"os"
 	"testing"
 
 	"github.com/google/uuid"
@@ -13,6 +14,7 @@ import (
 func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
+	os.Exit(m.Run())
 }
 
 func TestNetworkUserLogic(t *testing.T) {
@@ -33,7 +35,7 @@ func TestNetworkUserLogic(t *testing.T) {
 	}
 
 	clients := []models.ExtClient{
-		models.ExtClient{
+		{
 			ClientID: "coolclient",
 		},
 	}

+ 11 - 1
logic/telemetry.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
@@ -84,7 +85,7 @@ func fetchTelemetryData() (telemetryData, error) {
 	data.Users = getDBLength(database.USERS_TABLE_NAME)
 	data.Networks = getDBLength(database.NETWORKS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
-	//data.Servers = GetServerCount()
+	data.Servers = getServerCount()
 	nodes, err := GetAllNodes()
 	if err == nil {
 		data.Nodes = len(nodes)
@@ -93,6 +94,15 @@ func fetchTelemetryData() (telemetryData, error) {
 	return data, err
 }
 
+// getServerCount returns number of servers from database
+func getServerCount() int {
+	data, err := database.FetchRecords(database.SERVER_UUID_TABLE_NAME)
+	if err != nil {
+		logger.Log(0, "errror retrieving server data", err.Error())
+	}
+	return len(data)
+}
+
 // setTelemetryTimestamp - Give the entry in the DB a new timestamp
 func setTelemetryTimestamp(telRecord *models.Telemetry) error {
 	lastsend := time.Now().Unix()

+ 1 - 1
main.go

@@ -27,7 +27,7 @@ import (
 	stunserver "github.com/gravitl/netmaker/stun-server"
 )
 
-var version = "v0.18.3"
+var version = "v0.18.4"
 
 // Start DB Connection and start API Request Handler
 func main() {

+ 13 - 12
models/extclient.go

@@ -2,16 +2,17 @@ package models
 
 // ExtClient - struct for external clients
 type ExtClient struct {
-	ClientID               string `json:"clientid" bson:"clientid"`
-	Description            string `json:"description" bson:"description"`
-	PrivateKey             string `json:"privatekey" bson:"privatekey"`
-	PublicKey              string `json:"publickey" bson:"publickey"`
-	Network                string `json:"network" bson:"network"`
-	Address                string `json:"address" bson:"address"`
-	Address6               string `json:"address6" bson:"address6"`
-	IngressGatewayID       string `json:"ingressgatewayid" bson:"ingressgatewayid"`
-	IngressGatewayEndpoint string `json:"ingressgatewayendpoint" bson:"ingressgatewayendpoint"`
-	LastModified           int64  `json:"lastmodified" bson:"lastmodified"`
-	Enabled                bool   `json:"enabled" bson:"enabled"`
-	OwnerID                string `json:"ownerid" bson:"ownerid"`
+	ClientID               string              `json:"clientid" bson:"clientid"`
+	Description            string              `json:"description" bson:"description"`
+	PrivateKey             string              `json:"privatekey" bson:"privatekey"`
+	PublicKey              string              `json:"publickey" bson:"publickey"`
+	Network                string              `json:"network" bson:"network"`
+	Address                string              `json:"address" bson:"address"`
+	Address6               string              `json:"address6" bson:"address6"`
+	IngressGatewayID       string              `json:"ingressgatewayid" bson:"ingressgatewayid"`
+	IngressGatewayEndpoint string              `json:"ingressgatewayendpoint" bson:"ingressgatewayendpoint"`
+	LastModified           int64               `json:"lastmodified" bson:"lastmodified"`
+	Enabled                bool                `json:"enabled" bson:"enabled"`
+	OwnerID                string              `json:"ownerid" bson:"ownerid"`
+	ACLs                   map[string]struct{} `json:"acls,omitempty" bson:"acls,omitempty"`
 }

+ 18 - 2
models/host.go

@@ -7,6 +7,21 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
+// OS_Types - list of OS types Netmaker cares about
+var OS_Types = struct {
+	Linux   string
+	Windows string
+	Mac     string
+	FreeBSD string
+	IoT     string
+}{
+	Linux:   "linux",
+	Windows: "windows",
+	Mac:     "darwin",
+	FreeBSD: "freebsd",
+	IoT:     "iot",
+}
+
 // WIREGUARD_INTERFACE name of wireguard interface
 const WIREGUARD_INTERFACE = "netmaker"
 
@@ -29,7 +44,7 @@ type Host struct {
 	MTU              int              `json:"mtu" yaml:"mtu"`
 	PublicKey        wgtypes.Key      `json:"publickey" yaml:"publickey"`
 	MacAddress       net.HardwareAddr `json:"macaddress" yaml:"macaddress"`
-	TrafficKeyPublic []byte           `json:"traffickeypublic" yaml:"trafficekeypublic"`
+	TrafficKeyPublic []byte           `json:"traffickeypublic" yaml:"traffickeypublic"`
 	InternetGateway  net.UDPAddr      `json:"internetgateway" yaml:"internetgateway"`
 	Nodes            []string         `json:"nodes" yaml:"nodes"`
 	IsRelayed        bool             `json:"isrelayed" yaml:"isrelayed"`
@@ -37,9 +52,10 @@ type Host struct {
 	IsRelay          bool             `json:"isrelay" yaml:"isrelay"`
 	RelayedHosts     []string         `json:"relay_hosts" yaml:"relay_hosts"`
 	Interfaces       []Iface          `json:"interfaces" yaml:"interfaces"`
-	DefaultInterface string           `json:"defaultinterface" yaml:"defautlinterface"`
+	DefaultInterface string           `json:"defaultinterface" yaml:"defaultinterface"`
 	EndpointIP       net.IP           `json:"endpointip" yaml:"endpointip"`
 	ProxyEnabled     bool             `json:"proxy_enabled" yaml:"proxy_enabled"`
+	ProxyEnabledSet  bool             `json:"proxy_enabled_updated" yaml:"proxy_enabled_updated"`
 	IsDocker         bool             `json:"isdocker" yaml:"isdocker"`
 	IsK8S            bool             `json:"isk8s" yaml:"isk8s"`
 	IsStatic         bool             `json:"isstatic" yaml:"isstatic"`

+ 23 - 14
models/metrics.go

@@ -15,24 +15,33 @@ type Metrics struct {
 
 // Metric - holds a metric for data between nodes
 type Metric struct {
-	NodeName      string        `json:"node_name" bson:"node_name" yaml:"node_name"`
-	Uptime        int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
-	TotalTime     int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
-	Latency       int64         `json:"latency" bson:"latency" yaml:"latency"`
-	TotalReceived int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
-	TotalSent     int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
-	ActualUptime  time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
-	PercentUp     float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
-	Connected     bool          `json:"connected" bson:"connected" yaml:"connected"`
+	NodeName         string        `json:"node_name" bson:"node_name" yaml:"node_name"`
+	Uptime           int64         `json:"uptime" bson:"uptime" yaml:"uptime"`
+	TotalTime        int64         `json:"totaltime" bson:"totaltime" yaml:"totaltime"`
+	Latency          int64         `json:"latency" bson:"latency" yaml:"latency"`
+	TotalReceived    int64         `json:"totalreceived" bson:"totalreceived" yaml:"totalreceived"`
+	TotalSent        int64         `json:"totalsent" bson:"totalsent" yaml:"totalsent"`
+	ActualUptime     time.Duration `json:"actualuptime" bson:"actualuptime" yaml:"actualuptime"`
+	PercentUp        float64       `json:"percentup" bson:"percentup" yaml:"percentup"`
+	Connected        bool          `json:"connected" bson:"connected" yaml:"connected"`
+	CollectedByProxy bool          `json:"collected_by_proxy" bson:"collected_by_proxy" yaml:"collected_by_proxy"`
 }
 
 // IDandAddr - struct to hold ID and primary Address
 type IDandAddr struct {
-	ID              string  `json:"id" bson:"id" yaml:"id"`
-	Address         string  `json:"address" bson:"address" yaml:"address"`
-	Name            string  `json:"name" bson:"name" yaml:"name"`
-	IsServer        string  `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
-	Network         string  `json:"network" bson:"network" yaml:"network" validate:"network"`
+	ID              string `json:"id" bson:"id" yaml:"id"`
+	Address         string `json:"address" bson:"address" yaml:"address"`
+	Name            string `json:"name" bson:"name" yaml:"name"`
+	IsServer        string `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
+	Network         string `json:"network" bson:"network" yaml:"network" validate:"network"`
+	ProxyListenPort int    `json:"proxy_listen_port" yaml:"proxy_listen_port"`
+}
+
+// HostInfoMap - map of host public keys to host networking info
+type HostInfoMap map[string]HostNetworkInfo
+
+// HostNetworkInfo - holds info related to host networking (used for client side peer calculations)
+type HostNetworkInfo struct {
 	Interfaces      []Iface `json:"interfaces" yaml:"interfaces"`
 	ProxyListenPort int     `json:"proxy_listen_port" yaml:"proxy_listen_port"`
 }

+ 2 - 3
models/migrate.go

@@ -2,7 +2,6 @@ package models
 
 // MigrationData struct needed to create new v0.18.0 node from v.0.17.X node
 type MigrationData struct {
-	JoinData     JoinData
-	LegacyNodeID string
-	Password     string
+	NewHost     Host
+	LegacyNodes []LegacyNode
 }

+ 13 - 11
models/mqtt.go

@@ -8,17 +8,18 @@ import (
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host          Host                 `json:"host" bson:"host" yaml:"host"`
-	Server        string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs   []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers     []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers         []wgtypes.PeerConfig
-	HostPeerIDs   HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
-	ProxyUpdate   ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
-	EgressInfo    map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
-	IngressInfo   IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
-	PeerIDs       PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	Host            Host                 `json:"host" bson:"host" yaml:"host"`
+	Server          string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers           []wgtypes.PeerConfig
+	HostPeerIDs     HostPeerMap           `json:"hostpeerids" bson:"hostpeerids" yaml:"hostpeerids"`
+	ProxyUpdate     ProxyManagerPayload   `json:"proxy_update" bson:"proxy_update" yaml:"proxy_update"`
+	EgressInfo      map[string]EgressInfo `json:"egress_info" bson:"egress_info" yaml:"egress_info"` // map key is node ID
+	IngressInfo     IngressInfo           `json:"ingress_info" bson:"ext_peers" yaml:"ext_peers"`
+	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
 }
 
 // IngressInfo - struct for ingress info
@@ -41,6 +42,7 @@ type PeerRouteInfo struct {
 	PeerAddr net.IPNet `json:"peer_addr" yaml:"peer_addr"`
 	PeerKey  string    `json:"peer_key" yaml:"peer_key"`
 	Allow    bool      `json:"allow" yaml:"allow"`
+	ID       string    `json:"id,omitempty" yaml:"id,omitempty"`
 }
 
 // ExtClientInfo - struct for ext. client and it's peers

+ 1 - 0
models/proxy.go

@@ -37,6 +37,7 @@ type RelayedConf struct {
 type PeerConf struct {
 	Proxy            bool         `json:"proxy"`
 	PublicListenPort int32        `json:"public_listen_port"`
+	ProxyListenPort  int          `json:"proxy_listen_port"`
 	IsExtClient      bool         `json:"is_ext_client"`
 	Address          net.IP       `json:"address"`
 	ExtInternalIp    net.IP       `json:"ext_internal_ip"`

+ 44 - 2
mq/handlers.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"math"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -185,10 +186,14 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				logger.Log(0, "failed to send new node to host", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
 				return
 			} else {
-				if err = PublishSingleHostPeerUpdate(context.Background(), currentHost, nil); err != nil {
+				if err = PublishSingleHostPeerUpdate(context.Background(), currentHost, nil, nil); err != nil {
 					logger.Log(0, "failed peers publish after join acknowledged", hostUpdate.Host.Name, currentHost.ID.String(), err.Error())
 					return
 				}
+				if err = handleNewNodeDNS(&hu.Host, &hu.Node); err != nil {
+					logger.Log(0, "failed to send dns update after node,", hu.Node.ID.String(), ", added to host", hu.Host.Name, err.Error())
+					return
+				}
 			}
 		}
 	case models.UpdateHost:
@@ -279,7 +284,7 @@ func UpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 			logger.Log(2, "updating peers after node", currentNode.ID.String(), currentNode.Network, "detected connectivity issues")
 			host, err := logic.GetHost(currentNode.HostID.String())
 			if err == nil {
-				if err = PublishSingleHostPeerUpdate(context.Background(), host, nil); err != nil {
+				if err = PublishSingleHostPeerUpdate(context.Background(), host, nil, nil); err != nil {
 					logger.Log(0, "failed to publish update after failover peer change for node", currentNode.ID.String(), currentNode.Network)
 				}
 			}
@@ -362,6 +367,21 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 		oldMetric := oldMetrics.Connectivity[k]
 		currMetric.TotalTime += oldMetric.TotalTime
 		currMetric.Uptime += oldMetric.Uptime // get the total uptime for this connection
+		if currMetric.CollectedByProxy {
+			currMetric.TotalReceived += oldMetric.TotalReceived
+			currMetric.TotalSent += oldMetric.TotalSent
+		} else {
+			if currMetric.TotalReceived < oldMetric.TotalReceived {
+				currMetric.TotalReceived += oldMetric.TotalReceived
+			} else {
+				currMetric.TotalReceived += int64(math.Abs(float64(currMetric.TotalReceived) - float64(oldMetric.TotalReceived)))
+			}
+			if currMetric.TotalSent < oldMetric.TotalSent {
+				currMetric.TotalSent += oldMetric.TotalSent
+			} else {
+				currMetric.TotalSent += int64(math.Abs(float64(currMetric.TotalSent) - float64(oldMetric.TotalSent)))
+			}
+		}
 		if currMetric.Uptime == 0 || currMetric.TotalTime == 0 {
 			currMetric.PercentUp = 0
 		} else {
@@ -405,3 +425,25 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) boo
 	}
 	return shouldUpdate
 }
+
+func handleNewNodeDNS(host *models.Host, node *models.Node) error {
+	dns := models.DNSUpdate{
+		Action: models.DNSInsert,
+		Name:   host.Name + "." + node.Network,
+	}
+	if node.Address.IP != nil {
+		dns.Address = node.Address.IP.String()
+		if err := PublishDNSUpdate(node.Network, dns); err != nil {
+			return err
+		}
+	} else if node.Address6.IP != nil {
+		dns.Address = node.Address6.IP.String()
+		if err := PublishDNSUpdate(node.Network, dns); err != nil {
+			return err
+		}
+	}
+	if err := PublishAllDNS(node); err != nil {
+		return err
+	}
+	return nil
+}

+ 27 - 5
mq/publishers.go

@@ -27,7 +27,7 @@ func PublishPeerUpdate() error {
 	logic.ResetPeerUpdateContext()
 	for _, host := range hosts {
 		host := host
-		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil); err != nil {
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, nil); err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
 	}
@@ -49,7 +49,29 @@ func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
 	logic.ResetPeerUpdateContext()
 	for _, host := range hosts {
 		host := host
-		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, delNode); err != nil {
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, delNode, nil); err != nil {
+			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+		}
+	}
+	return err
+}
+
+// PublishDeletedClientPeerUpdate --- determines and publishes a peer update
+// to all the hosts with a deleted ext client to account for
+func PublishDeletedClientPeerUpdate(delClient *models.ExtClient) error {
+	if !servercfg.IsMessageQueueBackend() {
+		return nil
+	}
+
+	hosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(1, "err getting all hosts", err.Error())
+		return err
+	}
+	logic.ResetPeerUpdateContext()
+	for _, host := range hosts {
+		host := host
+		if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, delClient); err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
 	}
@@ -57,9 +79,9 @@ func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
 }
 
 // PublishSingleHostPeerUpdate --- determines and publishes a peer update to one host
-func PublishSingleHostPeerUpdate(ctx context.Context, host *models.Host, deletedNode *models.Node) error {
+func PublishSingleHostPeerUpdate(ctx context.Context, host *models.Host, deletedNode *models.Node, deletedClient *models.ExtClient) error {
 
-	peerUpdate, err := logic.GetPeerUpdateForHost(ctx, "", host, deletedNode)
+	peerUpdate, err := logic.GetPeerUpdateForHost(ctx, "", host, deletedNode, deletedClient)
 	if err != nil {
 		return err
 	}
@@ -429,7 +451,7 @@ func sendPeers() {
 		for _, host := range hosts {
 			host := host
 			logger.Log(2, "sending scheduled peer update (5 min)")
-			if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil); err != nil {
+			if err = PublishSingleHostPeerUpdate(logic.PeerUpdateCtx, &host, nil, nil); err != nil {
 				logger.Log(1, "error publishing peer updates for host: ", host.ID.String(), " Err: ", err.Error())
 			}
 		}

+ 8 - 0
mq/util.go

@@ -12,6 +12,10 @@ import (
 )
 
 func decryptMsgWithHost(host *models.Host, msg []byte) ([]byte, error) {
+	if host.OS == models.OS_Types.IoT { // just pass along IoT messages
+		return msg, nil
+	}
+
 	trafficKey, trafficErr := logic.RetrievePrivateTrafficKey() // get server private key
 	if trafficErr != nil {
 		return nil, trafficErr
@@ -41,6 +45,10 @@ func decryptMsg(node *models.Node, msg []byte) ([]byte, error) {
 }
 
 func encryptMsg(host *models.Host, msg []byte) ([]byte, error) {
+	if host.OS == models.OS_Types.IoT {
+		return msg, nil
+	}
+
 	// fetch server public key to be certain hasn't changed in transit
 	trafficKey, trafficErr := logic.RetrievePrivateTrafficKey()
 	if trafficErr != nil {

+ 14 - 25
release.md

@@ -1,38 +1,27 @@
-# Netmaker v0.18.3
+# Netmaker v0.18.4
 
 ## **Wait till out of pre-release to fully upgrade**
 
 ## whats new
-- Forced node deletions, if a host doesn't not receive message to delete a node, you can forcefully remove it by deleting it twice from UI/CLI  
-  - Allows user to remove orpahned Nodes + Hosts easier
-- EMQX ACLs, if using EMQX as broker, ACLs per host will be created, enhancing security around messages
-- You can now create ext clients with your own public key, but this feature will not be represented on current UI (new UI on the horizon)
-- STUN is now represented as a list including your NM server + 2 we are hosting + 2 of googles (clients will only use 2) for better NAT detection
-  - you specify which STUN servers to use with STUN_LIST env variable
+- Logic for ext client ACLs (not really usable until new UI is finished)
+- Default proxy mode, enables users to determine if all Hosts should have proxy enabled/disabled/auto by default
+  - specify with DEFAULT_PROXY_MODE="on/off/auto" 
     
 ## whats fixed
-- More Peer calculation improvements
-- JSON output on list commands for `nmctl`
+- Proxy Peer calculation improvements
+- DNS is populated correctly after registration by enrollment key
+- Migrate is functional for Windows/Mac **note** Ports may be set to 0 after an upgrade, can be adjusted via UI to fix
+- Interface data is sent on netclient register
 - Upgrade script
+- Latency issue with Node <-> Node Metrics
 - Ports set from server for Hosts on register/join are actually used
-- **CLients**
-  - More efficient Windows daemon handling
-  - Better peer route setting on clients
-  - Some commands involving the message queue on client have been fixed
-  - NFTables masquerading issue
-  - Some logging has been adjusted
-  - Migrations on Linux work for 0.17.x - 0.18.3
-  - EnrollmentKEys in an HA setup should function fine now
-  - Registration by enrollment key on client GUI
 
 ## known issues
-- Network interface routes may be removed after sometime/unintended network update
 - Caddy does not handle netmaker exporter well for EE
-- Incorrect latency on metrics (EE)
-- Swagger docs not up to date
-- Lengthy delay when you create an ext client
-- issues connecting over IPv6 on Macs
+- Migration causes a listen port of 0 for upgraded hosts
+- Docker clients can not re-join after deletion
+- Innacurate Ext Client Metrics 
+- Issue with Mac + IPv6 addressing
 - Nodes on same local network may not always connect
-- Netclient GUI shows egress range(s) twice
-- DNS entries are not sent after registration with EnrollmentKeys
+- List populates egress ranges twice
 - If you do NOT set STUN_LIST on server, it could lead to strange behavior on client

+ 544 - 359
scripts/nm-quick.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-LATEST="v0.18.3"
+LATEST="v0.18.4"
 
 print_logo() {(
 cat << "EOF"
@@ -30,100 +30,219 @@ unset INSTALL_TYPE
 unset BUILD_TYPE
 unset BUILD_TAG
 unset IMAGE_TAG
+unset AUTO_BUILD
 
-usage () {(
-    echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag]"
+# usage - displays usage instructions
+usage () {
+    echo "usage: ./nm-quick.sh [-e] [-b buildtype] [-t tag] [-a auto]"
     echo "  -e      if specified, will install netmaker EE"
     echo "  -b      type of build; options:"
 	echo "          \"version\" - will install a specific version of Netmaker using remote git and dockerhub"
 	echo "          \"local\": - will install by cloning repo and and building images from git"
-	echo "          \"branch\": - will install a specific branch using remote git and dockerhub "
+	echo "          \"branch\": - will install a specific branch using remote git and dockerhub"
     echo "  -t      tag of build; if buildtype=version, tag=version. If builtype=branch or builtype=local, tag=branch"
+    echo "  -a      auto-build; skip prompts and use defaults, if none provided"
     echo "examples:"
-	echo "          nm-quick.sh -e -b version -t v0.18.3"
+	echo "          nm-quick.sh -e -b version -t v0.18.4"
 	echo "          nm-quick.sh -e -b local -t feature_v0.17.2_newfeature"	
 	echo "          nm-quick.sh -e -b branch -t develop"
     exit 1
-)}
+}
 
-while getopts evb:t: flag
+while getopts evab:t: flag
 do
-    case "${flag}" in
-        e) 
+	case "${flag}" in
+		e) 
 			INSTALL_TYPE="ee"
 			;;
 		v) 
 			usage
 			exit 0
 			;;
-        b) 
+		a) 
+			AUTO_BUILD="on"
+			;;			
+		b) 
 			BUILD_TYPE=${OPTARG}
 			if [[ ! "$BUILD_TYPE" =~ ^(version|local|branch)$ ]]; then
-    			echo "error: $BUILD_TYPE is invalid"
+				echo "error: $BUILD_TYPE is invalid"
 				echo "valid options: version, local, branch"
 				usage
 				exit 1
 			fi
 			;;
-        t) 
+		t) 
 			BUILD_TAG=${OPTARG}
 			;;
-    esac
+	esac
 done
 
-if [ -z "$BUILD_TYPE" ]; then
-	BUILD_TYPE="version"
-	BUILD_TAG=$LATEST
-fi
+# print_logo - prints the netmaker logo
+print_logo() {
+cat << "EOF"
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+                                                                                         
+ __   __     ______     ______   __    __     ______     __  __     ______     ______    
+/\ "-.\ \   /\  ___\   /\__  _\ /\ "-./  \   /\  __ \   /\ \/ /    /\  ___\   /\  == \   
+\ \ \-.  \  \ \  __\   \/_/\ \/ \ \ \-./\ \  \ \  __ \  \ \  _"-.  \ \  __\   \ \  __<   
+ \ \_\\"\_\  \ \_____\    \ \_\  \ \_\ \ \_\  \ \_\ \_\  \ \_\ \_\  \ \_____\  \ \_\ \_\ 
+  \/_/ \/_/   \/_____/     \/_/   \/_/  \/_/   \/_/\/_/   \/_/\/_/   \/_____/   \/_/ /_/ 
+                                                                                                                                                                                                 
 
-if [ -z "$BUILD_TAG" ] && [ "$BUILD_TYPE" = "version" ]; then
-	BUILD_TAG=$LATEST
-fi
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+EOF
+}
 
-if [ -z "$BUILD_TAG" ] && [ ! -z "$BUILD_TYPE" ]; then
-	echo "error: must specify build tag when build type \"$BUILD_TYPE\" is specified"
-	usage		
-	exit 1
-fi
+# set_buildinfo - sets the information based on script input for how the installation should be run
+set_buildinfo() {
 
-IMAGE_TAG=$(sed 's/\//-/g' <<< "$BUILD_TAG")
+	if [ -z "$BUILD_TYPE" ]; then
+		BUILD_TYPE="version"
+		BUILD_TAG=$LATEST
+	fi
 
-if [ "$1" = "ce" ]; then
-	INSTALL_TYPE="ce"
-elif [ "$1" = "ee" ]; then
-	INSTALL_TYPE="ee"
-fi
+	if [ -z "$BUILD_TAG" ] && [ "$BUILD_TYPE" = "version" ]; then
+		BUILD_TAG=$LATEST
+	fi
 
-if [ -z "$INSTALL_TYPE" ]; then
-	echo "-----------------------------------------------------"
-	echo "Would you like to install Netmaker Community Edition (CE), or Netmaker Enterprise Edition (EE)?"
-	echo "EE will require you to create an account at https://dashboard.license.netmaker.io"
-	echo "-----------------------------------------------------"
-	select install_option in "Community Edition" "Enterprise Edition"; do
-	case $REPLY in
-		1)
-		echo "installing Netmaker CE"
+	if [ -z "$BUILD_TAG" ] && [ ! -z "$BUILD_TYPE" ]; then
+		echo "error: must specify build tag when build type \"$BUILD_TYPE\" is specified"
+		usage		
+		exit 1
+	fi
+
+	IMAGE_TAG=$(sed 's/\//-/g' <<< "$BUILD_TAG")
+
+	if [ "$1" = "ce" ]; then
 		INSTALL_TYPE="ce"
-		break
-		;;      
-		2)
-		echo "installing Netmaker EE"
+	elif [ "$1" = "ee" ]; then
 		INSTALL_TYPE="ee"
-		break
-		;;
-		*) echo "invalid option $REPLY";;
-	esac
-	done
-fi
-echo "-----------Build Options-----------------------------"
-echo "    EE or CE: $INSTALL_TYPE";
-echo "  Build Type: $BUILD_TYPE";
-echo "   Build Tag: $BUILD_TAG";
-echo "   Image Tag: $IMAGE_TAG";
-echo "-----------------------------------------------------"
+	fi
 
-print_logo
+	if [ "$AUTO_BUILD" = "on" ] && [ -z "$INSTALL_TYPE" ]; then
+		INSTALL_TYPE="ce"
+	elif [ -z "$INSTALL_TYPE" ]; then
+		echo "-----------------------------------------------------"
+		echo "Would you like to install Netmaker Community Edition (CE), or Netmaker Enterprise Edition (EE)?"
+		echo "EE will require you to create an account at https://dashboard.license.netmaker.io"
+		echo "-----------------------------------------------------"
+		select install_option in "Community Edition" "Enterprise Edition"; do
+		case $REPLY in
+			1)
+			echo "installing Netmaker CE"
+			INSTALL_TYPE="ce"
+			break
+			;;      
+			2)
+			echo "installing Netmaker EE"
+			INSTALL_TYPE="ee"
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
+	echo "-----------Build Options-----------------------------"
+	echo "    EE or CE: $INSTALL_TYPE";
+	echo "  Build Type: $BUILD_TYPE";
+	echo "   Build Tag: $BUILD_TAG";
+	echo "   Image Tag: $IMAGE_TAG";
+	echo "-----------------------------------------------------"
 
+}
+
+# install_yq - install yq if not present
+install_yq() {
+	if ! command -v yq &> /dev/null; then
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+		chmod +x /usr/bin/yq
+	fi
+	set +e
+	if ! command -v yq &> /dev/null; then
+		set -e
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
+		chmod +x /usr/bin/yq
+	fi
+	set -e
+	if ! command -v yq &> /dev/null; then
+		echo "failed to install yq. Please install yq and try again."
+		echo "https://github.com/mikefarah/yq/#install"
+		exit 1
+	fi	
+}
+
+# setup_netclient - adds netclient to docker-compose
+setup_netclient() {
+
+
+	# yq ".services.netclient += {\"container_name\": \"netclient\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient += {\"image\": \"gravitl/netclient:$IMAGE_TAG\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient += {\"hostname\": \"netmaker-1\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient += {\"network_mode\": \"host\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient.depends_on += [\"netmaker\"]" -i /root/docker-compose.yml
+	# yq ".services.netclient += {\"restart\": \"always\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient.environment += {\"TOKEN\": \"$TOKEN\"}" -i /root/docker-compose.yml
+	# yq ".services.netclient.volumes += [\"/etc/netclient:/etc/netclient\"]" -i /root/docker-compose.yml
+	# yq ".services.netclient.cap_add += [\"NET_ADMIN\"]" -i /root/docker-compose.yml
+	# yq ".services.netclient.cap_add += [\"NET_RAW\"]" -i /root/docker-compose.yml
+	# yq ".services.netclient.cap_add += [\"SYS_MODULE\"]" -i /root/docker-compose.yml
+
+	# docker-compose up -d
+
+	set +e
+	netclient uninstall
+	set -e
+
+	wget -O netclient https://github.com/gravitl/netclient/releases/download/$LATEST/netclient_linux_amd64
+	chmod +x netclient
+	./netclient install
+	netclient join -t $TOKEN
+
+	echo "waiting for client to become available"
+	wait_seconds 10 
+}
+
+# configure_netclient - configures server's netclient as a default host and an ingress gateway
+configure_netclient() {
+
+	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
+	echo "join complete. New node ID: $NODE_ID"
+	HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
+	echo "For first join, making host a default"
+	echo "Host ID: $HOST_ID"
+	# set as a default host
+	set +e
+	nmctl host update $HOST_ID --default
+	sleep 5
+	nmctl node create_ingress netmaker $NODE_ID
+	set -e
+}
+
+# setup_nmctl - pulls nmctl and makes it executable
+setup_nmctl() {
+
+	# DEV_TEMP - Temporary instructions for testing
+	wget -O /usr/bin/nmctl https://fileserver.netmaker.org/testing/nmctl
+
+	# RELEASE_REPLACE - Use this once release is ready
+	# wget https://github.com/gravitl/netmaker/releases/download/v0.17.1/nmctl
+    chmod +x /usr/bin/nmctl
+    echo "using server api.$NETMAKER_BASE_DOMAIN"
+    echo "using master key $MASTER_KEY"
+    nmctl context set default --endpoint="https://api.$NETMAKER_BASE_DOMAIN" --master_key="$MASTER_KEY"
+    nmctl context use default
+    RESP=$(nmctl network list)
+    if [[ $RESP == *"unauthorized"* ]]; then
+        echo "Unable to properly configure NMCTL, exiting..."
+        exit 1
+    fi
+}
+
+# wait_seconds - wait for the specified period of time
 wait_seconds() {(
   for ((a=1; a <= $1; a++))
   do
@@ -132,7 +251,11 @@ wait_seconds() {(
   done
 )}
 
+# confirm - get user input to confirm that they want to perform the next step
 confirm() {(
+  if [ "$AUTO_BUILD" = "on" ]; then
+	return 0
+  fi
   while true; do
       read -p 'Does everything look right? [y/n]: ' yn
       case $yn in
@@ -143,6 +266,7 @@ confirm() {(
   done
 )}
 
+# local_install_setup - builds artifacts based on specified branch locally to use in install
 local_install_setup() {(
 	rm -rf netmaker-tmp
 	mkdir netmaker-tmp
@@ -165,92 +289,82 @@ local_install_setup() {(
 	rm -rf netmaker-tmp
 )}
 
-echo "checking dependencies..."
-
-OS=$(uname)
-
-if [ -f /etc/debian_version ]; then
-	dependencies="git wireguard wireguard-tools jq docker.io docker-compose"
-	update_cmd='apt update'
-	install_cmd='apt-get install -y'
-elif [ -f /etc/alpine-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='apk update'
-	install_cmd='apk --update add'
-elif [ -f /etc/centos-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='yum update'
-	install_cmd='yum install -y'
-elif [ -f /etc/fedora-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='dnf update'
-	install_cmd='dnf install -y'
-elif [ -f /etc/redhat-release ]; then
-	dependencies="git wireguard jq docker.io docker-compose"
-	update_cmd='yum update'
-	install_cmd='yum install -y'
-elif [ -f /etc/arch-release ]; then
-    	dependecies="git wireguard-tools jq docker.io docker-compose"
-	update_cmd='pacman -Sy'
-	install_cmd='pacman -S --noconfirm'
-elif [ "${OS}" = "FreeBSD" ]; then
-	dependencies="git wireguard wget jq docker.io docker-compose"
-	update_cmd='pkg update'
-	install_cmd='pkg install -y'
-elif [ -f /etc/turris-version ]; then
-	dependencies="git wireguard-tools bash jq docker.io docker-compose"
-	OS="TurrisOS"
-	update_cmd='opkg update'	
-	install_cmd='opkg install'
-elif [ -f /etc/openwrt_release ]; then
-	dependencies="git wireguard-tools bash jq docker.io docker-compose"
-	OS="OpenWRT"
-	update_cmd='opkg update'	
-	install_cmd='opkg install'
-else
-	install_cmd=''
-fi
+# install_dependencies - install necessary packages to run netmaker 
+install_dependencies() {
+	echo "checking dependencies..."
+
+	OS=$(uname)
+	if [ -f /etc/debian_version ]; then
+		dependencies="git wireguard wireguard-tools jq docker.io docker-compose"
+		update_cmd='apt update'
+		install_cmd='apt-get install -y'
+	elif [ -f /etc/alpine-release ]; then
+		dependencies="git wireguard jq docker.io docker-compose"
+		update_cmd='apk update'
+		install_cmd='apk --update add'
+	elif [ -f /etc/centos-release ]; then
+		dependencies="git wireguard jq docker.io docker-compose"
+		update_cmd='yum update'
+		install_cmd='yum install -y'
+	elif [ -f /etc/fedora-release ]; then
+		dependencies="git wireguard jq docker.io docker-compose"
+		update_cmd='dnf update'
+		install_cmd='dnf install -y'
+	elif [ -f /etc/redhat-release ]; then
+		dependencies="git wireguard jq docker.io docker-compose"
+		update_cmd='yum update'
+		install_cmd='yum install -y'
+	elif [ -f /etc/arch-release ]; then
+			dependecies="git wireguard-tools jq docker.io docker-compose"
+		update_cmd='pacman -Sy'
+		install_cmd='pacman -S --noconfirm'
+	elif [ "${OS}" = "FreeBSD" ]; then
+		dependencies="git wireguard wget jq docker.io docker-compose"
+		update_cmd='pkg update'
+		install_cmd='pkg install -y'
+	elif [ -f /etc/turris-version ]; then
+		dependencies="git wireguard-tools bash jq docker.io docker-compose"
+		OS="TurrisOS"
+		update_cmd='opkg update'	
+		install_cmd='opkg install'
+	elif [ -f /etc/openwrt_release ]; then
+		dependencies="git wireguard-tools bash jq docker.io docker-compose"
+		OS="OpenWRT"
+		update_cmd='opkg update'	
+		install_cmd='opkg install'
+	else
+		install_cmd=''
+	fi
 
-if [ -z "${install_cmd}" ]; then
-        echo "OS unsupported for automatic dependency install"
-	exit 1
-fi
+	if [ -z "${install_cmd}" ]; then
+			echo "OS unsupported for automatic dependency install"
+		exit 1
+	fi
 
-set -- $dependencies
+	set -- $dependencies
 
-${update_cmd}
+	${update_cmd}
 
-while [ -n "$1" ]; do
-	if [ "${OS}" = "FreeBSD" ]; then
-		is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
-		if [ "$is_installed" != "" ]; then
-			echo "  " $1 is installed
-		else
-			echo "  " $1 is not installed. Attempting install.
-			${install_cmd} $1
-			sleep 5
+	while [ -n "$1" ]; do
+		if [ "${OS}" = "FreeBSD" ]; then
 			is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
 			if [ "$is_installed" != "" ]; then
 				echo "  " $1 is installed
-			elif [ -x "$(command -v $1)" ]; then
-				echo "  " $1 is installed
 			else
-				echo "  " FAILED TO INSTALL $1
-				echo "  " This may break functionality.
-			fi
-		fi	
-	else
-		if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
-			is_installed=$(opkg list-installed $1 | grep $1)
+				echo "  " $1 is not installed. Attempting install.
+				${install_cmd} $1
+				sleep 5
+				is_installed=$(pkg check -d $1 | grep "Checking" | grep "done")
+				if [ "$is_installed" != "" ]; then
+					echo "  " $1 is installed
+				elif [ -x "$(command -v $1)" ]; then
+					echo "  " $1 is installed
+				else
+					echo "  " FAILED TO INSTALL $1
+					echo "  " This may break functionality.
+				fi
+			fi	
 		else
-			is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
-		fi
-		if [ "${is_installed}" != "" ]; then
-			echo "    " $1 is installed
-		else
-			echo "    " $1 is not installed. Attempting install.
-			${install_cmd} $1
-			sleep 5
 			if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
 				is_installed=$(opkg list-installed $1 | grep $1)
 			else
@@ -258,290 +372,361 @@ while [ -n "$1" ]; do
 			fi
 			if [ "${is_installed}" != "" ]; then
 				echo "    " $1 is installed
-			elif [ -x "$(command -v $1)" ]; then
-				echo "  " $1 is installed
 			else
-				echo "  " FAILED TO INSTALL $1
-				echo "  " This may break functionality.
+				echo "    " $1 is not installed. Attempting install.
+				${install_cmd} $1
+				sleep 5
+				if [ "${OS}" = "OpenWRT" ] || [ "${OS}" = "TurrisOS" ]; then
+					is_installed=$(opkg list-installed $1 | grep $1)
+				else
+					is_installed=$(dpkg-query -W --showformat='${Status}\n' $1 | grep "install ok installed")
+				fi
+				if [ "${is_installed}" != "" ]; then
+					echo "    " $1 is installed
+				elif [ -x "$(command -v $1)" ]; then
+					echo "  " $1 is installed
+				else
+					echo "  " FAILED TO INSTALL $1
+					echo "  " This may break functionality.
+				fi
 			fi
 		fi
+		shift
+	done
+
+	echo "-----------------------------------------------------"
+	echo "dependency check complete"
+	echo "-----------------------------------------------------"
+} 
+set -e
+
+# set_install_vars - sets the variables that will be used throughout installation
+set_install_vars() {
+
+	IP_ADDR=$(dig -4 myip.opendns.com @resolver1.opendns.com +short)
+	if [ "$IP_ADDR" = "" ]; then
+		IP_ADDR=$(curl -s ifconfig.me)
 	fi
-	shift
-done
 
-echo "-----------------------------------------------------"
-echo "dependency check complete"
-echo "-----------------------------------------------------"
+	NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
+	COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
+	SERVER_PUBLIC_IP=$IP_ADDR
+	MASTER_KEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+	DOMAIN_TYPE=""
+	echo "-----------------------------------------------------"
+	echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
+	echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_PUBLIC_IP"
+	echo "-----------------------------------------------------"
 
-wait_seconds 3
+	if [ "$AUTO_BUILD" = "on" ]; then
+			DOMAIN_TYPE="auto"
+	else
+		select domain_option in "Auto Generated ($NETMAKER_BASE_DOMAIN)" "Custom Domain (e.x: netmaker.example.com)"; do
+		case $REPLY in
+			1)
+			echo "using $NETMAKER_BASE_DOMAIN for base domain"
+			DOMAIN_TYPE="auto"
+			break
+			;;      
+			2)
+			read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_PUBLIC_IP first): " domain
+			NETMAKER_BASE_DOMAIN=$domain
+			echo "using $NETMAKER_BASE_DOMAIN"
+			DOMAIN_TYPE="custom"
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
 
+	wait_seconds 2
 
-if [ "$BUILD_TYPE" = "local" ]; then
-	local_install_setup
-fi
+	echo "-----------------------------------------------------"
+	echo "The following subdomains will be used:"
+	echo "          dashboard.$NETMAKER_BASE_DOMAIN"
+	echo "                api.$NETMAKER_BASE_DOMAIN"
+	echo "             broker.$NETMAKER_BASE_DOMAIN"
 
-set -e
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		echo "         prometheus.$NETMAKER_BASE_DOMAIN"
+		echo "  netmaker-exporter.$NETMAKER_BASE_DOMAIN"
+		echo "            grafana.$NETMAKER_BASE_DOMAIN"
+	fi
 
-IP_ADDR=$(dig -4 myip.opendns.com @resolver1.opendns.com +short)
-if [ "$IP_ADDR" = "" ]; then
-	IP_ADDR=$(curl -s ifconfig.me)
-fi
+	echo "-----------------------------------------------------"
 
-NETMAKER_BASE_DOMAIN=nm.$(echo $IP_ADDR | tr . -).nip.io
-COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p')
-SERVER_PUBLIC_IP=$IP_ADDR
-MASTER_KEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
-DOMAIN_TYPE=""
-echo "-----------------------------------------------------"
-echo "Would you like to use your own domain for netmaker, or an auto-generated domain?"
-echo "To use your own domain, add a Wildcard DNS record (e.x: *.netmaker.example.com) pointing to $SERVER_PUBLIC_IP"
-echo "-----------------------------------------------------"
-select domain_option in "Auto Generated ($NETMAKER_BASE_DOMAIN)" "Custom Domain (e.x: netmaker.example.com)"; do
-  case $REPLY in
-    1)
-      echo "using $NETMAKER_BASE_DOMAIN for base domain"
-      DOMAIN_TYPE="auto"
-	  break
-      ;;      
-    2)
-      read -p "Enter Custom Domain (make sure  *.domain points to $SERVER_PUBLIC_IP first): " domain
-      NETMAKER_BASE_DOMAIN=$domain
-      echo "using $NETMAKER_BASE_DOMAIN"
-      DOMAIN_TYPE="custom"
-      break
-      ;;
-    *) echo "invalid option $REPLY";;
-  esac
-done
+	if [[ "$DOMAIN_TYPE" == "custom" ]]; then
+		echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_PUBLIC_IP"
+		confirm
+	fi
 
-wait_seconds 2
+	wait_seconds 1
 
-echo "-----------------------------------------------------"
-echo "The following subdomains will be used:"
-echo "          dashboard.$NETMAKER_BASE_DOMAIN"
-echo "                api.$NETMAKER_BASE_DOMAIN"
-echo "             broker.$NETMAKER_BASE_DOMAIN"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
 
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	echo "         prometheus.$NETMAKER_BASE_DOMAIN"
-	echo "  netmaker-exporter.$NETMAKER_BASE_DOMAIN"
-	echo "            grafana.$NETMAKER_BASE_DOMAIN"
-fi
+		echo "-----------------------------------------------------"
+		echo "Provide Details for EE installation:"
+		echo "    1. Log into https://dashboard.license.netmaker.io"
+		echo "    2. Copy License Key Value: https://dashboard.license.netmaker.io/license-keys"
+		echo "    3. Retrieve Account ID: https://dashboard.license.netmaker.io/user"
+		echo "    4. note email address"
+		echo "-----------------------------------------------------"
+		unset LICENSE_KEY
+		while [ -z "$LICENSE_KEY" ]; do
+			read -p "License Key: " LICENSE_KEY
+		done
+		unset ACCOUNT_ID
+		while [ -z ${ACCOUNT_ID} ]; do
+			read -p "Account ID: " ACCOUNT_ID
+		done
+	fi
+
+	unset GET_EMAIL
+	unset RAND_EMAIL
+	RAND_EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
+	if [ -z $AUTO_BUILD ]; then
+		read -p "Email Address for Domain Registration (click 'enter' to use $RAND_EMAIL): " GET_EMAIL
+	fi
+	if [ -z "$GET_EMAIL" ]; then
+	echo "using rand email"
+	EMAIL="$RAND_EMAIL"
+	else
+	EMAIL="$GET_EMAIL"
+	fi
+
+	wait_seconds 1
+
+	unset GET_MQ_USERNAME
+	unset GET_MQ_PASSWORD
+	unset CONFIRM_MQ_PASSWORD
+	echo "Enter Credentials For MQ..."
+	if [ -z $AUTO_BUILD ]; then
+		read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
+	fi
+	if [ -z "$GET_MQ_USERNAME" ]; then
+	echo "using default username for mq"
+	MQ_USERNAME="netmaker"
+	else
+	MQ_USERNAME="$GET_MQ_USERNAME"
+	fi
+
+	MQ_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
+
+	if [ -z $AUTO_BUILD ]; then  
+		select domain_option in "Auto Generated Password" "Input Your Own Password"; do
+			case $REPLY in
+			1)
+			echo "using random password for mq"
+			break
+			;;      
+			2)
+			while true
+			do
+				echo "Enter your Password For MQ: " 
+				read -s GET_MQ_PASSWORD
+				echo "Enter your password again to confirm: "
+				read -s CONFIRM_MQ_PASSWORD
+				if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
+					echo "wrong password entered, try again..."
+					continue
+				fi
+				MQ_PASSWORD="$GET_MQ_PASSWORD"
+				echo "MQ Password Saved Successfully!!"
+				break
+			done
+			break
+			;;
+			*) echo "invalid option $REPLY";;
+		esac
+		done
+	fi
 
-echo "-----------------------------------------------------"
+	wait_seconds 2
+
+	echo "-----------------------------------------------------------------"
+	echo "                SETUP ARGUMENTS"
+	echo "-----------------------------------------------------------------"
+	echo "        domain: $NETMAKER_BASE_DOMAIN"
+	echo "         email: $EMAIL"
+	echo "     public ip: $SERVER_PUBLIC_IP"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		echo "       license: $LICENSE_KEY"
+		echo "    account id: $ACCOUNT_ID"
+	fi
+	echo "-----------------------------------------------------------------"
+	echo "Confirm Settings for Installation"
+	echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
 
-if [[ "$DOMAIN_TYPE" == "custom" ]]; then
-	echo "before continuing, confirm DNS is configured correctly, with records pointing to $SERVER_PUBLIC_IP"
 	confirm
-fi
 
-wait_seconds 1
+}
 
-if [ "$INSTALL_TYPE" = "ee" ]; then
+# install_netmaker - sets the config files and starts docker-compose
+install_netmaker() {
 
-	echo "-----------------------------------------------------"
-	echo "Provide Details for EE installation:"
-	echo "    1. Log into https://dashboard.license.netmaker.io"
-	echo "    2. Copy License Key Value: https://dashboard.license.netmaker.io/license-keys"
-	echo "    3. Retrieve Account ID: https://dashboard.license.netmaker.io/user"
-	echo "    4. note email address"
-	echo "-----------------------------------------------------"
-	unset LICENSE_KEY
-	while [ -z "$LICENSE_KEY" ]; do
-		read -p "License Key: " LICENSE_KEY
-	done
-	unset ACCOUNT_ID
-	while [ -z ${ACCOUNT_ID} ]; do
-		read -p "Account ID: " ACCOUNT_ID
-	done
+	echo "-----------------------------------------------------------------"
+	echo "Beginning installation..."
+	echo "-----------------------------------------------------------------"
 
-fi
+	wait_seconds 3
 
-unset GET_EMAIL
-unset RAND_EMAIL
-RAND_EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
-read -p "Email Address for Domain Registration (click 'enter' to use $RAND_EMAIL): " GET_EMAIL
-if [ -z "$GET_EMAIL" ]; then
-  echo "using rand email"
-  EMAIL="$RAND_EMAIL"
-else
-  EMAIL="$GET_EMAIL"
-fi
+	echo "Pulling config files..."
 
-wait_seconds 1
-
-unset GET_MQ_USERNAME
-unset GET_MQ_PASSWORD
-unset CONFIRM_MQ_PASSWORD
-echo "Enter Credentials For MQ..."
-read -p "MQ Username (click 'enter' to use 'netmaker'): " GET_MQ_USERNAME
-if [ -z "$GET_MQ_USERNAME" ]; then
-  echo "using default username for mq"
-  MQ_USERNAME="netmaker"
-else
-  MQ_USERNAME="$GET_MQ_USERNAME"
-fi
 
-select domain_option in "Auto Generated Password" "Input Your Own Password"; do
-	case $REPLY in
-	1)
-	echo "generating random password for mq"
-	MQ_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 30 ; echo '')
-	break
-	;;      
-    2)
-	while true
-    do
-        echo "Enter your Password For MQ: " 
-        read -s GET_MQ_PASSWORD
-        echo "Enter your password again to confirm: "
-        read -s CONFIRM_MQ_PASSWORD
-        if [ ${GET_MQ_PASSWORD} != ${CONFIRM_MQ_PASSWORD} ]; then
-            echo "wrong password entered, try again..."
-            continue
-        fi
-		MQ_PASSWORD="$GET_MQ_PASSWORD"
-        echo "MQ Password Saved Successfully!!"
-        break
-    done
-      break
-      ;;
-    *) echo "invalid option $REPLY";;
-  esac
-done
+	COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.yml" 
+	CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile"
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.ee.yml" 
+		CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile-EE"
+	fi
+	if [ ! "$BUILD_TYPE" = "local" ]; then
+		wget -O /root/docker-compose.yml $COMPOSE_URL && wget -O /root/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/mosquitto.conf && wget -O /root/Caddyfile $CADDY_URL
+		wget -O /root/wait.sh https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/wait.sh
+	fi
 
+	chmod +x /root/wait.sh
+	mkdir -p /etc/netmaker
 
-wait_seconds 2
+	echo "Setting docker-compose and Caddyfile..."
 
-echo "-----------------------------------------------------------------"
-echo "                SETUP ARGUMENTS"
-echo "-----------------------------------------------------------------"
-echo "        domain: $NETMAKER_BASE_DOMAIN"
-echo "         email: $EMAIL"
-echo "     public ip: $SERVER_PUBLIC_IP"
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	echo "       license: $LICENSE_KEY"
-	echo "    account id: $ACCOUNT_ID"
-fi
-echo "-----------------------------------------------------------------"
-echo "Confirm Settings for Installation"
-echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
+	sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
+	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/Caddyfile
+	sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/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/Caddyfile
+	sed -i "s/REPLACE_MQ_PASSWORD/$MQ_PASSWORD/g" /root/docker-compose.yml
+	sed -i "s/REPLACE_MQ_USERNAME/$MQ_USERNAME/g" /root/docker-compose.yml 
+	if [ "$INSTALL_TYPE" = "ee" ]; then
+		sed -i "s~YOUR_LICENSE_KEY~$LICENSE_KEY~g" /root/docker-compose.yml
+		sed -i "s/YOUR_ACCOUNT_ID/$ACCOUNT_ID/g" /root/docker-compose.yml
+	fi
 
-confirm
+	if [ "$BUILD_TYPE" = "version" ] && [ "$INSTALL_TYPE" = "ee" ]; then
+		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG-ee/g" /root/docker-compose.yml
+	else
+		sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
+	fi
 
+	if [ "$BUILD_TYPE" = "local" ]; then
+		sed -i "s/REPLACE_UI_IMAGE_TAG/$LATEST/g" /root/docker-compose.yml
+	else
+		sed -i "s/REPLACE_UI_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
+	fi
 
-echo "-----------------------------------------------------------------"
-echo "Beginning installation..."
-echo "-----------------------------------------------------------------"
+	echo "Starting containers..."
 
-wait_seconds 3
+	docker-compose -f /root/docker-compose.yml up -d
 
-echo "Pulling config files..."
+	wait_seconds 2
 
+}
 
-COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.yml" 
-CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile"
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	COMPOSE_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/compose/docker-compose.ee.yml" 
-	CADDY_URL="https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/Caddyfile-EE"
-fi
-if [ ! "$BUILD_TYPE" = "local" ]; then
-	wget -O /root/docker-compose.yml $COMPOSE_URL && wget -O /root/mosquitto.conf https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/mosquitto.conf && wget -O /root/Caddyfile $CADDY_URL
-	wget -O /root/wait.sh https://raw.githubusercontent.com/gravitl/netmaker/$BUILD_TAG/docker/wait.sh
-fi
+# test_connection - tests to make sure Caddy has proper SSL certs
+test_connection() {
 
-chmod +x /root/wait.sh
-mkdir -p /etc/netmaker
-
-echo "Setting docker-compose and Caddyfile..."
-
-sed -i "s/SERVER_PUBLIC_IP/$SERVER_PUBLIC_IP/g" /root/docker-compose.yml
-sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/g" /root/Caddyfile
-sed -i "s/NETMAKER_BASE_DOMAIN/$NETMAKER_BASE_DOMAIN/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/Caddyfile
-sed -i "s/REPLACE_MQ_PASSWORD/$MQ_PASSWORD/g" /root/docker-compose.yml
-sed -i "s/REPLACE_MQ_USERNAME/$MQ_USERNAME/g" /root/docker-compose.yml 
-if [ "$INSTALL_TYPE" = "ee" ]; then
-	sed -i "s~YOUR_LICENSE_KEY~$LICENSE_KEY~g" /root/docker-compose.yml
-	sed -i "s/YOUR_ACCOUNT_ID/$ACCOUNT_ID/g" /root/docker-compose.yml
-fi
+	echo "Testing Caddy setup (please be patient, this may take 1-2 minutes)"
+	for i in 1 2 3 4 5 6 7 8
+	do
+	curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
 
-if [ "$BUILD_TYPE" = "version" ] && [ "$INSTALL_TYPE" = "ee" ]; then
-	sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG-ee/g" /root/docker-compose.yml
-else
-	sed -i "s/REPLACE_SERVER_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-fi
+	if [[ "$i" == 8 ]]; then
+	echo "    Caddy is having an issue setting up certificates, please investigate (docker logs caddy)"
+	echo "    Exiting..."
+	exit 1
+	elif [[ "$curlresponse" == *"failed to verify the legitimacy of the server"* ]]; then
+	echo "    Certificates not yet configured, retrying..."
 
-if [ "$BUILD_TYPE" = "local" ]; then
-	sed -i "s/REPLACE_UI_IMAGE_TAG/$LATEST/g" /root/docker-compose.yml
-else
-	sed -i "s/REPLACE_UI_IMAGE_TAG/$IMAGE_TAG/g" /root/docker-compose.yml
-fi
+	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
 
-echo "Starting containers..."
+}
 
-docker-compose -f /root/docker-compose.yml up -d
+# setup_mesh - sets up a default mesh network on the server
+setup_mesh() {
 
-sleep 2
+	wait_seconds 5
 
-test_connection() {
+	echo "Creating netmaker network (10.101.0.0/16)"
+
+	nmctl network create --name netmaker --ipv4_addr 10.101.0.0/16
+
+	wait_seconds 5
+
+	echo "Creating netmaker access key"
+
+	nmctl keys create test1 99999 --name netmaker-key
+	tokenJson=$(nmctl keys create netmaker 2)
+	TOKEN=$(jq -r '.accessstring' <<< ${tokenJson})
+
+	wait_seconds 3
 
-echo "Testing Caddy setup (please be patient, this may take 1-2 minutes)"
-for i in 1 2 3 4 5 6 7 8
-do
-curlresponse=$(curl -vIs https://api.${NETMAKER_BASE_DOMAIN} 2>&1)
-
-if [[ "$i" == 8 ]]; then
-  echo "    Caddy is having an issue setting up certificates, please investigate (docker logs caddy)"
-  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
 }
 
+# print_success - prints a success message upon completion
+print_success() {
+	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 "-----------------------------------------------------------------"
+}
 
-setup_mesh() {( set -e
+# 1. print netmaker logo
+print_logo
 
-wait_seconds 15
+# 2. setup the build instructions
+set_buildinfo
 
-echo "Creating netmaker network (10.101.0.0/16)"
+set +e
 
-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
+# 3. install necessary packages
+install_dependencies
 
-wait_seconds 5
+# 4. install yq if necessary
+install_yq
 
-echo "Creating netmaker access key"
+# 5. if running a local build, clone git and build artifacts
+if [ "$BUILD_TYPE" = "local" ]; then
+	local_install_setup
+fi
 
-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})
+set -e
 
-wait_seconds 3
+# 6. get user input for variables
+set_install_vars
 
-)}
+# 7. get and set config files, startup docker-compose
+install_netmaker
 
 set +e
+
+# 8. make sure Caddy certs are working
 test_connection
 
-wait_seconds 3
+# 9. install the netmaker CLI
+setup_nmctl
 
+# 10. create a default mesh network for netmaker
 setup_mesh
 
-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 "-----------------------------------------------------------------"
+set -e
+
+# 11. add netclient to docker-compose and start it up
+setup_netclient
+
+# 12. make the netclient a default host and ingress gw
+configure_netclient
+
+# 13. print success message
+print_success
 
 # cp -f /etc/skel/.bashrc /root/.bashrc

+ 28 - 22
scripts/nm-upgrade.sh

@@ -39,13 +39,8 @@ confirm() {
 # install_dependencies - install system dependencies necessary for script to run
 install_dependencies() {
   OS=$(uname)
-  is_ubuntu=$(sudo cat /etc/lsb-release | grep "Ubuntu")
-  if [ "${is_ubuntu}" != "" ]; then
-    dependencies="yq jq wireguard jq docker.io docker-compose"
-    update_cmd='apt update'
-    install_cmd='snap install'
-  elif [ -f /etc/debian_version ]; then
-    dependencies="yq jq wireguard jq docker.io docker-compose"
+  if [ -f /etc/debian_version ]; then
+    dependencies="jq wireguard jq docker.io docker-compose"
     update_cmd='apt update'
     install_cmd='apt install -y'
   elif [ -f /etc/centos-release ]; then
@@ -105,20 +100,24 @@ install_dependencies() {
   echo "-----------------------------------------------------"
 }
 
-# get_email- gets upgrader's email address 
-get_email() {
-
-  unset GET_EMAIL
-  unset RAND_EMAIL
-  RAND_EMAIL="$(echo $RANDOM | md5sum  | head -c 16)@email.com"
-  read -p "Email Address for Domain Registration (click 'enter' to use $RAND_EMAIL): " GET_EMAIL
-  if [ -z "$GET_EMAIL" ]; then
-    echo "using rand email"
-    EMAIL="$RAND_EMAIL"
-  else
-    EMAIL="$GET_EMAIL"
-  fi
-
+# install_yq - install yq if not present
+install_yq() {
+	if ! command -v yq &> /dev/null; then
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_$(dpkg --print-architecture)
+		chmod +x /usr/bin/yq
+	fi
+	set +e
+	if ! command -v yq &> /dev/null; then
+		set -e
+		wget -O /usr/bin/yq https://github.com/mikefarah/yq/releases/download/v4.31.1/yq_linux_amd64
+		chmod +x /usr/bin/yq
+	fi
+	set -e
+	if ! command -v yq &> /dev/null; then
+		echo "failed to install yq. Please install yq and try again."
+		echo "https://github.com/mikefarah/yq/#install"
+		exit 1
+	fi	
 }
 
 # collect_server_settings - retrieve server settings from existing compose file
@@ -358,7 +357,7 @@ set_compose() {
   STUN_PORT=3478
 
   # RELEASE_REPLACE - Use this once release is ready
-  #sed -i "s/v0.17.1/v0.18.3/g" /root/docker-compose.yml
+  #sed -i "s/v0.17.1/v0.18.4/g" /root/docker-compose.yml
   yq ".services.netmaker.environment.SERVER_NAME = \"$SERVER_NAME\"" -i /root/docker-compose.yml
   yq ".services.netmaker.environment += {\"BROKER_ENDPOINT\": \"wss://$BROKER_NAME\"}" -i /root/docker-compose.yml  
   yq ".services.netmaker.environment += {\"SERVER_BROKER_ENDPOINT\": \"ws://mq:1883\"}" -i /root/docker-compose.yml  
@@ -602,9 +601,16 @@ if [ $(id -u) -ne 0 ]; then
    exit 1
 fi
 
+set +e
+
 echo "...installing dependencies for script"
 install_dependencies
 
+echo "...installing yq if necessary"
+install_yq
+
+set -e
+
 echo "...confirming version is correct"
 check_version
 

+ 27 - 0
servercfg/serverconf.go

@@ -79,6 +79,7 @@ func GetServerConfig() config.ServerConfig {
 	if Is_EE {
 		cfg.IsEE = "yes"
 	}
+	cfg.DefaultProxyMode = GetDefaultProxyMode()
 
 	return cfg
 }
@@ -636,6 +637,32 @@ func IsProxyEnabled() bool {
 	return enabled
 }
 
+// GetDefaultProxyMode - default proxy mode for a server
+func GetDefaultProxyMode() config.ProxyMode {
+	var (
+		mode config.ProxyMode
+		def  string
+	)
+	if os.Getenv("DEFAULT_PROXY_MODE") != "" {
+		def = os.Getenv("DEFAULT_PROXY_MODE")
+	} else if config.Config.Server.DefaultProxyMode.Set {
+		return config.Config.Server.DefaultProxyMode
+	}
+	switch strings.ToUpper(def) {
+	case "ON":
+		mode.Set = true
+		mode.Value = true
+	case "OFF":
+		mode.Set = true
+		mode.Value = false
+	// AUTO or any other value
+	default:
+		mode.Set = false
+	}
+	return mode
+
+}
+
 // parseStunList - turn string into slice of StunServers
 func parseStunList(stunString string) ([]models.StunServer, error) {
 	var err error

+ 1 - 1
swagger.yaml

@@ -704,7 +704,7 @@ info:
 
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
-    version: 0.18.3
+    version: 0.18.4
 paths:
     /api/dns:
         get: