Check the address pool assigned¶
kubectl get ipaddresspools.metallb.io -n metallb
NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
example true false ["10.0.0.100-10.0.0.200"]
AUTO ASSIGN true means MetalLB will pull from this pool automatically for any LoadBalancer service that doesn't explicitly request an address from a different pool. If a service isn't getting an IP, confirm first that a pool actually exists and has free addresses left in its range — kubectl describe ipaddresspool example -n metallb will show how many addresses remain.
MetalLB controller binds to wrong interface¶
kubectl get pods -A -o wide| grep metallb
metallb metallb-controller-7bd5848b94-c826n 1/1 Running 0 14h 10.244.158.207 cluster1 <none> <none>
metallb metallb-speaker-22l62 4/4 Running 0 14h 10.0.0.10 cluster0 <none> <none>
metallb metallb-speaker-4vvsw 4/4 Running 0 14h 10.0.0.12 cluster2 <none> <none>
metallb metallb-speaker-zq25m 4/4 Running 0 14h 10.0.0.11 cluster1 <none> <none>
The giveaway here is cluster1's pods reporting an IP (10.244.158.207) from a completely different range than the other nodes (10.0.0.1x) — that's not this node's real LAN address.
Calico created a tunl0 interface with that network range:
ip a s tunl0
4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
inet 10.244.158.192/32 scope global tunl0
valid_lft forever preferred_lft forever
Why this breaks things: Calico's IPIP tunnel interface (tunl0) gets an address from its own pod-network range (commonly 10.244.0.0/16 by default) — and that address can look, to the kubelet's auto-detection logic, like a plausible node IP. So the kubelet picks the tunl0 address as the node's InternalIP instead of the real physical NIC's address on your LAN. Anything that relies on the node's reported IP (including which interface MetalLB's speaker binds to for ARP announcements) then follows that wrong choice, since it's reading the same node status the kubelet published.
To fix, pin the kubelet's node IP explicitly to the correct interface's address:
# /etc/default/kubelet
KUBELET_EXTRA_ARGS=--node-ip=10.0.0.11
sudo systemctl daemon-reload
sudo systemctl restart kubelet
(Corrected systemctl status to systemctl restart — checking status alone won't pick up the new --node-ip flag; the kubelet service needs an actual restart for the change to take effect.)
Verify the fix:
kubectl get node cluster1 -o wide
Confirm the INTERNAL-IP column now shows the real LAN address, not the tunl0 address.
For the broader set of known Calico/MetalLB interactions (this interface-detection issue is one of several), see MetalLB's own notes: https://metallb.universe.tf/configuration/calico/
Delete the metallb pods to restart them¶
Restarting the whole MetalLB deployment (e.g. after a config change that isn't being picked up, or to clear a stuck speaker):
kubectl rollout restart deployment metallb-controller -n metallb
kubectl rollout restart daemonset metallb-speaker -n metallb
Or, to force-restart every MetalLB pod directly instead of via a rollout (equivalent effect, since they're managed by a Deployment/DaemonSet that will recreate them):
kubectl delete pods -n metallb -l app.kubernetes.io/name=metallb
Watch them come back up:
kubectl get pods -n metallb -w
If a speaker pod keeps crash-looping after restart rather than settling into Running, check its logs before restarting again — kubectl logs -n metallb -l component=speaker --previous shows the crashed instance's last output, which is usually more informative than a repeated blind restart.