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
This commit is contained in:
Abhishek K
2025-09-11 15:24:17 +05:30
committed by GitHub
parent 57bf34da16
commit 9e0196126f
19 changed files with 674 additions and 165 deletions

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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"))

View File

@@ -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"`

View File

@@ -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

View File

@@ -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"`

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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,
})
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,
})
}
}
}
}
}
}
@@ -276,39 +307,78 @@ func GetFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []mode
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 {
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,
})
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 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)
}
}
}
}
}

View File

@@ -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"`

View File

@@ -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
}