Repository navigation
v0.4.0
[0.4.0] - 2026-10-09
Upgrade notes / operator action required
-
Controller RBAC. The controller now creates and reads a cluster-wide
flow-hash seed Secret,servicelb-flow-hash-seed, in--seed-namespace
(defaultkube-system).deploy/ships a namespacedRole/RoleBinding
(createon secrets,geton that one Secret) for the ServiceAccount. The
default deployment runs the controller as the CSR-mintedbeep-controller
user, which needs its own binding (deploy/README.md):kubectl create rolebinding beep-controller-csr-flow-hash-seed -n kube-system \ --role=servicelb-controller-flow-hash-seed --user=beep-controller
Without it the controller exits at startup (fail closed). Treat the Secret
as sensitive: anyone who can read it can precompute backend placement
(#190). -
Loader
--pin-diris now required. There is no default. Fixture mode
(--fixture) now prunes fronts andUPLINK_CONFIGrows absent from the
new invocation, and refuses the controller's pin directory (#198, #204). -
Pinned maps are recreated on upgrade, flushing conntrack. Pinned maps
whose type, key size, value size ormax_entriesdiffer from the new
definition are deleted and recreated at load time (one log line per map).
This release changes:CONFIG4 to 16 bytes (#190);NODE_ALLOW16 to 32
andPOD_TARGETS32 to 128 default entries (#158); the front maps are
consolidated intoFRONT_METAand a generation-taggedFRONT_ENDPOINTS
(#167);TARGET_PORTS,LB_FRONT_MAPandEGRESS_DROPSare removed (#156,
#167);REJECT_BUCKETis added (#206). In-flight connections are reset
once; plan the rollout accordingly. -
CLI changes.
--uplink-iface/--geneve-ifacereject names that are empty, contain
whitespace or:, or are otherwise invalid for an interface (#192, #197).- More
--uplink-ifacevalues thanUPLINK_CONFIGholds (8) are rejected
at startup instead of failing at map insert (#192). - New
--flow-hash-seed <u64>on the bare loader (random if omitted;
single-node rigs only) and--seed-namespaceon the controller (#190). - New
--node-allow-max-entriesand--pod-targets-max-entries; the
loader fails loud when one of those maps is full (#158). - The hidden test-only
evict-podone-shot takes several pod IPs (#179).
-
Behaviour change: an uplink whose ARPHRD type is neither Ethernet (1)
nor a known L3-only tunnel type (PPP, RAWIP, IPIP/IP6IP6, SIT, GRE/IP6GRE,
NONE) is refused. Previously every non-Ethernet type silently got an L2
header length of 0 (#171). -
Behaviour change: traffic to a front this node owns that has no ready
backends is now answered with a TCP RST or ICMP/ICMPv6 port-unreachable
instead of being passed to the host (#202). Replies are rate-limited
(#206) and the front stays in place at count 0 so pinned flows drain
(#208).
Added
- Reject path for owned fronts with no ready backends: TCP RST, or ICMP type
3/code 3 (v4) / ICMPv6 type 1/code 4 (UDP), so clients fail fast. Traffic
to fronts this node does not own is unchanged (#202). - Reject replies are rate-limited by a per-CPU token bucket (
REJECT_BUCKET):
100 replies/s with a burst of 25 per CPU, so the node-wide budget scales
with CPU count. Over budget the packet is dropped, never passed to the
host, so a spoofed-source flood is not reflected at the victim (#206). - Ingress pin-steering and seeded flow-hash backend selection in the
dataplane; selection is keyed by a cluster-wide seed so a client cannot
precompute source ports that pile onto one backend (#178, #190, #195). - Selective conntrack eviction: when a backend pod is removed, only its flows
are swept, in one pass per map (#134, #135, #179). - Controller dual-stack: honours
spec.ipFamilies, publishes dual-stack LB
ingress, parses dual-stack Nodes/EndpointSlices, picks underlay vs front
node addresses by role, and programs each front with a same-family backend
(#139, #141, #144, #154, #163). The dataplane's Geneve tunnel-key read is
family-aware for a v6 underlay (#140). - Configurable
NODE_ALLOW/POD_TARGETScapacities (#158). - Interim 6 MiB eBPF map-memory CI tripwire (#199).
- Smoke rigs: WireGuard-free 2-node cross-node, controller-driven dual-stack
and IPv6-only-node round trips, cold-neighbour-cache, forward-underlay-
follows-FIB, and deterministic restart-flow-continuity (#146, #166, #168,
#177, #180, #186).
Changed
- Front state is consolidated into
FRONT_METAplus a generation-tagged
FRONT_ENDPOINTStable; legacyvip_*identifiers are renamed to "front"
(#167, #169). One backend is still selected per front (see Known
Limitations). - The controller identifies the local node by Node identity rather than
--node-ipequality, and admits hostNetwork pod IPs against the endpoint
node's own addresses in every family (#157). - Non-reply uplink egress that misses
FLOW_TABLEnow passes instead of
being dropped; theEGRESS_DROPScounter is removed (#156). - Loader derives requested map shapes from the embedded ELF and no longer
does a probe load (#171). - eBPF object trimmed to fit the shared stack and size budgets; the
embedded-object gross-size ceiling is 160 KB (#195, #202). - Dependency and CI-action bumps (Renovate).
Fixed
- P1: an ingress node now drops its flows to a remote backend pod that
departs or whose IP is reused by a different pod, cluster-wide rather than
only for local pods. A failed sweep withholds only the affected IPs'
installs. Terminating-but-serving and probe-flapping pods keep their pinned
flows until they leave the slice (graceful drain); new flows select ready
endpoints only (#200). Draining backends are not swept early, and
cluster-wide departures wait for a complete slice set. - An owned front whose Service has no ready backends is kept at count 0
instead of being deleted, so new connections are refused while pinned flows
to draining pods keep working; the front is removed only when its Service
goes away. Repeated count-0 reconciles do not bump the generation (#208). - An EndpointSlice update that omits
endpoints(sent once the last pod is
gone) now empties the slice instead of being ignored, so the departed
endpoint and its pins are removed (#208). - Relists replace the watched set, so EndpointSlices deleted while the watch
was disconnected are removed. Implausible relists (empty Services or
slices, a Node list without this node, a paginated list) are refused and
retried, and an unresolved peer Node is not treated as a mass departure
(#203). - After a controller restart, no front, endpoint or pod-target rewrites occur
until Services, EndpointSlices and Nodes have all been listed, so live
fronts no longer drop to count 0 on a DaemonSet rollout. Once listed, a
one-time sweep removes pins to backends that left while the controller was
down (#209). - Departed pods stay pending until their flows are swept (#179).
- A failed reconcile is retried with backoff, and an armed retry is not
postponed by later events (#183). - The flow-hash seed converges on the stored Secret value after the Secret
changes (#193). - Pins are honoured before a count-0 front is rejected, and decap resolves a
drained front's target port from the prior generation (#202). CONFIGlayout and per-link-type uplink L2 header length are guarded; a
pinned map whose shape differs is recreated rather than silently kept
(#171, see upgrade notes).- The embedded eBPF object is rebuilt when
common/changes. - Stale
UPLINK_CONFIGrows and fixture fronts are pruned on loader restart
(#198).
Security
FLOW_TABLEvalues no longer carry uninitialised kernel stack bytes;
every field is fully initialised before insert (#185).- Backend selection is seeded with a cluster-wide secret, so external clients
cannot steer flows onto a chosen backend by choosing source ports (#190,
#193, #195).
Known Limitations
- One backend per front: the selection plumbing (hash, seed, pin-steering,
generation-tagged endpoints) is in, but multi-endpoint selection is not
enabled yet. - Decap resolves a drained front's target port from the prior generation as
an interim measure until the final design lands. It is reachable only on
count-0 fronts and survives one generation swap. - The reject rate limit is a compile-time constant (not a flag) and is per
CPU; the node-wide reply budget grows with CPU count. - The post-restart orphaned-pin sweep waits for a non-empty backend set, so
pins to departed backends persist while the cluster has no backends at
all.