A Kubernetes operator that manages dynamic IPv6 prefix delegation for bare-metal and home/SOHO Kubernetes clusters.
You can host services from as many IPv6 addresses as you want β until you can't.
IPv6 promises virtually unlimited addresses. With a /48 or /56 prefix, you could theoretically assign unique global addresses to every service, pod, and device in your infrastructure. No more NAT, no more port conflicts, just direct end-to-end connectivity.
Then reality hits.
Many residential and SOHO ISPs assign IPv6 prefixes dynamically. These prefixes change:
- Daily or weekly for "privacy" reasons
- After router reboots
- After DHCPv6 lease expiration
- Randomly, because ISPs gonna ISP
When your prefix changes from 2001:db8:1234::/64 to 2001:db8:5678::/64, everything breaks:
- LoadBalancer IPs become unreachable (Cilium LB-IPAM pools are static)
- DNS records point to stale addresses
- Firewall rules reference invalid CIDRs
- Network policies stop matching traffic
The "solution" many resort to? NAT66 β taking the beautiful end-to-end transparency of IPv6 and bolting the same ugly NAT architecture that made IPv4 a nightmare.
Kubernetes on bare-metal or at home/SOHO is increasingly popular:
- Talos Linux makes cluster management trivial
- Cilium provides powerful networking without cloud dependencies
- ArgoCD enables GitOps for home infrastructure
But all of this assumes stable IP addressing. Cloud providers give you static IPs. Your home ISP gives you a prefix that changes every time the wind blows.
Dynamic Prefix Operator bridges this gap by:
- Monitoring prefix changes via Router Advertisement observation
- Calculating address ranges from the received prefix automatically
- Updating Cilium resources (LoadBalancerIPPool, CIDRGroup) when prefixes change
- Managing graceful transitions to minimize service disruption
# Using Helm
helm install dynamic-prefix-operator oci://ghcr.io/jr42/dynamic-prefix-operator/helm/dynamic-prefix-operator
# Or using kubectl
kubectl apply -f https://github.com/jr42/dynamic-prefix-operator/releases/latest/download/install.yamlThe recommended approach for home/SOHO: reserve a portion of your /64 that your router won't hand out via DHCPv6/SLAAC.
apiVersion: dynamic-prefix.io/v1alpha1
kind: DynamicPrefix
metadata:
name: home-ipv6
spec:
acquisition:
routerAdvertisement:
interface: eth0
enabled: true
# Reserve ::f000:0:0:0 through ::ffff:ffff:ffff:ffff for Kubernetes services
# Configure your router to NOT assign addresses in this range via SLAAC/DHCPv6
addressRanges:
- name: loadbalancers
start: "::f000:0:0:0"
end: "::ffff:ffff:ffff:ffff"apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: ipv6-lb-pool
annotations:
dynamic-prefix.io/name: home-ipv6
dynamic-prefix.io/address-range: loadbalancers
spec:
blocks: [] # Operator manages thiskubectl get ciliumloadbalancerippool ipv6-lb-pool -o yaml
# spec.blocks now contains the actual address range from your prefix:
# - start: "2001:db8:1234:0:f000::"
# stop: "2001:db8:1234:0:ffff:ffff:ffff:ffff"When your prefix changes, the operator automatically updates all annotated pools.
Upstream Router / ISP
β
β Router Advertisement
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dynamic Prefix Operator β
β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
β β Prefix Receiver β β Pool Sync Controller β β
β β β β β β
β β β’ RA Monitor βββββββΆβ Updates pools that reference β β
β β β’ Prefix Detection β β DynamicPrefix via annotations: β β
β β β β β β
β βββββββββββββββββββββββ β β’ CiliumLoadBalancerIPPool β β
β β β β’ CiliumCIDRGroup β β
β βΌ βββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββ β β
β β DynamicPrefix CR β β β
β β β βΌ β
β β β’ Current prefix β βββββββββββββββββββββββββββββββββββ β
β β β’ Address ranges β β Pools with annotation: β β
β β β’ Lease state β β dynamic-prefix.io/name: xxx β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
For most home/SOHO setups, you receive a /64 prefix from your ISP. The operator lets you reserve a portion of that /64 for Kubernetes services.
How it works:
- Configure your router to NOT hand out addresses in a specific range (e.g.,
::f000:0:0:0to::ffff:ffff:ffff:ffff) - Tell the operator about this reserved range
- The operator monitors RAs for prefix changes and updates your Cilium pools with the full addresses
Advantages:
- Works with standard /64 allocations
- No BGP required
- Simple router configuration (just exclude a range from DHCPv6/SLAAC)
spec:
addressRanges:
- name: loadbalancers
start: "::f000:0:0:0" # Lower bound suffix
end: "::ffff:ffff:ffff:ffff" # Upper bound suffixWhen your ISP changes your prefix, the operator supports two transition modes to minimize service disruption:
Keeps multiple address blocks in pools during transitions. Services retain their old IPs until the historical blocks are removed.
spec:
transition:
mode: simple # Default
maxPrefixHistory: 2 # Keep 2 previous prefixes in pool blocksHow it works:
- Prefix changes from A β B
- Pool now has blocks for both prefix A and B
- Existing services keep their prefix-A IPs
- New services get prefix-B IPs
- After another prefix change (B β C), oldest block (A) is dropped
For zero-downtime transitions, HA mode manages both LoadBalancer IPs and DNS targeting:
spec:
transition:
mode: ha
maxPrefixHistory: 2How it works:
- Prefix changes from A β B
- Service gets both IPs via
lbipam.cilium.io/ipsannotation - DNS points to new IP only via
external-dns.alpha.kubernetes.io/target - Old connections continue working (both IPs active on Service)
- New clients connect to new IP via DNS
# HA Mode result on Service:
annotations:
lbipam.cilium.io/ips: "2001:db8:new::1,2001:db8:old::1" # Both IPs active
external-dns.alpha.kubernetes.io/target: "2001:db8:new::1" # DNS β new only| Annotation | Description |
|---|---|
dynamic-prefix.io/name |
Name of the DynamicPrefix CR (required) |
dynamic-prefix.io/service-address-range |
Which address range for IP calculation |
Add these annotations to Cilium resources to have them managed by the operator:
| Annotation | Description |
|---|---|
dynamic-prefix.io/name |
Name of the DynamicPrefix CR to reference |
dynamic-prefix.io/address-range |
Name of the address range to use |
- CiliumLoadBalancerIPPool β for Cilium LB-IPAM (
spec.blockswith start/stop) - CiliumCIDRGroup β for network policies (
spec.externalCIDRs)
apiVersion: dynamic-prefix.io/v1alpha1
kind: DynamicPrefix
metadata:
name: home-ipv6
spec:
# How to receive the prefix
acquisition:
routerAdvertisement:
interface: eth0 # Interface to monitor for RAs
enabled: true
# Address ranges within the /64 (recommended for home/SOHO)
addressRanges:
- name: loadbalancers
start: "::f000:0:0:0"
end: "::ffff:ffff:ffff:ffff"
# Transition settings
transition:
mode: simple # "simple" (default) or "ha" for high availability
maxPrefixHistory: 2 # Number of historical prefixes to retain in pool blocksstatus:
currentPrefix: "2001:db8:1234::/64"
prefixSource: "router-advertisement"
addressRanges:
- name: loadbalancers
start: "2001:db8:1234:0:f000::"
end: "2001:db8:1234:0:ffff:ffff:ffff:ffff"
conditions:
- type: PrefixAcquired
status: "True"
- type: PoolsSynced
status: "True"- Kubernetes 1.28+
- Cilium (for LB-IPAM pools)
hostNetwork: truefor the operator pod (to see Router Advertisements)NET_RAWcapability (for raw ICMPv6 sockets)
When your ISP changes your prefix:
- Detection: The RA receiver detects the new prefix within seconds
- Status Update: DynamicPrefix status is updated with new prefix and calculated ranges
- Pool Sync: All annotated Cilium pools are updated with both old and new blocks
- Service Sync (HA mode): Services get both IPs, DNS points to new IP only
- DNS Update: external-dns updates records based on Service IPs or target override
- Pools contain multiple blocks (current + historical prefixes)
- Existing Services keep their old IPs until pool blocks are pruned
- New Services get IPs from the current prefix block
- Services are updated with all active IPs (old + new)
- DNS target annotation ensures new clients get the new IP
- Old connections continue working until they naturally close
- Zero-downtime for properly configured setups
Recommendations:
- Use short DNS TTLs (60-300s) so clients get new IPs quickly
- Use HA mode if you need zero-downtime during prefix transitions
- Ensure your applications handle reconnection gracefully
- Monitor the
PrefixAcquiredcondition for alerting
- Core operator framework (kubebuilder)
- Router Advertisement monitoring
- Address range mode (within /64)
- Cilium LB-IPAM integration
- Cilium CIDRGroup integration
- Graceful prefix transitions (simple mode)
- HA mode with multi-IP Services and DNS targeting
- Subnet mode with BGP (carve /64s from larger prefix)
- DHCPv6-PD client (act as PD client)
- Calico IPPool backend
- MetalLB IPAddressPool backend
Contributions are welcome! See CONTRIBUTING.md for guidelines.
Apache License 2.0. See LICENSE for details.
- mdlayher/ndp β NDP/RA library
- 1Password Operator β Inspiration for annotation-based binding
- controller-runtime β Kubernetes controller framework