collapse dedup (also known as "greedy bucket") - #349
Conversation
Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ValidationUnit tests —
|
| workload | floor | result |
|---|---|---|
fully-observed AWS S3 /28 (52.216.1.0/28) |
/16 | 52.216.1.0/28 |
| scattered S3 / Cloudflare / Azure / GCP IPs | /16 | their exact /32s — no covering block |
fully-observed S3 /27 (52.216.2.0/27) |
/28 | 52.216.2.0/28 + 52.216.2.16/28 |
--- PASS: Test_34_NetworkNeighborsCIDRCollapse (380.97s)
matthyx
left a comment
There was a problem hiding this comment.
Thanks for tracking down the duplicate-CIDR bug — the root cause diagnosis is correct and the netipx-based dedupe/merge approach is the right idea. But I tested this against the actual 8,687-IP dataset from the real profile that originally motivated this feature (see #348), and the fix as written introduces a much larger regression that undoes the feature's core purpose. Requesting changes before merge.
The bug you're fixing is real
aggregateHosts computed a cover of fresh hosts only, then appended already-held pass-through CIDRs from prior saves verbatim, with no dedup or subsumption check. A held /26 + a held /27 (nested) + a freshly-recomputed /26 could all land in the same output — exactly the [52.216.0.0/26, 52.216.0.0/26, 52.216.0.0/27] garbage you found. Feeding hosts + held CIDRs into one netipx.IPSetBuilder and taking the exact minimal cover correctly fixes this (TestCollapseIPGroups_IncrementalReCollapseDeduplicatesAndAbsorbs demonstrates convergence to one /26).
Critical regression: floor-bucketing effectively stops working for realistic scattered traffic
coverPrefixes only runs splitToFloor on a prefix that netipx's exact cover already produced as broader than the floor. But netipx's exact cover only merges strictly adjacent, byte-aligned sibling prefixes. For genuinely scattered individual hosts — the real-world case — it just returns each host as its own /32 (occasionally a /31 pair), and a /32 is never "broader than the floor," so it's never bucketed at all.
I checked this out locally and ran it against the actual 8,687-IP dataset from the original motivating profile:
| Implementation | floor /16 |
floor /24 |
|---|---|---|
| Current on this branch (pre-#349) | 10 entries | 341 entries |
| This PR | 8,614 entries | 8,614 entries |
Of the 8,614 output entries: 8,545 are still bare /32 hosts, 67 are /31 pairs, 2 are /30s. This is essentially no collapsing — the exact profile that was flagged too-large and motivated this feature would remain flagged too-large after this change.
Notably, TestCollapseIPGroups_ScatteredHostsStayGranularAndCappedAtFloor (renamed from TestCollapseIPGroups_AboveThresholdBroaderThanFloorBuckets) now asserts this as the intended behavior — 60 hosts each in a different /16 stay as 60 separate /32s, where the old test asserted they got bucketed. That's the previous bucketing guarantee inverted into its opposite; worth calling out explicitly as an intentional behavior change if it is one, but I don't think it should be.
Secondary issue: silent scope expansion to IPv6, not consumed by policy generation
classifyGroupAddresses dropped the addr.Is4() gate, so bare IPv6 hosts are now aggregated into collapsed IPv6 CIDR entries in IPAddresses (previously pass-through only). networkpolicy.go (untouched here) explicitly skips non-IPv4 entries in buildIPAddressesPeers, so a group of 50+ external IPv6 peers would now collapse into an IPv6 CIDR that silently produces zero peers in the generated NetworkPolicy — and if that neighbor has no selector, the skipPorts guard drops ports too, so the whole rule vanishes. Same failure class (collapsed entries silently missing from generated policy) that took a few review rounds to catch for IPv4 in #348.
Minor: reverses a deliberated design choice
The original design explicitly rejected netipx as the primary production aggregation mechanism for this exact failure mode, and restricted any use of it to test-only code specifically to avoid promoting it from an indirect to a direct dependency. This PR makes it the core production mechanism and promotes the dependency. Not necessarily wrong, but flagging it since that design history isn't visible anywhere in the repo (it lived in local planning notes), so there was no way to know about that constraint going in.
Suggested direction
Keep the dedupe/merge-via-netipx idea, but run it within floor-length buckets rather than before bucketing: bucket everything (fresh hosts + held CIDRs) by floor-length network first, then run netipx per-bucket just to dedupe/merge/absorb overlaps within that bucket. That preserves the original bucketing guarantee (bounded output size regardless of scatter) while still fixing the real duplicate/nested-CIDR bug.
Happy to help iterate on this if useful.
Signed-off-by: entlein <einentlein@gmail.com>
Re-verified against the current head
Learnt egress collapsed to:
|
Ranges tested → how they collapsedEach workload egresses to the listed IPs; node-agent learns them, storage collapses:
Every scattered host is bucketed to its enclosing /16 (the floor); no bare host |
matthyx
left a comment
There was a problem hiding this comment.
Re-tested the rewrite (ac8721e) — both issues from the previous review are resolved.
Floor-bucketing regression: fixed correctly, and via exactly the structure I'd suggested — aggregateHostsToFloor buckets first (common-prefix collapse when tighter than the floor, else one block per distinct floor-length network), and coverPrefixes only runs the netipx dedupe/merge/absorb step on those buckets + held CIDRs afterward, rather than running netipx's exact cover across raw scattered hosts. Re-ran against the same real 8,687-IP dataset:
floor /16 |
floor /24 |
|
|---|---|---|
| Before (this PR, previous commit) | 8,614 | 8,614 |
| Now | 10 | 341 |
Matches the pre-#349 baseline exactly.
IPv6 scope creep: fixed — classifyGroupAddresses has the Is4() gate back, so IPv6 hosts are held pass-through rather than aggregated into CIDRs that buildIPAddressesPeers would silently drop. TestCollapseIPGroups_IPv6NotAggregatedHeldPassThrough covers it directly and explains why in the comment.
Also re-confirmed: the original duplicate-CIDR bug this PR set out to fix is still fixed (TestCollapseIPGroups_IncrementalReCollapseDeduplicatesAndAbsorbs passes), full go build/go vet/go test across the repo are clean, and the benchmark runs fine with no red flags.
Nice fix — the two-stage bucket-then-merge structure is the right shape for this. Approving.
67e466c
into
kubescape:feat/networkneighbors-cidr-collapsing
has a corresponding CT in nodeagent kubescape/node-agent#861
perf overhead was negligible but added bench
original PR was resulting in duplicate outputs: e.g.
learnt fan-out collapsed to CIDR(s) [52.216.0.0/26 52.216.0.0/26 52.216.0.0/27]