Repository navigation
fix(e2e): stop the sandbox cluster CIDR overlapping the pod range - #3750
Conversation
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]>
📝 WalkthroughWalkthroughThe sandbox pod subnet changes to ChangesSandbox CIDR separation
Estimated code review effort: 3 (Moderate) | ~30 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
hack/sandbox-cidr-disjoint.bats (1)
115-120: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTighten 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 as999.0.0.0/99pass the shape check and then reach the overlap arithmetic, where2 ^ (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
grepalso reads more directly thangrep -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
📒 Files selected for processing (3)
hack/e2e-cilium-endpoint-leak-healer.shhack/e2e-prepare-cluster.batshack/sandbox-cidr-disjoint.bats
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM.
Verified against the tree, not just the narrative:
- Root cause is real and correctly targeted.
hack/e2e-install-cozystack.batssetspodCIDR: "10.244.0.0/16"for kube-ovn, and the Taloscluster.network.podSubnetswas the same10.244.0.0/16. Since cilium is chained behind kube-ovn and carves its per-node router + Gateway-API Ingress IPs fromNode.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 to100.65.0.0/16makes 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, service10.96/16, join100.64/16, kilo transit100.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.batsis not ane2e-*file, soBATS_UNIT_FILES := $(filter-out hack/e2e-%.bats,$(wildcard hack/*.bats))picks it up inmake 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'sreturn 0rewrite 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.
…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 ```
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-ovnPOD_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, targetkube-ovn) and allocates no pod addresses at all, but it still carves its own per-node router IP and Ingress IP out ofNode.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 withIP 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: kubernetescilium also installs a route for the node's own pod CIDR viacilium_host. On the current default that route covers10.244.a.0/24on a node whose pods actually live behindovn0, 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 at100.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 refusedon a kubelet probe, withip route get <podIP>on the node resolving tolocal ... 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.batspins 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.podCIDRin this topology. What I did check is that kube-proxy is disabled and thatlocal-ccmruns the node-lifecycle controller only, with no route controller.This diff selects the full e2e suite, 21 Chainsaw suites with
gatewayandmongodbamong 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.shalready 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 thecozyreportartifact it lands atsnapshots/<suite>/host/namespaces/kube-system/v1/pod/cilium-leak-healer-*/healer/current.log, and the lines to grep for areIP 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
podCIDRto match the cluster's pod CIDR.There is a tradeoff in this worth naming rather than leaving to be discovered. Talos defaults
podSubnetsto10.244.0.0/16(DefaultIPv4PodNet,pkg/machinery/constants/constants.go:888at v1.13.6, the version this tree ships), and the platform defaultsnetworking.podCIDRto 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.batsentries in it are scoped to node prerequisites, the kernel modules a node must load, the containerd settings, the LVMglobal_filterand 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/talmsetspodSubnets: [10.244.0.0/16]incharts/cozystack/values.yaml, the same range the platform uses fornetworking.podCIDR, so the documented Talos bootstrap produces the overlap this PR removes from the sandbox.cozystack/ansible-cozystackreaches it from the other side:roles/cozystack/defaults/main.ymlsetscozystack_pod_cidr: "10.42.0.0/16"and renders it intonetworking.podCIDR, while its inventories setcluster-cidr: 10.42.0.0/16, so those two are identical as well. Andcozystack/websiteprescribes the equality outright,install/kubernetes/generic.mdstating thatpodCIDRandserviceCIDRmust match the Kubernetes cluster configuration.No issue is filed there yet on purpose: which side moves is not decided, and asking talm to change
podSubnetswould be the wrong request if the platform ends up moving instead. That decision belongs to #3740.Release note
Summary by CodeRabbit
Bug Fixes
Documentation