Skip to content

Gateway API + kubernetes (host-scope) IPAM: per-node Ingress IP can be re-allocated to a pod after an agent restart (sandbox rejected "IP already in use") #46799

Closed

Description

@lexfrei

Summary

With ipam: kubernetes (host-scope IPAM) and Gateway API enabled (enable-envoy-config=true), the per-node Cilium Ingress IP (CiliumNode.spec.ingress.ipv4, held by the reserved:ingress endpoint) is carved from the node Pod CIDR, but its reservation is not robust across a cilium-agent restart. After a restart the host-scope allocation bitmap is rebuilt empty and the Ingress IP is re-reserved via a path decoupled from local-endpoint restore; if the CNI server serves a pod ADD in that window, the pod can be handed the Ingress IP. The agent then rejects the pod sandbox with [PUT /endpoint/{id}][400] "IP <ingress-ip> is already in use", and the pod is stuck in ContainerCreating.

I hit this in Cozystack (a CNCF platform) E2E, where it intermittently wedges pods during cluster bring-up — reporting it upstream as a real downstream consumer of this configuration.

Cilium version

v1.19.5 (the mechanism is present throughout the v1.19 line; I could not find a released version that fixes it).

Configuration

  • IPAM: kubernetes (host-scope, per-node Node.spec.podCIDR)
  • Gateway API: enabled → enable-envoy-config: "true"
  • Legacy ingress controller: disabled
  • gatewayAPI.hostNetwork.enabled: false (per-tenant LoadBalancer-type Services)

Mechanism (source references, tag v1.19.5)

  1. The per-node Ingress IP is allocated by the agent, gated solely on EnableEnvoyConfig: daemon/infraendpoints/infra_ip_allocation.go → allocateIngressIPs() → AllocateNextFamilyWithoutSyncUpstream(IPv4, "ingress", PoolDefault()); the matching endpoint is created by daemon/infraendpoints/ingress_endpoint.go → registerIngressEndpoint (same gate). The IP comes from ipam.PoolDefault(), which in host-scope mode is the node Pod CIDR.
  2. In host-scope IPAM the allocator is a single in-memory bitmap with no persistence: pkg/ipam/hostscope.go (newHostScopeAllocator → ipallocator.NewCIDRRange; RestoreFinished() is a no-op). On every agent start the bitmap is empty and must be re-populated.
  3. After a restart the bitmap is re-populated by two decoupled paths: restored local endpoints (daemon/cmd/endpoint_restore.go → allocateIPsLocked → AllocateIPWithoutSyncUpstream(..., "<ep> [restored]")) and the Ingress IP (allocateIngressIPs, reading the value from the restored LocalNode / CiliumNode.spec.ingress.ipv4). Because these passes are not ordered against the CNI server beginning to serve pod ADDs, a pod ADD can be handed the Ingress IP before it is re-reserved. The Ingress IP is then held simultaneously by the reserved:ingress endpoint and a pod → the sandbox is rejected.

Impact

A pod can be left permanently wedged in ContainerCreating until it is rescheduled onto a different IP — and on a small per-node Pod CIDR, rescheduling can re-hit the same IP. Anything that restarts the agent (rollouts, node pressure, an operator restart) can re-enter the race. It surfaces as a FailedCreatePodSandBox event whose message IP equals CiliumNode.spec.ingress.ipv4 for that node.

Suggested fixes (any one)

  1. Reserve spec.ingress.ipv4 in the host-scope allocation bitmap before the CNI server starts serving pod allocations (treat it like a restored endpoint IP).
  2. During endpoint restore, reconcile the restored reserved:ingress endpoint's IP into the bitmap so the Ingress IP and pod IPs share one consistent reservation pass.
  3. Source the per-node Ingress IP from a dedicated range outside the Pod CIDR so it can never collide with pod IPAM.

How to reproduce (sketch)

  1. Cilium v1.19.x, ipam: kubernetes, Gateway API enabled, a small per-node Pod CIDR.
  2. Create Gateways so the per-node reserved:ingress IP is allocated; note CiliumNode.spec.ingress.ipv4 for a node.
  3. Restart that node's cilium-agent while pods are being created on the node (churn).
  4. Observe a pod stuck in ContainerCreating with FailedCreatePodSandBox … "IP <ingress-ip> is already in use" where <ingress-ip> equals that node's CiliumNode.spec.ingress.ipv4.

Activity

  1. added theissue type on Jul 1, 2026
  2. added
    kind/bugThis is a bug in the Cilium logic.
    kind/community-reportThis was reported by a user in the Cilium community, eg via Slack.
    on Jul 1, 2026
  3. squeed commented on Jul 1, 2026

    @squeed
    Contributor

    I'm surprised this hasn't happened before. Do you have any indication this is a regression?

  4. lexfrei commented on Jul 1, 2026

    @lexfrei
    ContributorAuthor

    No hard evidence of regression on my end — I don't have data from pre-1.19 releases to compare against. What I can confirm: it reproduced with the identical signature on both 1.19.3 and 1.19.5 in our CI, so it isn't something introduced by our own recent Cilium bump. It's not high-frequency in isolation, and part of why it took a while to pin down is that our CI also has a second, unrelated, older orphaned-endpoint-leak flake (#38313) that produces the exact same "IP already in use" surface error — the two got conflated until I isolated this one by matching the disputed IP against CiliumNode.spec.ingress.ipv4.

  5. youngnick commented on Jul 17, 2026

    @youngnick
    Contributor

    This does sound like an issue, but I'm interested in how you opened this issue, @lexfrei. All the available issue templates should automatically set labels for you, which will help issues get seen by the correct folks more quickly. What path did you use to open this issue? I'd like us to be able to make sure that the labels (like kind/bug etc) get set no matter the creation path.

  6. added
    need-more-infoMore information is required to further debug or fix the issue.
    on Jul 17, 2026
  7. lexfrei commented on Jul 17, 2026

    @lexfrei
    ContributorAuthor

    Via the GitHub API (gh CLI), not the web UI. I do follow the issue template when opening via the API — I have the model fill it out by its structure — but the API path doesn't apply the labels the template would auto-set in the web UI. I noticed that specific gap at creation time and judged it not worth switching to the browser over; @squeed set kind/bug and kind/community-report by hand afterwards. I'll keep it in mind for this repo going forward.

    Disclosure while I'm here: this issue was drafted with an LLM under my direction (AIL:3) — I define the problem and the mechanism, the model drafts, and I read and edit every word before it goes out, same as on my PRs. Per the AI policy I should have declared that in the body; doing it now.

    On closing the gap itself: as an external contributor I can't set labels directly either — --label is silently dropped without triage rights, so the template is the only path a drive-by contributor has to labels at all, and the API bypasses just that part. So closing this properly would need something server-side — e.g. a bot that labels anything opened without labels. For what it's worth, on the Cozystack side we currently use Dosu for this but aren't fully happy with it, and we've been kicking around moving to a nightly CronJob that sweeps issues and fills in whatever labels are missing — no strong opinion to offer yet, just sharing where a fellow CNCF project is on the same problem.

  8. added
    info-completedThe GH issue has received a reply from the author
    and removed
    need-more-infoMore information is required to further debug or fix the issue.
    on Jul 17, 2026
  9. youngnick commented on Jul 20, 2026

    @youngnick
    Contributor

    Thanks for that @lexfrei, I've been seeing it a bit more, especially with agent-opened issues, and wanted to understand where the issue was coming from.

  10. pinned this issue on Jul 20, 2026
  11. unpinned this issue on Jul 20, 2026
  12. jrajahalme commented on Jul 20, 2026

    @jrajahalme
    Member

    I was able to reproduce a pod getting stuck with "IP ... is already in use" error (with Codex running the repro) with these conditions:

    • CNI IP allocation is done before Cilium restart, getting the last available IP in a small /27 pod CIDR
    • Cilium agent is restarted, enabling CiliumEnvoyConfig (e.g., GW API) for the node, while it was not previously enabled
    • Restarted agent has no state about the ongoing CNI ADD, so the above allocated IP is available for the Ingress IP needed for GW API
    • CNI ADD proceeds to the endpoint create request, reconnecting with the new agent instance, with the above allocated IP that is already being used for the node's Ingress IP; Cilium endpoint PUT request fails, returning "IP ... is already in use" error
    • Thus sandbox creation fails for the pod, and any retried CNI ADDs also fail, as the node is now out of IPs
    • K8s may still consider the node schedule-able since the number of allocatable pods may be higher than the number of available IPs, and k8s would not know about the IPs needed for the router, health, and ingress
    • Any repeated CNI ADDs would then fail with "no more IPs available" rather than "IP ... is already in use", but the pod would be stuck nonetheless

    Some observations:

    • The (best effort) fix adopted by cozystack deleting the pod may get it scheduled on another node thus resolving the issue
    • There is no evidence that a Cilium API request (e.g., IP allocation or endpoint create) could be served before the ingress IP has already been allocated
    • The "IP ... is already in use" error is kind of a red herring, the real issue is that the node has no available IPs, as otherwise the repeated CNI ADD would succeed
    • Even if the IP was still available, the endpoint create on a different agent instance would still fail for other reasons (e.g., unknown expiration UUID), but then the repeated CNI ADD would succeed
    • Maybe tuning down kubelet --max-pods would prevent this from happening in the first place
    • Cilium fails to restart if node's pod CIDR is exhausted and the agent is restarted with a new configuration enabling GW API, as in this case Ingress IP allocation would fail

    I have issued #47360 to make Ingress IP restoration more robust, but that is mostly orthogonal with this issue. Should look into if we could make Endpoint PUT request to fail out earlier if the expiration UUID is unknown); this would improve diagnostics but not make the pod sandbox creation succeed in this case.

  13. lexfrei commented on Aug 11, 2026

    @lexfrei
    ContributorAuthor

    Correcting my own report: the downstream motivation I gave for this issue is wrong. The source analysis is not, and I want to be precise about which half I am withdrawing.

    What I wrote was "I hit this in Cozystack E2E, where it intermittently wedges pods during cluster bring-up". That is not what I hit.

    In that platform cilium is not the IPAM. It runs chained behind kube-ovn (cni.chainingMode: generic-veth, chainingTarget: kube-ovn), so pod addresses are allocated by kube-ovn and cilium only registers an endpoint for whatever address the chain hands it. kube-ovn allocates flat from its own subnet across all nodes and has no notion of Node.spec.podCIDR. Those two ranges were configured over the same prefix, so the address cilium had reserved for its per-node Ingress endpoint was, from kube-ovn's side, an ordinary free address in the pool. Sooner or later it went to a pod, and the agent refused the sandbox.

    No agent restart is involved and no window is needed. The evidence names both sides: cilium-dbg endpoint list on the node shows reserved:ingress holding the address, equal to that node's CiliumNode.spec.ingress.ipv4, and ovn-controller on the same node logs a claim on it for the pod that never got a sandbox. It is a configuration overlap between two allocators, and it is fixed downstream by making the ranges disjoint.

    What stands is everything in the body that comes from reading the code, and none of it depends on my deployment:

    • allocateIngressIPs() is gated only on EnableEnvoyConfig and takes the address from ipam.PoolDefault(), which in host-scope mode is the node Pod CIDR.
    • The host-scope allocator is an in-memory bitmap with no persistence, and RestoreFinished() is a no-op, so it starts empty on every agent start.
    • After a restart the bitmap is re-populated by two paths, restored local endpoints and the Ingress IP, and nothing orders either against the CNI server beginning to serve pod ADDs.

    For a cluster where cilium does run its own IPAM with Gateway API enabled, that window is still readable from the source, and the three fixes I suggested still apply to it. I am not asking for anything to be done with the issue either way; I just do not want it carried by a reproduction that turned out to be a different bug.

    What I can no longer offer is that reproduction. So the issue now rests on source reading rather than on an observed failure, which is weaker evidence than what I originally presented, and worth weighing as such.

  14. github-actions commented on Oct 10, 2026

    @github-actions

    This issue has been automatically marked as stale because it has not
    had recent activity. It will be closed if no further activity occurs.

  15. added
    staleThe stale bot thinks this issue is old. Add "pinned" label to prevent this from becoming stale.
    on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/agentCilium agent related.area/servicemeshGH issues or PRs regarding servicemeshfeature/k8s-ingressinfo-completedThe GH issue has received a reply from the authorkind/bugThis is a bug in the Cilium logic.kind/community-reportThis was reported by a user in the Cilium community, eg via Slack.staleThe stale bot thinks this issue is old. Add "pinned" label to prevent this from becoming stale.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions