Native vs encapsulated routing for a small homelab
Cilium gives you two main ways to move packets between nodes. The choice is small, but it decides what your network and firewall must do.
The short version
| Mode | What moves packets | Main requirement | Best fit |
|---|---|---|---|
| Native | Linux routing | PodCIDRs must be routable between nodes | A network you control, often one L2 segment |
| Encapsulation | VXLAN inside UDP 8472 | Nodes only need IP reachability | A simple lab, or nodes without PodCIDR routes |
Both keep kube-proxy in this lab. Cilium handles the CNI, policy and visibility; kube-proxy still handles ClusterIP services.
Native routing
Native routing tells Cilium to hand non-local packets to the Linux routing table. With autoDirectNodeRoutes, each node learns a route to every other node’s PodCIDR:
routingMode: native
autoDirectNodeRoutes: true
ipv4NativeRoutingCIDR: "10.42.0.0/16"Good:
- no encapsulation overhead;
- full MTU;
- simple packet capture and routing inspection.
Cost:
- the underlay must route PodCIDRs;
- every node needs to learn the other nodes’ pod routes;
- spanning multiple L2 or L3 domains usually means BGP or static routes.
Encapsulation
VXLAN wraps the pod packet in a UDP packet between nodes. The underlay only needs normal node-to-node IP reachability:
routingMode: tunnel
tunnelProtocol: vxlanGood:
- the simplest underlay requirement;
- no PodCIDR routes on the physical network;
- new nodes join without changing a router;
- works well on a flat home LAN.
Cost:
- about 50 bytes of overhead, so pod MTU drops to 1450 on a 1500 MTU LAN;
- extra encapsulation and decapsulation work;
- an extra failure surface: VXLAN device, UDP port and firewall rules.
What this lab uses
VXLAN. All three nodes are on one LAN, but the goal is to keep the network configuration small. Native routing worked, but encapsulation lets the nodes join with only IP reachability and no PodCIDR routes outside the cluster.
The relevant settings are in bakuran-gitops:
routingMode: tunnel
tunnelProtocol: vxlan
kubeProxyReplacement: "false"The NixOS hosts keep their firewall and trust the Cilium interfaces:
networking.firewall.checkReversePath = false;
networking.firewall.trustedInterfaces = [ "cni+" ];checkReversePath=false removes the strict NixOS reverse-path chain that can drop VXLAN return traffic. It is necessary here, but it is not enough by itself.
The failure that cost the most time
VXLAN would not start. Cilium agents logged:
failed to setup vxlan tunnel device:
address already in useThe old Flannel flannel.1 interface was still up and owned UDP 8472. Removing it and restarting Cilium made VXLAN work immediately:
ip link del flannel.1
kubectl -n kube-system rollout restart ds/ciliumNative routing had hidden the problem because it does not use VXLAN. A oneshot service now removes a stale flannel.1 before k3s starts.
What to test after switching
ip -d link show type vxlan
ss -lunp | grep 8472
kubectl run nettest --image=curlimages/curl:8.10.1 --restart=Never --command -- \
sh -c 'curl -s -o /dev/null -w "%{http_code}\n" https://example.com'Then check:
- new pods reach
Running; - pod DNS works;
- external egress works;
- ClusterIP services work;
- public ingress and LAN hostPort still work;
cilium-dbg statusshowsTunnel [vxlan]and health is 3/3.
The full failure path is in Cilium VXLAN troubleshooting: port 8472 already in use .
The earlier migration story is in Homelab network migration .
Sources
- Cilium routing concepts: https://docs.cilium.io/en/stable/network/concepts/routing/
- Cilium masquerading: https://docs.cilium.io/en/stable/network/concepts/masquerading/
- NixOS rpfilter issue: https://github.com/lucas-albers-lz4/rke2nixos/issues/10
- fwmark collision case: https://pokgak.xyz/notes/cilium-vxlan-tailscale-fwmark-collision/