From d0961f0f0b55399c66dd99aca447002c1cabd10e Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 13:22:57 +0200 Subject: [PATCH 01/13] fix(dnsmanager): scope IP-to-domain resolution cache per container - Scope addressToDomainMap in DNSManager per container using containerToAddressToDomain maps.SafeMap to prevent cross-container DNS attribution and Anycast/CDN IP collision poisoning. - Pass containerID to DNSResolver.ResolveIPAddress in createNetworkNeighbor and buildNetworkEvent so NetworkNeighborhood egress only carries domains resolved by that specific container. - Clean up per-container IP-to-domain resolution caches on container removal events. - Update DNSResolver, DnsCache interfaces, mocks, and tests. - Add unit tests verifying container DNS isolation and lifecycle cleanup. Signed-off-by: Matthias Bertschy --- .../v1/container_data.go | 6 +- .../v1/event_reporting_test.go | 2 +- pkg/dnsmanager/dns_manager.go | 79 ++++++--- pkg/dnsmanager/dns_manager_interface.go | 3 +- pkg/dnsmanager/dns_manager_mock.go | 2 +- pkg/dnsmanager/dns_manager_test.go | 158 +++++++++++++++--- pkg/networkstream/v1/network_stream.go | 5 +- pkg/networkstream/v1/network_stream_test.go | 2 +- pkg/objectcache/dnscache/dnscache.go | 4 +- pkg/objectcache/dnscache_interface.go | 6 +- pkg/objectcache/v1/mock.go | 2 +- 11 files changed, 214 insertions(+), 55 deletions(-) diff --git a/pkg/containerprofilemanager/v1/container_data.go b/pkg/containerprofilemanager/v1/container_data.go index 9ddb1ed555..99b1117f4b 100644 --- a/pkg/containerprofilemanager/v1/container_data.go +++ b/pkg/containerprofilemanager/v1/container_data.go @@ -262,7 +262,11 @@ func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namesp neighborEntry.IPAddress = networkEvent.Destination.IPAddress if dnsResolverClient != nil { - domain, ok := dnsResolverClient.ResolveIPAddress(networkEvent.Destination.IPAddress) + var containerID string + if cd.watchedContainerData != nil { + containerID = cd.watchedContainerData.ContainerID + } + domain, ok := dnsResolverClient.ResolveIPAddress(containerID, networkEvent.Destination.IPAddress) if ok { neighborEntry.DNS = domain neighborEntry.DNSNames = []string{domain} diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index 2de5abf703..697b587e9f 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -17,7 +17,7 @@ import ( // createNetworkNeighbor's DNS branch without a real dnsmanager. type fakeDNSResolver struct{ domain string } -func (f fakeDNSResolver) ResolveIPAddress(string) (string, bool) { return f.domain, true } +func (f fakeDNSResolver) ResolveIPAddress(string, string) (string, bool) { return f.domain, true } func (f fakeDNSResolver) ResolveContainerProcessToCloudServices(string, uint32) mapset.Set[string] { return nil } diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index fdbd7ea444..890fe422ee 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -3,6 +3,7 @@ package dnsmanager import ( "net" "strings" + "sync" "time" mapset "github.com/deckarep/golang-set/v2" @@ -17,10 +18,12 @@ import ( // DNSManager is used to manage DNS events and save IP resolutions. type DNSManager struct { - addressToDomainMap *lru.Cache[string, string] - lookupCache cache.ExpiringCache // Cache for DNS lookups - failureCache cache.ExpiringCache // Cache for failed lookups - containerToCloudServices maps.SafeMap[string, *maps.SafeMap[uint32, mapset.Set[string]]] // key: containerId, value: map of pid to cloud services + cacheSize int + cacheMu sync.RWMutex + containerToAddressToDomain maps.SafeMap[string, *lru.Cache[string, string]] + lookupCache cache.ExpiringCache // Cache for DNS lookups + failureCache cache.ExpiringCache // Cache for failed lookups + containerToCloudServices maps.SafeMap[string, *maps.SafeMap[uint32, mapset.Set[string]]] // key: containerId, value: map of pid to cloud services } type cacheEntry struct { @@ -37,32 +40,55 @@ var _ DNSManagerClient = (*DNSManager)(nil) var _ DNSResolver = (*DNSManager)(nil) func CreateDNSManager(size int) *DNSManager { - addressToDomainMap, err := lru.New[string, string](size) - if err != nil { - logger.L().Fatal("creating lru cache", helpers.Error(err)) - return nil + if size <= 0 { + size = 1000 } return &DNSManager{ - addressToDomainMap: addressToDomainMap, - lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), - failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), + cacheSize: size, + lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), + failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), } } +func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, string] { + if cache, found := dm.containerToAddressToDomain.Load(containerID); found { + return cache + } + dm.cacheMu.Lock() + defer dm.cacheMu.Unlock() + + if cache, found := dm.containerToAddressToDomain.Load(containerID); found { + return cache + } + + cache, err := lru.New[string, string](dm.cacheSize) + if err != nil { + logger.L().Error("creating per-container lru cache", helpers.Error(err), helpers.String("containerID", containerID)) + return nil + } + dm.containerToAddressToDomain.Set(containerID, cache) + return cache +} + func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { switch notif.Type { case containercollection.EventTypeAddContainer: dm.containerToCloudServices.Set(notif.Container.Runtime.ContainerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) + if c, err := lru.New[string, string](dm.cacheSize); err == nil { + dm.containerToAddressToDomain.Set(notif.Container.Runtime.ContainerID, c) + } case containercollection.EventTypeRemoveContainer: dm.containerToCloudServices.Delete(notif.Container.Runtime.ContainerID) + dm.containerToAddressToDomain.Delete(notif.Container.Runtime.ContainerID) } } func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { dnsName := dnsEvent.GetDNSName() + containerID := dnsEvent.GetContainerID() if isCloudService(dnsName) { - if pidToServices, found := dm.containerToCloudServices.Load(dnsEvent.GetContainerID()); found { + if pidToServices, found := dm.containerToCloudServices.Load(containerID); found { // Guard against cache size getting too large by checking the cardinality per container and pid if services, found := pidToServices.Load(dnsEvent.GetPID()); found { if services.Cardinality() < maxServiceCacheSize { @@ -77,10 +103,14 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { } } + containerCache := dm.getContainerCache(containerID) + if addresses := dnsEvent.GetAddresses(); len(addresses) > 0 { - for _, address := range addresses { - if address != "" { - dm.addressToDomainMap.Add(address, dnsName) + if containerCache != nil { + for _, address := range addresses { + if address != "" { + containerCache.Add(address, dnsName) + } } } @@ -100,8 +130,10 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { if cached, found := dm.lookupCache.Get(dnsName); found { entry := cached.(cacheEntry) // Use cached addresses - for _, addr := range entry.addresses { - dm.addressToDomainMap.Add(addr, dnsName) + if containerCache != nil { + for _, addr := range entry.addresses { + containerCache.Add(addr, dnsName) + } } return } @@ -119,7 +151,9 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { for _, addr := range ipAddresses { addrStr := addr.String() addrStrings = append(addrStrings, addrStr) - dm.addressToDomainMap.Add(addrStr, dnsName) + if containerCache != nil { + containerCache.Add(addrStr, dnsName) + } } // Cache the successful lookup @@ -128,9 +162,12 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { }) } -func (dm *DNSManager) ResolveIPAddress(ipAddr string) (string, bool) { - domain, found := dm.addressToDomainMap.Get(ipAddr) - return domain, found +func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (string, bool) { + if cache, found := dm.containerToAddressToDomain.Load(containerID); found && cache != nil { + domain, found := cache.Get(ipAddr) + return domain, found + } + return "", false } func (dm *DNSManager) ResolveContainerProcessToCloudServices(containerId string, pid uint32) mapset.Set[string] { diff --git a/pkg/dnsmanager/dns_manager_interface.go b/pkg/dnsmanager/dns_manager_interface.go index 66741c6d52..ca1351a2d8 100644 --- a/pkg/dnsmanager/dns_manager_interface.go +++ b/pkg/dnsmanager/dns_manager_interface.go @@ -12,6 +12,7 @@ type DNSManagerClient interface { } type DNSResolver interface { - ResolveIPAddress(ipAddr string) (string, bool) + ResolveIPAddress(containerId string, ipAddr string) (string, bool) ResolveContainerProcessToCloudServices(containerId string, pid uint32) mapset.Set[string] } + diff --git a/pkg/dnsmanager/dns_manager_mock.go b/pkg/dnsmanager/dns_manager_mock.go index 46f374205f..dc8fa269ac 100644 --- a/pkg/dnsmanager/dns_manager_mock.go +++ b/pkg/dnsmanager/dns_manager_mock.go @@ -22,7 +22,7 @@ func (n *DNSManagerMock) ReportEvent(_ utils.DNSEvent) { func (n *DNSManagerMock) ContainerCallback(_ containercollection.PubSubEvent) { } -func (n *DNSManagerMock) ResolveIPAddress(_ string) (string, bool) { +func (n *DNSManagerMock) ResolveIPAddress(_ string, _ string) (string, bool) { return "", false } diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index 98a6df81c6..b03f469433 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -20,18 +20,21 @@ import ( func TestResolveIPAddress(t *testing.T) { tests := []struct { - name string - dnsEvent *utils.StructEvent - ipAddr string - want string - wantOk bool + name string + containerID string + dnsEvent *utils.StructEvent + ipAddr string + want string + wantOk bool }{ { - name: "ip found", - ipAddr: "67.225.146.248", + name: "ip found with container id", + containerID: "container-123", + ipAddr: "67.225.146.248", dnsEvent: &utils.StructEvent{ - EventType: utils.DnsEventType, - DNSName: "test.com", + EventType: utils.DnsEventType, + ContainerID: "container-123", + DNSName: "test.com", Addresses: []string{ "67.225.146.248", }, @@ -40,11 +43,13 @@ func TestResolveIPAddress(t *testing.T) { wantOk: true, }, { - name: "ip not found", - ipAddr: "67.225.146.248", + name: "ip not found", + containerID: "container-123", + ipAddr: "67.225.146.248", dnsEvent: &utils.StructEvent{ - EventType: utils.DnsEventType, - DNSName: "test.com", + EventType: utils.DnsEventType, + ContainerID: "container-123", + DNSName: "test.com", Addresses: []string{ "54.23.332.4", }, @@ -53,15 +58,32 @@ func TestResolveIPAddress(t *testing.T) { wantOk: false, }, { - name: "no address", - ipAddr: "67.225.146.248", + name: "no address", + containerID: "container-123", + ipAddr: "67.225.146.248", dnsEvent: &utils.StructEvent{ - EventType: utils.DnsEventType, - DNSName: "test.com", + EventType: utils.DnsEventType, + ContainerID: "container-123", + DNSName: "test.com", }, want: "", wantOk: false, }, + { + name: "host process with empty container id", + containerID: "", + ipAddr: "1.1.1.1", + dnsEvent: &utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: "", + DNSName: "one.one.one.one", + Addresses: []string{ + "1.1.1.1", + }, + }, + want: "one.one.one.one", + wantOk: true, + }, } for _, tt := range tests { @@ -69,7 +91,7 @@ func TestResolveIPAddress(t *testing.T) { dm := CreateDNSManager(1000) dm.ReportEvent(tt.dnsEvent) - got, ok := dm.ResolveIPAddress(tt.ipAddr) + got, ok := dm.ResolveIPAddress(tt.containerID, tt.ipAddr) assert.Equal(t, tt.want, got) assert.Equal(t, tt.wantOk, ok) }) @@ -91,8 +113,9 @@ func TestResolveIPAddressFallback(t *testing.T) { { name: "dns resolution fallback", dnsEvent: &utils.StructEvent{ - EventType: utils.DnsEventType, - DNSName: "example.com", // Using example.com as it's guaranteed to exist + EventType: utils.DnsEventType, + ContainerID: "test-container-fallback", + DNSName: "example.com", // Using example.com as it's guaranteed to exist }, want: "example.com", wantOk: true, @@ -115,7 +138,7 @@ func TestResolveIPAddressFallback(t *testing.T) { } dm.ReportEvent(tt.dnsEvent) - got, ok := dm.ResolveIPAddress(addresses[0].String()) + got, ok := dm.ResolveIPAddress(tt.dnsEvent.ContainerID, addresses[0].String()) if got != tt.want || ok != tt.wantOk { t.Errorf("ResolveIPAddress() got = %v, ok = %v, want = %v, wantOk = %v", got, ok, tt.want, tt.wantOk) } @@ -123,6 +146,99 @@ func TestResolveIPAddressFallback(t *testing.T) { } } +func TestContainerDNSIsolation(t *testing.T) { + dm := CreateDNSManager(1000) + + container1 := "workload-ai-client-123" + container2 := "workload-kube-proxy-456" + sharedCDNIP := "104.18.7.192" + + // Container 1 queries an AI provider sitting behind a shared CDN IP + dm.ReportEvent(&utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: container1, + DNSName: "api.openai.com", + Addresses: []string{sharedCDNIP}, + }) + + // Container 2 queries an internal/unrelated service + dm.ReportEvent(&utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: container2, + DNSName: "internal-service.local", + Addresses: []string{"10.0.0.50"}, + }) + + // Verify Container 1 resolves the IP to api.openai.com + domain1, ok1 := dm.ResolveIPAddress(container1, sharedCDNIP) + assert.True(t, ok1) + assert.Equal(t, "api.openai.com", domain1) + + // Verify Container 2 does NOT inherit api.openai.com when connecting to the same IP + domain2, ok2 := dm.ResolveIPAddress(container2, sharedCDNIP) + assert.False(t, ok2) + assert.Equal(t, "", domain2) + + // Verify Container 2 resolves its own queried domain + domain2Internal, ok2Internal := dm.ResolveIPAddress(container2, "10.0.0.50") + assert.True(t, ok2Internal) + assert.Equal(t, "internal-service.local", domain2Internal) + + // Verify Container 1 does NOT resolve Container 2's domain + domain1Internal, ok1Internal := dm.ResolveIPAddress(container1, "10.0.0.50") + assert.False(t, ok1Internal) + assert.Equal(t, "", domain1Internal) +} + +func TestContainerDNSLifecycleCleanup(t *testing.T) { + dm := CreateDNSManager(1000) + + containerID := "short-lived-pod-789" + ip := "93.184.216.34" + + // Add container + dm.ContainerCallback(containercollection.PubSubEvent{ + Type: containercollection.EventTypeAddContainer, + Container: &containercollection.Container{ + Runtime: containercollection.RuntimeMetadata{ + BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ + ContainerID: containerID, + }, + }, + }, + }) + + // Report DNS event + dm.ReportEvent(&utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: containerID, + DNSName: "example.org", + Addresses: []string{ip}, + }) + + // Verify resolution works before removal + domain, ok := dm.ResolveIPAddress(containerID, ip) + assert.True(t, ok) + assert.Equal(t, "example.org", domain) + + // Remove container + dm.ContainerCallback(containercollection.PubSubEvent{ + Type: containercollection.EventTypeRemoveContainer, + Container: &containercollection.Container{ + Runtime: containercollection.RuntimeMetadata{ + BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ + ContainerID: containerID, + }, + }, + }, + }) + + // Verify resolution is pruned after container removal + domainAfter, okAfter := dm.ResolveIPAddress(containerID, ip) + assert.False(t, okAfter) + assert.Equal(t, "", domainAfter) +} + func TestCacheFallbackBehavior(t *testing.T) { dm := CreateDNSManager(1000) diff --git a/pkg/networkstream/v1/network_stream.go b/pkg/networkstream/v1/network_stream.go index c789b633cf..8c3017d110 100644 --- a/pkg/networkstream/v1/network_stream.go +++ b/pkg/networkstream/v1/network_stream.go @@ -488,8 +488,9 @@ func (ns *NetworkStream) buildNetworkEvent(event utils.NetworkEvent, processTree var domain string var ok bool dstEndpoint := event.GetDstEndpoint() + containerID := event.GetContainerID() if event.GetPktType() == "OUTGOING" { - domain, ok = ns.dnsResolver.ResolveIPAddress(dstEndpoint.Addr) + domain, ok = ns.dnsResolver.ResolveIPAddress(containerID, dstEndpoint.Addr) if !ok { // Try to resolve the domain name domains, err := net.LookupAddr(dstEndpoint.Addr) @@ -502,7 +503,7 @@ func (ns *NetworkStream) buildNetworkEvent(event utils.NetworkEvent, processTree } } } else { - domain, _ = ns.dnsResolver.ResolveIPAddress(dstEndpoint.Addr) + domain, _ = ns.dnsResolver.ResolveIPAddress(containerID, dstEndpoint.Addr) } networkEvent := armotypes.NetworkStreamEvent{ diff --git a/pkg/networkstream/v1/network_stream_test.go b/pkg/networkstream/v1/network_stream_test.go index 496c6919fc..718523c7c1 100644 --- a/pkg/networkstream/v1/network_stream_test.go +++ b/pkg/networkstream/v1/network_stream_test.go @@ -28,7 +28,7 @@ type stubResolver struct{} var _ dnsmanager.DNSResolver = (*stubResolver)(nil) -func (stubResolver) ResolveIPAddress(string) (string, bool) { return "", true } +func (stubResolver) ResolveIPAddress(string, string) (string, bool) { return "", true } func (stubResolver) ResolveContainerProcessToCloudServices(string, uint32) mapset.Set[string] { return nil } diff --git a/pkg/objectcache/dnscache/dnscache.go b/pkg/objectcache/dnscache/dnscache.go index cfe945d743..836741947d 100644 --- a/pkg/objectcache/dnscache/dnscache.go +++ b/pkg/objectcache/dnscache/dnscache.go @@ -18,13 +18,13 @@ func NewDnsCache(dnsResolver dnsmanager.DNSResolver) *DnsCacheImpl { } } -func (d *DnsCacheImpl) ResolveIpToDomain(ip string) string { +func (d *DnsCacheImpl) ResolveIpToDomain(containerID string, ip string) string { if d.dnsResolver == nil { logger.L().Debug("DnsCacheImpl - resolver is not set") return "" } - domain, ok := d.dnsResolver.ResolveIPAddress(ip) + domain, ok := d.dnsResolver.ResolveIPAddress(containerID, ip) if !ok { return "" } diff --git a/pkg/objectcache/dnscache_interface.go b/pkg/objectcache/dnscache_interface.go index b874d0d33e..699fa8b5f1 100644 --- a/pkg/objectcache/dnscache_interface.go +++ b/pkg/objectcache/dnscache_interface.go @@ -1,14 +1,14 @@ package objectcache type DnsCache interface { - ResolveIpToDomain(ip string) string + ResolveIpToDomain(containerID string, ip string) string } -var _DnsCache = (*DnsCacheMock)(nil) +var _ DnsCache = (*DnsCacheMock)(nil) type DnsCacheMock struct { } -func (dc *DnsCacheMock) ResolveIpToDomain(_ string) string { +func (dc *DnsCacheMock) ResolveIpToDomain(_ string, _ string) string { return "" } diff --git a/pkg/objectcache/v1/mock.go b/pkg/objectcache/v1/mock.go index bfc98a7e0d..5148d37b34 100644 --- a/pkg/objectcache/v1/mock.go +++ b/pkg/objectcache/v1/mock.go @@ -285,7 +285,7 @@ func (r *RuleObjectCacheMock) SetDnsCache(dnsCache map[string]string) { r.dnsCache = dnsCache } -func (r *RuleObjectCacheMock) ResolveIpToDomain(ip string) string { +func (r *RuleObjectCacheMock) ResolveIpToDomain(_ string, ip string) string { if domain, ok := r.dnsCache[ip]; ok { return domain } From a56bff68e86808dd2b1941af38b00344a8d167ee Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 13:42:54 +0200 Subject: [PATCH 02/13] fix(dnsmanager): harden cache lifecycle, prevent resurrection, and thread containerID - Add removedContainers cache to prevent resurrecting abandoned caches for removed containers upon late-arriving DNS events. - Add dedicated hostAddressToDomain cache for host and unscoped traffic. - Synchronize eager cache creation in ContainerCallback with lazy creation. - Thread containerID parameter through getEgressNetworkNeighbors, getIngressNetworkNeighbors, and createNetworkNeighbor to avoid depending on nilable watchedContainerData. - Add test assertion verifying late-arriving DNS events after container removal do not recreate caches. Signed-off-by: Matthias Bertschy --- .../v1/container_data.go | 18 ++--- .../v1/containerprofile_manager_test.go | 4 +- .../v1/event_reporting_test.go | 4 +- pkg/containerprofilemanager/v1/monitoring.go | 4 +- pkg/dnsmanager/dns_manager.go | 68 +++++++++++++++---- pkg/dnsmanager/dns_manager_test.go | 14 ++++ 6 files changed, 85 insertions(+), 27 deletions(-) diff --git a/pkg/containerprofilemanager/v1/container_data.go b/pkg/containerprofilemanager/v1/container_data.go index 99b1117f4b..44a69ad4c3 100644 --- a/pkg/containerprofilemanager/v1/container_data.go +++ b/pkg/containerprofilemanager/v1/container_data.go @@ -159,7 +159,7 @@ func (cd *containerData) getCallStacks() []v1beta1.IdentifiedCallStack { } // getIngressNetworkNeighbors returns ingress network neighbors for this container -func (cd *containerData) getIngressNetworkNeighbors(namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) []v1beta1.NetworkNeighbor { +func (cd *containerData) getIngressNetworkNeighbors(containerID string, namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) []v1beta1.NetworkNeighbor { var ingress []v1beta1.NetworkNeighbor if cd.networks == nil { return ingress @@ -167,7 +167,7 @@ func (cd *containerData) getIngressNetworkNeighbors(namespace string, k8sClient for _, event := range cd.networks.ToSlice() { if event.PktType == utils.HostPktType { - neighbor := cd.createNetworkNeighbor(event, namespace, k8sClient, dnsResolverClient) + neighbor := cd.createNetworkNeighbor(containerID, event, namespace, k8sClient, dnsResolverClient) if neighbor == nil { continue } @@ -179,7 +179,7 @@ func (cd *containerData) getIngressNetworkNeighbors(namespace string, k8sClient } // getEgressNetworkNeighbors returns egress network neighbors for this container -func (cd *containerData) getEgressNetworkNeighbors(namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) []v1beta1.NetworkNeighbor { +func (cd *containerData) getEgressNetworkNeighbors(containerID string, namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) []v1beta1.NetworkNeighbor { var egress []v1beta1.NetworkNeighbor if cd.networks == nil { return egress @@ -187,7 +187,7 @@ func (cd *containerData) getEgressNetworkNeighbors(namespace string, k8sClient k for _, event := range cd.networks.ToSlice() { if event.PktType != utils.HostPktType { - neighbor := cd.createNetworkNeighbor(event, namespace, k8sClient, dnsResolverClient) + neighbor := cd.createNetworkNeighbor(containerID, event, namespace, k8sClient, dnsResolverClient) if neighbor == nil { continue } @@ -199,7 +199,7 @@ func (cd *containerData) getEgressNetworkNeighbors(namespace string, k8sClient k } // createNetworkNeighbor creates a network neighbor from a network event -func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) *v1beta1.NetworkNeighbor { +func (cd *containerData) createNetworkNeighbor(containerID string, networkEvent NetworkEvent, namespace string, k8sClient k8sclient.K8sClientInterface, dnsResolverClient dnsmanager.DNSResolver) *v1beta1.NetworkNeighbor { var neighborEntry v1beta1.NetworkNeighbor portIdentifier := generatePortIdentifierFromEvent(networkEvent) @@ -262,11 +262,11 @@ func (cd *containerData) createNetworkNeighbor(networkEvent NetworkEvent, namesp neighborEntry.IPAddress = networkEvent.Destination.IPAddress if dnsResolverClient != nil { - var containerID string - if cd.watchedContainerData != nil { - containerID = cd.watchedContainerData.ContainerID + resolvedContainerID := containerID + if resolvedContainerID == "" && cd.watchedContainerData != nil { + resolvedContainerID = cd.watchedContainerData.ContainerID } - domain, ok := dnsResolverClient.ResolveIPAddress(containerID, networkEvent.Destination.IPAddress) + domain, ok := dnsResolverClient.ResolveIPAddress(resolvedContainerID, networkEvent.Destination.IPAddress) if ok { neighborEntry.DNS = domain neighborEntry.DNSNames = []string{domain} diff --git a/pkg/containerprofilemanager/v1/containerprofile_manager_test.go b/pkg/containerprofilemanager/v1/containerprofile_manager_test.go index c4338e421c..876919492c 100644 --- a/pkg/containerprofilemanager/v1/containerprofile_manager_test.go +++ b/pkg/containerprofilemanager/v1/containerprofile_manager_test.go @@ -562,11 +562,11 @@ func TestContainerDataMethods(t *testing.T) { assert.Empty(t, callStacks) // Test getIngressNetworkNeighbors with nil networks - ingress := cd.getIngressNetworkNeighbors("default", nil, nil) + ingress := cd.getIngressNetworkNeighbors("", "default", nil, nil) assert.Empty(t, ingress) // Test getEgressNetworkNeighbors with nil networks - egress := cd.getEgressNetworkNeighbors("default", nil, nil) + egress := cd.getEgressNetworkNeighbors("", "default", nil, nil) assert.Empty(t, egress) } diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index 697b587e9f..942ce98465 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -111,7 +111,7 @@ func TestNetworkNeighborIncrementCoversMaxDNSName(t *testing.T) { } cd := &containerData{} - neighbor := cd.createNetworkNeighbor(networkEvent, "default", nil, fakeDNSResolver{domain: maxDNSName}) + neighbor := cd.createNetworkNeighbor("", networkEvent, "default", nil, fakeDNSResolver{domain: maxDNSName}) if !assert.NotNil(t, neighbor) { return } @@ -151,7 +151,7 @@ func TestNetworkNeighborIncrementCoversSelectorPayload(t *testing.T) { // neighbor. watchedContainerData.Namespace is what networkNeighborIncrement reads to make // the same "different namespace" call createNetworkNeighbor's own namespace arg does below. cd := &containerData{watchedContainerData: &objectcache.WatchedContainerData{Namespace: "default"}} - neighbor := cd.createNetworkNeighbor(networkEvent, "default", nil, nil) + neighbor := cd.createNetworkNeighbor("", networkEvent, "default", nil, nil) if !assert.NotNil(t, neighbor) { return } diff --git a/pkg/containerprofilemanager/v1/monitoring.go b/pkg/containerprofilemanager/v1/monitoring.go index 696787e2a8..e90757f235 100644 --- a/pkg/containerprofilemanager/v1/monitoring.go +++ b/pkg/containerprofilemanager/v1/monitoring.go @@ -207,8 +207,8 @@ func (cpm *ContainerProfileManager) saveContainerProfile(watchedContainer *objec Endpoints: containerData.getEndpoints(), PolicyByRuleId: containerData.getRulePolicies(), IdentifiedCallStacks: containerData.getCallStacks(), - Egress: containerData.getEgressNetworkNeighbors(container.K8s.Namespace, cpm.k8sClient, cpm.dnsResolverClient), - Ingress: containerData.getIngressNetworkNeighbors(container.K8s.Namespace, cpm.k8sClient, cpm.dnsResolverClient), + Egress: containerData.getEgressNetworkNeighbors(watchedContainer.ContainerID, container.K8s.Namespace, cpm.k8sClient, cpm.dnsResolverClient), + Ingress: containerData.getIngressNetworkNeighbors(watchedContainer.ContainerID, container.K8s.Namespace, cpm.k8sClient, cpm.dnsResolverClient), LabelSelector: metav1.LabelSelector{ MatchLabels: watchedContainer.ParentWorkloadSelector.MatchLabels, MatchExpressions: watchedContainer.ParentWorkloadSelector.MatchExpressions, diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 890fe422ee..d32d3fd364 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -19,8 +19,10 @@ import ( // DNSManager is used to manage DNS events and save IP resolutions. type DNSManager struct { cacheSize int - cacheMu sync.RWMutex + cacheMu sync.Mutex + hostAddressToDomain *lru.Cache[string, string] containerToAddressToDomain maps.SafeMap[string, *lru.Cache[string, string]] + removedContainers *lru.Cache[string, struct{}] lookupCache cache.ExpiringCache // Cache for DNS lookups failureCache cache.ExpiringCache // Cache for failed lookups containerToCloudServices maps.SafeMap[string, *maps.SafeMap[uint32, mapset.Set[string]]] // key: containerId, value: map of pid to cloud services @@ -31,9 +33,10 @@ type cacheEntry struct { } const ( - defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups - defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups - maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container + defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups + defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups + maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container + maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection ) var _ DNSManagerClient = (*DNSManager)(nil) @@ -44,23 +47,43 @@ func CreateDNSManager(size int) *DNSManager { size = 1000 } + hostCache, err := lru.New[string, string](size) + if err != nil { + logger.L().Fatal("creating host lru cache", helpers.Error(err)) + return nil + } + + removedCache, _ := lru.New[string, struct{}](maxRemovedContainersEntries) + return &DNSManager{ - cacheSize: size, - lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), - failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), + cacheSize: size, + hostAddressToDomain: hostCache, + removedContainers: removedCache, + lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), + failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), } } func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, string] { + if containerID == "" { + return dm.hostAddressToDomain + } if cache, found := dm.containerToAddressToDomain.Load(containerID); found { return cache } + if dm.removedContainers != nil && dm.removedContainers.Contains(containerID) { + return nil + } + dm.cacheMu.Lock() defer dm.cacheMu.Unlock() if cache, found := dm.containerToAddressToDomain.Load(containerID); found { return cache } + if dm.removedContainers != nil && dm.removedContainers.Contains(containerID) { + return nil + } cache, err := lru.New[string, string](dm.cacheSize) if err != nil { @@ -74,13 +97,28 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { switch notif.Type { case containercollection.EventTypeAddContainer: - dm.containerToCloudServices.Set(notif.Container.Runtime.ContainerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) - if c, err := lru.New[string, string](dm.cacheSize); err == nil { - dm.containerToAddressToDomain.Set(notif.Container.Runtime.ContainerID, c) + containerID := notif.Container.Runtime.ContainerID + if dm.removedContainers != nil { + dm.removedContainers.Remove(containerID) + } + dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) + c, err := lru.New[string, string](dm.cacheSize) + if err != nil { + logger.L().Error("creating per-container lru cache on add", helpers.Error(err), helpers.String("containerID", containerID)) + return } + dm.cacheMu.Lock() + dm.containerToAddressToDomain.Set(containerID, c) + dm.cacheMu.Unlock() case containercollection.EventTypeRemoveContainer: - dm.containerToCloudServices.Delete(notif.Container.Runtime.ContainerID) - dm.containerToAddressToDomain.Delete(notif.Container.Runtime.ContainerID) + containerID := notif.Container.Runtime.ContainerID + dm.cacheMu.Lock() + if dm.removedContainers != nil { + dm.removedContainers.Add(containerID, struct{}{}) + } + dm.containerToCloudServices.Delete(containerID) + dm.containerToAddressToDomain.Delete(containerID) + dm.cacheMu.Unlock() } } @@ -163,6 +201,12 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { } func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (string, bool) { + if containerID == "" { + if dm.hostAddressToDomain != nil { + return dm.hostAddressToDomain.Get(ipAddr) + } + return "", false + } if cache, found := dm.containerToAddressToDomain.Load(containerID); found && cache != nil { domain, found := cache.Get(ipAddr) return domain, found diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index b03f469433..b8e9a895db 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -237,6 +237,20 @@ func TestContainerDNSLifecycleCleanup(t *testing.T) { domainAfter, okAfter := dm.ResolveIPAddress(containerID, ip) assert.False(t, okAfter) assert.Equal(t, "", domainAfter) + + // In-flight DNS event arrives after container removal + dm.ReportEvent(&utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: containerID, + DNSName: "late-arrival.org", + Addresses: []string{"1.2.3.4"}, + }) + + // Verify no cache is resurrected + assert.False(t, dm.containerToAddressToDomain.Has(containerID)) + domainLate, okLate := dm.ResolveIPAddress(containerID, "1.2.3.4") + assert.False(t, okLate) + assert.Equal(t, "", domainLate) } func TestCacheFallbackBehavior(t *testing.T) { From 97b524865d2b956d13afe15be2ef5ac7ee76d26b Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 13:53:51 +0200 Subject: [PATCH 03/13] fix(dnsmanager): bound per-container cache size and make AddContainer initialization idempotent - Set defaultPerContainerCacheSize to 1000 so nodes with many containers scale memory predictably without allocating node-wide cache capacity per container. - Make ContainerCallback EventTypeAddContainer check Has(containerID) before allocating to avoid clobbering an existing cache initialized by pre-announcement DNS events. - Restore Fatal on invalid size during CreateDNSManager. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 38 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index d32d3fd364..1ead8c6d68 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -19,6 +19,7 @@ import ( // DNSManager is used to manage DNS events and save IP resolutions. type DNSManager struct { cacheSize int + perContainerCacheSize int cacheMu sync.Mutex hostAddressToDomain *lru.Cache[string, string] containerToAddressToDomain maps.SafeMap[string, *lru.Cache[string, string]] @@ -36,6 +37,7 @@ const ( defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container + defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection ) @@ -43,24 +45,26 @@ var _ DNSManagerClient = (*DNSManager)(nil) var _ DNSResolver = (*DNSManager)(nil) func CreateDNSManager(size int) *DNSManager { - if size <= 0 { - size = 1000 - } - hostCache, err := lru.New[string, string](size) if err != nil { logger.L().Fatal("creating host lru cache", helpers.Error(err)) return nil } + perContainerSize := defaultPerContainerCacheSize + if size < perContainerSize { + perContainerSize = size + } + removedCache, _ := lru.New[string, struct{}](maxRemovedContainersEntries) return &DNSManager{ - cacheSize: size, - hostAddressToDomain: hostCache, - removedContainers: removedCache, - lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), - failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), + cacheSize: size, + perContainerCacheSize: perContainerSize, + hostAddressToDomain: hostCache, + removedContainers: removedCache, + lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), + failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), } } @@ -85,7 +89,7 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s return nil } - cache, err := lru.New[string, string](dm.cacheSize) + cache, err := lru.New[string, string](dm.perContainerCacheSize) if err != nil { logger.L().Error("creating per-container lru cache", helpers.Error(err), helpers.String("containerID", containerID)) return nil @@ -102,13 +106,15 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { dm.removedContainers.Remove(containerID) } dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) - c, err := lru.New[string, string](dm.cacheSize) - if err != nil { - logger.L().Error("creating per-container lru cache on add", helpers.Error(err), helpers.String("containerID", containerID)) - return - } dm.cacheMu.Lock() - dm.containerToAddressToDomain.Set(containerID, c) + if !dm.containerToAddressToDomain.Has(containerID) { + c, err := lru.New[string, string](dm.perContainerCacheSize) + if err != nil { + logger.L().Error("creating per-container lru cache on add", helpers.Error(err), helpers.String("containerID", containerID)) + } else { + dm.containerToAddressToDomain.Set(containerID, c) + } + } dm.cacheMu.Unlock() case containercollection.EventTypeRemoveContainer: containerID := notif.Container.Runtime.ContainerID From 981f97e2a110ba89bd86c685587316e857f575cd Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 14:05:28 +0200 Subject: [PATCH 04/13] fix(dnsmanager): add removal grace period and reject in-flight events for removed containers - Allow reading existing resolutions during removal grace period so terminal container profile saves retain DNS names. - Reject in-flight DNS ReportEvents for containers marked as removed to avoid resurrecting abandoned caches. - Synchronize tombstone removal and cache creation under cacheMu in AddContainer. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 50 ++++++++++++++++++------------ pkg/dnsmanager/dns_manager_test.go | 20 ++++++++---- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 1ead8c6d68..dca18ff50f 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -34,11 +34,12 @@ type cacheEntry struct { } const ( - defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups - defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups - maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container - defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container - maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection + defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups + defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups + maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container + defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container + maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection + defaultRemovalGracePeriod = 2 * time.Minute // Grace period before evicting resolution cache to allow terminal profile save ) var _ DNSManagerClient = (*DNSManager)(nil) @@ -59,35 +60,39 @@ func CreateDNSManager(size int) *DNSManager { removedCache, _ := lru.New[string, struct{}](maxRemovedContainersEntries) return &DNSManager{ - cacheSize: size, - perContainerCacheSize: perContainerSize, - hostAddressToDomain: hostCache, - removedContainers: removedCache, - lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), - failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), + cacheSize: size, + perContainerCacheSize: perContainerSize, + hostAddressToDomain: hostCache, + removedContainers: removedCache, + lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), + failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), } } +func (dm *DNSManager) isRemoved(containerID string) bool { + return dm.removedContainers != nil && dm.removedContainers.Contains(containerID) +} + func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, string] { if containerID == "" { return dm.hostAddressToDomain } + if dm.isRemoved(containerID) { + return nil + } if cache, found := dm.containerToAddressToDomain.Load(containerID); found { return cache } - if dm.removedContainers != nil && dm.removedContainers.Contains(containerID) { - return nil - } dm.cacheMu.Lock() defer dm.cacheMu.Unlock() + if dm.isRemoved(containerID) { + return nil + } if cache, found := dm.containerToAddressToDomain.Load(containerID); found { return cache } - if dm.removedContainers != nil && dm.removedContainers.Contains(containerID) { - return nil - } cache, err := lru.New[string, string](dm.perContainerCacheSize) if err != nil { @@ -102,11 +107,11 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { switch notif.Type { case containercollection.EventTypeAddContainer: containerID := notif.Container.Runtime.ContainerID + dm.cacheMu.Lock() if dm.removedContainers != nil { dm.removedContainers.Remove(containerID) } dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) - dm.cacheMu.Lock() if !dm.containerToAddressToDomain.Has(containerID) { c, err := lru.New[string, string](dm.perContainerCacheSize) if err != nil { @@ -123,8 +128,15 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { dm.removedContainers.Add(containerID, struct{}{}) } dm.containerToCloudServices.Delete(containerID) - dm.containerToAddressToDomain.Delete(containerID) dm.cacheMu.Unlock() + + time.AfterFunc(defaultRemovalGracePeriod, func() { + dm.cacheMu.Lock() + if dm.isRemoved(containerID) { + dm.containerToAddressToDomain.Delete(containerID) + } + dm.cacheMu.Unlock() + }) } } diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index b8e9a895db..739b585fca 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -233,10 +233,10 @@ func TestContainerDNSLifecycleCleanup(t *testing.T) { }, }) - // Verify resolution is pruned after container removal - domainAfter, okAfter := dm.ResolveIPAddress(containerID, ip) - assert.False(t, okAfter) - assert.Equal(t, "", domainAfter) + // Verify existing resolutions remain readable during removal grace period for terminal profile save + domainDuringGrace, okDuringGrace := dm.ResolveIPAddress(containerID, ip) + assert.True(t, okDuringGrace) + assert.Equal(t, "example.org", domainDuringGrace) // In-flight DNS event arrives after container removal dm.ReportEvent(&utils.StructEvent{ @@ -246,11 +246,19 @@ func TestContainerDNSLifecycleCleanup(t *testing.T) { Addresses: []string{"1.2.3.4"}, }) - // Verify no cache is resurrected - assert.False(t, dm.containerToAddressToDomain.Has(containerID)) + // Verify no cache was resurrected or updated for late-arriving event domainLate, okLate := dm.ResolveIPAddress(containerID, "1.2.3.4") assert.False(t, okLate) assert.Equal(t, "", domainLate) + + // Explicitly simulate grace period expiration + dm.cacheMu.Lock() + dm.containerToAddressToDomain.Delete(containerID) + dm.cacheMu.Unlock() + + domainAfterGrace, okAfterGrace := dm.ResolveIPAddress(containerID, ip) + assert.False(t, okAfterGrace) + assert.Equal(t, "", domainAfterGrace) } func TestCacheFallbackBehavior(t *testing.T) { From c5343360f9cd612114fc2723dbf8c8b15144f6b5 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 14:24:21 +0200 Subject: [PATCH 05/13] fix(dnsmanager): align removal grace period with containerprofilecache and support host container matching - Treat armotypes.HostContainerID and empty string uniformly via isHost. - Align defaultRemovalGracePeriod to 10s matching containerprofilecache. - Support containerID-keyed lookups in RuleObjectCacheMock. - Make containerToCloudServices initialization idempotent. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 25 ++++++++++++++++--------- pkg/dnsmanager/dns_manager_test.go | 23 +++++++++++++++++++++++ pkg/objectcache/v1/mock.go | 13 ++++++++++--- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index dca18ff50f..a9e61da1b8 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/armosec/armoapi-go/armotypes" mapset "github.com/deckarep/golang-set/v2" "github.com/goradd/maps" lru "github.com/hashicorp/golang-lru/v2" @@ -34,17 +35,21 @@ type cacheEntry struct { } const ( - defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups - defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups - maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container - defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container - maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection - defaultRemovalGracePeriod = 2 * time.Minute // Grace period before evicting resolution cache to allow terminal profile save + defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups + defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups + maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container + defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container + maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection + defaultRemovalGracePeriod = 10 * time.Second // Grace period before evicting resolution cache to allow terminal profile save ) var _ DNSManagerClient = (*DNSManager)(nil) var _ DNSResolver = (*DNSManager)(nil) +func isHost(containerID string) bool { + return containerID == "" || containerID == armotypes.HostContainerID +} + func CreateDNSManager(size int) *DNSManager { hostCache, err := lru.New[string, string](size) if err != nil { @@ -74,7 +79,7 @@ func (dm *DNSManager) isRemoved(containerID string) bool { } func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, string] { - if containerID == "" { + if isHost(containerID) { return dm.hostAddressToDomain } if dm.isRemoved(containerID) { @@ -111,7 +116,9 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { if dm.removedContainers != nil { dm.removedContainers.Remove(containerID) } - dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) + if !dm.containerToCloudServices.Has(containerID) { + dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) + } if !dm.containerToAddressToDomain.Has(containerID) { c, err := lru.New[string, string](dm.perContainerCacheSize) if err != nil { @@ -219,7 +226,7 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { } func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (string, bool) { - if containerID == "" { + if isHost(containerID) { if dm.hostAddressToDomain != nil { return dm.hostAddressToDomain.Get(ipAddr) } diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index 739b585fca..797873190a 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -10,6 +10,7 @@ import ( "math/rand/v2" + "github.com/armosec/armoapi-go/armotypes" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" "github.com/kubescape/node-agent/pkg/utils" "github.com/stretchr/testify/assert" @@ -188,6 +189,28 @@ func TestContainerDNSIsolation(t *testing.T) { domain1Internal, ok1Internal := dm.ResolveIPAddress(container1, "10.0.0.50") assert.False(t, ok1Internal) assert.Equal(t, "", domain1Internal) + + // Host queries a host service + dm.ReportEvent(&utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: armotypes.HostContainerID, + DNSName: "host-service.internal", + Addresses: []string{"192.168.1.10"}, + }) + + // Verify both "host" and "" resolve host traffic + domainHost, okHost := dm.ResolveIPAddress(armotypes.HostContainerID, "192.168.1.10") + assert.True(t, okHost) + assert.Equal(t, "host-service.internal", domainHost) + + domainEmpty, okEmpty := dm.ResolveIPAddress("", "192.168.1.10") + assert.True(t, okEmpty) + assert.Equal(t, "host-service.internal", domainEmpty) + + // Verify regular containers do NOT resolve host queries + domainContainer1Host, okContainer1Host := dm.ResolveIPAddress(container1, "192.168.1.10") + assert.False(t, okContainer1Host) + assert.Equal(t, "", domainContainer1Host) } func TestContainerDNSLifecycleCleanup(t *testing.T) { diff --git a/pkg/objectcache/v1/mock.go b/pkg/objectcache/v1/mock.go index 5148d37b34..2184b081c0 100644 --- a/pkg/objectcache/v1/mock.go +++ b/pkg/objectcache/v1/mock.go @@ -285,9 +285,16 @@ func (r *RuleObjectCacheMock) SetDnsCache(dnsCache map[string]string) { r.dnsCache = dnsCache } -func (r *RuleObjectCacheMock) ResolveIpToDomain(_ string, ip string) string { - if domain, ok := r.dnsCache[ip]; ok { - return domain +func (r *RuleObjectCacheMock) ResolveIpToDomain(containerID string, ip string) string { + if r.dnsCache != nil { + if containerID != "" { + if domain, ok := r.dnsCache[containerID+":"+ip]; ok { + return domain + } + } + if domain, ok := r.dnsCache[ip]; ok { + return domain + } } return "" } From 2744c1641570a48f95782de3937e96c9b252e22a Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 15:24:44 +0200 Subject: [PATCH 06/13] fix(dns,profile): handle non-positive dnsCacheSize and preserve host key in createNetworkNeighbor --- .../v1/container_data.go | 6 +-- .../v1/event_reporting_test.go | 38 +++++++++++++++++++ pkg/dnsmanager/dns_manager.go | 5 +++ pkg/dnsmanager/dns_manager_test.go | 10 +++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/pkg/containerprofilemanager/v1/container_data.go b/pkg/containerprofilemanager/v1/container_data.go index 44a69ad4c3..7f07ad5c22 100644 --- a/pkg/containerprofilemanager/v1/container_data.go +++ b/pkg/containerprofilemanager/v1/container_data.go @@ -262,11 +262,7 @@ func (cd *containerData) createNetworkNeighbor(containerID string, networkEvent neighborEntry.IPAddress = networkEvent.Destination.IPAddress if dnsResolverClient != nil { - resolvedContainerID := containerID - if resolvedContainerID == "" && cd.watchedContainerData != nil { - resolvedContainerID = cd.watchedContainerData.ContainerID - } - domain, ok := dnsResolverClient.ResolveIPAddress(resolvedContainerID, networkEvent.Destination.IPAddress) + domain, ok := dnsResolverClient.ResolveIPAddress(containerID, networkEvent.Destination.IPAddress) if ok { neighborEntry.DNS = domain neighborEntry.DNSNames = []string{domain} diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index 942ce98465..82c44a00da 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -214,3 +214,41 @@ func TestResolveExecPath(t *testing.T) { }) } } + +type trackingDNSResolver struct { + lastContainerID string + lastIPAddress string +} + +func (r *trackingDNSResolver) ResolveIPAddress(containerID string, ipAddr string) (string, bool) { + r.lastContainerID = containerID + r.lastIPAddress = ipAddr + return "resolved.domain", true +} + +func (r *trackingDNSResolver) ResolveContainerProcessToCloudServices(string, uint32) mapset.Set[string] { + return nil +} + +func TestCreateNetworkNeighbor_EmptyContainerIDWithWatchedContainerData(t *testing.T) { + cd := &containerData{ + watchedContainerData: &objectcache.WatchedContainerData{ + ContainerID: "watched-container-456", + }, + } + + networkEvent := NetworkEvent{ + Port: 80, + PktType: utils.OutgoingPktType, + Destination: Destination{ + IPAddress: "93.184.216.34", + }, + } + + resolver := &trackingDNSResolver{} + neighbor := cd.createNetworkNeighbor("", networkEvent, "default", nil, resolver) + assert.NotNil(t, neighbor) + assert.Equal(t, "", resolver.lastContainerID, "empty containerID must be preserved so host cache is selected") + assert.Equal(t, "93.184.216.34", resolver.lastIPAddress) + assert.Equal(t, "resolved.domain", neighbor.DNS) +} diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index a9e61da1b8..1ded36eeff 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -37,6 +37,7 @@ type cacheEntry struct { const ( defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups + defaultDNSCacheSize = 10000 // Default total DNS cache size when non-positive size provided maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection @@ -51,6 +52,10 @@ func isHost(containerID string) bool { } func CreateDNSManager(size int) *DNSManager { + if size <= 0 { + size = defaultDNSCacheSize + } + hostCache, err := lru.New[string, string](size) if err != nil { logger.L().Fatal("creating host lru cache", helpers.Error(err)) diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index 797873190a..297bdfbde1 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -655,3 +655,13 @@ func TestCloudServiceCacheLimit(t *testing.T) { services.Cardinality(), maxServiceCacheSize) } } + +func TestCreateDNSManager_NonPositiveSize(t *testing.T) { + for _, size := range []int{0, -1, -100} { + dm := CreateDNSManager(size) + assert.NotNil(t, dm, "CreateDNSManager(%d) must not return nil", size) + assert.Equal(t, defaultDNSCacheSize, dm.cacheSize) + assert.Equal(t, defaultPerContainerCacheSize, dm.perContainerCacheSize) + assert.NotNil(t, dm.hostAddressToDomain) + } +} From 7f0816b4e98e096044716ef3102c4cc149027ca5 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 15:40:34 +0200 Subject: [PATCH 07/13] fix(dnsmanager): use purely lazy per-container cache allocation and check error on removedContainers - Remove redundant eager pre-allocation in AddContainer to save memory for DNS-inactive containers. - Check and handle error on removedCache creation in CreateDNSManager. - Run gofmt on const block. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 1ded36eeff..1d23d33abf 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -39,7 +39,7 @@ const ( defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups defaultDNSCacheSize = 10000 // Default total DNS cache size when non-positive size provided maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container - defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container + defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection defaultRemovalGracePeriod = 10 * time.Second // Grace period before evicting resolution cache to allow terminal profile save ) @@ -67,7 +67,11 @@ func CreateDNSManager(size int) *DNSManager { perContainerSize = size } - removedCache, _ := lru.New[string, struct{}](maxRemovedContainersEntries) + removedCache, err := lru.New[string, struct{}](maxRemovedContainersEntries) + if err != nil { + logger.L().Fatal("creating removed containers cache", helpers.Error(err)) + return nil + } return &DNSManager{ cacheSize: size, @@ -124,14 +128,6 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { if !dm.containerToCloudServices.Has(containerID) { dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) } - if !dm.containerToAddressToDomain.Has(containerID) { - c, err := lru.New[string, string](dm.perContainerCacheSize) - if err != nil { - logger.L().Error("creating per-container lru cache on add", helpers.Error(err), helpers.String("containerID", containerID)) - } else { - dm.containerToAddressToDomain.Set(containerID, c) - } - } dm.cacheMu.Unlock() case containercollection.EventTypeRemoveContainer: containerID := notif.Container.Runtime.ContainerID From 91d3e068fbddcc5b88734fc36ff3fa9f96559026 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 15:52:10 +0200 Subject: [PATCH 08/13] fix(dnsmanager): bound containerToAddressToDomain via LRU cache and test mock lookup - Back containerToAddressToDomain with an LRU cache bounded to maxTrackedContainers (5000), guaranteeing aggregate memory cannot grow unbounded even under container churn. - Add unit test for RuleObjectCacheMock.ResolveIpToDomain testing both composite-key and fallback resolution. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 34 ++++++++++++++++---------- pkg/dnsmanager/dns_manager_test.go | 2 +- pkg/objectcache/v1/objectcache_test.go | 17 +++++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 1d23d33abf..72fe2c59da 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -23,7 +23,7 @@ type DNSManager struct { perContainerCacheSize int cacheMu sync.Mutex hostAddressToDomain *lru.Cache[string, string] - containerToAddressToDomain maps.SafeMap[string, *lru.Cache[string, string]] + containerToAddressToDomain *lru.Cache[string, *lru.Cache[string, string]] removedContainers *lru.Cache[string, struct{}] lookupCache cache.ExpiringCache // Cache for DNS lookups failureCache cache.ExpiringCache // Cache for failed lookups @@ -37,9 +37,10 @@ type cacheEntry struct { const ( defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups - defaultDNSCacheSize = 10000 // Default total DNS cache size when non-positive size provided + defaultDNSCacheSize = 10000 // Default total host DNS cache size when non-positive size provided maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container + maxTrackedContainers = 5000 // Maximum number of concurrent containers with DNS caches maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection defaultRemovalGracePeriod = 10 * time.Second // Grace period before evicting resolution cache to allow terminal profile save ) @@ -67,6 +68,12 @@ func CreateDNSManager(size int) *DNSManager { perContainerSize = size } + containerCache, err := lru.New[string, *lru.Cache[string, string]](maxTrackedContainers) + if err != nil { + logger.L().Fatal("creating container lru cache", helpers.Error(err)) + return nil + } + removedCache, err := lru.New[string, struct{}](maxRemovedContainersEntries) if err != nil { logger.L().Fatal("creating removed containers cache", helpers.Error(err)) @@ -74,12 +81,13 @@ func CreateDNSManager(size int) *DNSManager { } return &DNSManager{ - cacheSize: size, - perContainerCacheSize: perContainerSize, - hostAddressToDomain: hostCache, - removedContainers: removedCache, - lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), - failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), + cacheSize: size, + perContainerCacheSize: perContainerSize, + hostAddressToDomain: hostCache, + containerToAddressToDomain: containerCache, + removedContainers: removedCache, + lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL), + failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL), } } @@ -94,7 +102,7 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s if dm.isRemoved(containerID) { return nil } - if cache, found := dm.containerToAddressToDomain.Load(containerID); found { + if cache, found := dm.containerToAddressToDomain.Get(containerID); found { return cache } @@ -104,7 +112,7 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s if dm.isRemoved(containerID) { return nil } - if cache, found := dm.containerToAddressToDomain.Load(containerID); found { + if cache, found := dm.containerToAddressToDomain.Get(containerID); found { return cache } @@ -113,7 +121,7 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s logger.L().Error("creating per-container lru cache", helpers.Error(err), helpers.String("containerID", containerID)) return nil } - dm.containerToAddressToDomain.Set(containerID, cache) + dm.containerToAddressToDomain.Add(containerID, cache) return cache } @@ -141,7 +149,7 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { time.AfterFunc(defaultRemovalGracePeriod, func() { dm.cacheMu.Lock() if dm.isRemoved(containerID) { - dm.containerToAddressToDomain.Delete(containerID) + dm.containerToAddressToDomain.Remove(containerID) } dm.cacheMu.Unlock() }) @@ -233,7 +241,7 @@ func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (strin } return "", false } - if cache, found := dm.containerToAddressToDomain.Load(containerID); found && cache != nil { + if cache, found := dm.containerToAddressToDomain.Get(containerID); found && cache != nil { domain, found := cache.Get(ipAddr) return domain, found } diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index 297bdfbde1..7fab03125d 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -276,7 +276,7 @@ func TestContainerDNSLifecycleCleanup(t *testing.T) { // Explicitly simulate grace period expiration dm.cacheMu.Lock() - dm.containerToAddressToDomain.Delete(containerID) + dm.containerToAddressToDomain.Remove(containerID) dm.cacheMu.Unlock() domainAfterGrace, okAfterGrace := dm.ResolveIPAddress(containerID, ip) diff --git a/pkg/objectcache/v1/objectcache_test.go b/pkg/objectcache/v1/objectcache_test.go index 6af7e69c5e..d405aad1f5 100644 --- a/pkg/objectcache/v1/objectcache_test.go +++ b/pkg/objectcache/v1/objectcache_test.go @@ -19,3 +19,20 @@ func TestContainerProfileCache(t *testing.T) { k8sObjectCache := NewObjectCache(nil, cp, nil) assert.NotNil(t, k8sObjectCache.ContainerProfileCache()) } + +func TestRuleObjectCacheMock_ResolveIpToDomain(t *testing.T) { + mock := &RuleObjectCacheMock{} + mock.SetDnsCache(map[string]string{ + "cont-1:1.2.3.4": "scoped-domain.com", + "5.6.7.8": "global-domain.com", + }) + + // Test container-scoped resolution + assert.Equal(t, "scoped-domain.com", mock.ResolveIpToDomain("cont-1", "1.2.3.4")) + // Test container with fallback to unscoped key + assert.Equal(t, "global-domain.com", mock.ResolveIpToDomain("cont-2", "5.6.7.8")) + // Test unscoped lookup + assert.Equal(t, "global-domain.com", mock.ResolveIpToDomain("", "5.6.7.8")) + // Test missing resolution + assert.Equal(t, "", mock.ResolveIpToDomain("cont-1", "9.9.9.9")) +} From cfb056b1920217e357b689c72d6c9b1d5d321bf5 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 16:01:47 +0200 Subject: [PATCH 09/13] fix(dnsmanager): disallow empty containerID fallback to host and scale container LRU with size - Require explicit armotypes.HostContainerID for host DNS cache access; empty containerID safely misses to prevent cross-container leakage. - Scale containerCache capacity proportionally with configured cache size to strictly bound total memory budget. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 16 ++++++++++++---- pkg/dnsmanager/dns_manager_test.go | 28 ++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 72fe2c59da..8c0f17b797 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -40,7 +40,7 @@ const ( defaultDNSCacheSize = 10000 // Default total host DNS cache size when non-positive size provided maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container - maxTrackedContainers = 5000 // Maximum number of concurrent containers with DNS caches + minTrackedContainers = 100 // Minimum number of concurrent containers with DNS caches maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection defaultRemovalGracePeriod = 10 * time.Second // Grace period before evicting resolution cache to allow terminal profile save ) @@ -49,7 +49,7 @@ var _ DNSManagerClient = (*DNSManager)(nil) var _ DNSResolver = (*DNSManager)(nil) func isHost(containerID string) bool { - return containerID == "" || containerID == armotypes.HostContainerID + return containerID == armotypes.HostContainerID } func CreateDNSManager(size int) *DNSManager { @@ -68,7 +68,12 @@ func CreateDNSManager(size int) *DNSManager { perContainerSize = size } - containerCache, err := lru.New[string, *lru.Cache[string, string]](maxTrackedContainers) + maxContainers := size / perContainerSize + if maxContainers < minTrackedContainers { + maxContainers = minTrackedContainers + } + + containerCache, err := lru.New[string, *lru.Cache[string, string]](maxContainers) if err != nil { logger.L().Fatal("creating container lru cache", helpers.Error(err)) return nil @@ -99,7 +104,7 @@ func (dm *DNSManager) getContainerCache(containerID string) *lru.Cache[string, s if isHost(containerID) { return dm.hostAddressToDomain } - if dm.isRemoved(containerID) { + if containerID == "" || dm.isRemoved(containerID) { return nil } if cache, found := dm.containerToAddressToDomain.Get(containerID); found { @@ -241,6 +246,9 @@ func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (strin } return "", false } + if containerID == "" { + return "", false + } if cache, found := dm.containerToAddressToDomain.Get(containerID); found && cache != nil { domain, found := cache.Get(ipAddr) return domain, found diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index 7fab03125d..cc971d75ec 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -71,12 +71,12 @@ func TestResolveIPAddress(t *testing.T) { wantOk: false, }, { - name: "host process with empty container id", - containerID: "", + name: "host process with HostContainerID", + containerID: armotypes.HostContainerID, ipAddr: "1.1.1.1", dnsEvent: &utils.StructEvent{ EventType: utils.DnsEventType, - ContainerID: "", + ContainerID: armotypes.HostContainerID, DNSName: "one.one.one.one", Addresses: []string{ "1.1.1.1", @@ -85,6 +85,21 @@ func TestResolveIPAddress(t *testing.T) { want: "one.one.one.one", wantOk: true, }, + { + name: "empty container id returns miss", + containerID: "", + ipAddr: "1.1.1.1", + dnsEvent: &utils.StructEvent{ + EventType: utils.DnsEventType, + ContainerID: "", + DNSName: "one.one.one.one", + Addresses: []string{ + "1.1.1.1", + }, + }, + want: "", + wantOk: false, + }, } for _, tt := range tests { @@ -198,14 +213,15 @@ func TestContainerDNSIsolation(t *testing.T) { Addresses: []string{"192.168.1.10"}, }) - // Verify both "host" and "" resolve host traffic + // Verify "host" resolves host traffic domainHost, okHost := dm.ResolveIPAddress(armotypes.HostContainerID, "192.168.1.10") assert.True(t, okHost) assert.Equal(t, "host-service.internal", domainHost) + // Verify empty containerID does NOT resolve host traffic domainEmpty, okEmpty := dm.ResolveIPAddress("", "192.168.1.10") - assert.True(t, okEmpty) - assert.Equal(t, "host-service.internal", domainEmpty) + assert.False(t, okEmpty) + assert.Equal(t, "", domainEmpty) // Verify regular containers do NOT resolve host queries domainContainer1Host, okContainer1Host := dm.ResolveIPAddress(container1, "192.168.1.10") From e4f624bb7586f23afac15e26a8fe2678fb6bc290 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 16:15:17 +0200 Subject: [PATCH 10/13] fix(dnsmanager): reset container cache on AddContainer and filter empty address strings - Evict any previous cache instance in AddContainer on container ID reuse. - Filter empty address strings across all lookup and caching paths in ReportEvent. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 8c0f17b797..1c4702b497 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -138,6 +138,8 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { if dm.removedContainers != nil { dm.removedContainers.Remove(containerID) } + // Reset any lingering cache from a previous instance of this container ID + dm.containerToAddressToDomain.Remove(containerID) if !dm.containerToCloudServices.Has(containerID) { dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) } @@ -209,7 +211,9 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { // Use cached addresses if containerCache != nil { for _, addr := range entry.addresses { - containerCache.Add(addr, dnsName) + if addr != "" { + containerCache.Add(addr, dnsName) + } } } return @@ -227,9 +231,11 @@ func (dm *DNSManager) ReportEvent(dnsEvent utils.DNSEvent) { addrStrings := make([]string, 0, len(ipAddresses)) for _, addr := range ipAddresses { addrStr := addr.String() - addrStrings = append(addrStrings, addrStr) - if containerCache != nil { - containerCache.Add(addrStr, dnsName) + if addrStr != "" { + addrStrings = append(addrStrings, addrStr) + if containerCache != nil { + containerCache.Add(addrStr, dnsName) + } } } From 9744e287cf87059f9120e7f2e3b9afc471407219 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Mon, 31 Aug 2026 16:23:23 +0200 Subject: [PATCH 11/13] fix(dnsmanager): preserve pre-announcement DNS cache in AddContainer and decouple timer cleanup from tombstone LRU - Do not clear cache in AddContainer to avoid discarding resolutions from early DNS events. - In removal grace timer, check containerToCloudServices.Has to ensure removal even if tombstone was evicted under high churn. Signed-off-by: Matthias Bertschy --- pkg/dnsmanager/dns_manager.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index 1c4702b497..cf59427d74 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -138,8 +138,6 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { if dm.removedContainers != nil { dm.removedContainers.Remove(containerID) } - // Reset any lingering cache from a previous instance of this container ID - dm.containerToAddressToDomain.Remove(containerID) if !dm.containerToCloudServices.Has(containerID) { dm.containerToCloudServices.Set(containerID, maps.NewSafeMap[uint32, mapset.Set[string]]()) } @@ -155,7 +153,7 @@ func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) { time.AfterFunc(defaultRemovalGracePeriod, func() { dm.cacheMu.Lock() - if dm.isRemoved(containerID) { + if !dm.containerToCloudServices.Has(containerID) { dm.containerToAddressToDomain.Remove(containerID) } dm.cacheMu.Unlock() From c6cef089289f1adfe92114270df4d76eccabe8f2 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Tue, 1 Sep 2026 09:53:02 +0200 Subject: [PATCH 12/13] fix(dnsmanager): address copilot review comments on cache sizing and initialisms Signed-off-by: Matthias Bertschy --- .../v1/event_reporting_test.go | 2 +- pkg/dnsmanager/dns_manager.go | 35 +++++--- pkg/dnsmanager/dns_manager_interface.go | 4 +- pkg/dnsmanager/dns_manager_test.go | 81 ++++++++++++++----- 4 files changed, 91 insertions(+), 31 deletions(-) diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index 82c44a00da..d948cc980d 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -248,7 +248,7 @@ func TestCreateNetworkNeighbor_EmptyContainerIDWithWatchedContainerData(t *testi resolver := &trackingDNSResolver{} neighbor := cd.createNetworkNeighbor("", networkEvent, "default", nil, resolver) assert.NotNil(t, neighbor) - assert.Equal(t, "", resolver.lastContainerID, "empty containerID must be preserved so host cache is selected") + assert.Equal(t, "", resolver.lastContainerID, "empty containerID must be preserved without falling back to watchedContainerData") assert.Equal(t, "93.184.216.34", resolver.lastIPAddress) assert.Equal(t, "resolved.domain", neighbor.DNS) } diff --git a/pkg/dnsmanager/dns_manager.go b/pkg/dnsmanager/dns_manager.go index cf59427d74..d6801c1bd6 100644 --- a/pkg/dnsmanager/dns_manager.go +++ b/pkg/dnsmanager/dns_manager.go @@ -27,7 +27,7 @@ type DNSManager struct { removedContainers *lru.Cache[string, struct{}] lookupCache cache.ExpiringCache // Cache for DNS lookups failureCache cache.ExpiringCache // Cache for failed lookups - containerToCloudServices maps.SafeMap[string, *maps.SafeMap[uint32, mapset.Set[string]]] // key: containerId, value: map of pid to cloud services + containerToCloudServices maps.SafeMap[string, *maps.SafeMap[uint32, mapset.Set[string]]] // key: containerID, value: map of pid to cloud services } type cacheEntry struct { @@ -40,7 +40,7 @@ const ( defaultDNSCacheSize = 10000 // Default total host DNS cache size when non-positive size provided maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container defaultPerContainerCacheSize = 1000 // Default maximum number of DNS resolutions cached per container - minTrackedContainers = 100 // Minimum number of concurrent containers with DNS caches + minTrackedContainers = 10 // Minimum number of concurrent containers with DNS caches maxRemovedContainersEntries = 10000 // Maximum number of removed containers to track to prevent resurrection defaultRemovalGracePeriod = 10 * time.Second // Grace period before evicting resolution cache to allow terminal profile save ) @@ -57,12 +57,6 @@ func CreateDNSManager(size int) *DNSManager { size = defaultDNSCacheSize } - hostCache, err := lru.New[string, string](size) - if err != nil { - logger.L().Fatal("creating host lru cache", helpers.Error(err)) - return nil - } - perContainerSize := defaultPerContainerCacheSize if size < perContainerSize { perContainerSize = size @@ -71,6 +65,27 @@ func CreateDNSManager(size int) *DNSManager { maxContainers := size / perContainerSize if maxContainers < minTrackedContainers { maxContainers = minTrackedContainers + if maxContainers > size { + maxContainers = size + } + if maxContainers < 1 { + maxContainers = 1 + } + perContainerSize = size / maxContainers + if perContainerSize < 1 { + perContainerSize = 1 + } + } + + hostCacheSize := perContainerSize + if hostCacheSize < 1 { + hostCacheSize = 1 + } + + hostCache, err := lru.New[string, string](hostCacheSize) + if err != nil { + logger.L().Fatal("creating host lru cache", helpers.Error(err)) + return nil } containerCache, err := lru.New[string, *lru.Cache[string, string]](maxContainers) @@ -260,8 +275,8 @@ func (dm *DNSManager) ResolveIPAddress(containerID string, ipAddr string) (strin return "", false } -func (dm *DNSManager) ResolveContainerProcessToCloudServices(containerId string, pid uint32) mapset.Set[string] { - if pidToServices, found := dm.containerToCloudServices.Load(containerId); found { +func (dm *DNSManager) ResolveContainerProcessToCloudServices(containerID string, pid uint32) mapset.Set[string] { + if pidToServices, found := dm.containerToCloudServices.Load(containerID); found { if services, found := pidToServices.Load(pid); found { return services } diff --git a/pkg/dnsmanager/dns_manager_interface.go b/pkg/dnsmanager/dns_manager_interface.go index ca1351a2d8..9249189c36 100644 --- a/pkg/dnsmanager/dns_manager_interface.go +++ b/pkg/dnsmanager/dns_manager_interface.go @@ -12,7 +12,7 @@ type DNSManagerClient interface { } type DNSResolver interface { - ResolveIPAddress(containerId string, ipAddr string) (string, bool) - ResolveContainerProcessToCloudServices(containerId string, pid uint32) mapset.Set[string] + ResolveIPAddress(containerID string, ipAddr string) (string, bool) + ResolveContainerProcessToCloudServices(containerID string, pid uint32) mapset.Set[string] } diff --git a/pkg/dnsmanager/dns_manager_test.go b/pkg/dnsmanager/dns_manager_test.go index cc971d75ec..5ca8472e8d 100644 --- a/pkg/dnsmanager/dns_manager_test.go +++ b/pkg/dnsmanager/dns_manager_test.go @@ -496,7 +496,7 @@ func TestContainerCloudServices(t *testing.T) { t.Run("full container lifecycle with cloud services", func(t *testing.T) { // SETUP dm := CreateDNSManager(1000) - containerId := "test-container-123" + containerID := "test-container-123" testPid := uint32(1234) // Add container @@ -505,14 +505,14 @@ func TestContainerCloudServices(t *testing.T) { Container: &containercollection.Container{ Runtime: containercollection.RuntimeMetadata{ BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ - ContainerID: containerId, + ContainerID: containerID, }, }, }, }) // Verify container was added properly - pidToServices, found := dm.containerToCloudServices.Load(containerId) + pidToServices, found := dm.containerToCloudServices.Load(containerID) if !found { t.Fatal("Container was not added to containerToCloudServices map") } @@ -525,13 +525,13 @@ func TestContainerCloudServices(t *testing.T) { cloudEvents := []*utils.StructEvent{ { EventType: utils.DnsEventType, - ContainerID: containerId, + ContainerID: containerID, DNSName: "test.amazonaws.com.", Pid: testPid, }, { EventType: utils.DnsEventType, - ContainerID: containerId, + ContainerID: containerID, DNSName: "example.azure.com.", Pid: testPid, }, @@ -544,7 +544,7 @@ func TestContainerCloudServices(t *testing.T) { } // Verify services were added - resultServices := dm.ResolveContainerProcessToCloudServices(containerId, testPid) + resultServices := dm.ResolveContainerProcessToCloudServices(containerID, testPid) if resultServices == nil { t.Fatal("Expected non-nil service set") } @@ -560,14 +560,14 @@ func TestContainerCloudServices(t *testing.T) { Container: &containercollection.Container{ Runtime: containercollection.RuntimeMetadata{ BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ - ContainerID: containerId, + ContainerID: containerID, }, }, }, }) // Verify services are removed - resultServices = dm.ResolveContainerProcessToCloudServices(containerId, testPid) + resultServices = dm.ResolveContainerProcessToCloudServices(containerID, testPid) if resultServices != nil { t.Error("Expected nil services after container removal") } @@ -575,7 +575,7 @@ func TestContainerCloudServices(t *testing.T) { t.Run("max service cache size", func(t *testing.T) { dm := CreateDNSManager(1000) - containerId := "test-container-456" + containerID := "test-container-456" testPid := uint32(5678) // Add container @@ -584,14 +584,14 @@ func TestContainerCloudServices(t *testing.T) { Container: &containercollection.Container{ Runtime: containercollection.RuntimeMetadata{ BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ - ContainerID: containerId, + ContainerID: containerID, }, }, }, }) // Initialize the services set for the PID - if pidToServices, found := dm.containerToCloudServices.Load(containerId); found { + if pidToServices, found := dm.containerToCloudServices.Load(containerID); found { services := mapset.NewSet[string]() pidToServices.Set(testPid, services) } @@ -599,7 +599,7 @@ func TestContainerCloudServices(t *testing.T) { // Add more services than the cache size for i := 0; i <= maxServiceCacheSize+5; i++ { event := &utils.StructEvent{ - ContainerID: containerId, + ContainerID: containerID, DNSName: fmt.Sprintf("service%d.amazonaws.com.", i), Pid: testPid, } @@ -607,7 +607,7 @@ func TestContainerCloudServices(t *testing.T) { } // Verify cache size limit is enforced - services := dm.ResolveContainerProcessToCloudServices(containerId, testPid) + services := dm.ResolveContainerProcessToCloudServices(containerID, testPid) if services == nil { t.Fatal("Expected non-nil service set") } @@ -620,7 +620,7 @@ func TestContainerCloudServices(t *testing.T) { func TestCloudServiceCacheLimit(t *testing.T) { dm := CreateDNSManager(1000) - containerId := "test-container-456" + containerID := "test-container-456" testPid := uint32(5678) // Add container @@ -629,7 +629,7 @@ func TestCloudServiceCacheLimit(t *testing.T) { Container: &containercollection.Container{ Runtime: containercollection.RuntimeMetadata{ BasicRuntimeMetadata: eventtypes.BasicRuntimeMetadata{ - ContainerID: containerId, + ContainerID: containerID, }, }, }, @@ -639,7 +639,7 @@ func TestCloudServiceCacheLimit(t *testing.T) { for i := 0; i < maxServiceCacheSize+10; i++ { dm.ReportEvent(&utils.StructEvent{ EventType: utils.DnsEventType, - ContainerID: containerId, + ContainerID: containerID, DNSName: fmt.Sprintf("service%d.amazonaws.com.", i), Pid: testPid, }) @@ -648,11 +648,11 @@ func TestCloudServiceCacheLimit(t *testing.T) { // Give some time for events to be processed time.Sleep(100 * time.Millisecond) - services := dm.ResolveContainerProcessToCloudServices(containerId, testPid) + services := dm.ResolveContainerProcessToCloudServices(containerID, testPid) if services == nil { // Debug information t.Log("Debug: Checking container existence") - if pidToServices, found := dm.containerToCloudServices.Load(containerId); found { + if pidToServices, found := dm.containerToCloudServices.Load(containerID); found { t.Log("Container found in map") if services, found := pidToServices.Load(testPid); found { t.Log("PID found in map") @@ -681,3 +681,48 @@ func TestCreateDNSManager_NonPositiveSize(t *testing.T) { assert.NotNil(t, dm.hostAddressToDomain) } } + +func TestCreateDNSManager_BoundedSizing(t *testing.T) { + testCases := []struct { + name string + configuredSize int + expectedCacheSize int + expectedPerContainer int + }{ + { + name: "small size scales perContainerSize to bound memory", + configuredSize: 1000, + expectedCacheSize: 1000, + expectedPerContainer: 100, + }, + { + name: "very small size", + configuredSize: 50, + expectedCacheSize: 50, + expectedPerContainer: 5, + }, + { + name: "default size", + configuredSize: 10000, + expectedCacheSize: 10000, + expectedPerContainer: defaultPerContainerCacheSize, + }, + { + name: "large size", + configuredSize: 50000, + expectedCacheSize: 50000, + expectedPerContainer: defaultPerContainerCacheSize, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dm := CreateDNSManager(tc.configuredSize) + assert.NotNil(t, dm) + assert.Equal(t, tc.expectedCacheSize, dm.cacheSize) + assert.Equal(t, tc.expectedPerContainer, dm.perContainerCacheSize) + assert.NotNil(t, dm.hostAddressToDomain) + assert.NotNil(t, dm.containerToAddressToDomain) + }) + } +} From cf11181781cf189e8b2f99e52411b7bff5c49de2 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Tue, 1 Sep 2026 11:17:01 +0200 Subject: [PATCH 13/13] test(component): stabilize Test_30 and DaemonSet restart wait Signed-off-by: Matthias Bertschy --- tests/component_test.go | 8 +++++--- tests/testutils/k8s.go | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/component_test.go b/tests/component_test.go index dce0e20cb5..d4d9e736f1 100644 --- a/tests/component_test.go +++ b/tests/component_test.go @@ -3781,6 +3781,8 @@ func Test_30_IgnoreExcludeAndLearningDuration(t *testing.T) { restore := withNodeAgentConfig(t, func(cfg map[string]any) { cfg["excludeNamespaces"] = []string{excluded.Name} cfg["excludeLabels"] = map[string][]string{"skip-me": {"true"}} + cfg["maxSniffingTimePerContainer"] = "45s" + cfg["initialDelay"] = "10s" }) defer restore() @@ -3791,7 +3793,7 @@ func Test_30_IgnoreExcludeAndLearningDuration(t *testing.T) { require.NoError(t, err, "control workload") require.NoError(t, ctl.WaitForReady(80)) - time.Sleep(90 * time.Second) + require.NoError(t, ctl.WaitForContainerProfileCompletion(30), "control workload profile completion") exCPs, _ := exNS.GetContainerProfiles() require.Empty(t, exCPs, "an excluded-namespace workload must produce NO ContainerProfile") @@ -3819,8 +3821,8 @@ func Test_30_IgnoreExcludeAndLearningDuration(t *testing.T) { require.NoError(t, err, "workload") require.NoError(t, wl.WaitForReady(80)) - deadline := time.Now().Add(90 * time.Second) - require.NoError(t, wl.WaitForContainerProfileCompletion(90), "profile must complete within the shortened window") + deadline := time.Now().Add(180 * time.Second) + require.NoError(t, wl.WaitForContainerProfileCompletion(30), "profile must complete within the shortened window") require.True(t, time.Now().Before(deadline), "completion must track the configured maxSniffingTimePerContainer, not a longer default") }) diff --git a/tests/testutils/k8s.go b/tests/testutils/k8s.go index b2792d04b9..d98566fa7c 100644 --- a/tests/testutils/k8s.go +++ b/tests/testutils/k8s.go @@ -690,11 +690,13 @@ func RestartDaemonSet(namespace, name string) error { daemonset.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) // Update the daemonset - _, err = k8sClient.KubernetesClient.AppsV1().DaemonSets(namespace).Update(ctx, daemonset, metav1.UpdateOptions{}) + updated, err := k8sClient.KubernetesClient.AppsV1().DaemonSets(namespace).Update(ctx, daemonset, metav1.UpdateOptions{}) if err != nil { return fmt.Errorf("failed to update daemonset %s/%s: %w", namespace, name, err) } + targetGen := updated.Generation + // Wait for the daemonset to be ready err = backoff.RetryNotify(func() error { updatedDS, err := k8sClient.KubernetesClient.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) @@ -702,6 +704,11 @@ func RestartDaemonSet(namespace, name string) error { return err } + if updatedDS.Status.ObservedGeneration < targetGen { + return fmt.Errorf("daemonset %s/%s rollout not observed yet (observed gen %d < target %d)", + namespace, name, updatedDS.Status.ObservedGeneration, targetGen) + } + if updatedDS.Status.NumberReady != updatedDS.Status.DesiredNumberScheduled { return fmt.Errorf("daemonset %s/%s not ready: %d/%d pods ready", namespace, name, updatedDS.Status.NumberReady, updatedDS.Status.DesiredNumberScheduled)