From 143f358f6b1aafeed94032a64d08794cf84b3c3e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin <3811295@gmail.com> Date: Wed, 19 Aug 2026 14:16:43 +0300 Subject: [PATCH 1/2] docs(design-proposals): record LB announcer vendor-neutrality decision MetalLB stays the shipped default, Service.spec.loadBalancerClass is the integration point for alternative implementations, and Cilium L2 Announcements is not adopted as the platform announcer. The decision was executed in July 2026; this records the reasoning and the upstream conditions under which the announcer question reopens. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com> --- .../README.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 design-proposals/loadbalancer-announcer-neutrality/README.md diff --git a/design-proposals/loadbalancer-announcer-neutrality/README.md b/design-proposals/loadbalancer-announcer-neutrality/README.md new file mode 100644 index 0000000..e1aa2af --- /dev/null +++ b/design-proposals/loadbalancer-announcer-neutrality/README.md @@ -0,0 +1,124 @@ +# Vendor neutrality of the default LoadBalancer announcer + +- **Title:** `Vendor neutrality of the default LoadBalancer announcer` +- **Author(s):** `@lexfrei` +- **Date:** `2026-08-19` +- **Status:** Accepted +- **Decision date:** `2026-07-17` + + + +## Overview + +Cozystack ships MetalLB as a default system package and configures no announcer with it: no chart in the tree renders an `IPAddressPool`, `L2Advertisement`, `BGPAdvertisement`, `CiliumLoadBalancerIPPool` or `CiliumL2AnnouncementPolicy`. The admin stands up the pool and the announcer that suit the environment, and per-Service selection of which controller handles a `type: LoadBalancer` Service is native Kubernetes `Service.spec.loadBalancerClass`. This document records three decisions taken together in July 2026 and the evidence behind them: MetalLB stays the shipped default; `loadBalancerClass` is the platform's integration point for any other LoadBalancer implementation; Cilium L2 Announcements is not adopted as the platform announcer at this time. + +The framing that ties them together is what vendor neutrality means for a load-balancer datapath. Neutrality is a supported, documented mechanism for plugging in an alternative — not the absence of a default. A platform with no default forces every operator to solve the same problem before their first Service becomes reachable; a platform whose default cannot be displaced is locked in. Cozystack takes the third position: one default that works out of the box, and a native Kubernetes field that displaces it without a platform-owned API in the middle. + +## Scope and related proposals + +- **Executed by:** [cozystack/cozystack#3218](https://github.com/cozystack/cozystack/pull/3218) (merged 2026-07-16) — removes the `network.cozystack.io` API group in favour of native `loadBalancerClass`; [cozystack/cozystack#2586](https://github.com/cozystack/cozystack/issues/2586) (closed 2026-07-17) — the epic proposing the MetalLB-to-Cilium announcer migration, closed without adoption. +- **Discussion of record:** [cozystack/cozystack#3164](https://github.com/cozystack/cozystack/issues/3164) — the open question of whether a platform-owned exposure API group is needed at all, given that vendor-neutral backend selection is already native. This document answers the exposure half of it and does not touch the second half (whether `sdn.cozystack.io` and an exposure surface must be separate API groups). +- **Superseded by this decision:** [cozystack/cozystack#3081](https://github.com/cozystack/cozystack/pull/3081) — `ExposureClass` + `ServiceExposure`, the earlier platform-owned abstraction, merged 2026-06-30 and removed sixteen days later. It never shipped in a release. +- **Complementary, not competing:** [community#35](https://github.com/cozystack/community/pull/35) — public addresses as a first-class resource (`PublicIP` / `PublicIPClaim` / `PublicIPClass`). That proposal is about *owning an address across workload lifetimes*, a capability neither `loadBalancerClass` nor any announcer provides. It sits above this decision and consumes it; it is open at the time of writing. +- **Out of scope:** BGP announcement, Gateway API address allocation, and per-tenant egress identity. + +## Context + +### What is on `main` + +MetalLB is installed by every networking-carrying platform variant — `cozystack.metallb` is emitted from the common system package set in [`packages/core/platform/templates/_helpers.tpl`](https://github.com/cozystack/cozystack/blob/main/packages/core/platform/templates/_helpers.tpl) — and the vendored chart tracks upstream at `appVersion: v0.16.1` ([`packages/system/metallb/charts/metallb/Chart.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/system/metallb/charts/metallb/Chart.yaml)). What that install does *not* include is any announcer configuration. [`packages/extra/gateway/README.md`](https://github.com/cozystack/cozystack/blob/main/packages/extra/gateway/README.md) states it plainly: "Cozystack itself ships MetalLB installed but does not render any `IPAddressPool` / `L2Advertisement` / `BGPAdvertisement` from this chart; admins set up the allocator that suits their environment (MetalLB pool with L2 / BGP, Cilium LB-IPAM with announcer, robotlb against a cloud provider, or `Service.spec.externalIPs` pinning)." + +The integration point for a third-party implementation is `publishing.loadBalancerClass` in [`packages/core/platform/values.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/core/platform/values.yaml), which sets `spec.loadBalancerClass` on the host-ingress Service. Its documentation in that file spells out the contract — the admin provisions the matching address pool and announcer out of band, and an empty value lets the cluster's default LoadBalancer controller claim the Service. Both candidate implementations honour the field: MetalLB via `--lb-class`, wired in the vendored chart at [`packages/system/metallb/charts/metallb/templates/controller.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/system/metallb/charts/metallb/templates/controller.yaml), and Cilium via the announcement policy's selection rule, which requires a Service's `loadBalancerClass` to be either unset or `io.cilium/l2-announcer` ([Cilium L2 Announcements documentation](https://docs.cilium.io/en/stable/network/l2-announcements/)). The platform vendors Cilium 1.19.5 ([`packages/system/cilium/charts/cilium/Chart.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/system/cilium/charts/cilium/Chart.yaml)), so the Cilium-side machinery is present and can be enabled by an admin without any platform change. + +### How the decision was reached + +Two threads converged. The first was a platform-owned exposure API: [#3081](https://github.com/cozystack/cozystack/pull/3081) introduced `network.cozystack.io/v1alpha1` with `ExposureClass` (admin-owned, binds a class name to one of `externalIPs`, `metallb`, `cilium`, `robotlb`) and `ServiceExposure` (tenant intent), plus a controller rendering the pool and announcer per class. [#3164](https://github.com/cozystack/cozystack/issues/3164) asked the load-bearing question the day after it merged — what does `ServiceExposure` provide that `Service.spec.loadBalancerClass` plus an admin-provisioned pool does not? — and observed that two of the four backends rendered nothing at all. [#3218](https://github.com/cozystack/cozystack/pull/3218) answered by deleting the API group and its reconciler, keeping only the native field. Because `network.cozystack.io` had never appeared in a release, the removal needed no migration. + +The second thread was the announcer itself. [#2586](https://github.com/cozystack/cozystack/issues/2586) proposed replacing MetalLB with Cilium LB-IPAM plus `CiliumL2AnnouncementPolicy` in four phases, ending in the removal of `packages/system/metallb/`. It was closed on 2026-07-17 for a reason that is structural rather than a change of preference: its own premise did not hold. The epic's Phase-0 prerequisite never landed (`publishing.exposure=loadBalancer` was reverted in `f919afe15`), the tenant-chart file it referenced does not exist on `main`, and — the part that invalidates the framing — "MetalLB is our announcer, migrate it to Cilium" was never accurate, because there is no announcer configuration in the tree to migrate ([closing comment](https://github.com/cozystack/cozystack/issues/2586#issuecomment-5005241595)). + +A month later, a full re-examination of the upstream state of Cilium L2 Announcements was appended to the same issue ([research comment, 2026-08-17](https://github.com/cozystack/cozystack/issues/2586#issuecomment-5314819092)). That comment is the substance of the Rationale below and the reason this decision is recorded as settled rather than merely postponed. + +### The problem + +Without this written down, the same three questions recur on every networking pull request. Does shipping MetalLB by default make Cozystack MetalLB-specific? If an operator runs Cilium anyway, why is a second LoadBalancer controller installed? And when the CNI grows a native announcer, is the platform obliged to adopt it? The answers exist but have been scattered across a merged pull request, a closed epic, and a comment thread. + +## Decision + +**1. MetalLB remains the default LoadBalancer implementation shipped by the platform.** It is installed as a system package on every networking-carrying variant, with no pool and no announcement configured. It is a working implementation waiting for the admin's address plan, not a platform-imposed datapath. + +**2. `Service.spec.loadBalancerClass` is the platform's integration point for any other implementation.** Selection of the controller that handles a `type: LoadBalancer` Service is native Kubernetes and requires no Cozystack API. `publishing.loadBalancerClass` exposes it for the host ingress; managed applications that render their own `type: LoadBalancer` Service carry the field the same way. The platform owns no exposure API group, no backend enum, and no per-class reconciler — those were deliberately removed in [#3218](https://github.com/cozystack/cozystack/pull/3218). + +**3. Cilium L2 Announcements is not adopted as the platform announcer at this time.** The feature is available to any operator who wants it — set `l2announcements.enabled=true`, write a `CiliumL2AnnouncementPolicy`, and point Services at the `io.cilium/l2-announcer` class — but the platform neither enables it by default nor treats it as the migration target for MetalLB. This is a decision about the *default*, not a judgement that the feature is unusable. + +Neutrality is discharged by clause 2. Clauses 1 and 3 are engineering choices about which implementation the platform ships and supports, and they are revisable on the evidence below without touching the mechanism. + +## Rationale + +Each item below is a finding from the [2026-08-17 survey](https://github.com/cozystack/cozystack/issues/2586#issuecomment-5314819092), re-verified against the upstream sources cited. + +**The beta label is deliberate, and its author says so.** [cilium/cilium#47775](https://github.com/cilium/cilium/issues/47775) asked, in August 2026, what it would take to graduate L2 Announcements to stable. The feature's original author and maintainer answered that two shortcomings hold it back: the per-service lease mechanism, which "doesn't scale and can put significant pressure on the API server", and doing ARP replies in BPF, "which while a deliberate choice originally, is something I regret" ([comment](https://github.com/cilium/cilium/issues/47775#issuecomment-5216240818)). He states it never sat right with him to call the feature stable "until we figured out a better way to implement the feature". No redesign proposal exists. A platform default should not be built on a feature whose maintainer is publicly unwilling to call it stable. + +**Switching announcers would be a regression for `externalTrafficPolicy: Local`, not a wash.** The epic body assumed both implementations were equally incompatible with `Local` "by construction". They are not. MetalLB restricts the announcement candidate set to nodes that actually have serving endpoints — [`speaker/layer2_controller.go`](https://github.com/metallb/metallb/blob/main/speaker/layer2_controller.go) switches `availableNodes` to `nodesWithEndpoint(...)` when the Service requests `Local`. Cilium's announcer ignores the field; its own documentation lists the incompatibility as a limitation, warning that service IPs may be announced on nodes without pods, causing traffic drops ([L2 Announcements documentation](https://docs.cilium.io/en/stable/network/l2-announcements/)). The gap has been open as [cilium/cilium#27800](https://github.com/cilium/cilium/issues/27800) since August 2023, and the fix [cilium/cilium#46399](https://github.com/cilium/cilium/pull/46399) is unmerged. + +**The bug that breaks the normal creation path is fixed only in 1.20.** A Service observed by the announcer before its LoadBalancer frontend is written is never announced until an agent restart. That ordering is the ordinary sequence for a newly created LoadBalancer Service. The fix, [cilium/cilium#47579](https://github.com/cilium/cilium/pull/47579), merged on 2026-07-29 carrying `backport-done/1.20` and no 1.19 backport label. The platform vendors Cilium 1.19.5, so adopting the announcer today means adopting it with this defect. + +**Lease load scales with services and is silent when it bites.** Cilium's own sizing guidance gives the formula `QPS = #services * (1 / leaseRenewDeadline)` and works an example of 65 services at a 2 s renew deadline reaching 32.5 QPS ([Sizing client rate limit](https://docs.cilium.io/en/stable/network/l2-announcements/)). The documentation states the default client rate limit as 5 QPS with bursts to 10; the vendored chart's own defaults are 10 QPS and 20 burst ([`values.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/system/cilium/charts/cilium/values.yaml)), so the guidance and the shipped defaults do not even agree on the starting point. The failure mode when the limit is undersized is not an error: the documentation's own troubleshooting example shows it as an info-level "Waited for 1.39s due to client-side throttling" line, while the operator sees random VIP outages. + +**Failover is slower and announced less persistently than MetalLB's.** With default lease timings, Cilium documents failover as occurring between 10 s and 20 s, and the new holder sends "a gratuitous ARP reply" — one, once ([Failover](https://docs.cilium.io/en/stable/network/l2-announcements/)). MetalLB re-sends the gratuitous announcement on a 1100 ms ticker for 5 s after a takeover ([`internal/layer2/announcer.go`](https://github.com/metallb/metallb/blob/main/internal/layer2/announcer.go)), which matters precisely for the clients that ignore a single unsolicited reply. + +**There is nothing to alert on.** The only L2-announcement metric in the Cilium tree is a feature-enablement gauge — `ACLBL2LBEnabled`, "L2 LB announcement enabled on the agent", in [`pkg/metrics/features/metrics.go`](https://github.com/cilium/cilium/blob/main/pkg/metrics/features/metrics.go). There is no counter for lease acquisitions, transitions, or renewal failures, so the reported failure mode (lease churn) is not observable through metrics at all. + +**Two structural defects were closed by the stale bot rather than fixed.** A lease held by a powered-off node is not reclaimed ([cilium/cilium#32148](https://github.com/cilium/cilium/issues/32148), closed as not planned in September 2024, carrying the `stale` label), and there is no health check of the announcing path beyond the lease itself ([cilium/cilium#35507](https://github.com/cilium/cilium/issues/35507), closed as not planned in January 2025, likewise `stale`). Closed-as-stale is not fixed; both behaviours are still what the code does. + +**The 1.19 branch has an unfinished upgrade story for this feature.** [cilium/cilium#44222](https://github.com/cilium/cilium/issues/44222) — L2 announcement with a sharing key broken by the 1.18.6-to-1.19 upgrade — is open, with the reporter noting that rollback did not recover without node restarts. The IPv6/NDP work that landed on 1.19 brought multicast regressions with it; the solicited-node responder fix [cilium/cilium#46332](https://github.com/cilium/cilium/pull/46332) (for [cilium/cilium#46274](https://github.com/cilium/cilium/issues/46274)) merged only at the end of June 2026 and was backported to 1.19 after that. + +**On Talos specifically, the feature has broken twice on platform bumps alone.** Kernel 6.12 consumes ARP between XDP and TC, with `loadBalancer.acceleration: best-effort` as the workaround ([cilium/cilium discussion #36829](https://github.com/cilium/cilium/discussions/36829)), and a BPF verifier regression required a Talos patch release to clear ([cilium/cilium#44216](https://github.com/cilium/cilium/issues/44216)). Related, and unresolved: ARP requests arriving on VLAN sub-interfaces are processed by the network stack before reaching the BPF program ([cilium/cilium#37959](https://github.com/cilium/cilium/issues/37959), closed as not planned), which is the concrete consequence of the ARP-in-BPF design the maintainer named above. Talos is Cozystack's primary node platform, so a feature that regresses on kernel bumps is a poor fit for the default path. + +**A lease-name collision is reachable with generated namespace and service names.** The lease name is a plain concatenation — `fmt.Sprintf("%s-%s-%s", leasePrefix, namespace, name)` with `leasePrefix = "cilium-l2announce"` in [`pkg/l2announcer/l2announcer.go`](https://github.com/cilium/cilium/blob/main/pkg/l2announcer/l2announcer.go). Namespace `foo` with Service `bar-baz` and namespace `foo-bar` with Service `baz` produce the same lease object, putting two unrelated Services into one leader election with two renew loops writing the same object. Cozystack generates tenant namespace and release names, so this is reachable rather than theoretical. It is not reported upstream and should be filed before any adoption. + +**The field evidence points one way, and the incumbent is not stagnant.** The survey found no production-scale reports with measured failover — every success story located was a homelab — and counted four independent rollbacks from Cilium L2 back to MetalLB with none in the other direction. Meanwhile MetalLB is actively maintained: [v0.16.0](https://github.com/metallb/metallb/releases/tag/v0.16.0) shipped in May 2026, and the vendored chart already tracks the 0.16.1 follow-up. There is no maintenance pressure forcing a move. + +**Declining the migration costs nothing in neutrality terms.** Because selection runs through `loadBalancerClass`, an operator who wants Cilium L2 Announcements enables it on their own cluster and points Services at `io.cilium/l2-announcer` without a platform change, a fork, or a feature request. That is the whole point of clause 2: the default is a starting position, not a constraint. + +### The CNCF framing + +Cozystack is a [CNCF Sandbox project](https://www.cncf.io/sandbox-projects/) with an incubation application in flight ([cncf/toc#1916](https://github.com/cncf/toc/issues/1916), with governance and technical reviews tracked in [cncf/toc#2270](https://github.com/cncf/toc/issues/2270) and [cncf/toc#2271](https://github.com/cncf/toc/issues/2271)). The incubation application template requires that "governance clearly documents [vendor-neutrality](https://contribute.cncf.io/maintainers/community/vendor-neutrality/) of project direction" and that project metadata and resources are vendor-neutral ([template](https://github.com/cncf/toc/blob/main/.github/ISSUE_TEMPLATE/template-incubation-application.md)). The CNCF's own guidance covers communication, hosting, architecture decisions, and governance — it does not say a project may not have defaults, and a reading that forbade defaults would be unusable for an infrastructure platform. + +The operative test is therefore about the mechanism, not the default: an adopter or a competing implementation must be able to displace a shipped component through a documented, supported interface, without a fork and without asking the project for permission. `Service.spec.loadBalancerClass` is upstream Kubernetes, honoured by MetalLB, Cilium, and cloud LoadBalancer controllers alike. Choosing it over the platform-owned `ExposureClass` enum — which would have made "which announcers are supported" a Cozystack decision encoded in a Cozystack API — is what makes this decision a neutrality decision rather than a preference. + +## Consequences + +The platform carries a LoadBalancer controller that does nothing until an admin configures a pool. That is the deliberate trade: a working, unopinionated default beats either a broken-out-of-the-box experience or a hard binding to the CNI's datapath. An operator running Cilium LB-IPAM with a Cilium announcer keeps an idle MetalLB controller and speaker DaemonSet; whether the dormant default install is worth dropping on its own is a separate, smaller question, explicitly left open by the [closing comment](https://github.com/cozystack/cozystack/issues/2586#issuecomment-5005241595) on #2586. + +There is no platform API for exposure, and that is now a commitment rather than an accident. Anything that wants to sit above a LoadBalancer Service — quotas, address ownership, per-tenant pools — builds on the native Service surface, as [community#35](https://github.com/cozystack/community/pull/35) does. Reintroducing a platform-owned announcer enum would need to overturn both [#3164](https://github.com/cozystack/cozystack/issues/3164) and this record. + +The address plan stays the admin's responsibility. Cozystack documents the integration point and the immutability constraint on `spec.loadBalancerClass` — on a cluster whose host-ingress Service already exists, setting, changing, or clearing the value requires recreating that Service once, and the chart fails early with the exact command ([`values.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/core/platform/values.yaml)) — but it does not choose the pool, the announcement protocol, or the upstream routing design. + +Cozystack tracks Cilium L2 Announcements as an upstream dependency it may adopt later, not as a rejected technology. The triggers below are what "later" means concretely, and each one is checkable against a public source rather than a judgement call. + +## Revisit triggers + +Any one of the following reopens the announcer decision on its own merits: + +- **Cilium ≥ 1.20 in a stable branch the platform vendors, carrying the frontend-ordering fix.** [cilium/cilium#47579](https://github.com/cilium/cilium/pull/47579) is backported to 1.20 only; the platform is on 1.19.5. The trigger fires when Cozystack's vendored Cilium reaches a 1.20.x release that includes it. +- **A CFP or accepted design for redesigning the lease model.** The maintainer statement in [cilium/cilium#47775](https://github.com/cilium/cilium/issues/47775) names the per-service lease mechanism as a blocker to stability and notes no redesign exists. A concrete proposal to replace it changes the scaling argument. +- **A change in the feature's stability status.** Graduation of L2 Announcements out of beta — tracked in [cilium/cilium#47775](https://github.com/cilium/cilium/issues/47775) — removes the objection stated first in the Rationale. +- **A fix for `externalTrafficPolicy: Local`.** [cilium/cilium#46399](https://github.com/cilium/cilium/pull/46399) merging and shipping removes the regression that currently makes a switch strictly worse for those Services. +- **Operational metrics for the announcer.** Any counter for lease acquisitions, transitions, or renewal failures makes the reported failure mode alertable and removes the observability objection. +- **A change on the MetalLB side.** If MetalLB's maintenance stalls — no releases, unaddressed CVEs — the "no urgency" half of the argument expires even if nothing changes upstream in Cilium. + +Two things that are explicitly *not* triggers: a Cozystack release adding more managed applications that render LoadBalancer Services (this raises lease pressure, an argument against adoption, not for it), and an individual operator successfully running Cilium L2 Announcements on their cluster (already supported through `loadBalancerClass`, and requires no platform change). + +## Alternatives considered + +**Migrate the platform to Cilium LB-IPAM plus `CiliumL2AnnouncementPolicy` and drop MetalLB.** This was the four-phase plan in [#2586](https://github.com/cozystack/cozystack/issues/2586), and the attraction was real: one fewer controller, one fewer CRD set, one fewer chart to track. Rejected on the evidence in the Rationale, and separately on the fact that the epic's stated framing — migrating an existing announcer configuration — described something that does not exist in the tree. + +**A platform-owned exposure API (`ExposureClass` + `ServiceExposure`).** Merged as [#3081](https://github.com/cozystack/cozystack/pull/3081) and removed by [#3218](https://github.com/cozystack/cozystack/pull/3218) sixteen days later. It duplicated `loadBalancerClass` for the selection half, two of its four backends rendered nothing, and its one unique capability — dynamic per-namespace scoping of a shared VIP pool — did not justify a second tenant-networking API group next to `sdn.cozystack.io` ([#3164](https://github.com/cozystack/cozystack/issues/3164)). It never shipped in a release, so removing it required no migration. + +**Ship no LoadBalancer implementation at all and require the admin to install one.** Maximally neutral on paper, and worse in practice: every new cluster would need an out-of-band install before its first Service becomes reachable, and the platform would lose a known-good reference path for its own install documentation and e2e tests. Neutrality is satisfied by a displaceable default, not by an absent one. + +**Keep MetalLB but wrap it in a platform abstraction so the announcer can be swapped later.** This is the previous alternative in a different shape, and it fails the same way — it moves the choice of which implementations are supported into a Cozystack-owned enum, which is precisely the lock-in that `loadBalancerClass` avoids. From bb6fb76914f2124aa74999119ee1b1c48b9ceaa9 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin <3811295@gmail.com> Date: Wed, 19 Aug 2026 16:03:21 +0300 Subject: [PATCH 2/2] docs(design-proposals): link the filed lease-collision issue The lease-name collision is now reported upstream; replace the should-be-filed note with the issue link. Signed-off-by: Aleksei Sviridkin <3811295@gmail.com> --- design-proposals/loadbalancer-announcer-neutrality/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-proposals/loadbalancer-announcer-neutrality/README.md b/design-proposals/loadbalancer-announcer-neutrality/README.md index e1aa2af..02e72bd 100644 --- a/design-proposals/loadbalancer-announcer-neutrality/README.md +++ b/design-proposals/loadbalancer-announcer-neutrality/README.md @@ -78,7 +78,7 @@ Each item below is a finding from the [2026-08-17 survey](https://github.com/coz **On Talos specifically, the feature has broken twice on platform bumps alone.** Kernel 6.12 consumes ARP between XDP and TC, with `loadBalancer.acceleration: best-effort` as the workaround ([cilium/cilium discussion #36829](https://github.com/cilium/cilium/discussions/36829)), and a BPF verifier regression required a Talos patch release to clear ([cilium/cilium#44216](https://github.com/cilium/cilium/issues/44216)). Related, and unresolved: ARP requests arriving on VLAN sub-interfaces are processed by the network stack before reaching the BPF program ([cilium/cilium#37959](https://github.com/cilium/cilium/issues/37959), closed as not planned), which is the concrete consequence of the ARP-in-BPF design the maintainer named above. Talos is Cozystack's primary node platform, so a feature that regresses on kernel bumps is a poor fit for the default path. -**A lease-name collision is reachable with generated namespace and service names.** The lease name is a plain concatenation — `fmt.Sprintf("%s-%s-%s", leasePrefix, namespace, name)` with `leasePrefix = "cilium-l2announce"` in [`pkg/l2announcer/l2announcer.go`](https://github.com/cilium/cilium/blob/main/pkg/l2announcer/l2announcer.go). Namespace `foo` with Service `bar-baz` and namespace `foo-bar` with Service `baz` produce the same lease object, putting two unrelated Services into one leader election with two renew loops writing the same object. Cozystack generates tenant namespace and release names, so this is reachable rather than theoretical. It is not reported upstream and should be filed before any adoption. +**A lease-name collision is reachable with generated namespace and service names.** The lease name is a plain concatenation — `fmt.Sprintf("%s-%s-%s", leasePrefix, namespace, name)` with `leasePrefix = "cilium-l2announce"` in [`pkg/l2announcer/l2announcer.go`](https://github.com/cilium/cilium/blob/main/pkg/l2announcer/l2announcer.go). Namespace `foo` with Service `bar-baz` and namespace `foo-bar` with Service `baz` produce the same lease object, putting two unrelated Services into one leader election with two renew loops writing the same object. Cozystack generates tenant namespace and release names, so this is reachable rather than theoretical. Filed upstream as [cilium/cilium#48084](https://github.com/cilium/cilium/issues/48084). **The field evidence points one way, and the incumbent is not stagnant.** The survey found no production-scale reports with measured failover — every success story located was a homelab — and counted four independent rollbacks from Cilium L2 back to MetalLB with none in the other direction. Meanwhile MetalLB is actively maintained: [v0.16.0](https://github.com/metallb/metallb/releases/tag/v0.16.0) shipped in May 2026, and the vendored chart already tracks the 0.16.1 follow-up. There is no maintenance pressure forcing a move.