diff --git a/controllers/egress.go b/controllers/egress.go index 04dfa43b..2e0288ec 100644 --- a/controllers/egress.go +++ b/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") } diff --git a/controllers/hosts.go b/controllers/hosts.go index d655a892..f6a294ba 100644 --- a/controllers/hosts.go +++ b/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 { diff --git a/logic/acls.go b/logic/acls.go index b836f4ec..47007e44 100644 --- a/logic/acls.go +++ b/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 { diff --git a/logic/dns.go b/logic/dns.go index bc90db70..68606bef 100644 --- a/logic/dns.go +++ b/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() diff --git a/logic/egress.go b/logic/egress.go index 0c17e840..5b08acc0 100644 --- a/logic/egress.go +++ b/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 +} diff --git a/logic/extpeers.go b/logic/extpeers.go index 6fed82b5..03d979a9 100644 --- a/logic/extpeers.go +++ b/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 -} diff --git a/logic/hosts.go b/logic/hosts.go index bbdd740d..1b28e9f3 100644 --- a/logic/hosts.go +++ b/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 } diff --git a/logic/peers.go b/logic/peers.go index ce20eb07..5bc3d737 100644 --- a/logic/peers.go +++ b/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{ diff --git a/logic/util.go b/logic/util.go index 5824177e..97838843 100644 --- a/logic/util.go +++ b/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) +} diff --git a/migrate/migrate.go b/migrate/migrate.go index 082440fb..1ed796c4 100644 --- a/migrate/migrate.go +++ b/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")) diff --git a/models/egress.go b/models/egress.go index 02295c8c..fd50c357 100644 --- a/models/egress.go +++ b/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"` diff --git a/models/host.go b/models/host.go index 7094dabf..79373e64 100644 --- a/models/host.go +++ b/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 diff --git a/models/mqtt.go b/models/mqtt.go index bcd1e8b0..1800c81b 100644 --- a/models/mqtt.go +++ b/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"` diff --git a/models/structs.go b/models/structs.go index bdced39d..704f5738 100644 --- a/models/structs.go +++ b/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"` } diff --git a/mq/publishers.go b/mq/publishers.go index 34e11432..66f6d431 100644 --- a/mq/publishers.go +++ b/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 { diff --git a/pro/controllers/users.go b/pro/controllers/users.go index 1a835566..a7c576ef 100644 --- a/pro/controllers/users.go +++ b/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 } diff --git a/pro/logic/acls.go b/pro/logic/acls.go index 12831ed7..45f88af2 100644 --- a/pro/logic/acls.go +++ b/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, - }) + 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) + } + } + } } } diff --git a/schema/egress.go b/schema/egress.go index 69246afe..2017c366 100644 --- a/schema/egress.go +++ b/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"` diff --git a/utils/utils.go b/utils/utils.go index c093f44e..2d4c7572 100644 --- a/utils/utils.go +++ b/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 +}