This guide explains how to deploy ESDDNS on bare-metal (on-premises) Kubernetes clusters using MetalLB for LoadBalancer support.
MetalLB provides LoadBalancer service type capability for Kubernetes clusters that don’t run on cloud providers (AWS, GCP, Azure). This is essential for on-premises deployments.
✅ LoadBalancer Service Type - Enables cloud-like LoadBalancer on bare-metal
✅ Layer 2 Mode - Simple ARP-based IP advertisement (same-subnet only)
✅ BGP Mode - Enterprise-grade routing with multi-subnet support
✅ STUN Integration - Works perfectly with ESDDNS’s STUN-based WAN IP detection
✅ No Vendor Lock-in - Open-source, works on any Kubernetes cluster
On-Premises Network
┌────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ ESDDNS Operator (DaemonSet) │ │
│ │ • Detects WAN IP via STUN protocol │ │
│ │ • Updates Gandi DNS with public IP │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ ESDDNS Service (LoadBalancer) │ │
│ │ • Type: LoadBalancer │ │
│ │ • MetalLB assigns IP: 192.168.1.100 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ MetalLB Speaker (DaemonSet) │ │
│ │ • Advertises 192.168.1.100 on local network │ │
│ │ • Layer 2: ARP responses │ │
│ │ • BGP: Routes to upstream routers │ │
│ └──────────────────────────────────────────────┘ │
│ │
└────────────────┬───────────────────────────────────────┘
│
▼
Local Network Router
(receives ARP/BGP)
│
▼
NAT Gateway
│
▼
Internet
(public WAN IP)
│
┌──────────┴──────────┐
│ │
STUN Servers Gandi.net API
(detect WAN IP) (update DNS records)
Key Points:
export GANDI_API_KEY="your-gandi-api-key"
cd k8s
./deploy-metallb.sh production 192.168.1.100-192.168.1.110
This script:
./deploy-metallb.sh [environment] [ip-range]
# Examples:
./deploy-metallb.sh production 192.168.1.100/32 # Single IP
./deploy-metallb.sh production 192.168.1.100-192.168.1.110 # IP range
./deploy-metallb.sh development 10.0.0.100/28 # CIDR notation
| Format | Example | Description |
|---|---|---|
| Single IP | 192.168.1.100/32 |
Only one IP address |
| Range | 192.168.1.100-192.168.1.110 |
Range of IPs (11 addresses) |
| CIDR | 192.168.1.0/28 |
CIDR notation (16 addresses) |
Important: Choose IPs that are:
If you prefer manual control or need to customize the configuration:
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm install metallb metallb/metallb \
--namespace metallb-system \
--create-namespace
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90s
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: esddns-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.100-192.168.1.110 # Your IP range
autoAssign: true
EOF
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: esddns-l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- esddns-pool
EOF
export GANDI_API_KEY="your-gandi-api-key"
cd k8s
./deploy.sh production
kubectl get svc -n esddns-production esddns-service
# Should show something like:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# esddns-service LoadBalancer 10.96.123.45 192.168.1.100 80:30123/TCP
MetalLB supports two modes for IP advertisement:
How it works:
Use when:
Configuration:
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: esddns-l2
namespace: metallb-system
spec:
ipAddressPools:
- esddns-pool
How it works:
Use when:
Configuration:
# 1. Create BGP peer
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: router-peer
namespace: metallb-system
spec:
myASN: 64500 # Your ASN
peerASN: 64501 # Router's ASN
peerAddress: 192.168.1.1 # Router IP
---
# 2. Create BGP advertisement
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: esddns-bgp
namespace: metallb-system
spec:
ipAddressPools:
- esddns-pool
# All MetalLB resources
kubectl get all -n metallb-system
# IP address pools
kubectl get ipaddresspool -n metallb-system
# L2 advertisements
kubectl get l2advertisement -n metallb-system
# MetalLB speaker logs
kubectl logs -n metallb-system -l component=speaker -f
# Get service status
kubectl get svc -n esddns-production esddns-service
# Get LoadBalancer IP
EXTERNAL_IP=$(kubectl get svc -n esddns-production esddns-service \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "LoadBalancer IP: $EXTERNAL_IP"
# Test endpoint
curl http://$EXTERNAL_IP/
# View operator logs (should show STUN detection)
kubectl logs -n esddns-production -l app=esddns-operator -f
# Look for:
# INFO STUN protocol enabled for IP discovery
# INFO [stun] Successfully obtained IP: X.X.X.X
Symptoms:
kubectl get svc -n esddns-production esddns-service
# EXTERNAL-IP shows <pending>
Solutions:
kubectl get pods -n metallb-system
kubectl get ipaddresspool -n metallb-system esddns-pool
kubectl logs -n metallb-system -l component=speaker
Symptoms:
Solutions:
kubectl describe pod -n metallb-system
Symptoms:
Solutions:
kubectl get pods -n esddns-production
kubectl port-forward -n esddns-production svc/esddns-service 8080:80
curl http://localhost:8080/
Symptoms:
Solutions:
Symptoms:
Solutions:
kubectl logs -n metallb-system -l component=speaker | grep BGP
ESDDNS uses STUN (Session Traversal Utilities for NAT) protocol to detect your public WAN IP address. This is ideal for on-premises deployments because:
✅ NAT Traversal - Designed specifically for discovering public IPs behind NAT
✅ Fast - Typically ~2x faster than HTTP-based detection
✅ Reliable - RFC 8489 standard, widely supported
✅ Free Public Servers - Google, Cloudflare, Twilio provide STUN infrastructure
On-Prem Network NAT Gateway Internet
┌──────────────┐ ┌──────────┐ ┌─────────────────┐
│ ESDDNS Pod │──────▶│ NAT │──────▶│ STUN Server │
│ Private IP: │ │ Maps: │ │ stun.google.com │
│ 10.0.0.5 │ │ 10.0.0.5│ │ │
│ │ │ ──▶ │ │ Response: │
│ │ │ X.X.X.X │ │ "Your IP is │
│ │◀──────│ │◀──────│ X.X.X.X" │
└──────────────┘ └──────────┘ └─────────────────┘
STUN is enabled by default in ESDDNS. Configuration is in the ConfigMap:
[STUNConfig]
udp_host_list_url = https://raw.githubusercontent.com/pradt2/always-online-stun/master/valid_hosts.txt
tcp_host_list_url = https://raw.githubusercontent.com/pradt2/always-online-stun/master/valid_hosts_tcp.txt
bind_request = 1
magic_cookie = 554869826
xor_mapped_address = 32
udp_limit = 10
tcp_limit = 10
retry_attempts = 3
retry_cooldown_seconds = 5
ESDDNS uses multiple methods with automatic fallback:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: my-custom-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.100-192.168.1.110
autoAssign: true
avoidBuggyIPs: true # Skip .0 and .255
# Production pool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.100-192.168.1.110
---
# Development pool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: development-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.210
apiVersion: v1
kind: Service
metadata:
name: esddns-service
annotations:
metallb.universe.tf/address-pool: production-pool # Specific pool
metallb.universe.tf/loadBalancerIPs: 192.168.1.100 # Specific IP
spec:
type: LoadBalancer
# ... rest of service spec
# MetalLB
kubectl get all -n metallb-system
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system
kubectl logs -n metallb-system -l component=speaker -f
# ESDDNS
kubectl get all -n esddns-production
kubectl get svc -n esddns-production esddns-service
kubectl logs -n esddns-production -l app=esddns-operator -f
kubectl logs -n esddns-production -l app=esddns-service -f
# Get LoadBalancer IP
EXTERNAL_IP=$(kubectl get svc -n esddns-production esddns-service \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$EXTERNAL_IP/
# Restart ESDDNS
kubectl rollout restart daemonset/esddns-operator-daemon -n esddns-production
kubectl rollout restart deployment/esddns-service -n esddns-production
For issues or questions:
kubectl logs -n metallb-system -l component=speakerkubectl logs -n esddns-production -l app=esddns-operator