Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 08:59
· 396 commits to main since this release
1a71f52

[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
    (default kube-system). deploy/ ships a namespaced Role/RoleBinding
    (create on secrets, get on that one Secret) for the ServiceAccount. The
    default deployment runs the controller as the CSR-minted beep-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-dir is now required. There is no default. Fixture mode
    (--fixture) now prunes fronts and UPLINK_CONFIG rows 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 or max_entries differ from the new
    definition are deleted and recreated at load time (one log line per map).
    This release changes: CONFIG 4 to 16 bytes (#190); NODE_ALLOW 16 to 32
    and POD_TARGETS 32 to 128 default entries (#158); the front maps are
    consolidated into FRONT_META and a generation-tagged FRONT_ENDPOINTS
    (#167); TARGET_PORTS, LB_FRONT_MAP and EGRESS_DROPS are removed (#156,
    #167); REJECT_BUCKET is added (#206). In-flight connections are reset
    once; plan the rollout accordingly.

  • CLI changes.

    • --uplink-iface/--geneve-iface reject names that are empty, contain
      whitespace or :, or are otherwise invalid for an interface (#192, #197).
    • More --uplink-iface values than UPLINK_CONFIG holds (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-namespace on the controller (#190).
    • New --node-allow-max-entries and --pod-targets-max-entries; the
      loader fails loud when one of those maps is full (#158).
    • The hidden test-only evict-pod one-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_TARGETS capacities (#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_META plus a generation-tagged
    FRONT_ENDPOINTS table; legacy vip_* 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-ip equality, and admits hostNetwork pod IPs against the endpoint
    node's own addresses in every family (#157).
  • Non-reply uplink egress that misses FLOW_TABLE now passes instead of
    being dropped; the EGRESS_DROPS counter 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).
  • CONFIG layout 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_CONFIG rows and fixture fronts are pruned on loader restart
    (#198).

Security

  • FLOW_TABLE values 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.