2
0
Эх сурвалжийг харах

NM-79: Domain Based Egress Routing (#3607)

* add support for egress domain routing

* add domain info to egress range

* fix egress domain update

* send peer update domain resolution update

* add egress domain update in the peer update

* use range field for domain check

* add egress domain to host pull

* add egress domain model to egress host update

* add egress domain model to egress host update

* update egress domain model on acls

* add check of range if domain is set

* sync egress domains to dns system

* add egress domain to match domain list, fix egress nat rule for domains

* fix all rsrcs comms

* fix static checks

* fix egress acls on CE

* check for all resources access on a node

* simplify egress acl rules

* merged ce and pro acl rule func

* fix uni direction acl rule for static nodes

* allow relayed nodes traffic

* resolve merge conflicts

* remove anywhere dst rule on user node acls

* fix: broadcast  user groups update for acl changes

* add egress domain ans routes to nodes

* add egress ranges to DST

* add all egress ranges for all resources

* fix DNS routing acls rules
Abhishek K 11 цаг өмнө
parent
commit
9e0196126f

+ 93 - 7
controllers/egress.go

@@ -45,14 +45,27 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	var egressRange string
+	var cidrErr error
 	if !req.IsInetGw {
-		egressRange, err = logic.NormalizeCIDR(req.Range)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		if req.Range != "" {
+			egressRange, cidrErr = logic.NormalizeCIDR(req.Range)
+		}
+		isDomain := logic.IsFQDN(req.Range)
+		if cidrErr != nil && !isDomain {
+			if cidrErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(cidrErr, "badrequest"))
+			} else {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("bad domain name"), "badrequest"))
+			}
 			return
 		}
+		if isDomain {
+			req.Domain = req.Range
+			egressRange = ""
+		}
 	} else {
 		egressRange = "*"
+		req.Domain = ""
 	}
 
 	e := schema.Egress{
@@ -61,6 +74,8 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 		Network:     req.Network,
 		Description: req.Description,
 		Range:       egressRange,
+		Domain:      req.Domain,
+		DomainAns:   []string{},
 		Nat:         req.Nat,
 		Nodes:       make(datatypes.JSONMap),
 		Tags:        make(datatypes.JSONMap),
@@ -108,7 +123,35 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
 	// 	}
 
 	// }
-	go mq.PublishPeerUpdate(false)
+	if req.Domain != "" {
+		if req.Nodes != nil {
+			for nodeID := range req.Nodes {
+				node, err := logic.GetNodeByID(nodeID)
+				if err != nil {
+					continue
+				}
+				host, _ := logic.GetHost(node.HostID.String())
+				if host == nil {
+					continue
+				}
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.EgressUpdate,
+					Host:   *host,
+					EgressDomain: models.EgressDomain{
+						ID:     e.ID,
+						Host:   *host,
+						Node:   node,
+						Domain: e.Domain,
+					},
+					Node: node,
+				})
+			}
+		}
+
+	} else {
+		go mq.PublishPeerUpdate(false)
+	}
+
 	logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
 }
 
@@ -161,14 +204,25 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	var egressRange string
+	var cidrErr error
 	if !req.IsInetGw {
-		egressRange, err = logic.NormalizeCIDR(req.Range)
-		if err != nil {
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		egressRange, cidrErr = logic.NormalizeCIDR(req.Range)
+		isDomain := logic.IsFQDN(req.Range)
+		if cidrErr != nil && !isDomain {
+			if cidrErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(cidrErr, "badrequest"))
+			} else {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("bad domain name"), "badrequest"))
+			}
 			return
 		}
+		if isDomain {
+			req.Domain = req.Range
+			egressRange = ""
+		}
 	} else {
 		egressRange = "*"
+		req.Domain = ""
 	}
 
 	e := schema.Egress{ID: req.ID}
@@ -209,10 +263,14 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 	for nodeID, metric := range req.Nodes {
 		e.Nodes[nodeID] = metric
 	}
+	if e.Domain != req.Domain {
+		e.DomainAns = datatypes.JSONSlice[string]{}
+	}
 	e.Range = egressRange
 	e.Description = req.Description
 	e.Name = req.Name
 	e.Nat = req.Nat
+	e.Domain = req.Domain
 	e.Status = req.Status
 	e.UpdatedAt = time.Now().UTC()
 	if err := logic.ValidateEgressReq(&e); err != nil {
@@ -238,6 +296,34 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
 	}
 	event.Diff.New = e
 	logic.LogEvent(event)
+	if req.Domain != "" {
+		if req.Nodes != nil {
+			for nodeID := range req.Nodes {
+				node, err := logic.GetNodeByID(nodeID)
+				if err != nil {
+					continue
+				}
+				host, _ := logic.GetHost(node.HostID.String())
+				if host == nil {
+					continue
+				}
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.EgressUpdate,
+					Host:   *host,
+					EgressDomain: models.EgressDomain{
+						ID:     e.ID,
+						Host:   *host,
+						Node:   node,
+						Domain: e.Domain,
+					},
+					Node: node,
+				})
+			}
+		}
+
+	} else {
+		go mq.PublishPeerUpdate(false)
+	}
 	go mq.PublishPeerUpdate(false)
 	logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
 }

+ 16 - 3
controllers/hosts.go

@@ -253,11 +253,13 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		IsInternetGw:      hPU.IsInternetGw,
+		NameServers:       hPU.NameServers,
+		EgressWithDomains: hPU.EgressWithDomains,
 		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 		DnsNameservers:    hPU.DnsNameservers,
 	}
 
-	logger.Log(1, hostID, "completed a pull")
+	logger.Log(1, hostID, host.Name, "completed a pull")
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 }
@@ -374,7 +376,6 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	switch hostUpdate.Action {
 	case models.CheckIn:
 		sendPeerUpdate = mq.HandleHostCheckin(&hostUpdate.Host, currentHost)
-
 	case models.UpdateHost:
 		if hostUpdate.Host.PublicKey != currentHost.PublicKey {
 			//remove old peer entry
@@ -384,12 +385,24 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 		err := logic.UpsertHost(currentHost)
 		if err != nil {
 			slog.Error("failed to update host", "id", currentHost.ID, "error", err)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
 			return
 		}
 
 	case models.UpdateMetrics:
 		mq.UpdateMetricsFallBack(hostUpdate.Node.ID.String(), hostUpdate.NewMetrics)
+	case models.EgressUpdate:
+		e := schema.Egress{ID: hostUpdate.EgressDomain.ID}
+		err = e.Get(db.WithContext(r.Context()))
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+			return
+		}
+		if len(hostUpdate.Node.EgressGatewayRanges) > 0 {
+			e.DomainAns = hostUpdate.Node.EgressGatewayRanges
+			e.Update(db.WithContext(r.Context()))
+		}
+		sendPeerUpdate = true
 	}
 
 	if sendPeerUpdate {

+ 85 - 31
logic/acls.go

@@ -54,6 +54,9 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
 		if !nodeI.IsStatic || nodeI.IsUserNode {
 			continue
 		}
+		if !node.StaticNode.Enabled {
+			continue
+		}
 		// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
 		// 	continue
 		// }
@@ -292,35 +295,70 @@ func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 				if err != nil {
 					continue
 				}
-				dstI.Value = e.Range
+				if len(e.DomainAns) > 0 {
+					for _, domainAnsI := range e.DomainAns {
+						dstI.Value = domainAnsI
+
+						ip, cidr, err := net.ParseCIDR(dstI.Value)
+						if err == nil {
+							if ip.To4() != nil {
+								if node.Address.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address.IP,
+											Mask: net.CIDRMask(32, 32),
+										},
+										DstIP: *cidr,
+										Allow: true,
+									})
+								}
+							} else {
+								if node.Address6.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address6.IP,
+											Mask: net.CIDRMask(128, 128),
+										},
+										DstIP: *cidr,
+										Allow: true,
+									})
+								}
+							}
 
-				ip, cidr, err := net.ParseCIDR(dstI.Value)
-				if err == nil {
-					if ip.To4() != nil {
-						if node.Address.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								DstIP: *cidr,
-								Allow: true,
-							})
-						}
-					} else {
-						if node.Address6.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								DstIP: *cidr,
-								Allow: true,
-							})
 						}
 					}
+				} else {
+					dstI.Value = e.Range
 
+					ip, cidr, err := net.ParseCIDR(dstI.Value)
+					if err == nil {
+						if ip.To4() != nil {
+							if node.Address.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									DstIP: *cidr,
+									Allow: true,
+								})
+							}
+						} else {
+							if node.Address6.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									DstIP: *cidr,
+									Allow: true,
+								})
+							}
+						}
+
+					}
 				}
+
 			}
 		}
 	}
@@ -364,6 +402,9 @@ func GetStaticNodeIps(node models.Node) (ips []net.IP) {
 		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
 			continue
 		}
+		if !extclient.StaticNode.Enabled {
+			continue
+		}
 		if extclient.StaticNode.Address != "" {
 			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
 		}
@@ -673,7 +714,6 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
 }
 
 func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclRule) {
-	fmt.Println("==========> Getting Egress FW rules ", targetnode.ID)
 	rules = make(map[string]models.AclRule)
 	defer func() {
 		rules = GetEgressUserRulesForNode(&targetnode, rules)
@@ -720,14 +760,28 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
 		}
 		for egressID, egI := range egressIDMap {
 			if _, ok := dstTags[egressID]; ok || dstAll {
-				ip, cidr, err := net.ParseCIDR(egI.Range)
-				if err == nil {
-					if ip.To4() != nil {
-						aclRule.Dst = append(aclRule.Dst, *cidr)
-					} else {
-						aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+				if servercfg.IsPro && egI.Domain != "" && len(egI.DomainAns) > 0 {
+					for _, domainAnsI := range egI.DomainAns {
+						ip, cidr, err := net.ParseCIDR(domainAnsI)
+						if err == nil {
+							if ip.To4() != nil {
+								aclRule.Dst = append(aclRule.Dst, *cidr)
+							} else {
+								aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+							}
+						}
+					}
+				} else {
+					ip, cidr, err := net.ParseCIDR(egI.Range)
+					if err == nil {
+						if ip.To4() != nil {
+							aclRule.Dst = append(aclRule.Dst, *cidr)
+						} else {
+							aclRule.Dst6 = append(aclRule.Dst6, *cidr)
+						}
 					}
 				}
+
 				_, srcAll := srcTags["*"]
 				if srcAll {
 					if targetnode.NetworkRange.IP != nil {

+ 26 - 0
logic/dns.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 	"os"
 	"regexp"
 	"sort"
@@ -118,6 +119,31 @@ func GetDNS(network string) ([]models.DNSEntry, error) {
 	return dns, nil
 }
 
+func EgressDNs(network string) (entries []models.DNSEntry) {
+	egs, _ := (&schema.Egress{
+		Network: network,
+	}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, egI := range egs {
+		if egI.Domain != "" && len(egI.DomainAns) > 0 {
+			entry := models.DNSEntry{
+				Name: egI.Domain,
+			}
+			for _, domainAns := range egI.DomainAns {
+				ip, _, err := net.ParseCIDR(domainAns)
+				if err == nil {
+					if ip.To4() != nil {
+						entry.Address = ip.String()
+					} else {
+						entry.Address6 = ip.String()
+					}
+				}
+			}
+			entries = append(entries, entry)
+		}
+	}
+	return
+}
+
 // GetExtclientDNS - gets all extclients dns entries
 func GetExtclientDNS() []models.DNSEntry {
 	extclients, err := GetAllExtClients()

+ 119 - 12
logic/egress.go

@@ -36,6 +36,35 @@ func ValidateEgressReq(e *schema.Egress) error {
 	return nil
 }
 
+func DoesUserHaveAccessToEgress(user *models.User, e *schema.Egress, acls []models.Acl) bool {
+
+	if !e.Status {
+		return false
+	}
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		dstTags := ConvAclTagToValueMap(acl.Dst)
+		_, all := dstTags["*"]
+
+		if _, ok := dstTags[e.ID]; ok || all {
+			// get all src tags
+			for _, srcAcl := range acl.Src {
+				if srcAcl.ID == models.UserAclID && srcAcl.Value == user.UserName {
+					return true
+				} else if srcAcl.ID == models.UserGroupAclID {
+					// fetch all users in the group
+					if _, ok := user.UserGroups[models.UserGroupID(srcAcl.Value)]; ok {
+						return true
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
 func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress, acls []models.Acl) bool {
 	nodeTags := maps.Clone(node.Tags)
 	nodeTags[models.TagID(node.ID.String())] = struct{}{}
@@ -107,12 +136,31 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 				m64 = 256
 			}
 			m := uint32(m64)
-			req.Ranges = append(req.Ranges, e.Range)
-			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
-				Network:     e.Range,
-				Nat:         e.Nat,
-				RouteMetric: m,
-			})
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+			} else {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+			}
+
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     e.Range,
+					Nat:         e.Nat,
+					RouteMetric: m,
+				})
+			}
+			if e.Domain != "" && len(e.DomainAns) > 0 {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+				for _, domainAnsI := range e.DomainAns {
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     domainAnsI,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+
+			}
 		}
 	}
 	if targetNode.Mutex != nil {
@@ -132,6 +180,27 @@ func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egr
 	}
 }
 
+func GetEgressDomainsByAccess(user *models.User, network models.NetworkID) (domains []string) {
+	acls, _ := ListAclsByNetwork(network)
+	eli, _ := (&schema.Egress{Network: network.String()}).ListByNetwork(db.WithContext(context.TODO()))
+	defaultDevicePolicy, _ := GetDefaultPolicy(network, models.DevicePolicy)
+	isDefaultPolicyActive := defaultDevicePolicy.Enabled
+	for _, e := range eli {
+		if !e.Status || e.Network != network.String() {
+			continue
+		}
+		if !isDefaultPolicyActive {
+			if !DoesUserHaveAccessToEgress(user, &e, acls) {
+				continue
+			}
+		}
+		if e.Domain != "" && len(e.DomainAns) > 0 {
+			domains = append(domains, e.Domain)
+		}
+	}
+	return
+}
+
 func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []models.Acl) {
 
 	req := models.EgressGatewayRequest{
@@ -149,12 +218,25 @@ func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []mode
 				m64 = 256
 			}
 			m := uint32(m64)
-			req.Ranges = append(req.Ranges, e.Range)
-			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
-				Network:     e.Range,
-				Nat:         e.Nat,
-				RouteMetric: m,
-			})
+			if e.Range != "" {
+				req.Ranges = append(req.Ranges, e.Range)
+				req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+					Network:     e.Range,
+					Nat:         e.Nat,
+					RouteMetric: m,
+				})
+			}
+			if e.Domain != "" && len(e.DomainAns) > 0 {
+				req.Ranges = append(req.Ranges, e.DomainAns...)
+				for _, domainAnsI := range e.DomainAns {
+					req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+						Network:     domainAnsI,
+						Nat:         e.Nat,
+						RouteMetric: m,
+					})
+				}
+
+			}
 
 		}
 	}
@@ -218,3 +300,28 @@ func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]st
 	}
 	return nodeEgressMap, resultMap, nil
 }
+
+func ListAllByRoutingNodeWithDomain(egs []schema.Egress, nodeID string) (egWithDomain []models.EgressDomain) {
+	for _, egI := range egs {
+		if !egI.Status || egI.Domain == "" {
+			continue
+		}
+		if _, ok := egI.Nodes[nodeID]; ok {
+			node, err := GetNodeByID(nodeID)
+			if err != nil {
+				continue
+			}
+			host, err := GetHost(node.HostID.String())
+			if err != nil {
+				continue
+			}
+			egWithDomain = append(egWithDomain, models.EgressDomain{
+				ID:     egI.ID,
+				Domain: egI.Domain,
+				Node:   node,
+				Host:   *host,
+			})
+		}
+	}
+	return
+}

+ 0 - 18
logic/extpeers.go

@@ -722,21 +722,3 @@ func GetStaticNodesByNetwork(network models.NetworkID, onlyWg bool) (staticNode
 
 	return
 }
-
-func GetStaticNodesByGw(gwNode models.Node) (staticNode []models.Node) {
-	extClients, err := GetAllExtClients()
-	if err != nil {
-		return
-	}
-	for _, extI := range extClients {
-		if extI.IngressGatewayID == gwNode.ID.String() {
-			n := models.Node{
-				IsStatic:   true,
-				StaticNode: extI,
-				IsUserNode: extI.RemoteAccessClientID != "",
-			}
-			staticNode = append(staticNode, n)
-		}
-	}
-	return
-}

+ 9 - 4
logic/hosts.go

@@ -6,7 +6,6 @@ import (
 	"errors"
 	"fmt"
 	"os"
-	"reflect"
 	"sort"
 	"sync"
 
@@ -18,6 +17,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
+	"github.com/gravitl/netmaker/utils"
 )
 
 var (
@@ -310,17 +310,22 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 		sendPeerUpdate = true
 	}
 	isEndpointChanged := false
-	if currHost.EndpointIP.String() != newHost.EndpointIP.String() {
+	if !currHost.EndpointIP.Equal(newHost.EndpointIP) {
 		currHost.EndpointIP = newHost.EndpointIP
 		sendPeerUpdate = true
 		isEndpointChanged = true
 	}
-	if currHost.EndpointIPv6.String() != newHost.EndpointIPv6.String() {
+	if !currHost.EndpointIPv6.Equal(newHost.EndpointIPv6) {
 		currHost.EndpointIPv6 = newHost.EndpointIPv6
 		sendPeerUpdate = true
 		isEndpointChanged = true
 	}
-	if !reflect.DeepEqual(currHost.Interfaces, newHost.Interfaces) {
+	for i := range newHost.Interfaces {
+		newHost.Interfaces[i].AddressString = newHost.Interfaces[i].Address.String()
+	}
+	utils.SortIfacesByName(currHost.Interfaces)
+	utils.SortIfacesByName(newHost.Interfaces)
+	if !utils.CompareIfaces(currHost.Interfaces, newHost.Interfaces) {
 		currHost.Interfaces = newHost.Interfaces
 		sendPeerUpdate = true
 	}

+ 6 - 2
logic/peers.go

@@ -76,7 +76,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
-				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+				logger.Log(4, "no peer host", peer.HostID.String(), err.Error())
 				continue
 			}
 
@@ -182,6 +182,10 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
 		GetNodeEgressInfo(&node, eli, acls)
+		if node.EgressDetails.IsEgressGateway {
+			egsWithDomain := ListAllByRoutingNodeWithDomain(eli, node.ID.String())
+			hostPeerUpdate.EgressWithDomains = append(hostPeerUpdate.EgressWithDomains, egsWithDomain...)
+		}
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
@@ -231,7 +235,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 
 			peerHost, err := GetHost(peer.HostID.String())
 			if err != nil {
-				logger.Log(1, "no peer host", peer.HostID.String(), err.Error())
+				logger.Log(4, "no peer host", peer.HostID.String(), err.Error())
 				continue
 			}
 			peerConfig := wgtypes.PeerConfig{

+ 15 - 0
logic/util.go

@@ -12,6 +12,7 @@ import (
 	"net/http"
 	"os"
 	"reflect"
+	"regexp"
 	"strings"
 	"time"
 	"unicode"
@@ -273,3 +274,17 @@ func compareIface(a, b models.Iface) bool {
 		a.Address.Mask.String() == b.Address.Mask.String() &&
 		a.AddressString == b.AddressString
 }
+
+// IsFQDN checks if the given string is a valid Fully Qualified Domain Name (FQDN)
+func IsFQDN(domain string) bool {
+	// Basic check to ensure the domain is not empty and has at least one dot (.)
+	if domain == "" || !strings.Contains(domain, ".") {
+		return false
+	}
+
+	// Regular expression for validating FQDN (basic check for valid characters and structure)
+	fqdnRegex := `^(?i)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`
+	re := regexp.MustCompile(fqdnRegex)
+
+	return re.MatchString(domain)
+}

+ 4 - 0
migrate/migrate.go

@@ -343,6 +343,10 @@ func updateHosts() {
 			}
 			logic.UpsertHost(&host)
 		}
+		if host.IsDefault && !host.AutoUpdate {
+			host.AutoUpdate = true
+			logic.UpsertHost(&host)
+		}
 		if servercfg.IsPro && host.Location == "" {
 			if host.EndpointIP != nil {
 				host.Location = logic.GetHostLocInfo(host.EndpointIP.String(), os.Getenv("IP_INFO_TOKEN"))

+ 1 - 0
models/egress.go

@@ -8,6 +8,7 @@ type EgressReq struct {
 	Nodes       map[string]int `json:"nodes"`
 	Tags        []string       `json:"tags"`
 	Range       string         `json:"range"`
+	Domain      string         `json:"domain"`
 	Nat         bool           `json:"nat"`
 	Status      bool           `json:"status"`
 	IsInetGw    bool           `json:"is_internet_gateway"`

+ 8 - 5
models/host.go

@@ -124,6 +124,8 @@ const (
 	SignalPull HostMqAction = "SIGNAL_PULL"
 	// UpdateMetrics - updates metrics data
 	UpdateMetrics HostMqAction = "UPDATE_METRICS"
+	// EgressUpdate - const for egress update action
+	EgressUpdate HostMqAction = "EGRESS_UPDATE"
 )
 
 // SignalAction - turn peer signal action
@@ -138,11 +140,12 @@ const (
 
 // HostUpdate - struct for host update
 type HostUpdate struct {
-	Action     HostMqAction
-	Host       Host
-	Node       Node
-	Signal     Signal
-	NewMetrics Metrics
+	Action       HostMqAction
+	Host         Host
+	Node         Node
+	Signal       Signal
+	EgressDomain EgressDomain
+	NewMetrics   Metrics
 }
 
 // HostTurnRegister - struct for host turn registration

+ 24 - 17
models/mqtt.go

@@ -12,27 +12,34 @@ type HostPeerInfo struct {
 
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
-	Host            Host                  `json:"host"`
-	ChangeDefaultGw bool                  `json:"change_default_gw"`
-	DefaultGwIp     net.IP                `json:"default_gw_ip"`
-	IsInternetGw    bool                  `json:"is_inet_gw"`
-	NodeAddrs       []net.IPNet           `json:"nodes_addrs"`
-	Server          string                `json:"server"`
-	ServerVersion   string                `json:"serverversion"`
-	ServerAddrs     []ServerAddr          `json:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig  `json:"node_peers"`
-	Peers           []wgtypes.PeerConfig  `json:"host_peers"`
-	PeerIDs         PeerMap               `json:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ReplacePeers    bool                  `json:"replace_peers"`
-	NameServers     []string              `json:"name_servers"`
-	DnsNameservers  []Nameserver          `json:"dns_nameservers"`
+	Host              Host                  `json:"host"`
+	ChangeDefaultGw   bool                  `json:"change_default_gw"`
+	DefaultGwIp       net.IP                `json:"default_gw_ip"`
+	IsInternetGw      bool                  `json:"is_inet_gw"`
+	NodeAddrs         []net.IPNet           `json:"nodes_addrs"`
+	Server            string                `json:"server"`
+	ServerVersion     string                `json:"serverversion"`
+	ServerAddrs       []ServerAddr          `json:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig  `json:"node_peers"`
+	Peers             []wgtypes.PeerConfig  `json:"host_peers"`
+	PeerIDs           PeerMap               `json:"peerids"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ReplacePeers      bool                  `json:"replace_peers"`
+	NameServers       []string              `json:"name_servers"`
+	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
+	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	ServerConfig
 	OldPeerUpdateFields
 }
 
+type EgressDomain struct {
+	ID     string `json:"id"`
+	Node   Node   `json:"node"`
+	Host   Host   `json:"host"`
+	Domain string `json:"domain"`
+}
 type Nameserver struct {
 	IPs         []string `json:"ips"`
 	MatchDomain string   `json:"match_domain"`

+ 2 - 0
models/structs.go

@@ -262,6 +262,8 @@ type HostPull struct {
 	DefaultGwIp       net.IP                `json:"default_gw_ip"`
 	IsInternetGw      bool                  `json:"is_inet_gw"`
 	EndpointDetection bool                  `json:"endpoint_detection"`
+	NameServers       []string              `json:"name_servers"`
+	EgressWithDomains []EgressDomain        `json:"egress_with_domains"`
 	DnsNameservers    []Nameserver          `json:"dns_nameservers"`
 }
 

+ 2 - 0
mq/publishers.go

@@ -253,6 +253,7 @@ func sendPeers() {
 func SendDNSSyncByNetwork(network string) error {
 
 	k, err := logic.GetDNS(network)
+	k = append(k, logic.EgressDNs(network)...)
 	if err == nil && len(k) > 0 {
 		err = PushSyncDNS(k)
 		if err != nil {
@@ -269,6 +270,7 @@ func sendDNSSync() error {
 	if err == nil && len(networks) > 0 {
 		for _, v := range networks {
 			k, err := logic.GetDNS(v.NetID)
+			k = append(k, logic.EgressDNs(v.NetID)...)
 			if err == nil && len(k) > 0 {
 				err = PushSyncDNS(k)
 				if err != nil {

+ 2 - 0
pro/controllers/users.go

@@ -1580,6 +1580,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 			}
 		}
+		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
 		gws = append(gws, gw)
 		userGws[node.Network] = gws
 		delete(userGwNodes, node.ID.String())
@@ -1630,6 +1631,7 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
 				gw.MatchDomains = append(gw.MatchDomains, nsI.MatchDomain)
 			}
 		}
+		gw.MatchDomains = append(gw.MatchDomains, logic.GetEgressDomainsByAccess(user, models.NetworkID(node.Network))...)
 		gws = append(gws, gw)
 		userGws[node.Network] = gws
 	}

+ 196 - 56
pro/logic/acls.go

@@ -32,6 +32,9 @@ func GetFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []
 	defaultUserPolicy, _ := logic.GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 	userNodes := getStaticUserNodesByNetwork(models.NetworkID(node.Network))
 	for _, userNodeI := range userNodes {
+		if !userNodeI.StaticNode.Enabled {
+			continue
+		}
 		if defaultUserPolicy.Enabled {
 			if userNodeI.StaticNode.Address != "" {
 				rules = append(rules, models.FwRule{
@@ -107,28 +110,56 @@ func GetFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []
 							if err != nil {
 								continue
 							}
-							dstI.Value = e.Range
+							if e.Range != "" {
+								dstI.Value = e.Range
+
+								ip, cidr, err := net.ParseCIDR(dstI.Value)
+								if err == nil {
+									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
+										rules = append(rules, models.FwRule{
+											SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+											DstIP:           *cidr,
+											AllowedProtocol: policy.Proto,
+											AllowedPorts:    policy.Port,
+											Allow:           true,
+										})
+									}
+								}
+							} else if len(e.DomainAns) > 0 {
+								for _, domainAns := range e.DomainAns {
+									dstI.Value = domainAns
 
-							ip, cidr, err := net.ParseCIDR(dstI.Value)
-							if err == nil {
-								if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
-									rules = append(rules, models.FwRule{
-										SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
-										DstIP:           *cidr,
-										AllowedProtocol: policy.Proto,
-										AllowedPorts:    policy.Port,
-										Allow:           true,
-									})
-								} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
-									rules = append(rules, models.FwRule{
-										SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
-										DstIP:           *cidr,
-										AllowedProtocol: policy.Proto,
-										AllowedPorts:    policy.Port,
-										Allow:           true,
-									})
+									ip, cidr, err := net.ParseCIDR(dstI.Value)
+									if err == nil {
+										if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
+											rules = append(rules, models.FwRule{
+												SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
+												DstIP:           *cidr,
+												AllowedProtocol: policy.Proto,
+												AllowedPorts:    policy.Port,
+												Allow:           true,
+											})
+										} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
+											rules = append(rules, models.FwRule{
+												SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
+												DstIP:           *cidr,
+												AllowedProtocol: policy.Proto,
+												AllowedPorts:    policy.Port,
+												Allow:           true,
+											})
+										}
+									}
 								}
 							}
+
 						}
 					}
 
@@ -276,39 +307,78 @@ func GetFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
 				if err != nil {
 					continue
 				}
-				dstI.Value = e.Range
-
-				ip, cidr, err := net.ParseCIDR(dstI.Value)
-				if err == nil {
-					if ip.To4() != nil {
-						if node.Address.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-					} else {
-						if node.Address6.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
+				if e.Range != "" {
+					dstI.Value = e.Range
+
+					ip, cidr, err := net.ParseCIDR(dstI.Value)
+					if err == nil {
+						if ip.To4() != nil {
+							if node.Address.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address.IP,
+										Mask: net.CIDRMask(32, 32),
+									},
+									DstIP:           *cidr,
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
+						} else {
+							if node.Address6.IP != nil {
+								rules = append(rules, models.FwRule{
+									SrcIP: net.IPNet{
+										IP:   node.Address6.IP,
+										Mask: net.CIDRMask(128, 128),
+									},
+									DstIP:           *cidr,
+									AllowedProtocol: policy.Proto,
+									AllowedPorts:    policy.Port,
+									Allow:           true,
+								})
+							}
 						}
+
 					}
+				} else if len(e.DomainAns) > 0 {
+					for _, domainAnsI := range e.DomainAns {
+						dstI.Value = domainAnsI
+
+						ip, cidr, err := net.ParseCIDR(dstI.Value)
+						if err == nil {
+							if ip.To4() != nil {
+								if node.Address.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address.IP,
+											Mask: net.CIDRMask(32, 32),
+										},
+										DstIP:           *cidr,
+										AllowedProtocol: policy.Proto,
+										AllowedPorts:    policy.Port,
+										Allow:           true,
+									})
+								}
+							} else {
+								if node.Address6.IP != nil {
+									rules = append(rules, models.FwRule{
+										SrcIP: net.IPNet{
+											IP:   node.Address6.IP,
+											Mask: net.CIDRMask(128, 128),
+										},
+										DstIP:           *cidr,
+										AllowedProtocol: policy.Proto,
+										AllowedPorts:    policy.Port,
+										Allow:           true,
+									})
+								}
+							}
 
+						}
+					}
 				}
+
 			}
 		}
 	}
@@ -800,7 +870,14 @@ func GetEgressUserRulesForNode(targetnode *models.Node,
 			continue
 		}
 		if _, ok := egI.Nodes[targetnode.ID.String()]; ok {
-			targetNodeTags[models.TagID(egI.Range)] = struct{}{}
+			if egI.Range != "" {
+				targetNodeTags[models.TagID(egI.Range)] = struct{}{}
+			} else if len(egI.DomainAns) > 0 {
+				for _, domainAnsI := range egI.DomainAns {
+					targetNodeTags[models.TagID(domainAnsI)] = struct{}{}
+				}
+			}
+
 			targetNodeTags[models.TagID(egI.ID)] = struct{}{}
 		}
 	}
@@ -818,7 +895,14 @@ func GetEgressUserRulesForNode(targetnode *models.Node,
 						for nodeID := range e.Nodes {
 							dstTags[nodeID] = struct{}{}
 						}
-						dstTags[e.Range] = struct{}{}
+						if e.Range != "" {
+							dstTags[e.Range] = struct{}{}
+						} else if len(e.DomainAns) > 0 {
+							for _, domainAnsI := range e.DomainAns {
+								dstTags[domainAnsI] = struct{}{}
+							}
+						}
+
 					}
 				}
 			}
@@ -912,24 +996,57 @@ func GetEgressUserRulesForNode(targetnode *models.Node,
 						if err != nil {
 							continue
 						}
-						ip, cidr, err := net.ParseCIDR(e.Range)
-						if err == nil {
-							if ip.To4() != nil {
-								r.Dst = append(r.Dst, *cidr)
-							} else {
-								r.Dst6 = append(r.Dst6, *cidr)
+						if e.Range != "" {
+							ip, cidr, err := net.ParseCIDR(e.Range)
+							if err == nil {
+								if ip.To4() != nil {
+									r.Dst = append(r.Dst, *cidr)
+								} else {
+									r.Dst6 = append(r.Dst6, *cidr)
+								}
+
 							}
+						} else if len(e.DomainAns) > 0 {
+							for _, domainAnsI := range e.DomainAns {
+								ip, cidr, err := net.ParseCIDR(domainAnsI)
+								if err == nil {
+									if ip.To4() != nil {
+										r.Dst = append(r.Dst, *cidr)
+									} else {
+										r.Dst6 = append(r.Dst6, *cidr)
+									}
 
+								}
+							}
 						}
 
 					}
 
 				}
+				if userNode.StaticNode.Address6 != "" {
+					r.IP6List = append(r.IP6List, userNode.StaticNode.AddressIPNet6())
+				}
 				if aclRule, ok := rules[acl.ID]; ok {
+
 					aclRule.IPList = append(aclRule.IPList, r.IPList...)
 					aclRule.IP6List = append(aclRule.IP6List, r.IP6List...)
+
+					aclRule.Dst = append(aclRule.Dst, r.Dst...)
+					aclRule.Dst6 = append(aclRule.Dst6, r.Dst6...)
+
+					aclRule.IPList = logic.UniqueIPNetList(aclRule.IPList)
+					aclRule.IP6List = logic.UniqueIPNetList(aclRule.IP6List)
+
+					aclRule.Dst = logic.UniqueIPNetList(aclRule.Dst)
+					aclRule.Dst6 = logic.UniqueIPNetList(aclRule.Dst6)
+
 					rules[acl.ID] = aclRule
 				} else {
+					r.IPList = logic.UniqueIPNetList(r.IPList)
+					r.IP6List = logic.UniqueIPNetList(r.IP6List)
+
+					r.Dst = logic.UniqueIPNetList(r.Dst)
+					r.Dst6 = logic.UniqueIPNetList(r.Dst6)
 					rules[acl.ID] = r
 				}
 			}
@@ -1064,7 +1181,19 @@ func GetUserAclRulesForNode(targetnode *models.Node,
 											egressRanges6 = append(egressRanges6, *cidr)
 										}
 									}
+								} else if len(eI.DomainAns) > 0 {
+									for _, domainAnsI := range eI.DomainAns {
+										_, cidr, err := net.ParseCIDR(domainAnsI)
+										if err == nil {
+											if cidr.IP.To4() != nil {
+												egressRanges4 = append(egressRanges4, *cidr)
+											} else {
+												egressRanges6 = append(egressRanges6, *cidr)
+											}
+										}
+									}
 								}
+
 							}
 						}
 						break
@@ -1083,6 +1212,17 @@ func GetUserAclRulesForNode(targetnode *models.Node,
 											egressRanges6 = append(egressRanges6, *cidr)
 										}
 									}
+								} else if len(e.DomainAns) > 0 {
+									for _, domainAnsI := range e.DomainAns {
+										_, cidr, err := net.ParseCIDR(domainAnsI)
+										if err == nil {
+											if cidr.IP.To4() != nil {
+												egressRanges4 = append(egressRanges4, *cidr)
+											} else {
+												egressRanges6 = append(egressRanges6, *cidr)
+											}
+										}
+									}
 								}
 							}
 

+ 10 - 8
schema/egress.go

@@ -11,14 +11,16 @@ import (
 const egressTable = "egresses"
 
 type Egress struct {
-	ID          string            `gorm:"primaryKey" json:"id"`
-	Name        string            `gorm:"name" json:"name"`
-	Network     string            `gorm:"network" json:"network"`
-	Description string            `gorm:"description" json:"description"`
-	Nodes       datatypes.JSONMap `gorm:"nodes" json:"nodes"`
-	Tags        datatypes.JSONMap `gorm:"tags" json:"tags"`
-	Range       string            `gorm:"range" json:"range"`
-	Nat         bool              `gorm:"nat" json:"nat"`
+	ID          string                      `gorm:"primaryKey" json:"id"`
+	Name        string                      `gorm:"name" json:"name"`
+	Network     string                      `gorm:"network" json:"network"`
+	Description string                      `gorm:"description" json:"description"`
+	Nodes       datatypes.JSONMap           `gorm:"nodes" json:"nodes"`
+	Tags        datatypes.JSONMap           `gorm:"tags" json:"tags"`
+	Range       string                      `gorm:"range" json:"range"`
+	DomainAns   datatypes.JSONSlice[string] `gorm:"domain_ans" json:"domain_ans"`
+	Domain      string                      `gorm:"domain" json:"domain"`
+	Nat         bool                        `gorm:"nat" json:"nat"`
 	//IsInetGw    bool              `gorm:"is_inet_gw" json:"is_internet_gateway"`
 	Status    bool      `gorm:"status" json:"status"`
 	CreatedBy string    `gorm:"created_by" json:"created_by"`

+ 56 - 2
utils/utils.go

@@ -5,8 +5,11 @@ import (
 	"log/slog"
 	"net"
 	"runtime"
+	"sort"
 	"strings"
 	"time"
+
+	"github.com/gravitl/netmaker/models"
 )
 
 // RetryStrategy specifies a strategy to retry an operation after waiting a while,
@@ -59,8 +62,8 @@ func TraceCaller() {
 	funcName := runtime.FuncForPC(pc).Name()
 
 	// Print trace details
-	slog.Debug("Called from function: %s\n", "func-name", funcName)
-	slog.Debug("File: %s, Line: %d\n", "file", file, "line-no", line)
+	slog.Debug("Called from function: %s\n", "func", funcName)
+	slog.Debug("File: %s, Line: %d\n", "file", file, "line", line)
 }
 
 // NoEmptyStringToCsv takes a bunch of strings, filters out empty ones and returns a csv version of the string
@@ -86,3 +89,54 @@ func GetExtClientEndpoint(hostIpv4Endpoint, hostIpv6Endpoint net.IP, hostListenP
 		return fmt.Sprintf("%s:%d", hostIpv4Endpoint.String(), hostListenPort)
 	}
 }
+
+// SortIfacesByName sorts a slice of Iface by name in ascending order
+func SortIfacesByName(ifaces []models.Iface) {
+	sort.Slice(ifaces, func(i, j int) bool {
+		return ifaces[i].Name < ifaces[j].Name
+	})
+}
+
+// CompareIfaces compares two slices of Iface and returns true if they are equal
+// Two slices are considered equal if they have the same length and all corresponding
+// elements have the same Name, AddressString, and IP address
+func CompareIfaces(ifaces1, ifaces2 []models.Iface) bool {
+	// Check if lengths are different
+	if len(ifaces1) != len(ifaces2) {
+		return false
+	}
+
+	// Compare each element
+	for i := range ifaces1 {
+		if !CompareIface(ifaces1[i], ifaces2[i]) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// CompareIface compares two individual Iface structs and returns true if they are equal
+func CompareIface(iface1, iface2 models.Iface) bool {
+	// Compare Name
+	if iface1.Name != iface2.Name {
+		return false
+	}
+
+	// Compare AddressString
+	if iface1.AddressString != iface2.AddressString {
+		return false
+	}
+
+	// Compare IP addresses
+	if !iface1.Address.IP.Equal(iface2.Address.IP) {
+		return false
+	}
+
+	// Compare network masks
+	if iface1.Address.Mask.String() != iface2.Address.Mask.String() {
+		return false
+	}
+
+	return true
+}