Skip to content

fix(e2e): stop the sandbox cluster CIDR overlapping the pod range - #3750

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/e2e-sandbox-cidr-overlap
Aug 11, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/e2e-sandbox-cidr-overlap

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

The sandbox Talos config and the platform install step both used 10.244.0.0/16, one as the Kubernetes cluster CIDR and the other as the kube-ovn POD_CIDR, so two allocators drew from one range.

kube-ovn owns pod IPAM and allocates flat from POD_CIDR. Cilium is chained behind it (generic-veth, target kube-ovn) and allocates no pod addresses at all, but it still carves its own per-node router IP and Ingress IP out of Node.spec.podCIDR, which kube-controller-manager slices from the cluster CIDR. Sooner or later kube-ovn hands a pod an address cilium already holds. When that address is the Ingress IP the agent refuses the sandbox with IP ipv4:<addr> is already in use, and cilium never relents on it, so the pod proceeds only if something reschedules it onto a different address; in the run this was filed from, the same address was refused seven times across six pod UIDs before the suite ran out of budget. When it is the router IP nothing complains and only host-to-pod traffic breaks.

This moves the sandbox cluster CIDR to 100.65.0.0/16, which no other range in the tree uses, and adds a guard so the two cannot be brought back together by accident.

Pod addresses do not change. They still come from kube-ovn out of 10.244.0.0/16, and cilium does not require a chained endpoint's address to sit inside its own allocation CIDR, so only cilium's two per-node infrastructure addresses move.

The quiet variant gets fixed twice over, which is worth spelling out because only half of it was obvious. Under ipam.mode: kubernetes cilium also installs a route for the node's own pod CIDR via cilium_host. On the current default that route covers 10.244.a.0/24 on a node whose pods actually live behind ovn0, so it shadows real pod addresses. Moving the cluster CIDR both stops the router IP from ever being handed to a pod and retargets that route at 100.65.a.0/24, where it shadows nothing.

Excluding the addresses on the kube-ovn side instead is not an option: cilium picks its offsets by random scan over the node's range, so there is no static set to exclude.

The collision shows up in two shapes, and both have been seen. Loudly, on the Ingress IP, where the agent names the address in the event, which is the failure #3736 was filed for. Quietly, on the router IP, where the pod comes up and the only symptom is connection refused on a kubelet probe, with ip route get <podIP> on the node resolving to local ... dev lo; that variant is documented in #3445 and #3615. They were being tracked as unrelated flakes in different suites before the shared cause was identified.

What the guard covers, and what it does not

hack/sandbox-cidr-disjoint.bats pins the configuration, not the behaviour. It reads the values out of the tree and fails when they overlap. It does not reproduce the collision, which needs a running cluster.

It checks the invariant rather than the literals, so either value may move as long as they stay disjoint, and it checks the cluster CIDR against the service and join ranges too, because landing on one of those instead is the next way to get this wrong. An extraction that comes up empty, duplicated or misshapen fails the test instead of shrinking the comparison, and comments and blank lines inside the list are skipped rather than read as the end of it. It is not a YAML parser, though: anything it does not read as a bare dotted quad fails the shape validation rather than being understood, which is the safe direction but not the same promise.

Those three are the allocator ranges and nothing more. The sandbox node network, the load-balancer pool and the transit range of a component that is off by default are not read, so the guard is not a complete map of what the cluster CIDR has to avoid. An IPv6 entry fails the shape validation rather than getting an overlap check, so dual-stack means extending the guard rather than passing through it.

Residual risk

I did not prove from source that cilium is the only consumer of Node.spec.podCIDR in this topology. What I did check is that kube-proxy is disabled and that local-ccm runs the node-lifecycle controller only, with no route controller.

This diff selects the full e2e suite, 21 Chainsaw suites with gateway and mongodb among them, so a consumer I missed surfaces as a red run on this PR rather than staying an assumption.

Split of what covers what: the static checks and the unit guard cover the configuration, and the e2e run covers the behaviour. Nothing short of a green suite answers whether cilium's router and Ingress addresses behave once they sit outside the range kube-ovn allocates from.

A check that is not "the suite went green"

hack/e2e-cilium-endpoint-leak-healer.sh already prints a line of its own for this exact collision, so there is a signal to read that does not depend on the overall verdict. In the cozyreport artifact it lands at snapshots/<suite>/host/namespaces/kube-system/v1/pod/cilium-leak-healer-*/healer/current.log, and the lines to grep for are IP is this node's Cilium ingress IP. In the run this was filed from there are 42 of them, all naming the same address.

After this change that line cannot be produced by an address inside the kube-ovn range, because there is no longer an address that is in both. So the expectation is zero from the first run, not zero on average: one occurrence contradicts the change rather than being noise, and is worth stopping on. A green suite can be green for many reasons; this line disappearing has only one.

Scope

This fixes the sandbox and nothing else. For a real installation the cluster CIDR lives in the operator's own machine configuration, outside this repository, so the same collision is still reachable there. #3740 tracks the product side, including the finding that the install documentation does not merely allow the overlap but instructs operators to create it, by requiring podCIDR to match the cluster's pod CIDR.

There is a tradeoff in this worth naming rather than leaving to be discovered. Talos defaults podSubnets to 10.244.0.0/16 (DefaultIPv4PodNet, pkg/machinery/constants/constants.go:888 at v1.13.6, the version this tree ships), and the platform defaults networking.podCIDR to the same value, so an operator who takes both defaults gets the overlap. Until now the sandbox shared that topology and was the one place the bug surfaced where anyone would see it. After this change it no longer reproduces the operator-facing form, and nothing else does either. That is the right call for CI, which should not be wedged by a defect it did not introduce, but it does mean the production exposure loses its only accidental detector.

Closes #3736

To be explicit about what closing that issue does and does not mean: this removes the reproduction in the sandbox and leaves the product configuration untouched. The collision is still reachable for users, and #3740 is where that lives. The issue text describes the root mechanism, which is wider than its title, so the closure covers the e2e failure it was filed for and not the platform default.

Screenshots

Not a UI change.

Downstream repositories

No box is ticked deliberately, and no follow-up PR is opened, so a human decides.

Walking the trigger map against this diff: both hack/e2e-prepare-cluster.bats entries in it are scoped to node prerequisites, the kernel modules a node must load, the containerd settings, the LVM global_filter and the cluster domain. This diff changes none of those, it changes a network range, so no entry fires literally.

The underlying constraint does reach several of them all the same. cozystack/talm sets podSubnets: [10.244.0.0/16] in charts/cozystack/values.yaml, the same range the platform uses for networking.podCIDR, so the documented Talos bootstrap produces the overlap this PR removes from the sandbox. cozystack/ansible-cozystack reaches it from the other side: roles/cozystack/defaults/main.yml sets cozystack_pod_cidr: "10.42.0.0/16" and renders it into networking.podCIDR, while its inventories set cluster-cidr: 10.42.0.0/16, so those two are identical as well. And cozystack/website prescribes the equality outright, install/kubernetes/generic.md stating that podCIDR and serviceCIDR must match the Kubernetes cluster configuration.

No issue is filed there yet on purpose: which side moves is not decided, and asking talm to change podSubnets would be the wrong request if the platform ends up moving instead. That decision belongs to #3740.

Release note

NONE

Summary by CodeRabbit

  • Bug Fixes

    • Updated cluster networking ranges to avoid conflicts with Kubernetes, kube-ovn, kilo, and Cilium networks.
    • Added validation to detect missing, invalid, or overlapping cluster and platform CIDR ranges.
    • Preserved safeguards for environments where network ranges overlap, with clearer diagnostic logging.
  • Documentation

    • Documented network separation requirements and clarified expected sandbox networking behavior.

The sandbox Talos config and the platform install step both used
10.244.0.0/16, one as the Kubernetes cluster CIDR and the other as the
kube-ovn POD_CIDR, so two allocators drew from one range.

kube-ovn owns pod IPAM and allocates flat from POD_CIDR. Cilium is
chained behind it and allocates no pod addresses, but it still carves
its own per-node router IP and Ingress IP out of Node.spec.podCIDR,
which kube-controller-manager slices from the cluster CIDR. Sooner or
later kube-ovn hands a pod an address cilium already holds. When that
address is the Ingress IP the agent refuses the sandbox with "IP
ipv4:<addr> is already in use", and cilium never relents on it, so the
pod only proceeds if something reschedules it onto a different address.
In the failure this comes from, the same address was refused seven
times across six pod UIDs before the suite ran out of budget. When it
is the router IP nothing complains at all and only host-to-pod traffic
breaks.

Move the cluster CIDR to 100.65.0.0/16, which no other range in the
tree uses. That deliberately stops the sandbox matching the layout the
install documentation prescribes, so it is not a value to put back.
Pod addresses are unaffected: they still come from kube-ovn out of
10.244.0.0/16, and cilium does not require a chained endpoint's address
to sit inside its own allocation CIDR.

Excluding the addresses on the kube-ovn side would not work, because
cilium picks its offsets by random scan over the node's range.

The new guard checks the invariant rather than the literals, so either
value may move as long as they stay disjoint. It covers the service
and join ranges too, since landing on one of those instead is the next
way to get this wrong.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei Aleksei Sviridkin (lexfrei) added kind/bug Categorizes issue or PR as related to a bug area/testing Issues or PRs related to testing (e2e, bats, unit tests) labels Aug 11, 2026
@lexfrei Aleksei Sviridkin (lexfrei) added area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) kind/bug Categorizes issue or PR as related to a bug area/testing Issues or PRs related to testing (e2e, bats, unit tests) labels Aug 11, 2026
@github-actions github-actions Bot added the size/L This PR changes 100-499 lines, ignoring generated files label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The sandbox pod subnet changes to 100.65.0.0/16. A new Bats test validates cluster and platform CIDRs and rejects overlaps. Comments document the expected separation and the leak-healer diagnostic behavior.

Changes

Sandbox CIDR separation

Layer / File(s) Summary
Sandbox CIDR configuration
hack/e2e-prepare-cluster.bats, hack/e2e-cilium-endpoint-leak-healer.sh
The sandbox pod subnet changes to 100.65.0.0/16. Comments document required separation from platform ranges and the SURFACE diagnostic log.
CIDR overlap validation
hack/sandbox-cidr-disjoint.bats
A Bats test parses cluster and platform CIDRs, validates IPv4 values, and fails when any ranges overlap.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Suggested reviewers: myasnikovdaniil, kvaps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing the sandbox cluster CIDR from overlapping the pod range.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/e2e-sandbox-cidr-overlap

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
hack/sandbox-cidr-disjoint.bats (1)

115-120: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tighten the shape validation to reject out-of-range octets and prefix lengths.

The regex accepts [0-9]{1,3} per octet and [0-9]{1,2} for the prefix. Values such as 999.0.0.0/99 pass the shape check and then reach the overlap arithmetic, where 2 ^ (32 - 99) produces a fractional block size and the comparison result is meaningless. A guard that validates shape should not admit values it cannot compare.

Add an explicit numeric range check after the pattern match. The positive form of grep also reads more directly than grep -qvE.

♻️ Proposed range validation
   for cidr in $cluster_cidrs $platform_cidrs; do
-    if printf '%s\n' "$cidr" | grep -qvE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$'; then
+    if ! printf '%s\n' "$cidr" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$'; then
       echo "extracted value is not an IPv4 CIDR: '$cidr'" >&2
       return 1
     fi
+    if ! printf '%s\n' "$cidr" | awk -F'[./]' '
+      {
+        for (i = 1; i <= 4; i++) if ($i > 255) exit 1
+        if ($5 > 32) exit 1
+      }
+    '; then
+      echo "extracted value is not a valid IPv4 CIDR: '$cidr'" >&2
+      return 1
+    fi
   done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/sandbox-cidr-disjoint.bats` around lines 115 - 120, Update the CIDR
validation loop around the existing grep check to use a positive pattern match,
then add explicit numeric range validation for all IPv4 octets (0–255) and the
prefix length (0–32) before overlap arithmetic. Reject invalid values with the
existing error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@hack/sandbox-cidr-disjoint.bats`:
- Around line 115-120: Update the CIDR validation loop around the existing grep
check to use a positive pattern match, then add explicit numeric range
validation for all IPv4 octets (0–255) and the prefix length (0–32) before
overlap arithmetic. Reject invalid values with the existing error path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c9932f3-8c1f-4a43-8a1d-2531e53ca32f

📥 Commits

Reviewing files that changed from the base of the PR and between ec6a835 and 9ace63b.

📒 Files selected for processing (3)
  • hack/e2e-cilium-endpoint-leak-healer.sh
  • hack/e2e-prepare-cluster.bats
  • hack/sandbox-cidr-disjoint.bats

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Verified against the tree, not just the narrative:

  • Root cause is real and correctly targeted. hack/e2e-install-cozystack.bats sets podCIDR: "10.244.0.0/16" for kube-ovn, and the Talos cluster.network.podSubnets was the same 10.244.0.0/16. Since cilium is chained behind kube-ovn and carves its per-node router + Gateway-API Ingress IPs from Node.spec.podCIDR (sliced by kube-controller-manager from the cluster CIDR), the two allocators shared a range and eventually double-allocated. Moving the cluster CIDR to 100.65.0.0/16 makes them disjoint, which is the actual fix rather than masking the healer's SURFACE path.
  • The replacement is clean against every live range, including the two the guard does not check: pod 10.244/16, service 10.96/16, join 100.64/16, kilo transit 100.66/16, and the sandbox node network + MetalLB pool (192.168.123.0/24, node .10, pool .200-.250). No latent overlap introduced.
  • The guard is wired and live. hack/sandbox-cidr-disjoint.bats is not an e2e-* file, so BATS_UNIT_FILES := $(filter-out hack/e2e-%.bats,$(wildcard hack/*.bats)) picks it up in make bats-unit-tests. The overlap math (mask to the shorter prefix, compare network addresses) is correct and symmetric, shape validation fails closed, and the inlined-arithmetic constraint from cozytest.sh's return 0 rewrite is respected.

The test's own acknowledged gap (node network / LB pool / off-by-default component ranges are not read) is a documented limitation, not a defect, and the current value does not trip it.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 4ac02f5 into main Aug 11, 2026
40 of 49 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/e2e-sandbox-cidr-overlap branch August 11, 2026 13:50
myasnikovdaniil added a commit that referenced this pull request Sep 24, 2026
…pping the pod range (#4432)

Backport of #3750 to `release-1.6`.

Sandbox Talos config and platform install on this branch still both use
`10.244.0.0/16`, so kube-ovn and cilium allocate from one range and
cilium refuses pod sandboxes with `IP ... is already in use`. It hit
`mariadb-2-backup-roundtrip` on the E2E run of #4328 here.

Cherry-picked clean with `-x`.

### Testing

- `make unit-tests` green, POSIX sh sweep clean.
- Real check is E2E on this PR.

```release-note
NONE
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

e2e: pod sandbox fails with "IP already in use" because cilium and kube-ovn share one pod CIDR

2 participants