Networking

CNI Plugins — Calico, Flannel, Cilium

● Advanced ⏱ 20 min read

Kubernetes defines rules for how pods must be able to communicate — every pod gets a unique IP, pods on different nodes can talk directly without NAT. But Kubernetes itself doesn't implement any of this. That's the job of a CNI plugin. The choice of plugin determines how pods get IPs, what performance characteristics you get, and whether Network Policies are enforced at all.

Kubernetes Networking Model

Kubernetes mandates three guarantees, but leaves the implementation entirely to the CNI plugin:

These rules form the "flat networking model". Every pod behaves as if it were on the same LAN, even though in reality they may be on different physical machines in different availability zones.

What Is CNI?

CNI (Container Network Interface) is a specification — a simple contract between the container runtime and a network plugin. When a pod is created, the kubelet calls the CNI plugin binary with the container details. The plugin allocates an IP, creates a virtual network interface in the pod's network namespace, and sets up routing so the pod can send and receive packets.

CNI plugin interactions at pod creation
# 1. kubelet creates the pod (sandbox container)
# 2. kubelet calls the CNI plugin: ADD <container-id> <netns-path>
# 3. CNI plugin:
#    - allocates an IP from IPAM (e.g. 10.244.1.7)
#    - creates a veth pair: one end in pod netns, one end on the host
#    - assigns the IP to the pod-side veth (eth0 inside the pod)
#    - sets up routing rules on the host
#    - returns: { "ip": "10.244.1.7/24", "gateway": "10.244.1.1" }
# 4. pod can now send/receive packets

CNI plugins are just binaries dropped into /opt/cni/bin/ on every node, configured via JSON files in /etc/cni/net.d/. Multiple plugins can chain together — for example, a VXLAN plugin for routing combined with a bandwidth plugin for QoS.

How a Pod Gets Its IP

IP allocation is handled by the CNI's IPAM (IP Address Management) component. Most plugins divide the cluster's pod CIDR (e.g. 10.244.0.0/16) into per-node subnets, then allocate individual IPs from the node's subnet.

Pod IP allocation — per-node subnets
cluster pod CIDR
10.244.0.0/16
node-1
10.244.0.0/24
pod-a 10.244.0.2
pod-b 10.244.0.3
node-2
10.244.1.0/24
pod-c 10.244.1.2
pod-d 10.244.1.3
node-3
10.244.2.0/24
pod-e 10.244.2.2
The cluster CIDR is split into per-node subnets by the CNI plugin's IPAM component
Each node owns a subnet slice. The CNI IPAM allocates IPs from the local subnet, so pod IPs are unique cluster-wide without coordination overhead.

Overlay vs Underlay

To send a packet from pod-a on node-1 to pod-c on node-2, the CNI plugin must route the packet across the physical network. There are two main approaches:

ApproachHow it worksProsCons
Overlay (VXLAN, IP-in-IP)Wraps pod-to-pod packets in node-to-node UDP/IP packets. Physical network only sees node IPs.Works on any network; no router config neededEncapsulation overhead (~10% throughput); extra CPU for encap/decap
Underlay (BGP, direct routing)Announces pod CIDRs to the physical router via BGP. Packets route natively at layer 3.Near-native performance; full observabilityRequires BGP-capable routers; more network team coordination

Flannel

Flannel is the simplest option — VXLAN overlay, straightforward setup, no extras. It runs a flanneld DaemonSet on each node that creates a flannel.1 virtual network device and maintains a mapping of which node owns which pod CIDR.

install flannel
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

Calico

Calico supports both overlay (VXLAN or IP-in-IP) and BGP underlay modes. It enforces standard Kubernetes Network Policies and extends them with its own GlobalNetworkPolicy and NetworkPolicy CRDs that add per-node egress rules, DNS-based policies, and more.

install calico (operator method)
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.0/manifests/custom-resources.yaml

Cilium

Cilium replaces iptables with eBPF programs loaded directly into the Linux kernel. This eliminates the kube-proxy iptables chain entirely, enables L7-aware policies (allow/deny by HTTP method and path, gRPC service name), and provides rich observability through Hubble.

install cilium via Helm
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.16.0 \
  --namespace kube-system \
  --set kubeProxyReplacement=true   # replace kube-proxy with eBPF

Comparison

FlannelCalicoCilium
Network modelVXLAN overlayVXLAN / BGPeBPF / VXLAN
Network PolicyNoYes + extended CRDsYes + L7 CRDs
kube-proxy replacementNoOptionalYes (recommended)
ObservabilityMinimalFlow logs via calico-nodeHubble (L7 visibility)
PerformanceGoodVery good (BGP)Best (eBPF)
ComplexityLowMediumHigh
Kernel requirementAnyAny4.9+ (5.10+ for full features)
💡
Managed Kubernetes often pre-chooses the CNI

EKS uses the AWS VPC CNI (pod IPs are real VPC IPs — no overlay at all). GKE uses a proprietary datapath based on Cilium. AKS defaults to Azure CNI. If you're on a managed cluster, your CNI choice may be limited — check whether the managed CNI supports Network Policies before building your security model around them.

kubectl Commands

# Check which CNI is installed (look for DaemonSets in kube-system)
kubectl get daemonsets -n kube-system

# Inspect CNI config on a node (requires node access)
ls /etc/cni/net.d/
cat /etc/cni/net.d/10-flannel.conflist   # or calico/cilium equivalent

# Check pod IPs and which node they're on
kubectl get pods -o wide

# Verify pod-to-pod connectivity
kubectl exec -it pod-a -- ping 10.244.1.3   # another pod's IP

# Cilium: check plugin status
kubectl exec -n kube-system ds/cilium -- cilium status

# Cilium: view live traffic flows (requires Hubble)
kubectl exec -n kube-system ds/cilium -- hubble observe --follow

# Calico: check node BGP peers (BGP mode)
kubectl exec -n kube-system ds/calico-node -- calicoctl node status

# Check IPAM allocation on a node
kubectl get node node-1 -o jsonpath='{.spec.podCIDR}'