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:
- Router Agent: Runs a dedicated HTTP API agent on your router (UDM-Pro, pfSense, etc.) for reliable DHCP lease management
- Allocates Public IPs: Creates macvlan interfaces and obtains DHCP leases via the agent API over SSH tunnel
- Updates Cilium Pools: Automatically adds allocated IPs to CiliumLoadBalancerIPPool resources
- Manages Lifecycle: Handles cleanup when IPs are released with graceful lease termination
- 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 Tunnel ββββββββββββββββ
β Operator ββββββββββββββ>β Router Agent β
β (K8s) β HTTP API β (systemd) β
βββββββββββββββ ββββββββββββββββ
β β
β 1. AllocateLease() β 2. Create macvlan
β via agent API β + DHCP lease
β β
β 3. GetLease() β 4. Track renewals
β verify status β + proxy ARP
β β
β 5. Add IP to Pool β 6. Unicast DHCP
v v renewal
βββββββββββββββ ββββββββββββββββ
β Cilium β<βββ BGP ββββ>β WAN/ISP β
β IP Pool β β Network β
βββββββββββββββ ββββββββββββββββ
Traffic Flow: Internet β Router WAN (proxy ARP) β Router BGP table β K8s via LAN β Cilium LoadBalancer β Service
v0.3.2+ uses a dedicated router agent that replaces SSH scripts with an HTTP API for better reliability and structured error handling.
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)
1. Deploy the operator:
kubectl apply -f https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.3.2/config/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.3.2 - Set up necessary RBAC permissions
2. Install the router agent (v0.3.2+):
The router agent is a systemd service that runs on your router. Download and install:
# Download the agent binary for your router architecture
# For UDM-Pro (ARM64):
wget https://github.com/serialx/cilium-dhcp-wanip-operator/releases/download/v0.3.2/dhcp-wan-agent-linux-arm64
ssh [email protected] "mkdir -p /data/dhcp-wan-agent/bin"
scp dhcp-wan-agent-linux-arm64 [email protected]:/data/dhcp-wan-agent/bin/dhcp-wan-agent
ssh [email protected] "chmod +x /data/dhcp-wan-agent/bin/dhcp-wan-agent"
# Install systemd service
curl -O https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.3.2/deploy/agent/dhcp-wan-agent.service
scp dhcp-wan-agent.service [email protected]:/etc/systemd/system/
ssh [email protected] "systemctl enable --now dhcp-wan-agent"Verify the agent is running:
ssh [email protected] systemctl status dhcp-wan-agentOption B: Build and Deploy from Source
1. Build and install the router agent:
# Build agent for your router architecture
GOOS=linux GOARCH=arm64 go build -o dhcp-wan-agent cmd/agent/main.go
# Deploy to router
scp dhcp-wan-agent [email protected]:/data/dhcp-wan-agent/bin/
scp deploy/agent/dhcp-wan-agent.service [email protected]:/etc/systemd/system/
ssh [email protected] "systemctl enable --now dhcp-wan-agent"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
v0.3.2+ Router Agent Integration:
- β HTTP API Agent: Dedicated systemd service on router with structured JSON responses
- β Automatic DHCP Renewal: Agent tracks and renews leases automatically via unicast (no broadcast storms!)
- β Graceful Cleanup: Proper lease release via agent API on deletion
- β Renewal Tracking: Monitor DHCP renewal count in claim status
- β SSH Tunnel Security: All agent communication over SSH tunnel
Core Features:
- β Automatic IP Allocation: Creates macvlan interfaces and obtains DHCP leases
- β 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
- β SSH Connection Pooling: Efficient connection management with automatic reconnection
- β Periodic Verification: Validates router state every 60 minutes via agent API
- β 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 agent HTTP API design (v0.3.2+)
- Router script implementation (proxy ARP + Cilium BGP) - legacy
- CRD schema and validation
- Controller reconciliation logic
- Finalizer cleanup process
- Networking details (rp_filter, BGP routing, etc.)
See ROUTER_AGENT_DESIGN.md for router agent implementation details (v0.3.2+)
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.3.2 -m "Release v0.3.2 - Description of changes"
git push origin v0.3.2This will trigger the CI to build multi-platform images and push to ghcr.io/serialx/cilium-dhcp-wanip-operator:v0.3.2
2. Build and release the router agent
Build agent binaries for multiple architectures:
# Build for common platforms
GOOS=linux GOARCH=arm64 go build -o dhcp-wan-agent-linux-arm64 cmd/agent/main.go
GOOS=linux GOARCH=amd64 go build -o dhcp-wan-agent-linux-amd64 cmd/agent/main.go3. 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.3.2This updates config/install.yaml with the new image tag.
4. Commit and push the installer
git add config/install.yaml config/manager/kustomization.yaml
git commit -m "chore: update installer manifest for v0.3.2"
git push origin main5. Create a GitHub Release
gh release create v0.3.2 \
--title "v0.3.2 - Router Agent HTTP API Integration" \
--notes "Release notes here" \
dhcp-wan-agent-linux-arm64 \
dhcp-wan-agent-linux-amd64Or create it manually in the GitHub UI and attach the agent binaries.
Users can then install the new version:
kubectl apply -f https://raw.githubusercontent.com/serialx/cilium-dhcp-wanip-operator/v0.3.2/config/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.3.2/config/install.yaml
# Stop and remove the router agent
ssh [email protected] "systemctl disable --now dhcp-wan-agent"
ssh [email protected] "rm -f /etc/systemd/system/dhcp-wan-agent.service"If 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.