Cross-Node Pod Networking
The Kubernetes networking model has three rules:
- Every pod can reach every other pod without NAT
- Nodes can reach pods without NAT
- A pod sees its own IP the same way others see it
How this is implemented depends on the CNI plugin. Three patterns: overlay, direct routing, flat (VPC CNI).
Each mode below gets a quick knowledge check — track how many you've cleared as you go:
Before going mode-by-mode, here's how the four stack up side by side:
cbr0/docker0) forwards frames between each pod's veth pair based on its MAC table. Doesn't apply once pods land on different nodes.
1. Same-Node: Pod-to-Pod
flowchart LR
POD_A["Pod A<br/>10.0.1.2<br/>eth0"] -->|"veth pair"| VETH_A["vethAAA<br/>host side"]
VETH_A --> BRIDGE["cbr0 / docker0<br/>Linux bridge<br/>10.0.1.1"]
BRIDGE --> VETH_B["vethBBB<br/>host side"]
VETH_B -->|"veth pair"| POD_B["Pod B<br/>10.0.1.3<br/>eth0"]
Packet walk:
- Pod A sends to
10.0.1.3→ kernel looks up routing table → route viaeth0(default) - Packet exits Pod A's netns through the veth pair → appears on host side as
vethAAA - Linux bridge (
cbr0) sees the packet, looks up MAC table → forwards tovethBBB - Packet enters Pod B's netns through
vethBBB→ arrives at Pod B'seth0
Step through it:
10.0.1.3. The kernel checks Pod A's own routing table — default route is via its eth0, the pod-side end of the veth pair.
vethAAA — an ordinary host-side network interface.
cbr0/docker0) receives it, checks its MAC table, and forwards straight to vethBBB. No L3 routing decision happens here — it's a plain L2 bridge lookup.
vethBBB and arrives at Pod B's eth0.
# Inspect on a node
ip link show type veth # list all veth pairs
brctl show cbr0 # list bridge + attached veths
ip route # node routing table
On the same node, does traffic between two pods get an L3 routing decision made about it anywhere on the node?
cbr0/docker0) forwards it to Pod B's veth purely by MAC table lookup. That's an L2 bridging decision, not a routed one; same-node pod traffic never touches the node's IP routing table.2. Cross-Node: Overlay (VXLAN) — Flannel
Used when the underlay network can't route pod CIDRs (most cloud VPCs without CNI help, on-prem).
flowchart LR
subgraph "Node 1 (10.1.1.1)"
PA["Pod A<br/>10.0.1.2"] -->|veth| B1["flannel.1<br/>VXLAN device"]
end
subgraph "Underlay Network"
B1 -->|"VXLAN encap<br/>Outer: src=10.1.1.1 dst=10.1.2.1<br/>Inner: src=10.0.1.2 dst=10.0.2.3<br/>UDP port 4789"| WIRE["Physical Network"]
end
subgraph "Node 2 (10.1.2.1)"
WIRE -->|"VXLAN decap"| B2["flannel.1<br/>VXLAN device"] -->|veth| PB["Pod B<br/>10.0.2.3"]
end
VXLAN encapsulation adds:
- Outer Ethernet header (14 bytes)
- Outer IP header (20 bytes)
- Outer UDP header (8 bytes, dst port 4789)
- VXLAN header (8 bytes)
- Total overhead: 50 bytes per packet
MTU implication: If the underlay MTU is 1500, pod MTU must be set to 1450 (1500 - 50). Flannel sets this automatically on the flannel.1 interface. Wrong MTU → packets silently fragmented or dropped.
Step through it:
10.0.1.2) to Pod B (10.0.2.3), leaves via the veth pair and hits Node 1's flannel.1 VXLAN device.
flannel.1 wraps the whole original packet in a new outer header: outer src=Node 1's IP, outer dst=Node 2's IP, UDP port 4789 — 50 bytes of added overhead.
flannel.1 strips the outer header, recovers the original inner packet, and forwards it over the veth pair to Pod B.
# Check pod MTU
kubectl exec <pod> -- ip link show eth0 | grep mtu
# On the node: inspect the VXLAN interface
ip -d link show flannel.1
ip route | grep flannel # routes for each remote node's pod CIDR
# Test MTU: ping with DF bit set and large payload
ping -M do -s 1450 10.0.2.3 # if fails → MTU problem
The underlay network MTU is 1500. What pod MTU should Flannel VXLAN use, and why?
3. Cross-Node: Direct Routing (Calico BGP)
No encapsulation. Each node announces its pod CIDR to other nodes via BGP. The underlay routes packets between nodes directly.
flowchart LR
subgraph "Node 1 (10.1.1.1)<br/>pod CIDR: 10.0.1.0/24"
PA2["Pod A<br/>10.0.1.2"] -->|veth| RT1["Node routing table<br/>10.0.2.0/24 via 10.1.2.1"]
end
subgraph "BGP"
RT1 -->|"BGP: I own 10.0.1.0/24"| BGP_PEER["BGP peers<br/>(other nodes or ToR switch)"]
BGP_PEER -->|"BGP: Node 2 owns 10.0.2.0/24"| RT1
end
subgraph "Node 2 (10.1.2.1)<br/>pod CIDR: 10.0.2.0/24"
BGP_PEER --> RT2["Node routing table<br/>10.0.1.0/24 via 10.1.1.1"]
RT2 -->|veth| PB2["Pod B<br/>10.0.2.3"]
end
Packet walk:
- Pod A sends to
10.0.2.3 - Node 1 routing table:
10.0.2.0/24 via 10.1.2.1(learned from BGP) - Packet sent to Node 2's IP — no encapsulation, full MTU available
- Node 2 routing table:
10.0.2.3 via vethXXX→ forwarded to Pod B
Step through it:
10.0.2.3, an IP on Node 2's pod CIDR.
10.0.2.0/24 via 10.1.2.1 — learned from BGP, not configured by hand.
vethXXX, which hands it to Pod B.
Requirement: The underlay network must either:
- Support BGP (ToR switches in bare metal)
- Allow route injection (AWS VPC with
--disable-pod-vpc-route-table-management=false)
# Check BGP peers (calicoctl)
calicoctl node status
# Inspect Calico routes injected into node
ip route | grep bird # Bird is the BGP daemon Calico uses
# Check encapsulation mode
calicoctl get felixconfiguration default -o yaml | grep encapsulation
True or false: Calico BGP mode works unmodified on any network, the same way a VXLAN overlay does.
4. VPC CNI Flat Network (AWS)
Every pod gets a real ENI IP from the VPC subnet. No overlay, no BGP — pods are first-class citizens in the VPC.
flowchart LR
subgraph "VPC subnet 10.0.1.0/24"
subgraph "EC2 Node (10.0.1.5)"
PA3["Pod A<br/>10.0.1.20<br/>(ENI secondary IP)"]
PB3["Pod B<br/>10.0.1.21<br/>(ENI secondary IP)"]
end
subgraph "EC2 Node 2 (10.0.2.5)"
PC["Pod C<br/>10.0.2.30<br/>(ENI secondary IP)"]
end
RDS["RDS<br/>10.0.1.100"]
end
PA3 -->|"direct VPC routing<br/>no encap"| PC
PA3 -->|"direct VPC routing"| RDS
How it works:
aws-nodeDaemonSet runs on every node- It pre-allocates secondary IPs on the node's ENIs (or creates additional ENIs)
- When a pod is created, the CNI plugin assigns one of the pre-allocated IPs to the pod
- VPC routing table already knows these IPs belong to this EC2 instance
Step through a packet's actual cross-node journey (not the setup steps above):
10.0.2.30) — a real VPC address, not something translated later.
10.0.2.30 belongs to EC2 Node 2's ENI, and delivers it there directly — ordinary EC2-to-EC2 routing, no CNI-specific handling in the fabric at all.
Benefits:
- Zero encapsulation overhead — same performance as EC2-to-EC2
- Pod IP directly reachable from RDS, ElastiCache, on-prem via VPN
- Security Groups work per-pod (with Security Groups for Pods feature)
- No MTU penalty
Limitation: IP exhaustion
Each EC2 instance can hold a limited number of ENI secondary IPs (based on instance type):
# Check how many IPs are available
kubectl describe node <node> | grep eni
# m5.large: 3 ENIs × 10 IPs = 30 pods max (minus 1 per ENI for node = 27)
# Fix 1: use larger instance types
# Fix 2: enable prefix delegation (/28 prefix per ENI slot = 16 IPs per slot)
Prefix delegation (recommended for large clusters):
# Enable in aws-node DaemonSet env vars:
ENABLE_PREFIX_DELEGATION: "true"
WARM_PREFIX_TARGET: "1"
# Each ENI slot now holds a /28 (16 IPs) instead of 1 IP
# m5.large: 3 ENIs × (9 slots × 16 IPs) = 432 pods max
# Check current IP allocation
kubectl get node <node> -o json | jq '.metadata.annotations["vpc.amazonaws.com/node-capacity-suffix-ip-block"]'
# Check aws-node logs for IP allocation issues
kubectl logs -n kube-system -l k8s-app=aws-node --tail=50
Why does enabling prefix delegation let a node run far more pods without adding a single ENI?
5. MTU Summary
| CNI | Encap overhead | Recommended pod MTU |
|---|---|---|
| Flannel VXLAN | 50 bytes | 1450 |
| Calico IPIP | 20 bytes | 1480 |
| Calico BGP (no encap) | 0 bytes | 1500 |
| AWS VPC CNI | 0 bytes | 9001 (jumbo frames on EC2) |
| Cilium VXLAN | 50 bytes | 1450 |
| Cilium native routing | 0 bytes | 1500 |
# Check current MTU on a pod
kubectl exec <pod> -- cat /sys/class/net/eth0/mtu
# Test effective MTU with DF bit
kubectl exec <pod> -- ping -M do -s 1440 <other-pod-ip>
# Increase -s until it fails → that's your effective MTU
# Check CNI's configured MTU
cat /etc/cni/net.d/10-flannel.conflist | jq '.plugins[0].mtu'
A pod's MTU is left higher than what its CNI's encapsulation can actually deliver end to end. What's the typical symptom?
6. Live Simulator: Packet Path + MTU
The four walkthroughs above each trace one hardcoded packet. This one's live: pick same-node or cross-node, pick a CNI mode, then try packet sizes on either side of that mode's effective MTU from the table above and watch where the packet actually gets through.
7. Inspecting the Network on a Node
# All veth pairs (one per pod)
ip link show type veth
# Routing table (how pod CIDRs are reached)
ip route
# ARP/neighbor table
ip neigh
# Pod IP to veth mapping
for veth in $(ls /sys/class/net | grep veth); do
peer=$(ip link show $veth | grep -o 'veth[0-9a-f]*' | tail -1)
echo "$veth ↔ $peer"
done
# Which pod owns a veth (Linux netns approach)
nsenter --net=/proc/$(pgrep -f pause)/net ip addr show eth0