A Kubernetes operator that dynamically allocates public IP addresses from your ISP via DHCP and integrates them with Cilium's LoadBalancer IP pools. Perfect for home labs and edge deployments where you want to expose services with multiple public IPs without static IP assignments.
This operator bridges the gap between ISP-provided DHCP addresses and Kubernetes LoadBalancer services. It:
- Allocates Public IPs: SSHes into your router (UDM-Pro, pfSense, etc.) to create macvlan interfaces and obtain DHCP leases
- Updates Cilium Pools: Automatically adds allocated IPs to CiliumLoadBalancerIPPool resources
- Manages Lifecycle: Handles cleanup when IPs are released, including stopping DHCP daemons and removing router interfaces
- Integrates with BGP: Works with Cilium BGP to advertise routes dynamically (no static routes needed)
Perfect for homelabs where you have limited public IPs but want proper LoadBalancer support for services like Ingress controllers, game servers, or VPN endpoints
βββββββββββββββ     SSH      ββββββββββββββββ
β  Operator   ββββββββββββββ>β    Router    β
β   (K8s)     β              β  (UDM/etc)   β
βββββββββββββββ              ββββββββββββββββ
      β                             β
      β 1. Create macvlan           β 2. DHCP lease
      β    interface                β    from ISP
      β                             β
      β 3. Configure                β 4. Proxy ARP
      β    proxy ARP                β    enabled
      β                             β
      β 5. Add IP to Pool           β
      v                             v
βββββββββββββββ              ββββββββββββββββ
β   Cilium    β<βββ BGP ββββ>β  WAN/ISP     β
β  IP Pool    β              β  Network     β
βββββββββββββββ              ββββββββββββββββ
Traffic Flow: Internet β Router WAN (proxy ARP) β Router BGP table β K8s via LAN β Cilium LoadBalancer β Service
Infrastructure:
- Kubernetes v1.16+ cluster with Cilium installed
- Cilium BGP configured and peering with your router
- Router with SSH access (UDM-Pro, pfSense, Linux-based routers)
Development (optional - only needed if building from source):
- Go 1.24.0+
- Docker 17.03+
- kubectl 1.11.3+
Option A: Quick Install (Recommended - Uses pre-built images)
Deploy the operator directly from the release manifest:
kubectl apply -f https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.2.0/dist/install.yamlThis will:
- Create the cilium-dhcp-wanip-operator-systemnamespace
- Install the PublicIPClaimCRD
- Deploy the operator controller with image ghcr.io/serialx/cilium-dhcp-wanip-operator:v0.2.0
- Set up necessary RBAC permissions
Verify the installation:
kubectl get pods -n cilium-dhcp-wanip-operator-system
# You should see the controller manager pod runningOption B: Build and Deploy from Source
1. Install the router script
Copy the allocation script to your router:
scp config/samples/router-script-example.sh [email protected]:/data/cilium-dhcp-wanip-operator/alloc_public_ip.sh
# /data path is persistent on UDM Pro routers UniFi OS 2+
ssh [email protected] "chmod +x /data/cilium-dhcp-wanip-operator/alloc_public_ip.sh"2. Create SSH secret
ssh-keygen -t ed25519 -f ssh_id_m2m_router -N "" -C "cilium-dhcp-wanip-operator ssh key"
kubectl -n kube-system create secret generic router-ssh \
  --from-file=id_rsa=ssh_id_m2m_router3. Install CRDs
make install4. Deploy operator
export IMG=<your-registry>/cilium-dhcp-wanip-operator:latest
# Option A: Multi-arch build and push (recommended - supports AMD64, ARM64, s390x, ppc64le)
make docker-buildx IMG=$IMG
# Option B: Single-arch build (faster, builds for your host platform)
make docker-build docker-push IMG=$IMG
# Deploy to cluster
make deploy IMG=$IMGMulti-arch build options:
# Build for specific platforms only
make docker-buildx IMG=$IMG PLATFORMS=linux/amd64,linux/arm64
# Default platforms: linux/arm64,linux/amd64,linux/s390x,linux/ppc64le5. Create a Cilium IP Pool
kubectl apply -f config/samples/cilium-ippool-example.yaml6. Create a PublicIPClaim
apiVersion: network.serialx.net/v1alpha1
kind: PublicIPClaim
metadata:
  name: ip-wan-001
spec:
  poolName: public-pool
  router:
    host: 192.168.1.1
    user: root
    sshSecretRef: router-ssh
    command: /data/cilium-dhcp-wanip-operator/alloc_public_ip.sh
    wanParent: eth9  # Your router's WAN interfacekubectl apply -f config/samples/network_v1alpha1_publicipclaim.yaml7. Verify
kubectl get publicipclaims
# NAME         POOL          IP               PHASE   AGE
# ip-wan-001   public-pool   203.0.113.45     Ready   1mπ See DEPLOYMENT.md for detailed deployment instructions
- β Automatic IP Allocation: Creates macvlan interfaces and obtains DHCP leases via SSH
- β Cilium Integration: Updates CiliumLoadBalancerIPPool with allocated IPs
- β BGP-Ready: Works with Cilium BGP for dynamic route advertisement
- β Proxy ARP: Configures router to answer ARP for allocated IPs
- β Auto-Cleanup: Finalizers ensure proper cleanup on deletion
- β MAC Generation: Auto-generates unique MAC addresses for each claim
- β API Version Detection: Supports both Cilium v2 and v2alpha1 APIs
- β Status Tracking: Full status reporting with phase, IP, interface, and MAC
- β Automatic Reboot Recovery: Detects router reboots and automatically restores configuration (~40s)
- β SSH Connection Pooling: Efficient connection management with automatic reconnection
- β Periodic Verification: Validates router state every 60 minutes to detect configuration drift
- β Event-Driven Reconciliation: Reacts immediately to connection drops and router state changes
apiVersion: network.serialx.net/v1alpha1
kind: PublicIPClaim
metadata:
  name: ingress-ip
spec:
  poolName: public-pool
  router:
    host: 192.168.1.1
    user: root
    sshSecretRef: router-ssh
    wanParent: eth9apiVersion: network.serialx.net/v1alpha1
kind: PublicIPClaim
metadata:
  name: game-server-ip
spec:
  poolName: game-pool
  router:
    host: 192.168.1.1
    port: 22
    user: admin
    sshSecretRef: router-ssh
    command: /usr/local/bin/alloc_public_ip.sh
    wanParent: eth9
    wanInterface: wan-game
    macAddress: "02:aa:bb:cc:dd:01"See SPEC.md for complete architecture documentation including:
- Router script implementation (proxy ARP + Cilium BGP)
- CRD schema and validation
- Controller reconciliation logic
- Finalizer cleanup process
- Networking details (rp_filter, BGP routing, etc.)
Run locally:
make runBuild binary:
make buildBuild Docker images:
# Single-arch (for your host platform)
make docker-build IMG=<your-image>
# Multi-arch (cross-platform)
make docker-buildx IMG=<your-image>
# Multi-arch with custom platforms
make docker-buildx IMG=<your-image> PLATFORMS=linux/amd64,linux/arm64Run tests:
make testGenerate manifests:
make manifests generateWhen you're ready to release a new version:
1. Build and push the Docker image
The GitHub Actions workflow automatically builds and pushes images when you create a tag:
git tag -a v0.2.0 -m "Release v0.2.0 - Description of changes"
git push origin v0.2.0This will trigger the CI to build multi-platform images and push to ghcr.io/serialx/cilium-dhcp-wanip-operator:v0.2.0
2. Generate the installer manifest
After the CI completes, update the installer manifest with the new image:
make build-installer IMG=ghcr.io/serialx/cilium-dhcp-wanip-operator:v0.2.0This updates dist/install.yaml with the new image tag.
3. Commit and push the installer
git add dist/install.yaml config/manager/kustomization.yaml
git commit -m "chore: update installer manifest for v0.2.0"
git push origin main4. Create a GitHub Release
gh release create v0.2.0 \
  --title "v0.2.0" \
  --notes "Release notes here" \
  dist/install.yamlOr create it manually in the GitHub UI and attach dist/install.yaml.
Users can then install the new version:
kubectl apply -f https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.3.0/dist/install.yamlIf installed via Quick Install (Option A):
# Delete all claims first to ensure proper cleanup
kubectl delete publicipclaims --all
# Remove the operator
kubectl delete -f https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.2.0/dist/install.yamlIf installed from source (Option B):
# Delete all claims
kubectl delete publicipclaims --all
# Undeploy operator
make undeploy
# Remove CRDs
make uninstallCheck operator logs:
kubectl -n cilium-dhcp-wanip-operator-system logs deployment/cilium-dhcp-wanip-operator-controller-managerCheck claim status:
kubectl describe publicipclaim <name>Common issues:
- SSH authentication fails β Check SSH key in secret
- DHCP fails β Verify wanParentinterface name
- IP not added to pool β Check RBAC permissions for Cilium resources
The operator automatically detects and recovers from router reboots with no manual intervention required:
How It Works:
- SSH Connection Monitoring: Maintains persistent SSH connections to routers with keep-alive checks every 30 seconds
- Reboot Detection: Detects router reboots by monitoring uptime changes (~40 second worst-case detection time)
- Automatic Restoration: Immediately reapplies all configuration (interfaces, DHCP clients, proxy ARP) when reboot detected
- Periodic Verification: Validates router state every 60 minutes as a safety net to catch any configuration drift
- Connection Pooling: Multiple claims share a single SSH connection per router for efficiency
What This Means:
- β Router reboots are automatically handled
- β Services recover within ~40 seconds of router reboot
- β No manual intervention needed
- β Configuration drift is automatically corrected
- β Connection drops trigger immediate reconciliation
Observability:
# Check claim status to see last verification time
kubectl get publicipclaim my-claim -o yaml
# Status fields show:
# - lastVerified: timestamp of last successful verification
# - routerUptime: current router uptime in seconds
# - configurationVerified: whether config has been verified
# - lastReconciliationReason: why last reconciliation occurred
#   (router_reboot, interface_missing, periodic, etc.)Events: The operator emits Kubernetes events for key actions:
kubectl describe publicipclaim my-claim
# Events you may see:
# - RouterRebooted: Router reboot detected, reapplying configuration
# - ConfigurationApplied: Configuration applied successfully
# - ConfigurationDrift: Interface missing, reapplying configurationContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests: make test
- Submit a pull request
Run make help for all available make targets.
More information: Kubebuilder Documentation
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.