From 9a3be308e53378b8d2033ba384011f4ce98196eb Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Wed, 29 Oct 2025 15:34:32 -0700 Subject: [PATCH 1/7] [EV-6030] Adapt IP LPM netset trie with namespace precedence --- felix/calc/iplpm.go | 173 +++++++-- felix/calc/iplpm_test.go | 737 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 885 insertions(+), 25 deletions(-) create mode 100644 felix/calc/iplpm_test.go diff --git a/felix/calc/iplpm.go b/felix/calc/iplpm.go index c66b6a439fd..f2113e294e8 100644 --- a/felix/calc/iplpm.go +++ b/felix/calc/iplpm.go @@ -15,8 +15,11 @@ package calc import ( + "slices" "strings" + "unique" + "github.com/sirupsen/logrus" "github.com/tchap/go-patricia/v2/patricia" "github.com/projectcalico/calico/felix/ip" @@ -24,19 +27,17 @@ import ( "github.com/projectcalico/calico/libcalico-go/lib/set" ) -// Node is represented by cidr as KEY and v1 key data stored in keys. +// Node is represented by cidr as KEY and stores all keys for that CIDR. type IPTrieNode struct { cidr ip.CIDR - keys []model.Key + keys []unique.Handle[model.Key] } -// Root of IpTree type IpTrie struct { lpmCache *patricia.Trie existingCidrs set.Set[ip.CIDR] } -// NewIpTrie creates new Patricia trie and Initializes func NewIpTrie() *IpTrie { return &IpTrie{ lpmCache: patricia.NewTrie(), @@ -44,17 +45,70 @@ func NewIpTrie() *IpTrie { } } -// newIPTrieNode Function creates new empty node containing the CIDR and single key. +// getLowestSortingKey returns the key with the lexicographically smallest name from a slice of +// keys. This provides deterministic tie-breaking for network sets with the same prefix length. +func getLowestSortingKey(keys []unique.Handle[model.Key]) model.Key { + if len(keys) == 0 { + return nil + } + if len(keys) == 1 { + return keys[0].Value() + } + + // Linear scan to find the lexicographically lowest key + lowestHandle := keys[0] + lowestKeyStr := lowestHandle.Value().String() + + for i := 1; i < len(keys); i++ { + keyStr := keys[i].Value().String() + if keyStr < lowestKeyStr { + lowestHandle = keys[i] + lowestKeyStr = keyStr + } + } + + return lowestHandle.Value() +} + func newIPTrieNode(cidr ip.CIDR, key model.Key) *IPTrieNode { - return &IPTrieNode{cidr: cidr, keys: []model.Key{key}} + return &IPTrieNode{ + cidr: cidr, + keys: []unique.Handle[model.Key]{unique.Make(key)}, + } } -// GetLongestPrefixCidr finds longest prefix match CIDR for the Given IP and if successful return the last key -// recorded. -func (t *IpTrie) GetLongestPrefixCidr(ipAddr ip.Addr) (model.Key, bool) { +// NamespaceFilterFunc defines a function that filters keys based on namespace preferences. +// Returns: (isNamespaceMatch, isGlobal, accept) +// - isNamespaceMatch: true if the key matches the preferred namespace +// - isGlobal: true if the key is a global (non-namespaced) NetworkSet +// - accept: true if the key should be accepted for processing +type NamespaceFilterFunc func(key model.Key, preferredNamespace string) (isNamespaceMatch bool, isGlobal bool, accept bool) + +// DefaultNamespaceFilter is the standard namespace filtering logic +func DefaultNamespaceFilter(key model.Key, preferredNamespace string) (isNamespaceMatch bool, isGlobal bool, accept bool) { + nsKey, ok := key.(model.NetworkSetKey) + if !ok { + logrus.Debugf("Non-NetworkSet key detected: %v", key) + return false, false, false // Non-NetworkSet keys are treated as global + } + + namespace := nsKey.Namespace() + if namespace != "" { + // Namespaced NetworkSet + isMatch := namespace == preferredNamespace + return isMatch, false, true + } + + // Global NetworkSet + return false, true, true +} + +// GetLongestPrefixCidr finds the longest prefix match CIDR for the given IP and if successful returns the +// lexicographically lowest key associated with that CIDR. +func (trie *IpTrie) GetLongestPrefixCidr(ipAddr ip.Addr) (model.Key, bool) { var longestPrefix patricia.Prefix var longestItem patricia.Item - ptrie := t.lpmCache + ptrie := trie.lpmCache err := ptrie.VisitPrefixes(patricia.Prefix(ipAddr.AsBinary()), func(prefix patricia.Prefix, item patricia.Item) error { @@ -64,11 +118,79 @@ func (t *IpTrie) GetLongestPrefixCidr(ipAddr ip.Addr) (model.Key, bool) { } return nil }) - if err != nil || longestItem == nil { + + if err != nil || len(longestPrefix) == 0 { return nil, false } + node := longestItem.(*IPTrieNode) - return node.keys[len(node.keys)-1], true + return getLowestSortingKey(node.keys), true +} + +// GetLongestPrefixCidrWithNamespaceIsolation finds the best prefix match with namespace isolation. +// Priority order: +// 1) preferred namespace match +// 2) global match +// 3) any other namespace match +func (trie *IpTrie) GetLongestPrefixCidrWithNamespaceIsolation(ipAddr ip.Addr, preferredNamespace string) (model.Key, bool) { + ptrie := trie.lpmCache + searchPrefix := patricia.Prefix(ipAddr.AsBinary()) + + type bestMatch struct { + keys []unique.Handle[model.Key] + prefix patricia.Prefix + } + + var ( + bestPreferred bestMatch + bestGlobal bestMatch + bestOther bestMatch + ) + + updateBest := func(b *bestMatch, prefix patricia.Prefix, keyHandle unique.Handle[model.Key]) { + switch { + case len(prefix) > len(b.prefix): + b.keys = append(b.keys[:0], keyHandle) + b.prefix = prefix + case len(prefix) == len(b.prefix): + b.keys = append(b.keys, keyHandle) + } + } + + if err := ptrie.VisitPrefixes(searchPrefix, func(prefix patricia.Prefix, item patricia.Item) error { + node := item.(*IPTrieNode) + for _, keyHandle := range node.keys { + key := keyHandle.Value() + nsKey, ok := key.(model.NetworkSetKey) + if !ok { + continue + } + ns := nsKey.Namespace() + + switch { + case preferredNamespace != "" && ns == preferredNamespace: + updateBest(&bestPreferred, prefix, keyHandle) + case ns == "": + updateBest(&bestGlobal, prefix, keyHandle) + default: + updateBest(&bestOther, prefix, keyHandle) + } + } + return nil + }); err != nil { + return nil, false + } + + switch { + case len(bestPreferred.keys) > 0: + return getLowestSortingKey(bestPreferred.keys), true + case len(bestGlobal.keys) > 0: + return getLowestSortingKey(bestGlobal.keys), true + case len(bestOther.keys) > 0: + return getLowestSortingKey(bestOther.keys), true + default: + return nil, false + } } // GetKeys return list of keys for the Given CIDR @@ -79,7 +201,12 @@ func (t *IpTrie) GetKeys(cidr ip.CIDR) ([]model.Key, bool) { if val != nil { node := val.(*IPTrieNode) - return node.keys, true + // Convert handles back to keys + keys := make([]model.Key, len(node.keys)) + for i, h := range node.keys { + keys[i] = h.Value() + } + return keys, true } return nil, false @@ -100,14 +227,9 @@ func (t *IpTrie) DeleteKey(cidr ip.CIDR, key model.Key) { t.existingCidrs.Discard(cidr) ptrie.Delete(patricia.Prefix(cidrb)) } else { - ii := 0 - for _, val := range node.keys { - if val != key { - node.keys[ii] = val - ii++ - } - } - node.keys = node.keys[:ii] + node.keys = slices.DeleteFunc(node.keys, func(h unique.Handle[model.Key]) bool { + return h.Value() == key + }) } } @@ -126,16 +248,17 @@ func (t *IpTrie) InsertKey(cidr ip.CIDR, key model.Key) { ptrie.Insert(patricia.Prefix(cidrb), newNode) } else { node := val.(*IPTrieNode) + keyHandle := unique.Make(key) isExistingNetset := false - for i, val := range node.keys { - if key == val { - node.keys[i] = key + for i, existingHandle := range node.keys { + if key == existingHandle.Value() { + node.keys[i] = keyHandle isExistingNetset = true break } } if !isExistingNetset { - node.keys = append(node.keys, key) + node.keys = append(node.keys, keyHandle) } } } diff --git a/felix/calc/iplpm_test.go b/felix/calc/iplpm_test.go new file mode 100644 index 00000000000..4d8881f2b58 --- /dev/null +++ b/felix/calc/iplpm_test.go @@ -0,0 +1,737 @@ +// Copyright (c) 2019-2025 Tigera, Inc. All rights reserved. + +package calc_test + +import ( + "fmt" + "net" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + . "github.com/projectcalico/calico/felix/calc" + "github.com/projectcalico/calico/felix/ip" + "github.com/projectcalico/calico/libcalico-go/lib/backend/api" + "github.com/projectcalico/calico/libcalico-go/lib/backend/model" + calinet "github.com/projectcalico/calico/libcalico-go/lib/net" +) + +var _ = DescribeTable("Check Inserting CIDR and compare with network set names", + func(key model.NetworkSetKey, netset *model.NetworkSet) { + it := NewIpTrie() + + for _, cidr := range netset.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.InsertKey(cidrb, key) + } + for _, cidr := range netset.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + keys, ok := it.GetKeys(cidrb) + for _, ekey := range keys { + Expect(ok).To(Equal(true)) + Expect(ekey).To(Equal(key)) + } + } + }, + Entry("Insert network CIDR and match with ns name", netSet1Key, &netSet1), + Entry("Insert network CIDR and match with ns name", netSet2Key, &netSet2), +) + +var _ = DescribeTable("Insert and Delete CIDRs and compare with network set names", + func(key model.NetworkSetKey, netset *model.NetworkSet, key1 model.NetworkSetKey, netset1 *model.NetworkSet) { + it := NewIpTrie() + + for _, cidr := range netset1.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.InsertKey(cidrb, key1) + } + for _, cidr := range netset.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.InsertKey(cidrb, key) + } + for _, cidr := range netset1.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.DeleteKey(cidrb, key1) + } + for _, cidr := range netset.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + keys, ok := it.GetKeys(cidrb) + for _, ekey := range keys { + Expect(ok).To(Equal(true)) + Expect(ekey).To(Equal(key)) + } + } + }, + Entry("Insert network CIDR and match with ns name", netSet1Key, &netSet1, netSet2Key, &netSet2), + Entry("Insert network CIDR and match with ns name", netSet2Key, &netSet2, netSet1Key, &netSet1), +) + +var _ = DescribeTable("Test by finding Longest Prefix Match CIDR's name for given IP Address", + func(key1 model.NetworkSetKey, key2 model.NetworkSetKey, netset1 *model.NetworkSet, netset2 *model.NetworkSet, ipAddr net.IP, res model.NetworkSetKey) { + it := NewIpTrie() + ipaddr := ip.FromNetIP(ipAddr) + + for _, cidr := range netset1.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.InsertKey(cidrb, key1) + } + for _, cidr := range netset2.Nets { + cidrb := ip.CIDRFromCalicoNet(cidr) + it.InsertKey(cidrb, key2) + } + + key, ok := it.GetLongestPrefixCidr(ipaddr) + Expect(ok).To(Equal(true)) + Expect(key).To(Equal(res)) + }, + Entry("Longest Prefix Match find ns name", netSet1Key, netSet3Key, &netSet1, &netSet3, netset3Ip1a, netSet1Key), + Entry("Longest Prefix Match find ns name", netSet1Key, netSet3Key, &netSet1, &netSet3, netset3Ip1b, netSet3Key), +) + +var _ = Describe("IpTrie Namespace-Aware Functionality", func() { + var it *IpTrie + var ns1Key, ns2Key, globalKey model.NetworkSetKey + var testCIDR ip.CIDR + var testIP ip.Addr + + BeforeEach(func() { + it = NewIpTrie() + + // Create test keys with different namespaces + ns1Key = model.NetworkSetKey{Name: "namespace1/test-netset"} + ns2Key = model.NetworkSetKey{Name: "namespace2/test-netset"} + globalKey = model.NetworkSetKey{Name: "global-netset"} + + // Create test CIDR and IP + testCIDR = ip.MustParseCIDROrIP("10.0.0.0/24") + testIP = ip.FromNetIP(mustParseIP("10.0.0.100").IP) + }) + + Context("when testing namespace-aware insertion and retrieval", func() { + It("should organize keys by namespace correctly", func() { + // Insert keys from different namespaces for the same CIDR + it.InsertKey(testCIDR, globalKey) + it.InsertKey(testCIDR, ns1Key) + it.InsertKey(testCIDR, ns2Key) + + // Test namespace-specific retrieval + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns1Key)) + + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace2") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns2Key)) + + // Test fallback to global when namespace not found + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey)) + }) + + It("should use lexicographic tie-breaking for same namespace", func() { + // Insert two keys in the same namespace for the same CIDR + firstKey := model.NetworkSetKey{Name: "namespace1/first-netset"} + secondKey := model.NetworkSetKey{Name: "namespace1/second-netset"} + + it.InsertKey(testCIDR, firstKey) + it.InsertKey(testCIDR, secondKey) + + // Should return the lexicographically smallest key + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(firstKey)) // "first-netset" < "second-netset" + }) + + It("should return the namespace that matches the target IP", func() { + testIP2 := ip.FromNetIP(mustParseIP("192.168.0.100").IP) + + // Insert keys with overlapping CIDRs in different namespaces + ns1CIDR := ip.MustParseCIDROrIP("10.0.0.0/24") + ns2CIDR := ip.MustParseCIDROrIP("192.168.0.0/24") + it.InsertKey(ns1CIDR, ns1Key) + it.InsertKey(ns2CIDR, ns2Key) + + // Test that the correct namespace is returned based on the IP + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns1Key)) + + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP2, "namespace2") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns2Key)) + }) + + It("should fallback to other namespaces when preferred namespace is empty", func() { + nsKey := model.NetworkSetKey{Name: "namespace1/alpha-netset"} + otherNsKey := model.NetworkSetKey{Name: "namespace2/beta-netset"} + + it.InsertKey(testCIDR, nsKey) + it.InsertKey(testCIDR, otherNsKey) + + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(nsKey)) + }) + }) + + Context("when testing backward compatibility", func() { + It("should work with legacy NetworkSetKey", func() { + legacyKey := netSet1Key // This is a NetworkSetKey from test data + it.InsertKey(testCIDR, legacyKey) + + // Should work with legacy method + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(legacyKey)) + + // Should also work with namespace method (treats as global) + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "any-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(legacyKey)) + }) + + It("should maintain allKeys for backward compatibility", func() { + it.InsertKey(testCIDR, globalKey) + it.InsertKey(testCIDR, ns1Key) + + // GetKeys should return all keys + keys, found := it.GetKeys(testCIDR) + Expect(found).To(BeTrue()) + Expect(keys).To(ContainElement(globalKey)) + Expect(keys).To(ContainElement(ns1Key)) + }) + }) + + Context("when testing deletion", func() { + BeforeEach(func() { + it.InsertKey(testCIDR, globalKey) + it.InsertKey(testCIDR, ns1Key) + it.InsertKey(testCIDR, ns2Key) + }) + + It("should remove keys correctly from namespace buckets", func() { + // Delete namespace1 key + it.DeleteKey(testCIDR, ns1Key) + + // Should no longer find namespace1 key + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey)) // Should fallback to global + + // Should still find namespace2 key + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace2") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns2Key)) + }) + + It("should remove entire node when no keys remain", func() { + // Delete all keys + it.DeleteKey(testCIDR, globalKey) + it.DeleteKey(testCIDR, ns1Key) + it.DeleteKey(testCIDR, ns2Key) + + // Should not find anything + _, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeFalse()) + + _, found = it.GetKeys(testCIDR) + Expect(found).To(BeFalse()) + }) + }) + + Context("when testing edge cases", func() { + It("should handle empty namespace correctly", func() { + emptyNsKey := model.NetworkSetKey{Name: "empty-ns"} + it.InsertKey(testCIDR, emptyNsKey) + + // Should be treated as global + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "any-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(emptyNsKey)) + }) + + It("should handle non-existent CIDR gracefully", func() { + nonExistentIP := ip.FromNetIP(mustParseIP("192.168.1.1").IP) + + _, found := it.GetLongestPrefixCidrWithNamespaceIsolation(nonExistentIP, "namespace1") + Expect(found).To(BeFalse()) + }) + + It("should handle unknown key types", func() { + unknownKey := model.PolicyKey{Name: "test-policy"} + it.InsertKey(testCIDR, unknownKey) + + // Should not be found since key type is unexpected + _, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "any-namespace") + Expect(found).To(BeFalse()) + }) + }) + + Context("when testing multiple overlapping CIDRs", func() { + var broadCIDR, narrowCIDR ip.CIDR + var testIPInBoth, testIPInNarrowOnly ip.Addr + + BeforeEach(func() { + // Create overlapping CIDRs + broadCIDR = ip.MustParseCIDROrIP("10.0.0.0/16") + narrowCIDR = ip.MustParseCIDROrIP("10.0.1.0/24") + + // IP that matches both CIDRs + testIPInBoth = ip.FromNetIP(mustParseIP("10.0.1.100").IP) + // IP that matches only broad CIDR + testIPInNarrowOnly = ip.FromNetIP(mustParseIP("10.0.2.100").IP) + }) + + It("should prioritize namespace isolation over longest prefix match", func() { + // Insert keys for both CIDRs + broadNs1Key := model.NetworkSetKey{Name: "namespace1/broad"} + narrowNs2Key := model.NetworkSetKey{Name: "namespace2/narrow"} + + it.InsertKey(broadCIDR, broadNs1Key) + it.InsertKey(narrowCIDR, narrowNs2Key) + + // For IP in both: should prefer namespace match over longer prefix + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIPInBoth, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(broadNs1Key)) // Namespace1 key wins due to namespace isolation + + // For IP in narrow only: should prefer namespace2 match when requesting namespace2 + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIPInBoth, "namespace2") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(narrowNs2Key)) // Namespace2 key wins due to namespace isolation + + // For IP in broad only: should return broad CIDR + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIPInNarrowOnly, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(broadNs1Key)) + }) + + It("should follow three-tier priority: namespace > global > other namespace", func() { + // Create a more specific test scenario with all three types of keys + testCIDR := ip.MustParseCIDROrIP("10.0.1.0/24") + testIP := ip.FromNetIP(mustParseIP("10.0.1.100").IP) + + // Insert keys with different priorities - all matching the same CIDR + preferredNsKey := model.NetworkSetKey{Name: "target-namespace/preferred"} + globalKey := model.NetworkSetKey{Name: "global-netset"} + otherNsKey := model.NetworkSetKey{Name: "other-namespace/fallback"} + + it.InsertKey(testCIDR, preferredNsKey) + it.InsertKey(testCIDR, globalKey) + it.InsertKey(testCIDR, otherNsKey) + + // Test 1: When preferred namespace exists, it should win + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "target-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(preferredNsKey)) + + // Test 2: When preferred namespace doesn't exist, global should win over other namespace + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey)) + + // Test 3: When neither preferred nor global exists, other namespace should be returned + it2 := NewIpTrie() + it2.InsertKey(testCIDR, otherNsKey) + + key, found = it2.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(otherNsKey)) + }) + + It("should respect priority even with different prefix lengths", func() { + // Create test scenario where global has longer prefix than preferred namespace + broadCIDR := ip.MustParseCIDROrIP("10.0.0.0/16") // Less specific + narrowCIDR := ip.MustParseCIDROrIP("10.0.1.0/24") // More specific + testIP := ip.FromNetIP(mustParseIP("10.0.1.100").IP) // Matches both + + // Preferred namespace has broader CIDR, global has narrower CIDR + preferredNsKey := model.NetworkSetKey{Name: "target-namespace/broad"} + globalKey := model.NetworkSetKey{Name: "global-narrow"} + otherNsKey := model.NetworkSetKey{Name: "other-namespace/narrow"} + + it.InsertKey(broadCIDR, preferredNsKey) + it.InsertKey(narrowCIDR, globalKey) + it.InsertKey(narrowCIDR, otherNsKey) + + // Preferred namespace should still win despite having broader prefix + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "target-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(preferredNsKey)) // Namespace priority over prefix length + + // When preferred namespace doesn't exist, global should win (longer prefix) + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey)) // Global over other namespace + + // Test other namespace fallback by removing global + it3 := NewIpTrie() + it3.InsertKey(narrowCIDR, otherNsKey) + + key, found = it3.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(otherNsKey)) // Other namespace as last resort + }) + }) +}) + +// Tests for collector integration with namespace-aware lookups +var _ = Describe("Collector Integration Tests", func() { + var lc *LookupsCache + var nsCache *NetworkSetLookupsCache + var testIP [16]byte + + BeforeEach(func() { + lc = NewLookupsCache() + nsCache = NewNetworkSetLookupsCache() + + ipAddr := mustParseIP("10.0.0.100").IP + copy(testIP[:], ipAddr.To16()) + }) + + Context("when testing collector namespace interface", func() { + It("should expose namespace-aware lookups through LookupsCache", func() { + // Verify that the collector interface has the namespace method + Expect(lc).To(BeAssignableToTypeOf(&LookupsCache{})) + + // Test that the method exists and has the right signature + _, found := lc.GetNetworkSetWithNamespace(testIP, "test-namespace") + Expect(found).To(BeFalse()) // Should be false since no data loaded + + // Verify the method is available for collector usage + Expect(lc.GetNetworkSetWithNamespace).ToNot(BeNil()) + }) + + It("should delegate to optimized NetworkSetLookupsCache implementation", func() { + // Create test data + globalKey := model.NetworkSetKey{Name: "global-netset"} + globalNS := model.NetworkSet{ + Nets: []calinet.IPNet{ + calinet.MustParseNetwork("10.0.0.0/8"), // Broad CIDR + }, + } + + ns1Key := model.NetworkSetKey{Name: "namespace1/specific-netset"} + ns1NS := model.NetworkSet{ + Nets: []calinet.IPNet{ + calinet.MustParseNetwork("10.0.0.0/24"), // More specific than global + }, + } + + // Add to the underlying cache + nsCache.OnUpdate(api.Update{ + KVPair: model.KVPair{Key: globalKey, Value: &globalNS}, + UpdateType: api.UpdateTypeKVNew, + }) + nsCache.OnUpdate(api.Update{ + KVPair: model.KVPair{Key: ns1Key, Value: &ns1NS}, + UpdateType: api.UpdateTypeKVNew, + }) + + // Test direct NetworkSetLookupsCache usage (what collector uses) + ed, found := nsCache.GetNetworkSetFromIPWithNamespace(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(ed.Key()).To(Equal(ns1Key)) + + // Test fallback behavior + ed, found = nsCache.GetNetworkSetFromIPWithNamespace(testIP, "nonexistent") + Expect(found).To(BeTrue()) + Expect(ed.Key()).To(Equal(globalKey)) // Should return global as fallback + + // Verify legacy method still works + ed2, found2 := nsCache.GetNetworkSetFromIP(testIP) + Expect(found2).To(BeTrue()) + Expect(ed2.Key()).To(Equal(globalKey)) // Should return longest prefix match + }) + + It("should maintain performance with namespace-aware lookups", func() { + // Add multiple NetworkSets across namespaces + for i := 0; i < 10; i++ { + key := model.NetworkSetKey{ + Name: fmt.Sprintf("namespace%d/test-netset", i), + } + ns := model.NetworkSet{ + Nets: []calinet.IPNet{ + calinet.MustParseNetwork(fmt.Sprintf("10.%d.0.0/16", i)), + }, + } + + nsCache.OnUpdate(api.Update{ + KVPair: model.KVPair{Key: key, Value: &ns}, + UpdateType: api.UpdateTypeKVNew, + }) + } + + // Performance test: many lookups should complete quickly + start := time.Now() + for i := 0; i < 100; i++ { + namespace := fmt.Sprintf("namespace%d", i%10) + testIPLoop := [16]byte{} + loopIP := mustParseIP(fmt.Sprintf("10.%d.1.1", i%10)).IP + copy(testIPLoop[:], loopIP.To16()) + + _, found := nsCache.GetNetworkSetFromIPWithNamespace(testIPLoop, namespace) + Expect(found).To(BeTrue()) + } + elapsed := time.Since(start) + + // Should complete 100 lookups in reasonable time + Expect(elapsed).To(BeNumerically("<", 100*time.Millisecond)) + }) + }) +}) + +// Tests for tie-breaking functionality - ensuring deterministic behavior when multiple +// network sets match the same IP with equal prefix lengths +var _ = Describe("IpTrie Tie-Breaking Functionality", func() { + var it *IpTrie + var testCIDR ip.CIDR + var testIP ip.Addr + + BeforeEach(func() { + it = NewIpTrie() + testCIDR = ip.MustParseCIDROrIP("10.0.0.0/24") + testIP = ip.FromNetIP(mustParseIP("10.0.0.100").IP) + }) + + Context("when multiple keys have the same prefix length", func() { + It("should return the lexicographically smallest key name", func() { + // Insert keys in reverse alphabetical order to test sorting + zebraKey := model.NetworkSetKey{Name: "zebra-netset"} + betaKey := model.NetworkSetKey{Name: "beta-netset"} + alphaKey := model.NetworkSetKey{Name: "alpha-netset"} + + it.InsertKey(testCIDR, zebraKey) + it.InsertKey(testCIDR, betaKey) + it.InsertKey(testCIDR, alphaKey) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(alphaKey)) // Should return alpha (lexicographically smallest) + }) + + It("should be deterministic regardless of insertion order", func() { + keys := []model.NetworkSetKey{ + {Name: "gamma-netset"}, + {Name: "alpha-netset"}, + {Name: "delta-netset"}, + {Name: "beta-netset"}, + } + + // Test multiple insertion orders + for i := 0; i < 5; i++ { + it := NewIpTrie() + + // Insert in different orders + for j := i; j < len(keys)+i; j++ { + it.InsertKey(testCIDR, keys[j%len(keys)]) + } + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(model.NetworkSetKey{Name: "alpha-netset"})) + } + }) + + It("should handle single key correctly", func() { + singleKey := model.NetworkSetKey{Name: "single-netset"} + it.InsertKey(testCIDR, singleKey) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(singleKey)) + }) + + It("should handle keys with special characters", func() { + key1 := model.NetworkSetKey{Name: "z-special/netset"} + key2 := model.NetworkSetKey{Name: "a-special_netset"} + key3 := model.NetworkSetKey{Name: "m-special.netset"} + + it.InsertKey(testCIDR, key1) + it.InsertKey(testCIDR, key2) + it.InsertKey(testCIDR, key3) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(key2)) // "a-special_netset" is lexicographically smallest + }) + }) + + Context("when testing namespace-aware tie-breaking", func() { + It("should break ties within the same namespace", func() { + ns1Key1 := model.NetworkSetKey{Name: "namespace1/zebra-netset"} + ns1Key2 := model.NetworkSetKey{Name: "namespace1/alpha-netset"} + ns1Key3 := model.NetworkSetKey{Name: "namespace1/beta-netset"} + + it.InsertKey(testCIDR, ns1Key1) + it.InsertKey(testCIDR, ns1Key2) + it.InsertKey(testCIDR, ns1Key3) + + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns1Key2)) // namespace1/alpha-netset is lexicographically smallest + }) + + It("should prioritize global over other namespace keys when no preferred namespace match", func() { + ns1Key := model.NetworkSetKey{Name: "namespace2/some-netset"} + ns2Key := model.NetworkSetKey{Name: "namespace1/some-netset"} + globalKey := model.NetworkSetKey{Name: "global-netset"} + + it.InsertKey(testCIDR, ns1Key) + it.InsertKey(testCIDR, ns2Key) + it.InsertKey(testCIDR, globalKey) + + // Request a namespace that doesn't exist, should prefer global over other namespaces + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey)) // Global key should be returned over other namespaces + + // Test fallback to other namespace when no global exists + it2 := NewIpTrie() + it2.InsertKey(testCIDR, ns1Key) + it2.InsertKey(testCIDR, ns2Key) + + key, found = it2.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent") + Expect(found).To(BeTrue()) + // Should return one of the namespace keys (lexicographically smallest) + Expect(key).To(Equal(ns2Key)) // namespace1/some-netset is lexicographically smaller + }) + + It("should break ties between global keys", func() { + globalKey1 := model.NetworkSetKey{Name: "zebra-global"} + globalKey2 := model.NetworkSetKey{Name: "alpha-global"} + + it.InsertKey(testCIDR, globalKey1) + it.InsertKey(testCIDR, globalKey2) + + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "any-namespace") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey2)) // alpha-global is lexicographically smallest + }) + + It("should prefer namespace match over global even if global is lexicographically smaller", func() { + globalKey := model.NetworkSetKey{Name: "alpha-global"} + nsKey := model.NetworkSetKey{Name: "namespace1/zebra-netset"} + + it.InsertKey(testCIDR, globalKey) + it.InsertKey(testCIDR, nsKey) + + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(nsKey)) // Namespace match takes priority + }) + + It("should handle mixed global and namespace keys with proper tie-breaking", func() { + // Same prefix length, different key types + globalKey1 := model.NetworkSetKey{Name: "zebra-global"} + globalKey2 := model.NetworkSetKey{Name: "alpha-global"} + ns1Key1 := model.NetworkSetKey{Name: "namespace1/zebra-netset"} + ns1Key2 := model.NetworkSetKey{Name: "namespace1/alpha-netset"} + + it.InsertKey(testCIDR, globalKey1) + it.InsertKey(testCIDR, globalKey2) + it.InsertKey(testCIDR, ns1Key1) + it.InsertKey(testCIDR, ns1Key2) + + // When requesting namespace1, should get the lexicographically smallest within namespace1 + key, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "namespace1") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(ns1Key2)) // namespace1/alpha-netset + + // When requesting non-existent namespace, should get lexicographically smallest global + key, found = it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "nonexistent") + Expect(found).To(BeTrue()) + Expect(key).To(Equal(globalKey2)) // alpha-global + }) + }) + + Context("when testing with different prefix lengths", func() { + It("should prioritize longer prefix over lexicographic order", func() { + // Longer prefix should win even if name is lexicographically larger + broadKey := model.NetworkSetKey{Name: "alpha-broad"} + narrowKey := model.NetworkSetKey{Name: "zebra-narrow"} + + broadCIDR := ip.MustParseCIDROrIP("10.0.0.0/16") // Less specific + narrowCIDR := ip.MustParseCIDROrIP("10.0.0.0/24") // More specific + + it.InsertKey(broadCIDR, broadKey) + it.InsertKey(narrowCIDR, narrowKey) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(narrowKey)) // Longer prefix wins despite larger name + }) + + It("should only apply tie-breaking when prefix lengths are equal", func() { + // Different prefix lengths with multiple keys each + broadKey1 := model.NetworkSetKey{Name: "zebra-broad"} + broadKey2 := model.NetworkSetKey{Name: "alpha-broad"} + narrowKey1 := model.NetworkSetKey{Name: "zebra-narrow"} + narrowKey2 := model.NetworkSetKey{Name: "alpha-narrow"} + + broadCIDR := ip.MustParseCIDROrIP("10.0.0.0/16") + narrowCIDR := ip.MustParseCIDROrIP("10.0.0.0/24") + + it.InsertKey(broadCIDR, broadKey1) + it.InsertKey(broadCIDR, broadKey2) + it.InsertKey(narrowCIDR, narrowKey1) + it.InsertKey(narrowCIDR, narrowKey2) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + // Should get alpha-narrow (lexicographically smallest among the longest prefix matches) + Expect(key).To(Equal(narrowKey2)) + }) + }) + + Context("when testing edge cases", func() { + It("should handle empty trie", func() { + _, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeFalse()) + }) + + It("should handle keys that differ only in casing", func() { + key1 := model.NetworkSetKey{Name: "Alpha-netset"} + key2 := model.NetworkSetKey{Name: "alpha-netset"} + + it.InsertKey(testCIDR, key1) + it.InsertKey(testCIDR, key2) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + // "Alpha-netset" comes before "alpha-netset" in ASCII ordering + Expect(key).To(Equal(key1)) + }) + + It("should handle numeric suffixes correctly", func() { + key1 := model.NetworkSetKey{Name: "netset-10"} + key2 := model.NetworkSetKey{Name: "netset-2"} + key3 := model.NetworkSetKey{Name: "netset-20"} + + it.InsertKey(testCIDR, key1) + it.InsertKey(testCIDR, key2) + it.InsertKey(testCIDR, key3) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + // String comparison: "netset-10" < "netset-2" < "netset-20" + Expect(key).To(Equal(key1)) + }) + + It("should handle very long key names", func() { + shortKey := model.NetworkSetKey{Name: "z"} + longKey := model.NetworkSetKey{Name: "a" + strings.Repeat("-very-long-name", 100)} + + it.InsertKey(testCIDR, shortKey) + it.InsertKey(testCIDR, longKey) + + key, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue()) + Expect(key).To(Equal(longKey)) // Long key starting with "a" is lexicographically smaller + }) + }) +}) From d5550c6739854a14ead8a696713475b34b6452d0 Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Wed, 29 Oct 2025 15:35:08 -0700 Subject: [PATCH 2/7] [EV-6030] Adapt network set lookup cache with namespace preference --- felix/calc/lookups_cache.go | 6 ++++ felix/calc/networkset_lookup_cache.go | 35 +++++++++++++++------- felix/calc/networkset_lookup_cache_test.go | 18 +++++++++-- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/felix/calc/lookups_cache.go b/felix/calc/lookups_cache.go index b8a2464a51e..c9d140a9247 100644 --- a/felix/calc/lookups_cache.go +++ b/felix/calc/lookups_cache.go @@ -79,6 +79,12 @@ func (lc *LookupsCache) GetNetworkSet(addr [16]byte) (EndpointData, bool) { return lc.nsCache.GetNetworkSetFromIP(addr) } +// GetNetworkSetWithNamespace returns the NetworkSet information for an address with namespace +// precedence. If preferredNamespace is provided, NetworkSets in that namespace are prioritized. +func (lc *LookupsCache) GetNetworkSetWithNamespace(addr [16]byte, preferredNamespace string) (EndpointData, bool) { + return lc.nsCache.GetNetworkSetFromIPWithNamespace(addr, preferredNamespace) +} + // GetRuleIDFromNFLOGPrefix returns the RuleID associated with the supplied NFLOG prefix. func (lc *LookupsCache) GetRuleIDFromNFLOGPrefix(prefix [64]byte) *RuleID { return lc.polCache.GetRuleIDFromNFLOGPrefix(prefix) diff --git a/felix/calc/networkset_lookup_cache.go b/felix/calc/networkset_lookup_cache.go index c3052cbe640..fbb807353e6 100644 --- a/felix/calc/networkset_lookup_cache.go +++ b/felix/calc/networkset_lookup_cache.go @@ -104,7 +104,7 @@ func (nc *NetworkSetLookupsCache) RegisterWith(allUpdateDispatcher *dispatcher.D } // OnUpdate is the callback method registered with the AllUpdatesDispatcher for -// the model.NetworkSet type. This method updates the mapping between networkSets +// the NetworkSet type. This method updates the mapping between networkSets // and the corresponding CIDRs that they contain. func (nc *NetworkSetLookupsCache) OnUpdate(nsUpdate api.Update) (_ bool) { switch k := nsUpdate.Key.(type) { @@ -177,25 +177,40 @@ func (nc *NetworkSetLookupsCache) removeNetworkSet(key model.Key) { nc.ipTree.DeleteKey(oldCIDR, key) } delete(nc.networkSets, key) + nc.reportNetworksetCacheMetrics() } // GetNetworkSetFromIP finds Longest Prefix Match CIDR from given IP ADDR and return last observed // Networkset for that CIDR func (nc *NetworkSetLookupsCache) GetNetworkSetFromIP(addr [16]byte) (ed EndpointData, ok bool) { + return nc.GetNetworkSetFromIPWithNamespace(addr, "") +} + +// GetNetworkSetFromIPWithNamespace finds NetworkSet for the Given IP with namespace precedence. +// It prioritizes NetworkSets in the preferredNamespace, falling back to longest prefix match of +// the global networkSets if none found, then the first lexicographically ordered matching +// NetworkSet if no other matches are found. If no preferred namespace is provided, it prioritizes +// global NetworkSets. +func (nc *NetworkSetLookupsCache) GetNetworkSetFromIPWithNamespace(ipAddr [16]byte, preferredNamespace string) (ed EndpointData, ok bool) { + netIP := net.IP(ipAddr[:]) + addr := ip.FromNetIP(netIP) + nc.nsMutex.RLock() defer nc.nsMutex.RUnlock() - // Find the first cidr that contains the ip address to use for the lookup. - ipAddr := ip.FromNetIP(net.IP(addr[:])) - if key, _ := nc.ipTree.GetLongestPrefixCidr(ipAddr); key != nil { - if ns := nc.networkSets[key]; ns != nil { - // Found a NetworkSet, so set the return variables. - ed = ns - ok = true - } + // Use the namespace isolation lookup from IpTrie for collector use case + key, found := nc.ipTree.GetLongestPrefixCidrWithNamespaceIsolation(addr, preferredNamespace) + if !found { + return nil, false } - return + + // Get the NetworkSet data for the key + if ns := nc.networkSets[key]; ns != nil { + return ns, true + } + + return nil, false } func (nc *NetworkSetLookupsCache) DumpNetworksets() string { diff --git a/felix/calc/networkset_lookup_cache_test.go b/felix/calc/networkset_lookup_cache_test.go index 5585a09bba7..f6e8722f6a5 100644 --- a/felix/calc/networkset_lookup_cache_test.go +++ b/felix/calc/networkset_lookup_cache_test.go @@ -113,8 +113,22 @@ var _ = Describe("NetworkSetLookupsCache IP tests", func() { By("verifying networkset2 is in the mapping") // This check validates that netSet2 is found since one subnet is outside the range of netSet1's subnets. - for _, cidr = range netSet2.Nets { - verifyIpToNetworkset(netSet2Key, cidr.IP, true, netSet2Labels) + for _, cidr := range netSet2.Nets { + // For overlapping CIDRs (12.0.0.0/24), lowest-lexicographic-name-wins applies + // netSet1 ("netset-1") comes before netSet2 ("netset-2") lexicographically, so netSet1 wins + // For unique CIDRs (13.1.0.0/24), netSet2 should still be returned + var expectedKey model.Key + var expectedLabels map[string]string + if cidr.String() == "12.0.0.0/24" { + // This overlaps with netSet1, so netSet1 should win due to lexicographic ordering + expectedKey = netSet1Key + expectedLabels = origNetSetLabels + } else { + // This is unique to netSet2 + expectedKey = netSet2Key + expectedLabels = netSet2Labels + } + verifyIpToNetworkset(expectedKey, cidr.IP, true, expectedLabels) } By("deleting networkset2") From a3f10dabf52249fcb1972d734340585edba9d47e Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Thu, 27 Nov 2025 15:40:29 -0800 Subject: [PATCH 3/7] [EV-6030] Integrate namespace-aware network set lookup in collector --- felix/collector/collector.go | 101 +- felix/collector/collector_test.go | 1274 +++++++++++++++++ libcalico-go/lib/backend/model/networkset.go | 12 + libcalico-go/lib/backend/model/resource.go | 5 + .../lib/backend/model/workloadendpoint.go | 12 + 5 files changed, 1377 insertions(+), 27 deletions(-) diff --git a/felix/collector/collector.go b/felix/collector/collector.go index 4cb27fc1f6a..6db641c2f3d 100644 --- a/felix/collector/collector.go +++ b/felix/collector/collector.go @@ -111,6 +111,11 @@ type Config struct { PolicyStoreManager policystore.PolicyStoreManager } +// namespacedEpKey is an interface for keys that have namespace information. +type namespacedEpKey interface { + GetNamespace() string +} + // A collector (a StatsManager really) collects StatUpdates from data sources // and stores them as a Data object in a map keyed by Tuple. // @@ -305,13 +310,9 @@ func (c *collector) getDataAndUpdateEndpoints(t tuple.Tuple, expired bool, packe return data } - // Get the source endpoint. Set the clientIP to an empty value, as it is not used for to get - // the source endpoint. - srcEp := c.lookupEndpoint([16]byte{}, t.Src) + // Get the source and destination endpoints + srcEp, dstEp := c.findEndpointBestMatch(t) srcEpIsNotLocal := srcEp == nil || !srcEp.IsLocal() - - // Get the destination endpoint. If the source is local then we can use egress domain lookups if required. - dstEp := c.lookupEndpoint(t.Src, t.Dst) dstEpIsNotLocal := dstEp == nil || !dstEp.IsLocal() if !exists { @@ -321,8 +322,8 @@ func (c *collector) getDataAndUpdateEndpoints(t tuple.Tuple, expired bool, packe } // Ignore HEP reporters. - if (srcEp != nil && srcEp.IsLocal() && srcEp.IsHostEndpoint()) || - (dstEp != nil && dstEp.IsLocal() && dstEp.IsHostEndpoint()) { + if (!srcEpIsNotLocal && srcEp.IsHostEndpoint()) || + (!dstEpIsNotLocal && dstEp.IsHostEndpoint()) { return nil } @@ -389,22 +390,61 @@ func endpointChanged(ep1, ep2 calc.EndpointData) bool { return ep1.Key() != ep2.Key() } -func (c *collector) lookupEndpoint(clientIPBytes, ip [16]byte) calc.EndpointData { - // Get the endpoint data for this entry. - if ep, ok := c.luc.GetEndpoint(ip); ok { - return ep +// lookupNetworkSetWithNamespace looks up NetworkSets for the given IP address. +// If canCheckEgressDomains is true, then egress domain lookups will be performed if no +// NetworkSet is found for the IP. If preferredNamespace is set, then it will be used for +// namespace-aware NetworkSet resolution. +func (c *collector) lookupNetworkSetWithNamespace(clientIPBytes, ip [16]byte, canCheckEgressDomains bool, preferredNamespace string) calc.EndpointData { + if !c.config.EnableNetworkSets { + return nil } - // No matching endpoint. If NetworkSets are enabled for flows then check if the IP matches a NetworkSet and - // return that. - if c.config.EnableNetworkSets { - if ep, ok := c.luc.GetNetworkSet(ip); ok { - return ep - } + // Check if the IP matches a NetworkSet + if ep, ok := c.luc.GetNetworkSetWithNamespace(ip, preferredNamespace); ok { + return ep } + return nil } +// findEndpointBestMatch performs endpoint lookups for both source and destination. It first tries +// direct endpoint lookups, and only falls back to NetworkSet lookups if needed, using namespace +// context from the other endpoint when available. +func (c *collector) findEndpointBestMatch(t tuple.Tuple) (srcEp, dstEp calc.EndpointData) { + var ep calc.EndpointData + var srcEpFound, dstEpFound bool + if ep, srcEpFound = c.luc.GetEndpoint(t.Src); srcEpFound { + // Only set srcEp if lookup succeeded (to avoid storing a typed nil). + srcEp = ep + } + + if ep, dstEpFound = c.luc.GetEndpoint(t.Dst); dstEpFound { + // Only set dstEp if lookup succeeded (to avoid storing a typed nil). + dstEp = ep + } + + if (srcEpFound && dstEpFound) || !c.config.EnableNetworkSets { + // If both endpoints were found directly, or if NetworkSets are not enabled, return what we + // found from direct lookups + return srcEp, dstEp + } + + if !srcEpFound { + dstNamespace := getNamespaceFromEp(dstEp) + srcEp = c.lookupNetworkSetWithNamespace([16]byte{}, t.Src, false, dstNamespace) + } + + if !dstEpFound { + srcNamespace := getNamespaceFromEp(srcEp) + // This controls whether egress-domain lookups should be performed when resolving the + // destination NetworkSet. Only local source endpoints can trigger egress-domain resolution. + srcEpLocal := srcEp != nil && srcEp.IsLocal() + dstEp = c.lookupNetworkSetWithNamespace(t.Src, t.Dst, srcEpLocal, srcNamespace) + } + + return srcEp, dstEp +} + // updateEpStatsCache updates/add entry to the epStats cache (map[Tuple]*Data) and update the // prometheus reporting func (c *collector) updateEpStatsCache(t tuple.Tuple, data *Data) { @@ -824,16 +864,8 @@ func (c *collector) updatePendingRuleTraces() { func (c *collector) evaluatePendingRuleTraceForLocalEp(data *Data) { flow := TupleAsFlow(data.Tuple) - srcEp := c.lookupEndpoint([16]byte{}, data.Tuple.Src) - srcEpIsNotLocal := srcEp == nil || !srcEp.IsLocal() - - dstEp := c.lookupEndpoint(data.Tuple.Src, data.Tuple.Dst) - dstEpIsNotLocal := dstEp == nil || !dstEp.IsLocal() + srcEp, dstEp := c.findEndpointBestMatch(data.Tuple) - // If neither endpoint is local, skip evaluation. - if srcEpIsNotLocal && dstEpIsNotLocal { - return - } // If endpoints have changed compared to what Data currently holds, skip evaluation. if endpointChanged(data.SrcEp, srcEp) || endpointChanged(data.DstEp, dstEp) { return @@ -937,6 +969,21 @@ func (f *MessageOnlyFormatter) Format(entry *log.Entry) ([]byte, error) { return b.Bytes(), nil } +// getNamespaceFromEp extracts the namespace from the endpoint. Returns an empty string if +// the namespace cannot be determined. +func getNamespaceFromEp(ep calc.EndpointData) (namespace string) { + if ep == nil { + return + } + if key := ep.Key(); key != nil { + if nk, ok := key.(namespacedEpKey); ok { + namespace = nk.GetNamespace() + } + } + + return +} + // equal returns true if the rule IDs are equal. The order of the content should also the same for // equal to return true. func equal(a, b []*calc.RuleID) bool { diff --git a/felix/collector/collector_test.go b/felix/collector/collector_test.go index 7caac611606..23577b22d06 100644 --- a/felix/collector/collector_test.go +++ b/felix/collector/collector_test.go @@ -18,6 +18,7 @@ package collector import ( "fmt" + net2 "net" "testing" "time" @@ -1802,6 +1803,1183 @@ func (mr *mockReporter) Report(u any) error { return nil } +var _ = Describe("Collector Namespace-Aware NetworkSet Lookups", func() { + var c *collector + var testIP [16]byte + + // Convert IP string to [16]byte format + ipToBytes := func(ipStr string) [16]byte { + ip := net2.ParseIP(ipStr) + var result [16]byte + copy(result[:], ip.To16()) + return result + } + + // Helper function to replace the old lookupEndpointWithNamespace behavior + // This mimics the original function: first try direct endpoint lookup, then NetworkSet fallback + testLookupEndpoint := func(c *collector, clientIPBytes, ip [16]byte, canCheckEgressDomains bool, preferredNamespace string) calc.EndpointData { + // Get the endpoint data for this entry. + if ep, ok := c.luc.GetEndpoint(ip); ok { + return ep + } + // No matching endpoint, try NetworkSet lookup if enabled + if !c.config.EnableNetworkSets { + return nil + } + return c.lookupNetworkSetWithNamespace(clientIPBytes, ip, canCheckEgressDomains, preferredNamespace) + } + + BeforeEach(func() { + // Test IP that will match our NetworkSets + testIP = ipToBytes("10.1.1.1") + }) + + Context("when testing endpoint lookup with NetworkSet fallback", func() { + It("should prioritize more specific NetworkSets correctly", func() { + // Create test NetworkSets + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Broad CIDR + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "global", + }), + } + + specificNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // More specific than global + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "specific", + "env": "test", + }), + } + + // Create test keys - using NetworkSetKey for global, ResourceKey for namespaced + globalKey := model.NetworkSetKey{Name: "global-netset"} + specificKey := model.NetworkSetKey{Name: "specific-netset"} // Using NetworkSetKey for simplicity + + // Create lookups cache with both NetworkSets + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + specificKey: specificNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + // Create collector with NetworkSets enabled + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test: Should return the more specific NetworkSet (longest prefix match) + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "any-namespace") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(specificKey)) + + // Verify it's actually the specific NetworkSet + Expect(result.Labels().String()).To(ContainSubstring("specific")) + }) + + It("should fallback to global NetworkSet when no better match exists", func() { + // Create only global NetworkSet + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Covers our test IP + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "global", + }), + } + + globalKey := model.NetworkSetKey{Name: "global-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test with any namespace - should return global NetworkSet + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "any-namespace") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(globalKey)) + }) + + It("should return nil when NetworkSets are disabled", func() { + // Create NetworkSet data but disable NetworkSets in config + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), + }, + } + + globalKey := model.NetworkSetKey{Name: "global-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: false, // Disabled + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Should return nil because NetworkSets are disabled + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "namespace1") + Expect(result).To(BeNil()) + }) + + It("should prioritize endpoints over NetworkSets", func() { + // Create test endpoint key and data + testEPKey := model.WorkloadEndpointKey{ + Hostname: "test-host", + OrchestratorID: "k8s", + WorkloadID: "test-workload", + EndpointID: "test-endpoint", + } + + testWlEP := &model.WorkloadEndpoint{ + Labels: uniquelabels.Make(map[string]string{ + "type": "endpoint", + }), + } + + endpoint := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(testEPKey, testWlEP), + } + + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), + }, + } + + nsKey := model.NetworkSetKey{Name: "netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + testIP: endpoint, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Should return endpoint, not NetworkSet + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "namespace1") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(testEPKey)) + }) + + It("should handle no matching NetworkSets gracefully", func() { + // Create NetworkSet that doesn't match our test IP + nonMatchingNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("192.168.0.0/16"), // Different range + }, + } + + nonMatchingKey := model.NetworkSetKey{Name: "non-matching"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nonMatchingKey: nonMatchingNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Should return nil since no NetworkSet matches + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "namespace1") + Expect(result).To(BeNil()) + }) + }) + + Context("when testing namespace optimization benefits", func() { + It("should demonstrate namespace-aware optimization with GetNetworkSetWithNamespace", func() { + // This test validates that the collector uses the namespace-aware lookup + // Create multiple overlapping NetworkSets to show specificity + broadNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Very broad + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "broad", + }), + } + + mediumNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // Medium specificity + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "medium", + }), + } + + narrowNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.1.0/24"), // Most specific + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "narrow", + }), + } + + broadKey := model.NetworkSetKey{Name: "broad-netset"} + mediumKey := model.NetworkSetKey{Name: "medium-netset"} + narrowKey := model.NetworkSetKey{Name: "narrow-netset"} + + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + broadKey: broadNetworkSet, + mediumKey: mediumNetworkSet, + narrowKey: narrowNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test that collector picks most specific match (narrowest CIDR) + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "test-namespace") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(narrowKey)) + + // Verify it's the narrow NetworkSet + Expect(result.Labels().String()).To(ContainSubstring("narrow")) + }) + + It("should handle performance efficiently with multiple NetworkSets", func() { + // Create multiple NetworkSets for performance testing + nsMap := make(map[model.NetworkSetKey]*model.NetworkSet) + + for i := 0; i < 10; i++ { + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet(fmt.Sprintf("10.%d.0.0/16", i)), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": fmt.Sprintf("test-%d", i), + }), + } + + key := model.NetworkSetKey{Name: fmt.Sprintf("test-netset-%d", i)} + nsMap[key] = networkSet + } + + lm := newMockLookupsCache(nil, nil, nsMap, nil) + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Performance test: multiple namespace lookups should complete quickly + start := time.Now() + for i := 0; i < 50; i++ { + namespace := fmt.Sprintf("namespace-%d", i%5) + testIPLoop := ipToBytes(fmt.Sprintf("10.%d.1.1", i%10)) + + result := testLookupEndpoint(c, [16]byte{}, testIPLoop, false, namespace) + // Should find a match for IPs in our test ranges + if i < 10 { + Expect(result).ToNot(BeNil()) + } + } + elapsed := time.Since(start) + + // Should complete 50 lookups in reasonable time (namespace optimization) + Expect(elapsed).To(BeNumerically("<", 25*time.Millisecond)) + }) + }) + + Context("when testing namespace-specific NetworkSets", func() { + It("should prioritize namespace-specific NetworkSets over global ones", func() { + // Create a global NetworkSet + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Broad global range + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "global", + "tier": "base", + }), + } + + // Create a namespace-specific NetworkSet that overlaps with global + namespaceNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // More specific range within global + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "namespace-specific", + "tier": "application", + }), + } + + // Create keys - ResourceKey for namespace-scoped, NetworkSetKey for global + globalKey := model.NetworkSetKey{Name: "global-netset"} + namespaceKey := model.ResourceKey{ + Kind: "NetworkSet", + Name: "app-netset", + Namespace: "production", // This has a namespace + } + + // Create the mock lookup cache + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + // For ResourceKey, we need to convert it to NetworkSetKey for the mock + // The real LookupsCache handles ResourceKey properly, but our mock uses NetworkSetKey + } + + // We need to also include the namespaced NetworkSet in the map + // In the real system, ResourceKeys are internally mapped properly + namespacedNSKey := model.NetworkSetKey{Name: namespaceKey.Name} + nsMap[namespacedNSKey] = namespaceNetworkSet + + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test with the specific namespace - should prefer namespace-specific NetworkSet + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "production") + Expect(result).ToNot(BeNil()) + + // Should get the namespace-specific NetworkSet (more specific CIDR) + Expect(result.Labels().String()).To(ContainSubstring("namespace-specific")) + }) + + It("should fall back to global NetworkSet when no namespace match exists", func() { + // Test true namespace isolation: when a more specific NetworkSet exists in a different namespace, + // it should NOT be selected, and instead fall back to a global NetworkSet + + // Create global NetworkSet with broader range + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Broad range that includes testIP (10.1.1.1) + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "global-fallback", + }), + } + + // Create namespace-specific NetworkSet that DOES contain testIP but is from different namespace + // This should NOT be selected for 'staging' namespace requests due to namespace isolation + productionNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // More specific range that INCLUDES testIP (10.1.1.1) + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-specific", + "namespace": "production", + }), + } + + // Use the correct namespace naming format: namespace/name + globalKey := model.NetworkSetKey{Name: "global-netset"} // Global NetworkSet (no namespace prefix) + productionKey := model.NetworkSetKey{Name: "production/prod-netset"} // Namespaced NetworkSet + + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + productionKey: productionNetworkSet, + } + lc := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lc, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Request with 'staging' namespace - different from the 'production' namespace NetworkSet + // testIP (10.1.1.1) matches both: + // - global-netset: 10.0.0.0/8 (broader, global) + // - production/prod-netset: 10.1.0.0/16 (more specific, but wrong namespace) + // Should return global NetworkSet due to namespace isolation + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "staging") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(globalKey)) + Expect(result.Labels().String()).To(ContainSubstring("global-fallback")) + }) + + It("should handle multiple namespaced NetworkSets correctly", func() { + // Create NetworkSets for different namespaces with overlapping CIDRs + productionNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.1.0/24"), // Specific production range + }, + Labels: uniquelabels.Make(map[string]string{ + "env": "production", + "tier": "frontend", + }), + } + + stagingNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.2.0/24"), // Specific staging range + }, + Labels: uniquelabels.Make(map[string]string{ + "env": "staging", + "tier": "frontend", + }), + } + + developmentNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // Broader dev range + }, + Labels: uniquelabels.Make(map[string]string{ + "env": "development", + "tier": "all", + }), + } + + prodKey := model.NetworkSetKey{Name: "prod-frontend"} + stagingKey := model.NetworkSetKey{Name: "staging-frontend"} + devKey := model.NetworkSetKey{Name: "dev-all"} + + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + prodKey: productionNetworkSet, + stagingKey: stagingNetworkSet, + devKey: developmentNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test production namespace with IP in production range + prodIP := ipToBytes("10.1.1.100") + result := testLookupEndpoint(c, [16]byte{}, prodIP, false, "production") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(prodKey)) + Expect(result.Labels().String()).To(ContainSubstring("production")) + + // Test staging namespace with IP in staging range + stagingIP := ipToBytes("10.1.2.100") + result = testLookupEndpoint(c, [16]byte{}, stagingIP, false, "staging") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(stagingKey)) + Expect(result.Labels().String()).To(ContainSubstring("staging")) + + // Test development namespace with IP that matches broader dev range + devIP := ipToBytes("10.1.5.100") // In 10.1.0.0/16 but not in specific /24s + result = testLookupEndpoint(c, [16]byte{}, devIP, false, "development") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(devKey)) + Expect(result.Labels().String()).To(ContainSubstring("development")) + }) + + It("should demonstrate namespace isolation in NetworkSet lookups", func() { + // Create identical CIDR ranges in different namespaces + // This tests that namespace isolation works properly + commonCIDR := utils.MustParseNet("10.1.0.0/16") + + frontendNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{commonCIDR}, + Labels: uniquelabels.Make(map[string]string{ + "app": "frontend", + "tier": "web", + }), + } + + backendNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{commonCIDR}, // Same CIDR, different namespace + Labels: uniquelabels.Make(map[string]string{ + "app": "backend", + "tier": "api", + }), + } + + frontendKey := model.NetworkSetKey{Name: "frontend-netset"} + backendKey := model.NetworkSetKey{Name: "backend-netset"} + + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + frontendKey: frontendNetworkSet, + backendKey: backendNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Same IP, different namespaces should potentially return different NetworkSets + // depending on the namespace-aware logic and CIDR specificity + testIPCommon := ipToBytes("10.1.1.100") + + // Frontend namespace lookup + frontendResult := testLookupEndpoint(c, [16]byte{}, testIPCommon, false, "frontend") + Expect(frontendResult).ToNot(BeNil()) + + // Backend namespace lookup + backendResult := testLookupEndpoint(c, [16]byte{}, testIPCommon, false, "backend") + Expect(backendResult).ToNot(BeNil()) + + // In this case with identical CIDRs, the longest-prefix-match logic will determine + // which NetworkSet is returned, but namespace awareness is being tested + }) + + It("should handle empty namespace gracefully", func() { + // Test with empty/default namespace + defaultNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "scope": "default", + }), + } + + defaultKey := model.NetworkSetKey{Name: "default-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + defaultKey: defaultNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + // Test with empty namespace + result := testLookupEndpoint(c, [16]byte{}, testIP, false, "") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(defaultKey)) + }) + }) + + Context("when testing getEndpointsWithNamespaceContext optimization", func() { + It("should optimize lookups when both endpoints are found directly", func() { + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") + + // Create endpoints for both source and destination + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "src-ns/src-workload", + EndpointID: "src-endpoint", + } + dstEPKey := model.WorkloadEndpointKey{ + Hostname: "dst-host", + OrchestratorID: "k8s", + WorkloadID: "dst-ns/dst-workload", + EndpointID: "dst-endpoint", + } + + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + dstEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(dstEPKey, &model.WorkloadEndpoint{}), + } + + // Create NetworkSets that could match these IPs (but shouldn't be used) + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // Could match both IPs + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "fallback", + }), + } + + nsKey := model.NetworkSetKey{Name: "fallback-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + dstIP: dstEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Both should return direct endpoints (not NetworkSets) + Expect(srcResult).ToNot(BeNil()) + Expect(dstResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + Expect(dstResult.Key()).To(Equal(dstEPKey)) + }) + + It("should use namespace context from destination when source needs NetworkSet lookup", func() { + srcIP := ipToBytes("10.1.1.10") // No direct endpoint for this + dstIP := ipToBytes("10.1.1.20") + + // Create destination endpoint with namespace + dstEPKey := model.WorkloadEndpointKey{ + Hostname: "dst-host", + OrchestratorID: "k8s", + WorkloadID: "production/dst-workload", // namespace: production + EndpointID: "dst-endpoint", + } + dstEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(dstEPKey, &model.WorkloadEndpoint{}), + } + + // Create NetworkSets - one generic, one namespace-specific + genericNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Broader range + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "generic", + }), + } + productionNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // More specific for production + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-specific", + "namespace": "production", + }), + } + + genericKey := model.NetworkSetKey{Name: "generic-netset"} + productionKey := model.NetworkSetKey{Name: "production-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + genericKey: genericNetworkSet, + productionKey: productionNetworkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + dstIP: dstEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Destination should be direct endpoint + Expect(dstResult).ToNot(BeNil()) + Expect(dstResult.Key()).To(Equal(dstEPKey)) + + // Source should use NetworkSet (preferably production-specific due to namespace context) + Expect(srcResult).ToNot(BeNil()) + // Should get the more specific NetworkSet (production-specific) + Expect(srcResult.Key()).To(Equal(productionKey)) + }) + + It("should use namespace context from source when destination needs NetworkSet lookup", func() { + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") // No direct endpoint for this + + // Create source endpoint with namespace + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "staging/src-workload", // namespace: staging + EndpointID: "src-endpoint", + } + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + + // Create NetworkSets + globalNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Broader range + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "global", + }), + } + stagingNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // More specific for staging + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "staging-specific", + "namespace": "staging", + }), + } + + globalKey := model.NetworkSetKey{Name: "global-netset"} + stagingKey := model.NetworkSetKey{Name: "staging-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + globalKey: globalNetworkSet, + stagingKey: stagingNetworkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Source should be direct endpoint + Expect(srcResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + + // Destination should use NetworkSet with namespace context from source + Expect(dstResult).ToNot(BeNil()) + // Should get the staging-specific NetworkSet + Expect(dstResult.Key()).To(Equal(stagingKey)) + }) + + It("should return both NetworkSets when no direct endpoints exist", func() { + srcIP := utils.IpStrTo16Byte("10.1.1.10") // No direct endpoints + dstIP := utils.IpStrTo16Byte("10.1.1.20") + + // Create only NetworkSets + srcNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.1.0/24"), // Specific for source + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "src-network", + }), + } + dstNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.2.0/24"), // Different range for dest + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "dst-network", + }), + } + fallbackNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.0.0.0/8"), // Fallback for both + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "fallback", + }), + } + + srcKey := model.NetworkSetKey{Name: "src-netset"} + dstKey := model.NetworkSetKey{Name: "dst-netset"} + fallbackKey := model.NetworkSetKey{Name: "fallback-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + srcKey: srcNetworkSet, + dstKey: dstNetworkSet, + fallbackKey: fallbackNetworkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Both should return NetworkSets + Expect(srcResult).ToNot(BeNil()) + Expect(dstResult).ToNot(BeNil()) + + // Due to how NetworkSet lookup works, it may return the first matching NetworkSet + // The actual behavior depends on the order of NetworkSets returned by the lookup cache + // Let's check that we get some NetworkSet match for both + Expect(srcResult.Key()).To(BeAssignableToTypeOf(model.NetworkSetKey{})) + Expect(dstResult.Key()).To(BeAssignableToTypeOf(model.NetworkSetKey{})) + }) + + It("should handle NetworkSets disabled gracefully", func() { + srcIP := utils.IpStrTo16Byte("10.1.1.10") + dstIP := utils.IpStrTo16Byte("10.1.1.20") + + // Create NetworkSets that would match + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + } + + nsKey := model.NetworkSetKey{Name: "test-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: false, // Disabled + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Both should be nil since NetworkSets are disabled and no direct endpoints exist + Expect(srcResult).To(BeNil()) + Expect(dstResult).To(BeNil()) + }) + + It("should handle mixed endpoint and NetworkSet scenarios efficiently", func() { + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") + + // Source has direct endpoint + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "development/src-workload", + EndpointID: "src-endpoint", + } + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + + // Destination only has NetworkSet + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "dev-network", + "namespace": "development", + }), + } + + nsKey := model.NetworkSetKey{Name: "dev-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Source should be direct endpoint + Expect(srcResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + + // Destination should be NetworkSet (with namespace context from source) + Expect(dstResult).ToNot(BeNil()) + Expect(dstResult.Key()).To(Equal(nsKey)) + }) + }) + + Context("when testing lookupNetworkSetWithNamespace function", func() { + It("should return nil when NetworkSets are disabled", func() { + testIPLocal := ipToBytes("10.1.1.100") + + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + } + + nsKey := model.NetworkSetKey{Name: "test-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: false, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace([16]byte{}, testIPLocal, false, "test-namespace") + Expect(result).To(BeNil()) + }) + + It("should return NetworkSet when one matches", func() { + testIPLocal := ipToBytes("10.1.1.100") + + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "test", + }), + } + + nsKey := model.NetworkSetKey{Name: "test-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace([16]byte{}, testIPLocal, false, "test-namespace") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(nsKey)) + }) + + It("should return nil when no NetworkSet matches", func() { + testIPLocal := ipToBytes("192.168.1.100") // Different range + + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), // Won't match testIPLocal + }, + } + + nsKey := model.NetworkSetKey{Name: "test-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace([16]byte{}, testIPLocal, false, "test-namespace") + Expect(result).To(BeNil()) + }) + }) + + Context("when testing namespace extraction from endpoints", func() { + It("should extract namespace from WorkloadEndpoint correctly", func() { + // Test the getNamespaceFromEp function indirectly by testing the full lookup flow + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") + + // Create source endpoint with namespace in WorkloadID + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "frontend/src-workload", // namespace: frontend + EndpointID: "src-endpoint", + } + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + + // Create namespace-specific NetworkSet that should be used for destination + frontendNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "frontend-network", + "namespace": "frontend", + }), + } + + nsKey := model.NetworkSetKey{Name: "frontend-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: frontendNetworkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Source should be the direct endpoint + Expect(srcResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + + // Destination should use the NetworkSet (demonstrating namespace context was used) + Expect(dstResult).ToNot(BeNil()) + Expect(dstResult.Key()).To(Equal(nsKey)) + }) + + It("should handle endpoints with ResourceKey correctly", func() { + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") + + // Create a WorkloadEndpoint that represents a production namespace + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "production/src-workload", // namespace: production + EndpointID: "src-endpoint", + } + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + + // Create destination NetworkSet that should use the namespace from source + productionNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-network", + "namespace": "production", + }), + } + + nsKey := model.NetworkSetKey{Name: "production-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: productionNetworkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Source should be the WorkloadEndpoint + Expect(srcResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + + // Destination should use the NetworkSet (with namespace context from source) + Expect(dstResult).ToNot(BeNil()) + Expect(dstResult.Key()).To(Equal(nsKey)) + }) + + It("should handle endpoints with no extractable namespace gracefully", func() { + srcIP := ipToBytes("10.1.1.10") + dstIP := ipToBytes("10.1.1.20") + + // Create endpoint with WorkloadID that doesn't follow namespace/name pattern + srcEPKey := model.WorkloadEndpointKey{ + Hostname: "src-host", + OrchestratorID: "k8s", + WorkloadID: "invalid-workload-format", // No namespace separator + EndpointID: "src-endpoint", + } + srcEP := &calc.LocalEndpointData{ + CommonEndpointData: calc.CalculateCommonEndpointData(srcEPKey, &model.WorkloadEndpoint{}), + } + + // Create a generic NetworkSet + genericNetworkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "generic", + }), + } + + nsKey := model.NetworkSetKey{Name: "generic-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: genericNetworkSet, + } + epMap := map[[16]byte]calc.EndpointData{ + srcIP: srcEP, + } + lm := newMockLookupsCache(epMap, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + t := tuple.Make(srcIP, dstIP, proto_tcp, 8080, 80) + srcResult, dstResult := c.findEndpointBestMatch(t) + + // Source should be the direct endpoint + Expect(srcResult).ToNot(BeNil()) + Expect(srcResult.Key()).To(Equal(srcEPKey)) + + // Destination should still get the NetworkSet (with empty namespace context) + Expect(dstResult).ToNot(BeNil()) + Expect(dstResult.Key()).To(Equal(nsKey)) + }) + }) +}) + +// Mock EgressDomainCache for testing +type mockEgressDomainCache struct { + domains map[string]map[[16]byte][]string +} + +func (m *mockEgressDomainCache) GetTopLevelDomainsForIP(clientIP string, ip [16]byte) []string { + if clientDomains, ok := m.domains[clientIP]; ok { + if domains, ok := clientDomains[ip]; ok { + return domains + } + } + return nil +} + +func (m *mockEgressDomainCache) IterWatchedDomainsForIP(clientIP string, ip [16]byte, fn func(domain string) bool) { + if clientDomains, ok := m.domains[clientIP]; ok { + if domains, ok := clientDomains[ip]; ok { + for _, domain := range domains { + if fn(domain) { + break + } + } + } + } +} + func BenchmarkNflogPktToStat(b *testing.B) { epMap := map[[16]byte]calc.EndpointData{ localIp1: localEd1, @@ -2251,6 +3429,102 @@ func TestRunPendingRuleTraceEvaluation(t *testing.T) { } }) + Context("lookupNetworkSetWithNamespace function", func() { + It("should return nil when IP does not match any NetworkSet", func() { + srcIP := utils.IpStrTo16Byte("10.1.1.10") + dstIP := utils.IpStrTo16Byte("192.168.1.10") + + // Create a NetworkSet that doesn't match either IP + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("172.16.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-network", + "namespace": "production", + }), + } + + nsKey := model.NetworkSetKey{Name: "production-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace(srcIP, dstIP, false, "production") + Expect(result).To(BeNil()) + }) + + It("should return NetworkSet endpoint for NetworkSet-based lookups", func() { + srcIP := utils.IpStrTo16Byte("10.1.1.10") + dstIP := utils.IpStrTo16Byte("10.1.1.20") + + // Create a NetworkSet that matches the destination IP + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-network", + "namespace": "production", + }), + } + + nsKey := model.NetworkSetKey{Name: "production-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: true, + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace(srcIP, dstIP, false, "production") + Expect(result).ToNot(BeNil()) + Expect(result.Key()).To(Equal(nsKey)) + }) + + It("should return nil when NetworkSets are disabled", func() { + srcIP := utils.IpStrTo16Byte("10.1.1.10") + dstIP := utils.IpStrTo16Byte("10.1.1.20") + + // Create a NetworkSet that would match if enabled + networkSet := &model.NetworkSet{ + Nets: []net.IPNet{ + utils.MustParseNet("10.1.0.0/16"), + }, + Labels: uniquelabels.Make(map[string]string{ + "type": "production-network", + "namespace": "production", + }), + } + + nsKey := model.NetworkSetKey{Name: "production-netset"} + nsMap := map[model.NetworkSetKey]*model.NetworkSet{ + nsKey: networkSet, + } + lm := newMockLookupsCache(nil, nil, nsMap, nil) + + c = newCollector(lm, &Config{ + EnableNetworkSets: false, // NetworkSets disabled + ExportingInterval: time.Second, + FlowLogsFlushInterval: time.Second, + }).(*collector) + + result := c.lookupNetworkSetWithNamespace(srcIP, dstIP, false, "production") + Expect(result).To(BeNil()) + }) + }) + // Test endpoint deletion scenario t.Run("EndpointDeletion", func(t *testing.T) { // Remove localEd1 from the lookup cache to simulate endpoint deletion diff --git a/libcalico-go/lib/backend/model/networkset.go b/libcalico-go/lib/backend/model/networkset.go index 48562a0b832..728285247a1 100644 --- a/libcalico-go/lib/backend/model/networkset.go +++ b/libcalico-go/lib/backend/model/networkset.go @@ -18,6 +18,7 @@ import ( "fmt" "reflect" "regexp" + "strings" log "github.com/sirupsen/logrus" @@ -63,6 +64,17 @@ func (key NetworkSetKey) String() string { return fmt.Sprintf("NetworkSet(name=%s)", key.Name) } +// Namespace extracts the namespace from a namespaced NetworkSetKey. +// NetworkSets with names in the format "namespace/name" are namespaced. +// Returns empty string for global (non-namespaced) NetworkSets. +func (key NetworkSetKey) Namespace() string { + if strings.Contains(key.Name, "/") { + parts := strings.SplitN(key.Name, "/", 2) + return parts[0] + } + return "" +} + type NetworkSetListOptions struct { Name string } diff --git a/libcalico-go/lib/backend/model/resource.go b/libcalico-go/lib/backend/model/resource.go index 8e89353592a..98a4cd51b74 100644 --- a/libcalico-go/lib/backend/model/resource.go +++ b/libcalico-go/lib/backend/model/resource.go @@ -208,6 +208,11 @@ func (key ResourceKey) String() string { return fmt.Sprintf("%s(%s)", key.Kind, key.Name) } +// GetNamespace returns the namespace field of the ResourceKey. +func (key ResourceKey) GetNamespace() string { + return key.Namespace +} + type ResourceListOptions struct { // The name of the resource. Name string diff --git a/libcalico-go/lib/backend/model/workloadendpoint.go b/libcalico-go/lib/backend/model/workloadendpoint.go index cd6869f6b5c..7d271f1924c 100644 --- a/libcalico-go/lib/backend/model/workloadendpoint.go +++ b/libcalico-go/lib/backend/model/workloadendpoint.go @@ -18,6 +18,7 @@ import ( "fmt" "reflect" "regexp" + "strings" log "github.com/sirupsen/logrus" @@ -94,6 +95,17 @@ func (key WorkloadEndpointKey) String() string { key.Hostname, key.OrchestratorID, key.WorkloadID, key.EndpointID) } +// GetNamespace extracts and returns the namespace from the WorkloadID. +// WorkloadID is expected to be in the format "namespace/name". +// Returns an empty string if the WorkloadID doesn't contain a namespace. +func (key WorkloadEndpointKey) GetNamespace() string { + parts := strings.SplitN(key.WorkloadID, "/", 2) + if len(parts) == 2 { + return parts[0] + } + return "" +} + var _ EndpointKey = WorkloadEndpointKey{} type WorkloadEndpointListOptions struct { From 2a9b8520c895af8d9ff3c1a197515d5af1542c65 Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Mon, 1 Dec 2025 15:22:36 -0800 Subject: [PATCH 4/7] [EV-5992][EV-6030] Add FV flow log test for netset precedence --- felix/fv/flow_logs_goldmane_test.go | 230 ++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/felix/fv/flow_logs_goldmane_test.go b/felix/fv/flow_logs_goldmane_test.go index 5530cc6bb3a..765b9d4f321 100644 --- a/felix/fv/flow_logs_goldmane_test.go +++ b/felix/fv/flow_logs_goldmane_test.go @@ -802,6 +802,212 @@ var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ goldmane local server tests }) }) +var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ goldmane flow log networkset precedence tests", []apiconfig.DatastoreType{apiconfig.EtcdV3}, func(getInfra infrastructure.InfraFactory) { + bpfEnabled := os.Getenv("FELIX_FV_ENABLE_BPF") == "true" + + var ( + infra infrastructure.DatastoreInfra + tc infrastructure.TopologyContainers + opts infrastructure.TopologyOptions + client client.Interface + swl1, swl2, swl3, swl4 *workload.Workload + dwl1, dwl2 *workload.Workload + cc *connectivity.Checker + ) + + BeforeEach(func() { + infra = getInfra() + opts.FelixLogSeverity = "Debug" + opts = infrastructure.DefaultTopologyOptions() + opts.IPIPMode = api.IPIPModeNever + opts.FlowLogSource = infrastructure.FlowLogSourceLocalSocket + + opts.ExtraEnvVars["FELIX_FLOWLOGSCOLLECTORDEBUGTRACE"] = "true" + opts.ExtraEnvVars["FELIX_FLOWLOGSFLUSHINTERVAL"] = "2" + opts.ExtraEnvVars["FELIX_FLOWLOGSGOLDMANESERVER"] = local.SocketAddress + opts.ExtraEnvVars["FELIX_FLOWLOGSENABLENETWORKSETS"] = "true" + }) + + JustBeforeEach(func() { + var err error + numNodes := 2 + tc, client = infrastructure.StartNNodeTopology(numNodes, opts, infra) + + if BPFMode() { + ensureBPFProgramsAttached(tc.Felixes[0]) + ensureBPFProgramsAttached(tc.Felixes[1]) + } + + infra.AddDefaultAllow() + + // Source workloads on Node 0 + // swl1 in ns1 + infrastructure.AssignIP("swl1", "10.65.0.2", tc.Felixes[0].Hostname, client) + swl1 = workload.Run(tc.Felixes[0], "swl1", "ns1", "10.65.0.2", "8055", "tcp") + swl1.WorkloadEndpoint.GenerateName = "swl1-" + swl1.WorkloadEndpoint.Namespace = "ns1" + swl1.ConfigureInInfra(infra) + + // swl2 in ns2 + infrastructure.AssignIP("swl2", "10.65.0.3", tc.Felixes[0].Hostname, client) + swl2 = workload.Run(tc.Felixes[0], "swl2", "ns2", "10.65.0.3", "8055", "tcp") + swl2.WorkloadEndpoint.GenerateName = "swl2-" + swl2.WorkloadEndpoint.Namespace = "ns2" + swl2.ConfigureInInfra(infra) + + // swl3 in ns3 + infrastructure.AssignIP("swl3", "10.65.0.4", tc.Felixes[0].Hostname, client) + swl3 = workload.Run(tc.Felixes[0], "swl3", "ns3", "10.65.0.4", "8055", "tcp") + swl3.WorkloadEndpoint.GenerateName = "swl3-" + swl3.WorkloadEndpoint.Namespace = "ns3" + swl3.ConfigureInInfra(infra) + + // swl4 in ns3 + infrastructure.AssignIP("swl4", "10.65.0.5", tc.Felixes[0].Hostname, client) + swl4 = workload.Run(tc.Felixes[0], "swl4", "ns3", "10.65.0.5", "8055", "tcp") + swl4.WorkloadEndpoint.GenerateName = "swl4-" + swl4.WorkloadEndpoint.Namespace = "ns3" + swl4.ConfigureInInfra(infra) + + // Destination workloads on Node 1 (Host Networked to simulate external/non-WEP IPs) + + // dwl1 + infrastructure.AssignIP("dwl1", "10.65.1.2", tc.Felixes[1].Hostname, client) + dwl1 = workload.New(tc.Felixes[1], "dwl1", "", "10.65.1.2", "8055", "tcp", workload.WithHostNetworked()) + // Add IP before starting workload so it can bind + err = tc.Felixes[1].ExecMayFail("ip", "addr", "add", "10.65.1.2/32", "dev", "lo") + Expect(err).NotTo(HaveOccurred()) + Expect(dwl1.Start(tc.Felixes[1])).NotTo(HaveOccurred()) + + // dwl2 + infrastructure.AssignIP("dwl2", "10.65.1.3", tc.Felixes[1].Hostname, client) + dwl2 = workload.New(tc.Felixes[1], "dwl2", "", "10.65.1.3", "8055", "tcp", workload.WithHostNetworked()) + // Add IP before starting workload so it can bind + err = tc.Felixes[1].ExecMayFail("ip", "addr", "add", "10.65.1.3/32", "dev", "lo") + Expect(err).NotTo(HaveOccurred()) + Expect(dwl2.Start(tc.Felixes[1])).NotTo(HaveOccurred()) + + // Add a policy to allow all traffic + policy := api.NewGlobalNetworkPolicy() + policy.Name = "allow-all" + order := float64(20) + policy.Spec.Order = &order + policy.Spec.Selector = "all()" + policy.Spec.Ingress = []api.Rule{{Action: api.Allow}} + policy.Spec.Egress = []api.Rule{{Action: api.Allow}} + _, err = client.GlobalNetworkPolicies().Create(utils.Ctx, policy, utils.NoOptions) + Expect(err).NotTo(HaveOccurred()) + + if !BPFMode() { + Eventually(getRuleFuncTable(tc.Felixes[0], "API0|default.allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) + Eventually(getRuleFuncTable(tc.Felixes[0], "APE0|default.allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) + } else { + bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "ingress", "default.allow-all") + bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "egress", "default.allow-all") + } + + // NetworkSets + // netset-1 in ns1 matches dwl1 + netset1 := api.NewNetworkSet() + netset1.Name = "netset-1" + netset1.Namespace = "ns1" + netset1.Spec.Nets = []string{dwl1.IP + "/32"} + _, err = client.NetworkSets().Create(utils.Ctx, netset1, utils.NoOptions) + Expect(err).NotTo(HaveOccurred()) + + // netset-2 in ns2 matches dwl1 + netset2 := api.NewNetworkSet() + netset2.Name = "netset-2" + netset2.Namespace = "ns2" + netset2.Spec.Nets = []string{dwl1.IP + "/32"} + _, err = client.NetworkSets().Create(utils.Ctx, netset2, utils.NoOptions) + Expect(err).NotTo(HaveOccurred()) + + // gns-1 (global) matches dwl1 + gnetset := api.NewGlobalNetworkSet() + gnetset.Name = "gns-1" + gnetset.Spec.Nets = []string{dwl1.IP + "/32"} + _, err = client.GlobalNetworkSets().Create(utils.Ctx, gnetset, utils.NoOptions) + Expect(err).NotTo(HaveOccurred()) + + // netset-4 in ns4 matches dwl2 + netset4 := api.NewNetworkSet() + netset4.Name = "netset-4" + netset4.Namespace = "ns4" + netset4.Spec.Nets = []string{dwl2.IP + "/32"} + _, err = client.NetworkSets().Create(utils.Ctx, netset4, utils.NoOptions) + Expect(err).NotTo(HaveOccurred()) + + if BPFMode() { + ensureAllNodesBPFProgramsAttached(tc.Felixes) + } + }) + + It("should report correct network sets based on namespace precedence", func() { + // Connectivity check + cc = &connectivity.Checker{} + cc.ExpectSome(swl1, dwl1) + cc.ExpectSome(swl2, dwl1) + cc.ExpectSome(swl3, dwl1) + cc.ExpectSome(swl4, dwl2) + cc.CheckConnectivity() + + flowlogs.WaitForConntrackScan(bpfEnabled) + + for ii := range tc.Felixes { + tc.Felixes[ii].Exec("conntrack", "-F") + } + + Eventually(func() error { + wepPort := 8055 + flowTester := flowlogs.NewFlowTester(flowlogs.FlowTesterOptions{ + ExpectLabels: true, + ExpectEnforcedPolicies: true, + MatchEnforcedPolicies: true, + MatchLabels: false, + Includes: []flowlogs.IncludeFilter{flowlogs.IncludeByDestPort(wepPort)}, + }) + + err := flowTester.PopulateFromFlowLogs(tc.Felixes[0]) + if err != nil { + return fmt.Errorf("error populating flow logs from Felix[0]: %s", err) + } + + aggrTuple := tuple.Make(flowlog.EmptyIP, flowlog.EmptyIP, 6, flowlogs.SourcePortIsNotIncluded, wepPort) + + type checkArgs struct { + desc string + srcNS string + srcAggName string + dstNS string + dstAggName string + } + check := func(args checkArgs) { + flowTester.CheckFlow(flowlog.FlowLog{ + FlowMeta: flowlog.FlowMeta{ + Tuple: aggrTuple, + SrcMeta: endpoint.Metadata{Type: "wep", Namespace: args.srcNS, Name: flowlog.FieldNotIncluded, AggregatedName: args.srcAggName}, + DstMeta: endpoint.Metadata{Type: "ns", Namespace: args.dstNS, Name: flowlog.FieldNotIncluded, AggregatedName: args.dstAggName}, + DstService: flowlog.FlowService{Namespace: flowlog.FieldNotIncluded, Name: flowlog.FieldNotIncluded, PortName: flowlog.FieldNotIncluded, PortNum: 0}, + Action: "allow", Reporter: "src", + }, + FlowEnforcedPolicySet: flowlog.FlowPolicySet{"0|default|default.allow-all|allow|0": {}}, + }) + } + + check(checkArgs{desc: "ns1 -> netset-1", srcNS: "ns1", srcAggName: "swl1-*", dstNS: "ns1", dstAggName: "netset-1"}) + check(checkArgs{desc: "ns2 -> netset-2", srcNS: "ns2", srcAggName: "swl2-*", dstNS: "ns2", dstAggName: "netset-2"}) + check(checkArgs{desc: "ns3 -> gns-1", srcNS: "ns3", srcAggName: "swl3-*", dstNS: flowlog.FieldNotIncluded, dstAggName: "gns-1"}) + check(checkArgs{desc: "ns3 -> netset-3", srcNS: "ns3", srcAggName: "swl4-*", dstNS: "ns4", dstAggName: "netset-4"}) + + if err := flowTester.Finish(); err != nil { + return fmt.Errorf("Flows incorrect on Felix[0]:\n%v", err) + } + return nil + }, "30s", "3s").ShouldNot(HaveOccurred()) + }) +}) + func countNodesWithNodeIP(c client.Interface) int { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() @@ -818,3 +1024,27 @@ func countNodesWithNodeIP(c client.Interface) int { return count } + +func getRuleFuncTable(felix *infrastructure.Felix, chain string, table string) func() error { + return func() error { + if NFTMode() { + out, err := felix.ExecOutput("nft", "list", "ruleset") + if err != nil { + return err + } + if strings.Contains(out, chain) { + return nil + } + return fmt.Errorf("chain %s not found in nft ruleset", chain) + } + + out, err := felix.ExecOutput("iptables-save", "-t", table) + if err != nil { + return err + } + if strings.Contains(out, chain) { + return nil + } + return fmt.Errorf("chain %s not found in table %s", chain, table) + } +} From 361fdb2881f27da2d28135f5e86df581002d88ad Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Fri, 31 Oct 2025 00:17:41 -0700 Subject: [PATCH 5/7] [EV-6030] Fix CleanupAllNetworkSets Delete call --- felix/fv/infrastructure/infra_k8s.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/felix/fv/infrastructure/infra_k8s.go b/felix/fv/infrastructure/infra_k8s.go index 1393b6a4776..353d36ca3ca 100644 --- a/felix/fv/infrastructure/infra_k8s.go +++ b/felix/fv/infrastructure/infra_k8s.go @@ -1212,7 +1212,7 @@ func cleanupAllNetworkSets(clientset *kubernetes.Clientset, client client.Interf } log.WithField("count", len(ns.Items)).Info("networksets present") for _, n := range ns.Items { - _, err = client.NetworkSets().Delete(ctx, n.Name, n.Namespace, options.DeleteOptions{}) + _, err = client.NetworkSets().Delete(ctx, n.Namespace, n.Name, options.DeleteOptions{}) if err != nil { panic(err) } From 9e2f9d8ce1c01c2692ef837e9fdfe39b39039540 Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Thu, 4 Dec 2025 15:32:05 -0800 Subject: [PATCH 6/7] [EV-6030] Remove unused function NamespaceFilterFunc --- felix/calc/iplpm.go | 31 +----------- felix/calc/iplpm_test.go | 14 ++++++ felix/collector/collector_test.go | 52 ++++++++++++++++---- felix/fv/flow_logs_goldmane_test.go | 2 +- libcalico-go/lib/backend/model/networkset.go | 4 +- 5 files changed, 61 insertions(+), 42 deletions(-) diff --git a/felix/calc/iplpm.go b/felix/calc/iplpm.go index f2113e294e8..d450744b766 100644 --- a/felix/calc/iplpm.go +++ b/felix/calc/iplpm.go @@ -19,7 +19,6 @@ import ( "strings" "unique" - "github.com/sirupsen/logrus" "github.com/tchap/go-patricia/v2/patricia" "github.com/projectcalico/calico/felix/ip" @@ -77,32 +76,6 @@ func newIPTrieNode(cidr ip.CIDR, key model.Key) *IPTrieNode { } } -// NamespaceFilterFunc defines a function that filters keys based on namespace preferences. -// Returns: (isNamespaceMatch, isGlobal, accept) -// - isNamespaceMatch: true if the key matches the preferred namespace -// - isGlobal: true if the key is a global (non-namespaced) NetworkSet -// - accept: true if the key should be accepted for processing -type NamespaceFilterFunc func(key model.Key, preferredNamespace string) (isNamespaceMatch bool, isGlobal bool, accept bool) - -// DefaultNamespaceFilter is the standard namespace filtering logic -func DefaultNamespaceFilter(key model.Key, preferredNamespace string) (isNamespaceMatch bool, isGlobal bool, accept bool) { - nsKey, ok := key.(model.NetworkSetKey) - if !ok { - logrus.Debugf("Non-NetworkSet key detected: %v", key) - return false, false, false // Non-NetworkSet keys are treated as global - } - - namespace := nsKey.Namespace() - if namespace != "" { - // Namespaced NetworkSet - isMatch := namespace == preferredNamespace - return isMatch, false, true - } - - // Global NetworkSet - return false, true, true -} - // GetLongestPrefixCidr finds the longest prefix match CIDR for the given IP and if successful returns the // lexicographically lowest key associated with that CIDR. func (trie *IpTrie) GetLongestPrefixCidr(ipAddr ip.Addr) (model.Key, bool) { @@ -119,7 +92,7 @@ func (trie *IpTrie) GetLongestPrefixCidr(ipAddr ip.Addr) (model.Key, bool) { return nil }) - if err != nil || len(longestPrefix) == 0 { + if err != nil || longestItem == nil { return nil, false } @@ -165,7 +138,7 @@ func (trie *IpTrie) GetLongestPrefixCidrWithNamespaceIsolation(ipAddr ip.Addr, p if !ok { continue } - ns := nsKey.Namespace() + ns := nsKey.GetNamespace() switch { case preferredNamespace != "" && ns == preferredNamespace: diff --git a/felix/calc/iplpm_test.go b/felix/calc/iplpm_test.go index 4d8881f2b58..b7c24febb88 100644 --- a/felix/calc/iplpm_test.go +++ b/felix/calc/iplpm_test.go @@ -269,6 +269,20 @@ var _ = Describe("IpTrie Namespace-Aware Functionality", func() { _, found := it.GetLongestPrefixCidrWithNamespaceIsolation(testIP, "any-namespace") Expect(found).To(BeFalse()) }) + + It("should handle 0.0.0.0/0 correctly", func() { + zeroCIDR := ip.MustParseCIDROrIP("0.0.0.0/0") + key := model.NetworkSetKey{Name: "zero-netset"} + + it.InsertKey(zeroCIDR, key) + + // Test with an arbitrary IP + testIP := ip.FromNetIP(net.ParseIP("1.2.3.4")) + + foundKey, found := it.GetLongestPrefixCidr(testIP) + Expect(found).To(BeTrue(), "Should find key for 0.0.0.0/0") + Expect(foundKey).To(Equal(key)) + }) }) Context("when testing multiple overlapping CIDRs", func() { diff --git a/felix/collector/collector_test.go b/felix/collector/collector_test.go index 23577b22d06..659ddb9a55a 100644 --- a/felix/collector/collector_test.go +++ b/felix/collector/collector_test.go @@ -1815,18 +1815,50 @@ var _ = Describe("Collector Namespace-Aware NetworkSet Lookups", func() { return result } - // Helper function to replace the old lookupEndpointWithNamespace behavior - // This mimics the original function: first try direct endpoint lookup, then NetworkSet fallback + // Helper function to replace the old lookupEndpointWithNamespace behavior. + // This uses the public findEndpointBestMatch interface by setting up a dummy source endpoint + // to provide the preferredNamespace context. testLookupEndpoint := func(c *collector, clientIPBytes, ip [16]byte, canCheckEgressDomains bool, preferredNamespace string) calc.EndpointData { - // Get the endpoint data for this entry. - if ep, ok := c.luc.GetEndpoint(ip); ok { - return ep + srcIP := clientIPBytes + + // If we need a namespace context but don't have a source IP, generate a dummy one. + if preferredNamespace != "" && srcIP == [16]byte{} { + srcIP = ipToBytes("192.0.2.1") } - // No matching endpoint, try NetworkSet lookup if enabled - if !c.config.EnableNetworkSets { - return nil + + if preferredNamespace != "" { + // Create a dummy endpoint in the preferred namespace to simulate the source + epKey := model.WorkloadEndpointKey{ + Hostname: "test-host", + OrchestratorID: "k8s", + WorkloadID: "test-workload-src", + EndpointID: "test-endpoint-src", + } + ep := &model.WorkloadEndpoint{ + Name: "test-endpoint-src", + Labels: uniquelabels.Make(map[string]string{"env": "test"}), + } + + common := calc.CalculateCommonEndpointData(epKey, ep) + var endpoint calc.EndpointData + if canCheckEgressDomains { + endpoint = &calc.LocalEndpointData{ + CommonEndpointData: common, + } + } else { + endpoint = &calc.RemoteEndpointData{ + CommonEndpointData: common, + } + } + + // Inject the dummy endpoint into the cache + c.luc.SetMockData(map[[16]byte]calc.EndpointData{srcIP: endpoint}, nil, nil, nil) } - return c.lookupNetworkSetWithNamespace(clientIPBytes, ip, canCheckEgressDomains, preferredNamespace) + + // Perform the lookup using the public interface + t := tuple.Tuple{Src: srcIP, Dst: ip} + _, dstEp := c.findEndpointBestMatch(t) + return dstEp } BeforeEach(func() { @@ -2010,7 +2042,7 @@ var _ = Describe("Collector Namespace-Aware NetworkSet Lookups", func() { }) Context("when testing namespace optimization benefits", func() { - It("should demonstrate namespace-aware optimization with GetNetworkSetWithNamespace", func() { + It("should select the most specific NetworkSet match (narrowest CIDR)", func() { // This test validates that the collector uses the namespace-aware lookup // Create multiple overlapping NetworkSets to show specificity broadNetworkSet := &model.NetworkSet{ diff --git a/felix/fv/flow_logs_goldmane_test.go b/felix/fv/flow_logs_goldmane_test.go index 765b9d4f321..6c2c2f9381b 100644 --- a/felix/fv/flow_logs_goldmane_test.go +++ b/felix/fv/flow_logs_goldmane_test.go @@ -998,7 +998,7 @@ var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ goldmane flow log networkse check(checkArgs{desc: "ns1 -> netset-1", srcNS: "ns1", srcAggName: "swl1-*", dstNS: "ns1", dstAggName: "netset-1"}) check(checkArgs{desc: "ns2 -> netset-2", srcNS: "ns2", srcAggName: "swl2-*", dstNS: "ns2", dstAggName: "netset-2"}) check(checkArgs{desc: "ns3 -> gns-1", srcNS: "ns3", srcAggName: "swl3-*", dstNS: flowlog.FieldNotIncluded, dstAggName: "gns-1"}) - check(checkArgs{desc: "ns3 -> netset-3", srcNS: "ns3", srcAggName: "swl4-*", dstNS: "ns4", dstAggName: "netset-4"}) + check(checkArgs{desc: "ns3 -> netset-4", srcNS: "ns3", srcAggName: "swl4-*", dstNS: "ns4", dstAggName: "netset-4"}) if err := flowTester.Finish(); err != nil { return fmt.Errorf("Flows incorrect on Felix[0]:\n%v", err) diff --git a/libcalico-go/lib/backend/model/networkset.go b/libcalico-go/lib/backend/model/networkset.go index 728285247a1..20fadd5c7d8 100644 --- a/libcalico-go/lib/backend/model/networkset.go +++ b/libcalico-go/lib/backend/model/networkset.go @@ -64,10 +64,10 @@ func (key NetworkSetKey) String() string { return fmt.Sprintf("NetworkSet(name=%s)", key.Name) } -// Namespace extracts the namespace from a namespaced NetworkSetKey. +// GetNamespace extracts the namespace from a namespaced NetworkSetKey. // NetworkSets with names in the format "namespace/name" are namespaced. // Returns empty string for global (non-namespaced) NetworkSets. -func (key NetworkSetKey) Namespace() string { +func (key NetworkSetKey) GetNamespace() string { if strings.Contains(key.Name, "/") { parts := strings.SplitN(key.Name, "/", 2) return parts[0] From 51850c88f8b0551557ed337f8cd190ce79b84587 Mon Sep 17 00:00:00 2001 From: Dimitri Nicolopoulos Date: Fri, 5 Dec 2025 17:15:27 -0800 Subject: [PATCH 7/7] [EV-6030] Update flow log policy name expectations, remove tier --- felix/fv/flow_logs_goldmane_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/felix/fv/flow_logs_goldmane_test.go b/felix/fv/flow_logs_goldmane_test.go index 6c2c2f9381b..c9e2ebe5047 100644 --- a/felix/fv/flow_logs_goldmane_test.go +++ b/felix/fv/flow_logs_goldmane_test.go @@ -899,11 +899,11 @@ var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ goldmane flow log networkse Expect(err).NotTo(HaveOccurred()) if !BPFMode() { - Eventually(getRuleFuncTable(tc.Felixes[0], "API0|default.allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) - Eventually(getRuleFuncTable(tc.Felixes[0], "APE0|default.allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) + Eventually(getRuleFuncTable(tc.Felixes[0], "API0|gnp/allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) + Eventually(getRuleFuncTable(tc.Felixes[0], "APE0|gnp/allow-all", "filter"), "10s", "1s").ShouldNot(HaveOccurred()) } else { - bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "ingress", "default.allow-all") - bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "egress", "default.allow-all") + bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "ingress", "allow-all") + bpfWaitForPolicy(tc.Felixes[0], swl1.InterfaceName, "egress", "allow-all") } // NetworkSets @@ -991,7 +991,7 @@ var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ goldmane flow log networkse DstService: flowlog.FlowService{Namespace: flowlog.FieldNotIncluded, Name: flowlog.FieldNotIncluded, PortName: flowlog.FieldNotIncluded, PortNum: 0}, Action: "allow", Reporter: "src", }, - FlowEnforcedPolicySet: flowlog.FlowPolicySet{"0|default|default.allow-all|allow|0": {}}, + FlowEnforcedPolicySet: flowlog.FlowPolicySet{"0|default|allow-all|allow|0": {}}, }) }