From 183f910a05d7e649f63f1207ae90604cf9c6dea3 Mon Sep 17 00:00:00 2001 From: entlein Date: Thu, 23 Jul 2026 19:54:17 +0200 Subject: [PATCH 1/3] first implementation, not optimized Signed-off-by: entlein --- .../file/networkneighborhood_ipcollapse.go | 62 ++++++++++++++++++- .../networkneighborhood_ipcollapse_test.go | 49 +++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/pkg/registry/file/networkneighborhood_ipcollapse.go b/pkg/registry/file/networkneighborhood_ipcollapse.go index 347822d4f..8b67ad770 100644 --- a/pkg/registry/file/networkneighborhood_ipcollapse.go +++ b/pkg/registry/file/networkneighborhood_ipcollapse.go @@ -79,8 +79,12 @@ func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dy } cidrs := aggregateHosts(hosts, floorBits) - values := append(cidrs, passthrough...) - sort.Strings(values) + // Merge the freshly aggregated CIDR(s) with the group's already-collapsed + // pass-through CIDRs into a minimal set. Without this, incremental + // learning re-collapses newly observed hosts to a CIDR that duplicates or + // nests inside a block already held from an earlier save, producing garbage + // like [52.216.0.0/26, 52.216.0.0/26, 52.216.0.0/27]. + values := minimizeCIDRs(append(cidrs, passthrough...)) var dnsNames []string var ports []softwarecomposition.NetworkPort @@ -172,6 +176,60 @@ func aggregateHosts(hosts []netip.Addr, floorBits int) []string { return out } +// minimizeCIDRs reduces a set of address values to the smallest equivalent set: +// CIDR values are deduplicated and any prefix wholly contained in a broader +// prefix of the set is dropped. This keeps incremental re-collapsing a fixpoint +// on the CIDR set — freshly aggregated blocks that duplicate or nest inside a +// group's already-collapsed pass-through CIDRs are absorbed rather than +// accumulated. Non-CIDR values (the "*" sentinel, IPv6, unparseable) are held +// verbatim and deduplicated. The result is sorted. +func minimizeCIDRs(values []string) []string { + seenPfx := map[string]struct{}{} + seenOther := map[string]struct{}{} + var prefixes []netip.Prefix + var others []string + for _, v := range values { + if p, err := netip.ParsePrefix(v); err == nil { + m := p.Masked() + key := m.String() + if _, ok := seenPfx[key]; ok { + continue + } + seenPfx[key] = struct{}{} + prefixes = append(prefixes, m) + continue + } + if _, ok := seenOther[v]; ok { + continue + } + seenOther[v] = struct{}{} + others = append(others, v) + } + + out := make([]string, 0, len(prefixes)+len(others)) + for i, a := range prefixes { + contained := false + for j, b := range prefixes { + if i == j { + continue + } + // A broader prefix (fewer bits) that covers a's network address + // subsumes a; drop a. Equal-width prefixes never subsume each other + // and identical prefixes are already deduplicated above. + if b.Bits() < a.Bits() && b.Contains(a.Addr()) { + contained = true + break + } + } + if !contained { + out = append(out, a.String()) + } + } + out = append(out, others...) + sort.Strings(out) + return out +} + // commonPrefixLen returns the number of leading bits shared by every address in // the set. All addresses are assumed to be IPv4. func commonPrefixLen(addrs []netip.Addr) int { diff --git a/pkg/registry/file/networkneighborhood_ipcollapse_test.go b/pkg/registry/file/networkneighborhood_ipcollapse_test.go index beefdcda5..9be155558 100644 --- a/pkg/registry/file/networkneighborhood_ipcollapse_test.go +++ b/pkg/registry/file/networkneighborhood_ipcollapse_test.go @@ -256,3 +256,52 @@ func TestCollapseIPGroups_IPv6PassThrough(t *testing.T) { func TestCollapseIPGroups_NilInput(t *testing.T) { assert.Nil(t, collapseIPGroups(nil, testSettings())) } + +func TestCollapseIPGroups_IncrementalReCollapseDeduplicatesAndAbsorbs(t *testing.T) { + // Regression for the incremental-learning garbage [/26, /26, /27]: a group + // that already holds collapsed CIDRs from earlier saves (a /27 and a /26) + // plus freshly observed hosts that re-aggregate to 52.216.0.0/26 must + // converge to exactly one 52.216.0.0/26 — the duplicate /26 deduplicated and + // the nested /27 absorbed — instead of accumulating all three entries. + settings := dynamicpathdetector.CollapseSettings{ + NetworkIPGroupThreshold: 5, + NetworkCIDRFloorBits: 16, + } + cidr := func(c string) softwarecomposition.NetworkNeighbor { + return softwarecomposition.NetworkNeighbor{ + Type: softwarecomposition.CommunicationTypeEgress, + DNS: "example.com", + IPAddresses: []string{c}, + } + } + in := []softwarecomposition.NetworkNeighbor{ + cidr("52.216.0.0/27"), + cidr("52.216.0.0/26"), + } + for _, h := range []string{"52.216.0.1", "52.216.0.10", "52.216.0.20", "52.216.0.40", "52.216.0.55", "52.216.0.60"} { + in = append(in, hostNeighbor(h)) + } + + out := collapseIPGroups(in, settings) + + var cidrs []string + for _, e := range out { + cidrs = append(cidrs, e.IPAddresses...) + assert.Empty(t, e.IPAddress) + } + assert.Equal(t, []string{"52.216.0.0/26"}, cidrs, "must converge to a single covering /26, not [/26 /26 /27]") +} + +func TestMinimizeCIDRs_DropsDuplicatesAndSubsumed(t *testing.T) { + // exact duplicate + nested prefix + an unrelated block + non-CIDR sentinel + got := minimizeCIDRs([]string{ + "52.216.0.0/26", "52.216.0.0/27", "52.216.0.0/26", + "10.0.0.0/24", "*", + }) + assert.Equal(t, []string{"*", "10.0.0.0/24", "52.216.0.0/26"}, got) +} + +func TestMinimizeCIDRs_KeepsDisjointEqualWidth(t *testing.T) { + got := minimizeCIDRs([]string{"10.1.0.0/24", "10.2.0.0/24"}) + assert.Equal(t, []string{"10.1.0.0/24", "10.2.0.0/24"}, got) +} From c20f4371ba5bbca9b83207b0166cc6dc019b0eac Mon Sep 17 00:00:00 2001 From: entlein Date: Thu, 23 Jul 2026 20:44:54 +0200 Subject: [PATCH 2/3] grouping CIDRs to avoid dedup Signed-off-by: entlein --- go.mod | 2 +- .../file/containerprofile_processor_test.go | 4 +- .../file/dynamicpathdetector/types.go | 10 + .../file/networkneighborhood_ipcollapse.go | 197 ++++++++---------- ...tworkneighborhood_ipcollapse_bench_test.go | 63 ++++++ .../networkneighborhood_ipcollapse_test.go | 126 +++++++---- .../networkneighborhood_processor_test.go | 4 +- 7 files changed, 255 insertions(+), 151 deletions(-) create mode 100644 pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go diff --git a/go.mod b/go.mod index 46719c0ba..af8e3606a 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.1 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/sync v0.20.0 golang.org/x/text v0.37.0 k8s.io/api v0.35.0 @@ -191,7 +192,6 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.35.0 // indirect diff --git a/pkg/registry/file/containerprofile_processor_test.go b/pkg/registry/file/containerprofile_processor_test.go index bacfb1629..af9fe4199 100644 --- a/pkg/registry/file/containerprofile_processor_test.go +++ b/pkg/registry/file/containerprofile_processor_test.go @@ -26,10 +26,10 @@ import ( ) func TestDeflateContainerProfileSpec_NetworkNeighborsCollapse(t *testing.T) { - const hostCount = 60 + const hostCount = 64 // a fully-observed /26 (10.0.0.0..10.0.0.63) newIngress := func() []softwarecomposition.NetworkNeighbor { ingress := make([]softwarecomposition.NetworkNeighbor, 0, hostCount) - for i := 1; i <= hostCount; i++ { + for i := 0; i < hostCount; i++ { ingress = append(ingress, softwarecomposition.NetworkNeighbor{ Identifier: fmt.Sprintf("external-%d", i), Type: "external", diff --git a/pkg/registry/file/dynamicpathdetector/types.go b/pkg/registry/file/dynamicpathdetector/types.go index 40e728777..7489b943c 100644 --- a/pkg/registry/file/dynamicpathdetector/types.go +++ b/pkg/registry/file/dynamicpathdetector/types.go @@ -25,11 +25,21 @@ const ( // NetworkNeighbor entries differing only by IP gets CIDR-collapsed. // NetworkCIDRFloorBits is the minimum CIDR prefix length (maximum breadth) // a single aggregated block may have. +// NetworkMaxCIDRSplitBits caps, PER prefix, how far a single cover block broader +// than the floor is split into floor-width children: up to 2^NetworkMaxCIDRSplitBits +// blocks (4096 here). A prefix whose split would exceed that is kept as-is rather +// than exploding the entry list. This bounds ONE block's fan-out (not the sum +// across a group — the whole neighborhood is separately capped by +// MaxNetworkNeighborhoodSize); it only bites when a held pass-through block is +// much broader than a tightened floor (e.g. a /16 under a /28 floor -> 4096 +// children; a /16 under a /24 floor is only 256). Not currently exposed as a +// CollapseConfiguration field. const ( OpenDynamicThreshold = 50 EndpointDynamicThreshold = 100 NetworkIPGroupThreshold = 50 NetworkCIDRFloorBits = 24 + NetworkMaxCIDRSplitBits = 12 ) // --- Collapse configuration --- diff --git a/pkg/registry/file/networkneighborhood_ipcollapse.go b/pkg/registry/file/networkneighborhood_ipcollapse.go index 8b67ad770..2dd5a02f3 100644 --- a/pkg/registry/file/networkneighborhood_ipcollapse.go +++ b/pkg/registry/file/networkneighborhood_ipcollapse.go @@ -9,6 +9,7 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition" "github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector" + "go4.org/netipx" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -18,14 +19,15 @@ const ipCollapseFieldSep = "\x00" // into a small number of CIDR-bearing entries. Entries are grouped by // (Type, DNS, NamespaceSelector, PodSelector); within a group whose count of // aggregatable IPv4 host addresses exceeds settings.NetworkIPGroupThreshold, -// those hosts are replaced by covering CIDR block(s) no broader than -// settings.NetworkCIDRFloorBits. +// those hosts plus any already-collapsed pass-through CIDRs are replaced by the +// minimal EXACT CIDR cover of exactly those addresses (see coverPrefixes), with +// no block broader than settings.NetworkCIDRFloorBits. The cover never +// over-approximates to a block the workload did not actually reach. // -// The pass is a fixpoint (AC10): already-collapsed CIDR values and the "*" -// sentinel / IPv6 values are treated as pass-through and are never re-parsed as -// host IPs or re-tightened, and collapsed output carries a deterministic -// Identifier, so a second run — whose groups now hold only CIDRs and thus have -// zero aggregatable hosts — leaves everything untouched. +// The pass is a fixpoint: an exact cover re-covered is itself, and the "*" +// sentinel / bare IPv6 values are pass-through held verbatim, so a second run — +// whose groups now hold only CIDRs and thus have zero aggregatable hosts — +// leaves everything untouched. func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dynamicpathdetector.CollapseSettings) []softwarecomposition.NetworkNeighbor { if entries == nil { return nil @@ -78,13 +80,25 @@ func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dy continue } - cidrs := aggregateHosts(hosts, floorBits) - // Merge the freshly aggregated CIDR(s) with the group's already-collapsed - // pass-through CIDRs into a minimal set. Without this, incremental - // learning re-collapses newly observed hosts to a CIDR that duplicates or - // nests inside a block already held from an earlier save, producing garbage - // like [52.216.0.0/26, 52.216.0.0/26, 52.216.0.0/27]. - values := minimizeCIDRs(append(cidrs, passthrough...)) + // Split pass-through into already-collapsed CIDRs (folded into the cover) + // and non-CIDR sentinels ("*", bare IPv6, unparseable) held verbatim. + var cidrPass []netip.Prefix + var sentinels []string + for _, v := range passthrough { + if p, err := netip.ParsePrefix(v); err == nil { + cidrPass = append(cidrPass, p.Masked()) + } else { + sentinels = append(sentinels, v) + } + } + + // Exact minimal CIDR cover of the hosts plus already-held CIDRs, capped at + // the floor. Because it is an exact cover, incremental re-collapsing is a + // fixpoint and never accumulates duplicate or nested blocks — the bug that + // produced [52.216.0.0/26, 52.216.0.0/26, 52.216.0.0/27]. + values := coverPrefixes(hosts, cidrPass, floorBits) + values = append(values, sentinels...) + sort.Strings(values) var dnsNames []string var ports []softwarecomposition.NetworkPort @@ -112,12 +126,13 @@ func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dy return out } -// classifyGroupAddresses splits a group's address values into aggregatable IPv4 -// host addresses (deduped) and pass-through values held verbatim. An entry's -// value comes from the singular IPAddress when set, otherwise from each element -// of IPAddresses. CIDRs, the "*" sentinel, IPv6 and unparseable values are -// pass-through and are never fed to aggregation, which is what makes the pass a -// fixpoint on already-collapsed input. +// classifyGroupAddresses splits a group's address values into aggregatable host +// addresses (bare IPv4 or IPv6, deduped) and pass-through values held verbatim. +// An entry's value comes from the singular IPAddress when set, otherwise from +// each element of IPAddresses. The "*" sentinel and unparseable values are +// pass-through; already-collapsed CIDRs are pass-through here but the caller +// folds them back into the exact cover. Both address families are aggregated — +// netipx covers IPv4 and IPv6 alike. func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]netip.Addr, []string) { seenHost := map[netip.Addr]struct{}{} seenPass := map[string]struct{}{} @@ -128,7 +143,7 @@ func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]ne if v == "" { return } - if addr, err := netip.ParseAddr(v); err == nil && addr.Is4() { + if addr, err := netip.ParseAddr(v); err == nil { if _, ok := seenHost[addr]; !ok { seenHost[addr] = struct{}{} hosts = append(hosts, addr) @@ -153,110 +168,72 @@ func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]ne return hosts, passthrough } -// aggregateHosts returns the CIDR block(s) covering the given IPv4 hosts. If the -// hosts share a common prefix at least as long as floorBits it is emitted as a -// single block; otherwise each host is bucketed into a floorBits-length prefix -// so no emitted block is ever broader than the floor. -func aggregateHosts(hosts []netip.Addr, floorBits int) []string { - if len(hosts) == 0 { +// coverPrefixes returns the minimal set of CIDR strings that covers EXACTLY the +// given IPv4 host addresses together with the group's already-collapsed +// pass-through CIDRs, capped so no prefix is broader than floorBits. netipx does +// the aggregation — deduplicating, dropping subsumed prefixes and merging +// adjacent siblings into the minimal exact cover in near-linear time — and any +// resulting prefix broader than the floor is then split into floorBits-wide +// children (all fully covered, since the parent lies wholly within the set). +// +// Because the cover is exact, it never over-approximates to a block the workload +// did not actually reach, and re-running on already-collapsed input is a +// fixpoint: no duplicate or nested blocks can accumulate across incremental +// saves. The result is sorted. +func coverPrefixes(hosts []netip.Addr, cidrPass []netip.Prefix, floorBits int) []string { + if len(hosts) == 0 && len(cidrPass) == 0 { return nil } - if commonLen := commonPrefixLen(hosts); commonLen >= floorBits { - return []string{netip.PrefixFrom(hosts[0], commonLen).Masked().String()} + var b netipx.IPSetBuilder + for _, h := range hosts { + b.Add(h) } - seen := map[string]struct{}{} - var out []string - for _, addr := range hosts { - cidr := netip.PrefixFrom(addr, floorBits).Masked().String() - if _, ok := seen[cidr]; !ok { - seen[cidr] = struct{}{} - out = append(out, cidr) - } + for _, p := range cidrPass { + b.AddPrefix(p) } - return out -} - -// minimizeCIDRs reduces a set of address values to the smallest equivalent set: -// CIDR values are deduplicated and any prefix wholly contained in a broader -// prefix of the set is dropped. This keeps incremental re-collapsing a fixpoint -// on the CIDR set — freshly aggregated blocks that duplicate or nest inside a -// group's already-collapsed pass-through CIDRs are absorbed rather than -// accumulated. Non-CIDR values (the "*" sentinel, IPv6, unparseable) are held -// verbatim and deduplicated. The result is sorted. -func minimizeCIDRs(values []string) []string { - seenPfx := map[string]struct{}{} - seenOther := map[string]struct{}{} - var prefixes []netip.Prefix - var others []string - for _, v := range values { - if p, err := netip.ParsePrefix(v); err == nil { - m := p.Masked() - key := m.String() - if _, ok := seenPfx[key]; ok { - continue - } - seenPfx[key] = struct{}{} - prefixes = append(prefixes, m) - continue - } - if _, ok := seenOther[v]; ok { - continue - } - seenOther[v] = struct{}{} - others = append(others, v) + set, err := b.IPSet() + if err != nil || set == nil { + return nil } - out := make([]string, 0, len(prefixes)+len(others)) - for i, a := range prefixes { - contained := false - for j, b := range prefixes { - if i == j { - continue - } - // A broader prefix (fewer bits) that covers a's network address - // subsumes a; drop a. Equal-width prefixes never subsume each other - // and identical prefixes are already deduplicated above. - if b.Bits() < a.Bits() && b.Contains(a.Addr()) { - contained = true - break - } - } - if !contained { - out = append(out, a.String()) + var out []string + for _, p := range set.Prefixes() { + // The floor is an IPv4 breadth cap; IPv6 covers are emitted as-is (a /24 + // floor is meaningless for v6, whose covers are already narrow). + if p.Addr().Is4() && p.Bits() < floorBits { + out = append(out, splitToFloor(p, floorBits)...) + continue } + out = append(out, p.String()) } - out = append(out, others...) sort.Strings(out) return out } -// commonPrefixLen returns the number of leading bits shared by every address in -// the set. All addresses are assumed to be IPv4. -func commonPrefixLen(addrs []netip.Addr) int { - base := addrs[0].As4() - common := 32 - for _, addr := range addrs[1:] { - b := addr.As4() - n := 0 - for i := 0; i < 4 && n < common; i++ { - x := base[i] ^ b[i] - if x == 0 { - n += 8 - continue - } - for bit := 7; bit >= 0; bit-- { - if x&(1< dynamicpathdetector.NetworkMaxCIDRSplitBits { + return []string{p.String()} + } + count := 1 << shift + out := make([]string, 0, count) + child := netip.PrefixFrom(p.Addr(), floorBits).Masked() + for i := 0; i < count; i++ { + out = append(out, child.String()) + next := netipx.RangeOfPrefix(child).To().Next() + if !next.IsValid() { break } - if n < common { - common = n - } + child = netip.PrefixFrom(next, floorBits).Masked() } - return common + return out } func neighborGroupKey(n softwarecomposition.NetworkNeighbor) string { diff --git a/pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go b/pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go new file mode 100644 index 000000000..f6dfdeaf2 --- /dev/null +++ b/pkg/registry/file/networkneighborhood_ipcollapse_bench_test.go @@ -0,0 +1,63 @@ +package file + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition" + "github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector" +) + +func cidrNeighbor(c string) softwarecomposition.NetworkNeighbor { + return softwarecomposition.NetworkNeighbor{ + Type: softwarecomposition.CommunicationTypeEgress, + DNS: "example.com", + IPAddresses: []string{c}, + } +} + +// BenchmarkCollapseIPGroups measures CPU/allocations of the full deflate path on +// a realistic incremental-learning snapshot: 200 contiguous hosts in a /24 plus +// two already-collapsed pass-through CIDRs from earlier saves, at a /16 and a +// /24 floor (the latter exercising the floor-cap split). +func BenchmarkCollapseIPGroups(b *testing.B) { + var in []softwarecomposition.NetworkNeighbor + for i := 0; i < 200; i++ { + in = append(in, hostNeighbor(fmt.Sprintf("52.216.%d.%d", i/256, i%256))) + } + in = append(in, cidrNeighbor("52.216.4.0/24"), cidrNeighbor("52.216.0.0/16")) + + for _, floor := range []int{16, 24} { + settings := dynamicpathdetector.CollapseSettings{NetworkIPGroupThreshold: 5, NetworkCIDRFloorBits: floor} + b.Run(fmt.Sprintf("floor%d", floor), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = collapseIPGroups(in, settings) + } + }) + } +} + +// BenchmarkCoverPrefixes isolates the netipx exact-cover step: 256 scattered +// hosts across a /16 plus two pass-through CIDRs, at a /16 and a /24 floor. +func BenchmarkCoverPrefixes(b *testing.B) { + hosts := make([]netip.Addr, 0, 256) + for i := 0; i < 256; i++ { + hosts = append(hosts, netip.AddrFrom4([4]byte{52, 216, byte(i), byte((i * 7) % 256)})) + } + cidrPass := []netip.Prefix{ + netip.MustParsePrefix("52.216.4.0/24"), + netip.MustParsePrefix("52.216.128.0/17"), + } + for _, floor := range []int{16, 24} { + b.Run(fmt.Sprintf("floor%d", floor), func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = coverPrefixes(hosts, cidrPass, floor) + } + }) + } +} diff --git a/pkg/registry/file/networkneighborhood_ipcollapse_test.go b/pkg/registry/file/networkneighborhood_ipcollapse_test.go index 9be155558..90cc1ae74 100644 --- a/pkg/registry/file/networkneighborhood_ipcollapse_test.go +++ b/pkg/registry/file/networkneighborhood_ipcollapse_test.go @@ -44,23 +44,23 @@ func TestCollapseIPGroups_BelowThresholdUntouched(t *testing.T) { } func TestCollapseIPGroups_AboveThresholdSingleCoveringCIDR(t *testing.T) { - // 60 hosts spread across the full third octet of 10.1.0.0/16 (0..236, - // spanning the top bit) -> common prefix exactly the floor (16) -> one block. + // A fully-observed /24 (all 256 hosts) exact-covers to exactly one /24 block. var in []softwarecomposition.NetworkNeighbor - for i := 0; i < 60; i++ { - in = append(in, hostNeighbor(fmt.Sprintf("10.1.%d.0", i*4))) + for i := 0; i < 256; i++ { + in = append(in, hostNeighbor(fmt.Sprintf("10.1.5.%d", i))) } out := collapseIPGroups(in, testSettings()) require.Len(t, out, 1) - assert.Equal(t, []string{"10.1.0.0/16"}, out[0].IPAddresses) + assert.Equal(t, []string{"10.1.5.0/24"}, out[0].IPAddresses) assert.Empty(t, out[0].IPAddress) } -func TestCollapseIPGroups_AboveThresholdBroaderThanFloorBuckets(t *testing.T) { - // 60 hosts spread across many /16s -> common prefix broader than floor -> - // floor-bucket into distinct /16 blocks, none broader than the floor. +func TestCollapseIPGroups_ScatteredHostsStayGranularAndCappedAtFloor(t *testing.T) { + // 60 lone hosts, each in its own /16 -> exact cover keeps them granular (one + // /32 apiece, since none are adjacent); no emitted block is broader than the + // floor, and the cover never over-approximates to a covering block. var in []softwarecomposition.NetworkNeighbor for i := 0; i < 60; i++ { in = append(in, hostNeighbor(fmt.Sprintf("%d.%d.0.1", 10+i, i))) @@ -110,12 +110,12 @@ func TestCollapseIPGroups_DifferentSelectorsNotMerged(t *testing.T) { return &metav1.LabelSelector{MatchLabels: map[string]string{"app": v}} } var in []softwarecomposition.NetworkNeighbor - for i := 0; i < 60; i++ { + for i := 0; i < 64; i++ { // a full /26 per selector -> one exact block each e := hostNeighbor(fmt.Sprintf("10.3.0.%d", i)) e.PodSelector = sel("a") in = append(in, e) } - for i := 0; i < 60; i++ { + for i := 0; i < 64; i++ { e := hostNeighbor(fmt.Sprintf("10.3.0.%d", i)) e.PodSelector = sel("b") in = append(in, e) @@ -135,17 +135,18 @@ func TestCollapseIPGroups_DifferentSelectorsNotMerged(t *testing.T) { func TestCollapseIPGroups_RealWorldShapeOrdersOfMagnitude(t *testing.T) { var in []softwarecomposition.NetworkNeighbor - // ~500 IPs clustered in 100.68.x.x - for i := 0; i < 250; i++ { - in = append(in, hostNeighbor(fmt.Sprintf("100.68.%d.%d", i/256, i%256))) + // 256 IPs fully covering 100.68.0.0/24 + for i := 0; i < 256; i++ { + in = append(in, hostNeighbor(fmt.Sprintf("100.68.0.%d", i))) } - // ~250 IPs clustered in 16.15.183.x plus neighboring /24s - for i := 0; i < 250; i++ { - in = append(in, hostNeighbor(fmt.Sprintf("16.15.%d.%d", 180+i/256, i%256))) + // 256 IPs fully covering 16.15.180.0/24 + for i := 0; i < 256; i++ { + in = append(in, hostNeighbor(fmt.Sprintf("16.15.180.%d", i))) } out := collapseIPGroups(in, testSettings()) + // 512 contiguous hosts exact-cover to a handful of blocks (two /24s here). assert.Less(t, len(out), 10) assert.Less(t, len(out), len(in)/50) for _, e := range out { @@ -175,7 +176,7 @@ func TestCollapseIPGroups_Idempotent(t *testing.T) { DNS: "example.com", IPAddresses: []string{"*"}, }) - // IPv6 entry + // IPv6 entry — a lone v6 host exact-covers to its /128 in = append(in, softwarecomposition.NetworkNeighbor{ Type: softwarecomposition.CommunicationTypeEgress, DNS: "example.com", @@ -187,14 +188,14 @@ func TestCollapseIPGroups_Idempotent(t *testing.T) { assert.Equal(t, once, twice, "collapseIPGroups must be a fixpoint") - // pass-through values survived + // pass-through + covered values survived var values []string for _, e := range once { values = append(values, e.IPAddresses...) } assert.Contains(t, values, "*") assert.Contains(t, values, "200.0.0.0/16") - assert.Contains(t, values, "2001:db8::1") + assert.Contains(t, values, "2001:db8::1/128") } func TestCollapseIPGroups_FieldContract(t *testing.T) { @@ -233,16 +234,17 @@ func TestCollapseIPGroups_MultiBucketReplicatesDNSNamesAndPorts(t *testing.T) { } } -func TestCollapseIPGroups_IPv6PassThrough(t *testing.T) { +func TestCollapseIPGroups_IPv6Aggregated(t *testing.T) { + // A full IPv6 /120 (256 contiguous v6 hosts) exact-covers to that /120, and a + // lone v6 host to its /128 — alongside a v4 group, in one mixed-family pass. var in []softwarecomposition.NetworkNeighbor + for i := 0; i < 256; i++ { + in = append(in, hostNeighbor(fmt.Sprintf("2606:4700:0:1::%x", i))) + } + in = append(in, hostNeighbor("2001:db8::42")) for i := 0; i < 60; i++ { in = append(in, hostNeighbor(fmt.Sprintf("10.5.0.%d", i))) } - in = append(in, softwarecomposition.NetworkNeighbor{ - Type: softwarecomposition.CommunicationTypeEgress, - DNS: "example.com", - IPAddress: "2001:db8::42", - }) out := collapseIPGroups(in, testSettings()) @@ -250,7 +252,45 @@ func TestCollapseIPGroups_IPv6PassThrough(t *testing.T) { for _, e := range out { values = append(values, e.IPAddresses...) } - assert.Contains(t, values, "2001:db8::42") + assert.Contains(t, values, "2606:4700:0:1::/120", "contiguous v6 hosts aggregate") + assert.Contains(t, values, "2001:db8::42/128", "lone v6 host covers to /128") +} + +func TestCoverPrefixes_IPv6ExactAndMerge(t *testing.T) { + // Two adjacent v6 /33 halves merge into the parent /32 (Cloudflare 2606:4700::/32), + // independent of any IPv4 floor. + got := coverPrefixes(nil, []netip.Prefix{ + netip.MustParsePrefix("2606:4700::/33"), + netip.MustParsePrefix("2606:4700:8000::/33"), + }, 24) + assert.Equal(t, []string{"2606:4700::/32"}, got) +} + +// TestCoverPrefixes_RealCloudRangesDedupAndMerge feeds netipx the kind of messy, +// overlapping, non-aggregated CIDR lists cloud providers publish (a subsumed +// range, two adjacent siblings that merge, and disjoint blocks across families) +// and asserts the minimal exact cover. +func TestCoverPrefixes_RealCloudRangesDedupAndMerge(t *testing.T) { + pass := []netip.Prefix{ + // AWS S3 us-east-1: 52.216.0.0/15 subsumes the more specific 52.216.4.0/24 + netip.MustParsePrefix("52.216.0.0/15"), + netip.MustParsePrefix("52.216.4.0/24"), + // Cloudflare: 104.16.0.0/13 subsumes 104.16.0.0/14 + netip.MustParsePrefix("104.16.0.0/13"), + netip.MustParsePrefix("104.16.0.0/14"), + // Cloudflare v6 siblings that merge to a /31 + netip.MustParsePrefix("2606:4700::/32"), + netip.MustParsePrefix("2606:4701::/32"), + } + // Permissive floor (/8) so the cap does not split these broad blocks — this + // isolates the dedup/merge behavior (the floor cap has its own test). + got := coverPrefixes(nil, pass, 8) + // sorted lexicographically (the collapse output order) + assert.Equal(t, []string{ + "104.16.0.0/13", + "2606:4700::/31", + "52.216.0.0/15", + }, got) } func TestCollapseIPGroups_NilInput(t *testing.T) { @@ -292,16 +332,30 @@ func TestCollapseIPGroups_IncrementalReCollapseDeduplicatesAndAbsorbs(t *testing assert.Equal(t, []string{"52.216.0.0/26"}, cidrs, "must converge to a single covering /26, not [/26 /26 /27]") } -func TestMinimizeCIDRs_DropsDuplicatesAndSubsumed(t *testing.T) { - // exact duplicate + nested prefix + an unrelated block + non-CIDR sentinel - got := minimizeCIDRs([]string{ - "52.216.0.0/26", "52.216.0.0/27", "52.216.0.0/26", - "10.0.0.0/24", "*", - }) - assert.Equal(t, []string{"*", "10.0.0.0/24", "52.216.0.0/26"}, got) +func TestCoverPrefixes_ExactNoOverApproximation(t *testing.T) { + // Three non-adjacent hosts cover exactly {.1,.2,.3} — never a single /30 + // (which would admit the unobserved .0). + hosts := []netip.Addr{ + netip.MustParseAddr("52.216.0.1"), + netip.MustParseAddr("52.216.0.2"), + netip.MustParseAddr("52.216.0.3"), + } + got := coverPrefixes(hosts, nil, 16) + assert.Equal(t, []string{"52.216.0.1/32", "52.216.0.2/31"}, got) +} + +func TestCoverPrefixes_MergesAdjacentSiblings(t *testing.T) { + // The two /25 halves of a /24 merge into the single parent /24. + got := coverPrefixes(nil, []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/25"), + netip.MustParsePrefix("10.0.0.128/25"), + }, 16) + assert.Equal(t, []string{"10.0.0.0/24"}, got) } -func TestMinimizeCIDRs_KeepsDisjointEqualWidth(t *testing.T) { - got := minimizeCIDRs([]string{"10.1.0.0/24", "10.2.0.0/24"}) - assert.Equal(t, []string{"10.1.0.0/24", "10.2.0.0/24"}, got) +func TestCoverPrefixes_FloorCapSplitsBroadBlock(t *testing.T) { + // A pass-through /22 under a /24 floor splits into its four /24 children; + // none is broader than the floor. + got := coverPrefixes(nil, []netip.Prefix{netip.MustParsePrefix("10.9.0.0/22")}, 24) + assert.Equal(t, []string{"10.9.0.0/24", "10.9.1.0/24", "10.9.2.0/24", "10.9.3.0/24"}, got) } diff --git a/pkg/registry/file/networkneighborhood_processor_test.go b/pkg/registry/file/networkneighborhood_processor_test.go index 67d8eba77..6d5e0fc7c 100644 --- a/pkg/registry/file/networkneighborhood_processor_test.go +++ b/pkg/registry/file/networkneighborhood_processor_test.go @@ -133,9 +133,9 @@ func TestNetworkNeighborhoodProcessor_PreSave(t *testing.T) { } func TestNetworkNeighborhoodProcessor_PreSave_IPCollapse(t *testing.T) { - const hostCount = 60 + const hostCount = 64 // a fully-observed /26 (10.0.0.0..10.0.0.63) ingress := make([]softwarecomposition.NetworkNeighbor, 0, hostCount) - for i := 1; i <= hostCount; i++ { + for i := 0; i < hostCount; i++ { ingress = append(ingress, softwarecomposition.NetworkNeighbor{ Identifier: fmt.Sprintf("external-%d", i), Type: "external", From ac8721e489f5f9f5edf77b65924dbd144f4d9760 Mon Sep 17 00:00:00 2001 From: entlein Date: Fri, 24 Jul 2026 12:32:14 +0200 Subject: [PATCH 3/3] rewrote to address review, lets retest Signed-off-by: entlein --- .../file/networkneighborhood_ipcollapse.go | 135 ++++++++++++++---- .../networkneighborhood_ipcollapse_test.go | 51 ++++--- 2 files changed, 138 insertions(+), 48 deletions(-) diff --git a/pkg/registry/file/networkneighborhood_ipcollapse.go b/pkg/registry/file/networkneighborhood_ipcollapse.go index 2dd5a02f3..8a21bd1c0 100644 --- a/pkg/registry/file/networkneighborhood_ipcollapse.go +++ b/pkg/registry/file/networkneighborhood_ipcollapse.go @@ -19,15 +19,16 @@ const ipCollapseFieldSep = "\x00" // into a small number of CIDR-bearing entries. Entries are grouped by // (Type, DNS, NamespaceSelector, PodSelector); within a group whose count of // aggregatable IPv4 host addresses exceeds settings.NetworkIPGroupThreshold, -// those hosts plus any already-collapsed pass-through CIDRs are replaced by the -// minimal EXACT CIDR cover of exactly those addresses (see coverPrefixes), with -// no block broader than settings.NetworkCIDRFloorBits. The cover never -// over-approximates to a block the workload did not actually reach. +// those hosts plus any already-collapsed pass-through CIDRs are replaced by +// covering CIDR blocks no broader than settings.NetworkCIDRFloorBits (see +// coverPrefixes). Output size is bounded by the number of distinct floor-length +// networks the workload actually reached, not by the host count: scattered hosts +// collapse to one block per floor network rather than one /32 apiece. // -// The pass is a fixpoint: an exact cover re-covered is itself, and the "*" -// sentinel / bare IPv6 values are pass-through held verbatim, so a second run — -// whose groups now hold only CIDRs and thus have zero aggregatable hosts — -// leaves everything untouched. +// The pass is a fixpoint: collapsed blocks re-fed through the same aggregation +// converge to themselves, and the "*" sentinel / bare IPv6 values are +// pass-through held verbatim, so a second run — whose groups now hold only CIDRs +// and thus have zero aggregatable hosts — leaves everything untouched. func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dynamicpathdetector.CollapseSettings) []softwarecomposition.NetworkNeighbor { if entries == nil { return nil @@ -126,13 +127,15 @@ func collapseIPGroups(entries []softwarecomposition.NetworkNeighbor, settings dy return out } -// classifyGroupAddresses splits a group's address values into aggregatable host -// addresses (bare IPv4 or IPv6, deduped) and pass-through values held verbatim. -// An entry's value comes from the singular IPAddress when set, otherwise from -// each element of IPAddresses. The "*" sentinel and unparseable values are -// pass-through; already-collapsed CIDRs are pass-through here but the caller -// folds them back into the exact cover. Both address families are aggregated — -// netipx covers IPv4 and IPv6 alike. +// classifyGroupAddresses splits a group's address values into aggregatable IPv4 +// host addresses (deduped) and pass-through values held verbatim. An entry's +// value comes from the singular IPAddress when set, otherwise from each element +// of IPAddresses. CIDRs, the "*" sentinel, IPv6 and unparseable values are +// pass-through (already-collapsed CIDRs are folded back into the cover by the +// caller; the rest are held verbatim). Only IPv4 hosts are aggregated: policy +// generation (buildIPAddressesPeers) skips non-IPv4 entries, so collapsing IPv6 +// hosts into a CIDR would silently drop them — and any ports — from the derived +// NetworkPolicy. IPv6 is therefore kept as individual pass-through entries. func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]netip.Addr, []string) { seenHost := map[netip.Addr]struct{}{} seenPass := map[string]struct{}{} @@ -143,7 +146,7 @@ func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]ne if v == "" { return } - if addr, err := netip.ParseAddr(v); err == nil { + if addr, err := netip.ParseAddr(v); err == nil && addr.Is4() { if _, ok := seenHost[addr]; !ok { seenHost[addr] = struct{}{} hosts = append(hosts, addr) @@ -168,28 +171,36 @@ func classifyGroupAddresses(entries []softwarecomposition.NetworkNeighbor) ([]ne return hosts, passthrough } -// coverPrefixes returns the minimal set of CIDR strings that covers EXACTLY the -// given IPv4 host addresses together with the group's already-collapsed -// pass-through CIDRs, capped so no prefix is broader than floorBits. netipx does -// the aggregation — deduplicating, dropping subsumed prefixes and merging -// adjacent siblings into the minimal exact cover in near-linear time — and any -// resulting prefix broader than the floor is then split into floorBits-wide -// children (all fully covered, since the parent lies wholly within the set). +// coverPrefixes returns the set of CIDR strings covering the given IPv4 host +// addresses together with the group's already-collapsed pass-through CIDRs, with +// no block broader than floorBits and a bounded entry count. // -// Because the cover is exact, it never over-approximates to a block the workload -// did not actually reach, and re-running on already-collapsed input is a -// fixpoint: no duplicate or nested blocks can accumulate across incremental -// saves. The result is sorted. +// It works in two stages. First the raw hosts are aggregated to the floor +// (aggregateHostsToFloor): a group of hosts sharing a common prefix at least as +// long as the floor collapses to that single tight block — kept tighter than the +// floor when the traffic really is that tight, e.g. a fully-observed /26 — while +// scattered hosts are bucketed into their floor-length networks so the output is +// bounded by the number of distinct floor networks reached, not the host count. +// Second, those host blocks are folded together with the already-held CIDRs +// through netipx, which deduplicates, drops subsumed prefixes and merges adjacent +// siblings into a canonical set. That fold is what fixes incremental-learning +// garbage like [52.216.0.0/26, 52.216.0.0/26, 52.216.0.0/27] — the duplicate +// deduplicated and the nested /27 absorbed — and makes re-collapsing a fixpoint. +// +// Any IPv4 block still broader than the floor after the fold (a held block from a +// coarser prior floor, or floor networks that merged into a shorter parent) is +// split back into floorBits-wide children. IPv6 has no floor and is emitted as +// covered. The result is sorted. func coverPrefixes(hosts []netip.Addr, cidrPass []netip.Prefix, floorBits int) []string { if len(hosts) == 0 && len(cidrPass) == 0 { return nil } var b netipx.IPSetBuilder - for _, h := range hosts { - b.Add(h) + for _, p := range aggregateHostsToFloor(hosts, floorBits) { + b.AddPrefix(p) } for _, p := range cidrPass { - b.AddPrefix(p) + b.AddPrefix(p.Masked()) } set, err := b.IPSet() if err != nil || set == nil { @@ -210,6 +221,70 @@ func coverPrefixes(hosts []netip.Addr, cidrPass []netip.Prefix, floorBits int) [ return out } +// aggregateHostsToFloor collapses raw host addresses into CIDR blocks no broader +// than floorBits. IPv4 hosts sharing a common prefix at least as long as the +// floor collapse to that single common block (kept tighter than the floor when +// the traffic is genuinely that tight, e.g. a fully-observed /26); otherwise each +// host is bucketed into its floor-length network, so scattered traffic yields at +// most one block per distinct floor network. IPv6 hosts — which the caller keeps +// out of aggregation, but which are handled defensively here — are emitted as +// individual host prefixes and never widened by the IPv4 floor. +func aggregateHostsToFloor(hosts []netip.Addr, floorBits int) []netip.Prefix { + var v4 []netip.Addr + var out []netip.Prefix + for _, h := range hosts { + if h.Is4() { + v4 = append(v4, h) + } else { + out = append(out, netip.PrefixFrom(h, h.BitLen()).Masked()) + } + } + if len(v4) == 0 { + return out + } + if commonLen := commonPrefixLen(v4); commonLen >= floorBits { + return append(out, netip.PrefixFrom(v4[0], commonLen).Masked()) + } + seen := make(map[netip.Prefix]struct{}, len(v4)) + for _, h := range v4 { + p := netip.PrefixFrom(h, floorBits).Masked() + if _, ok := seen[p]; !ok { + seen[p] = struct{}{} + out = append(out, p) + } + } + return out +} + +// commonPrefixLen returns the number of leading bits shared by every address in +// the set. All addresses are assumed to be IPv4. +func commonPrefixLen(addrs []netip.Addr) int { + base := addrs[0].As4() + common := 32 + for _, addr := range addrs[1:] { + b := addr.As4() + n := 0 + for i := 0; i < 4 && n < common; i++ { + x := base[i] ^ b[i] + if x == 0 { + n += 8 + continue + } + for bit := 7; bit >= 0; bit-- { + if x&(1< exact cover keeps them granular (one - // /32 apiece, since none are adjacent); no emitted block is broader than the - // floor, and the cover never over-approximates to a covering block. +func TestCollapseIPGroups_ScatteredHostsBucketedToFloor(t *testing.T) { + // 60 lone hosts, each in its own /16, do not share a common prefix as long as + // the floor, so each is bucketed into its floor-length (/16) network. Output + // is one block per distinct floor network — bounded by the number of networks + // reached, not the host count — and no block is broader than the floor. var in []softwarecomposition.NetworkNeighbor for i := 0; i < 60; i++ { in = append(in, hostNeighbor(fmt.Sprintf("%d.%d.0.1", 10+i, i))) @@ -68,12 +69,12 @@ func TestCollapseIPGroups_ScatteredHostsStayGranularAndCappedAtFloor(t *testing. out := collapseIPGroups(in, testSettings()) - assert.Greater(t, len(out), 1) + require.Len(t, out, 60, "one bucket per distinct /16") for _, e := range out { require.Len(t, e.IPAddresses, 1) p, err := netip.ParsePrefix(e.IPAddresses[0]) require.NoError(t, err) - assert.GreaterOrEqual(t, p.Bits(), 16, "no emitted block may be broader than the floor") + assert.Equal(t, 16, p.Bits(), "each lone host is bucketed into its floor-length network") } } @@ -146,7 +147,9 @@ func TestCollapseIPGroups_RealWorldShapeOrdersOfMagnitude(t *testing.T) { out := collapseIPGroups(in, testSettings()) - // 512 contiguous hosts exact-cover to a handful of blocks (two /24s here). + // The two /24s fall in different /16s and share no common prefix as long as + // the /16 floor, so each is bucketed into its floor network: a handful of + // blocks (two /16s here), orders of magnitude below the host count. assert.Less(t, len(out), 10) assert.Less(t, len(out), len(in)/50) for _, e := range out { @@ -176,7 +179,8 @@ func TestCollapseIPGroups_Idempotent(t *testing.T) { DNS: "example.com", IPAddresses: []string{"*"}, }) - // IPv6 entry — a lone v6 host exact-covers to its /128 + // IPv6 entry — held as a pass-through value verbatim (IPv6 is not aggregated, + // since policy generation consumes only IPv4 collapsed entries) in = append(in, softwarecomposition.NetworkNeighbor{ Type: softwarecomposition.CommunicationTypeEgress, DNS: "example.com", @@ -195,7 +199,7 @@ func TestCollapseIPGroups_Idempotent(t *testing.T) { } assert.Contains(t, values, "*") assert.Contains(t, values, "200.0.0.0/16") - assert.Contains(t, values, "2001:db8::1/128") + assert.Contains(t, values, "2001:db8::1") } func TestCollapseIPGroups_FieldContract(t *testing.T) { @@ -234,9 +238,13 @@ func TestCollapseIPGroups_MultiBucketReplicatesDNSNamesAndPorts(t *testing.T) { } } -func TestCollapseIPGroups_IPv6Aggregated(t *testing.T) { - // A full IPv6 /120 (256 contiguous v6 hosts) exact-covers to that /120, and a - // lone v6 host to its /128 — alongside a v4 group, in one mixed-family pass. +func TestCollapseIPGroups_IPv6NotAggregatedHeldPassThrough(t *testing.T) { + // IPv6 hosts are NOT aggregated into CIDRs: policy generation + // (buildIPAddressesPeers) consumes only IPv4 collapsed entries, so folding + // IPv6 hosts into an IPv6 CIDR would silently drop them — and their ports — + // from the derived NetworkPolicy. They are held as individual pass-through + // values instead, even when contiguous, while the co-located v4 group still + // collapses normally. var in []softwarecomposition.NetworkNeighbor for i := 0; i < 256; i++ { in = append(in, hostNeighbor(fmt.Sprintf("2606:4700:0:1::%x", i))) @@ -252,8 +260,12 @@ func TestCollapseIPGroups_IPv6Aggregated(t *testing.T) { for _, e := range out { values = append(values, e.IPAddresses...) } - assert.Contains(t, values, "2606:4700:0:1::/120", "contiguous v6 hosts aggregate") - assert.Contains(t, values, "2001:db8::42/128", "lone v6 host covers to /128") + // v6 hosts survive verbatim, never merged into a /120 or /128 CIDR + assert.Contains(t, values, "2606:4700:0:1::0", "contiguous v6 hosts stay individual") + assert.Contains(t, values, "2001:db8::42", "lone v6 host stays verbatim") + assert.NotContains(t, values, "2606:4700:0:1::/120", "v6 must not be aggregated into a CIDR") + // the co-located IPv4 group still collapses (a fully-observed /26) + assert.Contains(t, values, "10.5.0.0/26", "co-located IPv4 group still collapses") } func TestCoverPrefixes_IPv6ExactAndMerge(t *testing.T) { @@ -332,16 +344,19 @@ func TestCollapseIPGroups_IncrementalReCollapseDeduplicatesAndAbsorbs(t *testing assert.Equal(t, []string{"52.216.0.0/26"}, cidrs, "must converge to a single covering /26, not [/26 /26 /27]") } -func TestCoverPrefixes_ExactNoOverApproximation(t *testing.T) { - // Three non-adjacent hosts cover exactly {.1,.2,.3} — never a single /30 - // (which would admit the unobserved .0). +func TestCoverPrefixes_HostsCollapseToCommonPrefixWhenTighterThanFloor(t *testing.T) { + // Hosts sharing a common prefix at least as long as the floor collapse to that + // single common block. {.1,.2,.3} share a /30, which is tighter than the /16 + // floor, so they aggregate to 52.216.0.0/30 (bounding the entry count to one + // rather than emitting a /32 and a /31). The block is capped at the floor, but + // the workload's own common prefix is honored when it is already narrower. hosts := []netip.Addr{ netip.MustParseAddr("52.216.0.1"), netip.MustParseAddr("52.216.0.2"), netip.MustParseAddr("52.216.0.3"), } got := coverPrefixes(hosts, nil, 16) - assert.Equal(t, []string{"52.216.0.1/32", "52.216.0.2/31"}, got) + assert.Equal(t, []string{"52.216.0.0/30"}, got) } func TestCoverPrefixes_MergesAdjacentSiblings(t *testing.T) {