From bcf32f215c7ccea0555ceab690664f33a752ac08 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 17 Dec 2024 16:07:20 +0300 Subject: [PATCH] [management] Generate network map for new network concept (#3044) --- .../networks/resources/types/resource.go | 46 ++ management/server/peer_test.go | 74 ++- management/server/route_test.go | 446 ++++++++++++++++++ management/server/types/account.go | 181 ++++++- management/server/types/firewall_rule.go | 1 + .../server/types/route_firewall_rule.go | 7 + 6 files changed, 748 insertions(+), 7 deletions(-) diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 63a915f19..a1413dfd2 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -6,6 +6,10 @@ import ( "net/netip" "regexp" + nbDomain "github.com/netbirdio/netbird/management/domain" + routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/route" "github.com/rs/xid" "github.com/netbirdio/netbird/management/server/http/api" @@ -88,9 +92,51 @@ func (n *NetworkResource) Copy() *NetworkResource { Description: n.Description, Type: n.Type, Address: n.Address, + Domain: n.Domain, + Prefix: n.Prefix, } } +func (n *NetworkResource) ToRoute(peer *nbpeer.Peer, router *routerTypes.NetworkRouter) *route.Route { + r := &route.Route{ + ID: route.ID(n.ID), + AccountID: n.AccountID, + KeepRoute: true, + NetID: route.NetID(n.Name), + Description: n.Description, + Peer: peer.Key, + PeerGroups: nil, + Masquerade: router.Masquerade, + Metric: router.Metric, + Enabled: true, + Groups: nil, + AccessControlGroups: nil, + } + + if n.Type == host || n.Type == subnet { + r.Network = n.Prefix + + r.NetworkType = route.IPv4Network + if n.Prefix.Addr().Is6() { + r.NetworkType = route.IPv6Network + } + } + + if n.Type == domain { + domainList, err := nbDomain.FromStringList([]string{n.Domain}) + if err != nil { + return nil + } + r.Domains = domainList + r.NetworkType = route.DomainNetwork + + // add default placeholder for domain network + r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32) + } + + return r +} + // GetResourceType returns the type of the resource based on the address func GetResourceType(address string) (NetworkResourceType, string, netip.Prefix, error) { if prefix, err := netip.ParsePrefix(address); err == nil { diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 29461ecd8..c015f77c8 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -13,6 +13,9 @@ import ( "testing" "time" + resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" + routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" + networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/rs/xid" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -758,8 +761,66 @@ func setupTestAccountManager(b *testing.B, peers int, groups int) (*DefaultAccou peerIndex := i*(peers/groups) + j group.Peers = append(group.Peers, fmt.Sprintf("peer-%d", peerIndex)) } + + // Create network, router and resource for this group + network := &networkTypes.Network{ + ID: fmt.Sprintf("network-%d", i), + AccountID: account.Id, + Name: fmt.Sprintf("Network for Group %d", i), + } + account.Networks = append(account.Networks, network) + + ips := account.GetTakenIPs() + peerIP, err := types.AllocatePeerIP(account.Network.Net, ips) + if err != nil { + return nil, "", "", err + } + + peerKey, _ := wgtypes.GeneratePrivateKey() + peer := &nbpeer.Peer{ + ID: fmt.Sprintf("peer-%d", len(account.Peers)+1), + DNSLabel: fmt.Sprintf("peer-%d", len(account.Peers)+1), + Key: peerKey.PublicKey().String(), + IP: peerIP, + Status: &nbpeer.PeerStatus{}, + UserID: regularUser, + Meta: nbpeer.PeerSystemMeta{ + Hostname: fmt.Sprintf("peer-%d", len(account.Peers)+1), + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + } + account.Peers[peer.ID] = peer + + group.Peers = append(group.Peers, peer.ID) account.Groups[groupID] = group + router := &routerTypes.NetworkRouter{ + ID: fmt.Sprintf("network-router-%d", i), + NetworkID: network.ID, + AccountID: account.Id, + Peer: peer.ID, + PeerGroups: []string{}, + Masquerade: false, + Metric: 9999, + } + account.NetworkRouters = append(account.NetworkRouters, router) + + resource := &resourceTypes.NetworkResource{ + ID: fmt.Sprintf("network-resource-%d", i), + NetworkID: network.ID, + AccountID: account.Id, + Name: fmt.Sprintf("Network resource for Group %d", i), + Type: "host", + Address: "192.0.2.0/32", + } + account.NetworkResources = append(account.NetworkResources, resource) + // Create a policy for this group policy := &types.Policy{ ID: fmt.Sprintf("policy-%d", i), @@ -767,11 +828,14 @@ func setupTestAccountManager(b *testing.B, peers int, groups int) (*DefaultAccou Enabled: true, Rules: []*types.PolicyRule{ { - ID: fmt.Sprintf("rule-%d", i), - Name: fmt.Sprintf("Rule for Group %d", i), - Enabled: true, - Sources: []string{groupID}, - Destinations: []string{groupID}, + ID: fmt.Sprintf("rule-%d", i), + Name: fmt.Sprintf("Rule for Group %d", i), + Enabled: true, + Sources: []string{groupID}, + Destinations: []string{groupID}, + DestinationResource: types.Resource{ + ID: resource.ID, + }, Bidirectional: true, Protocol: types.PolicyRuleProtocolALL, Action: types.PolicyTrafficActionAccept, diff --git a/management/server/route_test.go b/management/server/route_test.go index 5e2e24611..ef884ec5d 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -9,6 +9,9 @@ import ( "testing" "time" + resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" + routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" + networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/rs/xid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1876,6 +1879,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { Action: "accept", Destination: "192.0.2.0/32", Protocol: "all", + Domains: domain.List{"example.com"}, IsDynamic: true, }, { @@ -1883,6 +1887,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { Action: "accept", Destination: "192.0.2.0/32", Protocol: "all", + Domains: domain.List{"example.com"}, IsDynamic: true, }, } @@ -2160,3 +2165,444 @@ func TestRouteAccountPeersUpdate(t *testing.T) { } }) } + +func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) { + var ( + peerBIp = "100.65.80.39" + peerCIp = "100.65.254.139" + peerHIp = "100.65.29.55" + peerJIp = "100.65.29.65" + peerKIp = "100.65.29.66" + ) + + account := &types.Account{ + Peers: map[string]*nbpeer.Peer{ + "peerA": { + ID: "peerA", + IP: net.ParseIP("100.65.14.88"), + Key: "peerA", + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + GoOS: "linux", + }, + }, + "peerB": { + ID: "peerB", + IP: net.ParseIP(peerBIp), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{}, + }, + "peerC": { + ID: "peerC", + IP: net.ParseIP(peerCIp), + Status: &nbpeer.PeerStatus{}, + }, + "peerD": { + ID: "peerD", + IP: net.ParseIP("100.65.62.5"), + Key: "peerD", + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + GoOS: "linux", + }, + }, + "peerE": { + ID: "peerE", + IP: net.ParseIP("100.65.32.206"), + Key: "peerE", + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + GoOS: "linux", + }, + }, + "peerF": { + ID: "peerF", + IP: net.ParseIP("100.65.250.202"), + Status: &nbpeer.PeerStatus{}, + }, + "peerG": { + ID: "peerG", + IP: net.ParseIP("100.65.13.186"), + Status: &nbpeer.PeerStatus{}, + }, + "peerH": { + ID: "peerH", + IP: net.ParseIP(peerHIp), + Status: &nbpeer.PeerStatus{}, + }, + "peerJ": { + ID: "peerJ", + IP: net.ParseIP(peerJIp), + Status: &nbpeer.PeerStatus{}, + }, + "peerK": { + ID: "peerK", + IP: net.ParseIP(peerKIp), + Status: &nbpeer.PeerStatus{}, + }, + }, + Groups: map[string]*types.Group{ + "router1": { + ID: "router1", + Name: "router1", + Peers: []string{ + "peerA", + }, + }, + "router2": { + ID: "router2", + Name: "router2", + Peers: []string{ + "peerD", + }, + }, + "finance": { + ID: "finance", + Name: "Finance", + Peers: []string{ + "peerF", + "peerG", + }, + }, + "dev": { + ID: "dev", + Name: "Dev", + Peers: []string{ + "peerC", + "peerH", + "peerB", + }, + Resources: []types.Resource{ + {ID: "resource2"}, + }, + }, + "qa": { + ID: "qa", + Name: "QA", + Peers: []string{ + "peerJ", + "peerK", + }, + }, + "restrictQA": { + ID: "restrictQA", + Name: "restrictQA", + Peers: []string{ + "peerJ", + }, + Resources: []types.Resource{ + {ID: "resource4"}, + }, + }, + "unrestrictedQA": { + ID: "unrestrictedQA", + Name: "unrestrictedQA", + Peers: []string{ + "peerK", + }, + Resources: []types.Resource{ + {ID: "resource4"}, + }, + }, + "contractors": { + ID: "contractors", + Name: "Contractors", + Peers: []string{}, + }, + }, + Networks: []*networkTypes.Network{ + { + ID: "network1", + Name: "Finance Network", + }, + { + ID: "network2", + Name: "Devs Network", + }, + { + ID: "network3", + Name: "Contractors Network", + }, + { + ID: "network4", + Name: "QA Network", + }, + }, + NetworkRouters: []*routerTypes.NetworkRouter{ + { + ID: "router1", + NetworkID: "network1", + Peer: "peerE", + PeerGroups: nil, + Masquerade: false, + Metric: 9999, + }, + { + ID: "router2", + NetworkID: "network2", + PeerGroups: []string{"router1", "router2"}, + Masquerade: false, + Metric: 9999, + }, + { + ID: "router3", + NetworkID: "network3", + Peer: "peerE", + PeerGroups: []string{}, + }, + { + ID: "router4", + NetworkID: "network4", + PeerGroups: []string{"router1"}, + Masquerade: false, + Metric: 9999, + }, + }, + NetworkResources: []*resourceTypes.NetworkResource{ + { + ID: "resource1", + NetworkID: "network1", + Name: "Resource 1", + Type: "subnet", + Prefix: netip.MustParsePrefix("10.10.10.0/24"), + }, + { + ID: "resource2", + NetworkID: "network2", + Name: "Resource 2", + Type: "subnet", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + }, + { + ID: "resource3", + NetworkID: "network3", + Name: "Resource 3", + Type: "domain", + Domain: "example.com", + }, + { + ID: "resource4", + NetworkID: "network4", + Name: "Resource 4", + Type: "domain", + Domain: "example.com", + }, + }, + Policies: []*types.Policy{ + { + ID: "policyResource1", + Name: "Policy for resource 1", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "ruleResource1", + Name: "ruleResource1", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolTCP, + Action: types.PolicyTrafficActionAccept, + PortRanges: []types.RulePortRange{ + { + Start: 80, + End: 350, + }, { + Start: 80, + End: 350, + }, + }, + Sources: []string{ + "finance", + }, + DestinationResource: types.Resource{ID: "resource1"}, + }, + }, + }, + { + ID: "policyResource2", + Name: "Policy for resource 2", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "ruleResource2", + Name: "ruleResource2", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolALL, + Action: types.PolicyTrafficActionAccept, + Ports: []string{"80", "320"}, + Sources: []string{"dev"}, + Destinations: []string{"dev"}, + }, + }, + }, + { + ID: "policyResource3", + Name: "policyResource3", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "ruleResource3", + Name: "ruleResource3", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolTCP, + Action: types.PolicyTrafficActionAccept, + Ports: []string{"80"}, + Sources: []string{"restrictQA"}, + Destinations: []string{"restrictQA"}, + }, + }, + }, + { + ID: "policyResource4", + Name: "policyResource4", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "ruleResource4", + Name: "ruleResource4", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolALL, + Action: types.PolicyTrafficActionAccept, + Sources: []string{"unrestrictedQA"}, + Destinations: []string{"unrestrictedQA"}, + }, + }, + }, + }, + } + + validatedPeers := make(map[string]struct{}) + for p := range account.Peers { + validatedPeers[p] = struct{}{} + } + + t.Run("validate applied policies for different network resources", func(t *testing.T) { + getNetworkResourceByID := func(account *types.Account, id string) *resourceTypes.NetworkResource { + for _, resource := range account.NetworkResources { + if resource.ID == id { + return resource + } + } + return nil + } + + getNetworkRouterByID := func(account *types.Account, id string) *routerTypes.NetworkRouter { + for _, router := range account.NetworkRouters { + if router.ID == id { + return router + } + } + return nil + } + + // Test case: Resource1 is directly applied to the policy (policyResource1) + peerE := account.GetPeer("peerE") + router1 := getNetworkRouterByID(account, "router1") + route1 := getNetworkResourceByID(account, "resource1").ToRoute(peerE, router1) + policies := account.GetPoliciesForNetworkResourceRoute(route1) + assert.Len(t, policies, 1, "resource1 should have exactly 1 policy applied directly") + + // Test case: Resource2 is applied to an access control group (dev), + // which is part of the destination in the policy (policyResource2) + peerA := account.GetPeer("peerA") + router2 := getNetworkRouterByID(account, "router2") + route2 := getNetworkResourceByID(account, "resource2").ToRoute(peerA, router2) + policies = account.GetPoliciesForNetworkResourceRoute(route2) + assert.Len(t, policies, 1, "resource2 should have exactly 1 policy applied via access control group") + + // Test case: Resource3 is not applied to any access control group or policy + router3 := getNetworkRouterByID(account, "router3") + route3 := getNetworkResourceByID(account, "resource3").ToRoute(peerE, router3) + policies = account.GetPoliciesForNetworkResourceRoute(route3) + assert.Len(t, policies, 0, "resource3 should have no policies applied") + + // Test case: Resource4 is applied to the access control groups (restrictQA and unrestrictedQA), + // which is part of the destination in the policies (policyResource3 and policyResource4) + router4 := getNetworkRouterByID(account, "router4") + route4 := getNetworkResourceByID(account, "resource4").ToRoute(peerA, router4) + policies = account.GetPoliciesForNetworkResourceRoute(route4) + assert.Len(t, policies, 2, "resource4 should have exactly 2 policy applied via access control groups") + }) + + t.Run("validate routing peer firewall rules for network resources", func(t *testing.T) { + firewallRules := account.GetPeerNetworkResourceFirewallRules(context.Background(), "peerA", validatedPeers) + assert.Len(t, firewallRules, 4) + + expectedFirewallRules := []*types.RouteFirewallRule{ + { + SourceRanges: []string{ + fmt.Sprintf(types.AllowedIPsFormat, peerCIp), + fmt.Sprintf(types.AllowedIPsFormat, peerHIp), + fmt.Sprintf(types.AllowedIPsFormat, peerBIp), + }, + Action: "accept", + Destination: "192.168.0.0/16", + Protocol: "all", + Port: 80, + }, + { + SourceRanges: []string{ + fmt.Sprintf(types.AllowedIPsFormat, peerCIp), + fmt.Sprintf(types.AllowedIPsFormat, peerHIp), + fmt.Sprintf(types.AllowedIPsFormat, peerBIp), + }, + Action: "accept", + Destination: "192.168.0.0/16", + Protocol: "all", + Port: 320, + }, + } + + additionalFirewallRules := []*types.RouteFirewallRule{ + { + SourceRanges: []string{ + fmt.Sprintf(types.AllowedIPsFormat, peerJIp), + }, + Action: "accept", + Destination: "192.0.2.0/32", + Protocol: "tcp", + Port: 80, + Domains: domain.List{"example.com"}, + IsDynamic: true, + }, + { + SourceRanges: []string{ + fmt.Sprintf(types.AllowedIPsFormat, peerKIp), + }, + Action: "accept", + Destination: "192.0.2.0/32", + Protocol: "all", + Domains: domain.List{"example.com"}, + IsDynamic: true, + }, + } + assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(append(expectedFirewallRules, additionalFirewallRules...))) + + // peerD is also the routing peer for resource2 + firewallRules = account.GetPeerNetworkResourceFirewallRules(context.Background(), "peerD", validatedPeers) + assert.Len(t, firewallRules, 2) + assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules)) + + // peerE is a single routing peer for resource1 and resource3 + // PeerE should only receive rules for resource1 since resource3 has no applied policy + firewallRules = account.GetPeerNetworkResourceFirewallRules(context.Background(), "peerE", validatedPeers) + assert.Len(t, firewallRules, 1) + + expectedFirewallRules = []*types.RouteFirewallRule{ + { + SourceRanges: []string{"100.65.250.202/32", "100.65.13.186/32"}, + Action: "accept", + Destination: "10.10.10.0/24", + Protocol: "tcp", + PortRange: types.RulePortRange{Start: 80, End: 350}, + }, + } + assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules)) + + // peerC is part of distribution groups for resource2 but should not receive the firewall rules + firewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerC", validatedPeers) + assert.Len(t, firewallRules, 0) + }) +} diff --git a/management/server/types/account.go b/management/server/types/account.go index 2b8c37178..86c3947e1 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -256,6 +256,9 @@ func (a *Account) GetPeerNetworkMap( routesUpdate := a.GetRoutesToSync(ctx, peerID, peersToConnect) routesFirewallRules := a.GetPeerRoutesFirewallRules(ctx, peerID, validatedPeersMap) + networkResourcesRoutes := a.GetNetworkResourcesRoutesToSync(ctx, peerID, peersToConnect) + networkResourcesFirewallRules := a.GetPeerNetworkResourceFirewallRules(ctx, peerID, validatedPeersMap) + dnsManagementStatus := a.getPeerDNSManagementStatus(peerID) dnsUpdate := nbdns.Config{ ServiceEnable: dnsManagementStatus, @@ -274,11 +277,11 @@ func (a *Account) GetPeerNetworkMap( nm := &NetworkMap{ Peers: peersToConnect, Network: a.Network.Copy(), - Routes: routesUpdate, + Routes: slices.Concat(networkResourcesRoutes, routesUpdate), DNSConfig: dnsUpdate, OfflinePeers: expiredPeers, FirewallRules: firewallRules, - RoutesFirewallRules: routesFirewallRules, + RoutesFirewallRules: slices.Concat(networkResourcesFirewallRules, routesFirewallRules), } if metrics != nil { @@ -1158,6 +1161,7 @@ func getDefaultPermit(route *route.Route) []*RouteFirewallRule { Action: string(PolicyTrafficActionAccept), Destination: route.Network.String(), Protocol: string(PolicyRuleProtocolALL), + Domains: route.Domains, IsDynamic: route.IsDynamic(), } @@ -1198,3 +1202,176 @@ func GetAllRoutePoliciesFromGroups(account *Account, accessControlGroups []strin return routePolicies } + +// getRoutingPeerNetworkResourcesRoutes returns the network resources routes associated with a routing peer ID for the account. +func (a *Account) getRoutingPeerNetworkResourcesRoutes(ctx context.Context, peerID string) []*route.Route { + var routes []*route.Route + + peer := a.GetPeer(peerID) + if peer == nil { + log.WithContext(ctx).Errorf("peer %s that doesn't exist under account %s", peerID, a.Id) + return routes + } + + // currently we support only linux routing peers + if peer.Meta.GoOS != "linux" { + return routes + } + + for _, router := range a.NetworkRouters { + for _, groupID := range router.PeerGroups { + group := a.GetGroup(groupID) + if group == nil { + log.WithContext(ctx).Errorf("router %s has peers group %s that doesn't exist under account %s", router.ID, groupID, a.Id) + continue + } + + for _, id := range group.Peers { + if id != peerID { + continue + } + + resources := a.getNetworkResources(router.NetworkID) + routes = append(routes, a.getNetworkResourcesRoutes(resources, router, peer)...) + } + } + + if router.Peer == peerID { + resources := a.getNetworkResources(router.NetworkID) + routes = append(routes, a.getNetworkResourcesRoutes(resources, router, peer)...) + } + } + + return routes +} + +// GetPeerNetworkResourceFirewallRules gets the network resources firewall rules associated with a routing peer ID for the account. +func (a *Account) GetPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, validatedPeersMap map[string]struct{}) []*RouteFirewallRule { + routesFirewallRules := make([]*RouteFirewallRule, 0) + routes := a.getRoutingPeerNetworkResourcesRoutes(ctx, peerID) + + for _, route := range routes { + resourceAppliedPolicies := a.GetPoliciesForNetworkResourceRoute(route) + distributionPeers := getPoliciesSourcePeers(resourceAppliedPolicies, a.Groups) + + rules := a.getRouteFirewallRules(ctx, peerID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers) + routesFirewallRules = append(routesFirewallRules, rules...) + } + + return routesFirewallRules +} + +// getNetworkResourceGroups retrieves all groups associated with the given network resource route. +func (a *Account) getNetworkResourceGroups(route *route.Route) []*Group { + var networkResourceGroups []*Group + + for _, group := range a.Groups { + for _, resource := range group.Resources { + if resource.ID == string(route.ID) { + networkResourceGroups = append(networkResourceGroups, group) + } + } + } + + return networkResourceGroups +} + +// GetNetworkResourcesRoutesToSync returns network routes for syncing with a specific peer and its ACL peers. +func (a *Account) GetNetworkResourcesRoutesToSync(ctx context.Context, peerID string, aclPeers []*nbpeer.Peer) []*route.Route { + routes := a.getRoutingPeerNetworkResourcesRoutes(ctx, peerID) + peerRoutesMembership := make(LookupMap) + for _, r := range routes { + peerRoutesMembership[string(r.GetHAUniqueID())] = struct{}{} + } + + for _, peer := range aclPeers { + peerRoutes := a.getRoutingPeerNetworkResourcesRoutes(ctx, peer.ID) + filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(peerRoutes, peerRoutesMembership) + routes = append(routes, filteredRoutes...) + } + + return routes +} + +// getNetworkResources filters and returns a list of network resources associated with the given network ID. +func (a *Account) getNetworkResources(networkID string) []*resourceTypes.NetworkResource { + var resources []*resourceTypes.NetworkResource + for _, resource := range a.NetworkResources { + if resource.NetworkID == networkID { + resources = append(resources, resource) + } + } + return resources +} + +// GetPoliciesForNetworkResourceRoute retrieves the list of policies that apply to a specific network resource route. +// A policy is deemed applicable if its destination groups include any of the given network resource groups +// or if its destination resource explicitly matches the provided route. +func (a *Account) GetPoliciesForNetworkResourceRoute(route *route.Route) []*Policy { + var resourceAppliedPolicies []*Policy + + networkResourceGroups := a.getNetworkResourceGroups(route) + + for _, policy := range a.Policies { + if !policy.Enabled { + continue + } + + for _, rule := range policy.Rules { + if !rule.Enabled { + continue + } + + for _, group := range networkResourceGroups { + if slices.Contains(rule.Destinations, group.ID) { + resourceAppliedPolicies = append(resourceAppliedPolicies, policy) + break + } + } + + if rule.DestinationResource.ID == string(route.ID) { + resourceAppliedPolicies = append(resourceAppliedPolicies, policy) + } + } + } + + return resourceAppliedPolicies +} + +// getNetworkResourcesRoutes convert the network resources list to routes list. +func (a *Account) getNetworkResourcesRoutes(resources []*resourceTypes.NetworkResource, router *routerTypes.NetworkRouter, peer *nbpeer.Peer) []*route.Route { + routes := make([]*route.Route, 0, len(resources)) + for _, resource := range resources { + resourceRoute := resource.ToRoute(peer, router) + resourceAppliedPolicies := a.GetPoliciesForNetworkResourceRoute(resourceRoute) + + // distribute the resource routes only if there is policy applied to it + if len(resourceAppliedPolicies) > 0 { + routes = append(routes, resourceRoute) + } + } + + return routes +} + +// getPoliciesSourcePeers collects all unique peers from the source groups defined in the given policies. +func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[string]struct{} { + sourcePeers := make(map[string]struct{}) + + for _, policy := range policies { + for _, rule := range policy.Rules { + for _, sourceGroup := range rule.Sources { + group := groups[sourceGroup] + if group == nil { + continue + } + + for _, peer := range group.Peers { + sourcePeers[peer] = struct{}{} + } + } + } + } + + return sourcePeers +} diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go index 1c9b6ca5b..3d1b7e225 100644 --- a/management/server/types/firewall_rule.go +++ b/management/server/types/firewall_rule.go @@ -53,6 +53,7 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule Action: string(rule.Action), Destination: route.Network.String(), Protocol: string(rule.Protocol), + Domains: route.Domains, IsDynamic: route.IsDynamic(), } diff --git a/management/server/types/route_firewall_rule.go b/management/server/types/route_firewall_rule.go index 73d49d01d..64708d68a 100644 --- a/management/server/types/route_firewall_rule.go +++ b/management/server/types/route_firewall_rule.go @@ -1,5 +1,9 @@ package types +import ( + "github.com/netbirdio/netbird/management/domain" +) + // RouteFirewallRule a firewall rule applicable for a routed network. type RouteFirewallRule struct { // SourceRanges IP ranges of the routing peers. @@ -20,6 +24,9 @@ type RouteFirewallRule struct { // PortRange represents the range of ports for a firewall rule PortRange RulePortRange + // Domains list of network domains for the routed traffic + Domains domain.List + // isDynamic indicates whether the rule is for DNS routing IsDynamic bool }