Andrew Mercer
on this page

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.