Understanding Kubernetes Networking, Part 2: The Pod Network, CNI, and Flannel

In Part 1 of this series we built the vocabulary: the OSI layers as a debugging framework, physical-to-virtual appliance mapping, overlay networking as the answer to “how do pods get routable IPs without touching the physical fabric,” and the Linux primitives — network namespaces, veth pairs, and bridges — that make container networking possible at all.
This post puts those primitives to work. We’ll look at what “the pod network” actually means as a design requirement, what the Container Network Interface (CNI) specification is and why Kubernetes needed it, and how Flannel — one of the earliest and still most widely deployed CNI plugins — implements all of it in practice.
The Pod Network: What Kubernetes Actually Requires
Before touching any specific plugin, it’s worth being precise about the networking model Kubernetes mandates. It’s a short list, but every item on it has real implementation consequences:
- Every pod gets its own IP address.
- Pods can reach all other pods across the entire cluster using that IP, on every node, without NAT.
- Nodes can reach all pods, and vice versa, without NAT.
- Whatever a pod believes its own IP is, that’s the same IP everyone else sees it as.
That’s it. Kubernetes itself does not implement this — it delegates the job entirely. Which raises the obvious question: delegates to what, exactly, and how does the delegation work? That’s what CNI answers.
The CNI Specification: A Contract, Not a Product
CNI is not a piece of software you install — it’s a specification. It defines a minimal, well-structured contract between two parties: a container runtime (containerd, CRI-O) that needs a container’s network set up or torn down, and a plugin that knows how to do it.
The contract is deliberately narrow. A CNI plugin is an executable that supports a handful of operations, the two that matter most being:
- ADD — “here’s a network namespace, set up networking for it and tell me what you did.”
- DEL — “tear down the networking you set up for this namespace.”
The runtime invokes the plugin binary directly, passing configuration as JSON over stdin and environment variables (container ID, namespace path, interface name). The plugin does its work — creating a veth pair, attaching it to a bridge, assigning an IP — and returns a JSON result describing the interfaces and IPs it configured.
This narrowness is the entire point. Because the contract is so small, wildly different implementations can sit behind it: a plugin can create a simple bridge network, program BGP routes, set up VXLAN tunnels, or attach an SR-IOV virtual function — the runtime doesn’t need to know or care which. It just knows the plugin was invoked and got a valid result back.
IPAM as a separate plugin. CNI further splits IP address management out as its own plugin type, invoked by the main CNI plugin rather than by the runtime directly. This is why you’ll see Flannel’s config reference a host-local IPAM plugin — Flannel handles the “how do I get packets to this pod” problem, and delegates “which IP does this pod get” to a dedicated, swappable component. This separation is one of the more underappreciated design decisions in the spec: it’s why almost every CNI plugin can share the same handful of IPAM plugins instead of reimplementing address allocation from scratch.
Where kubelet fits in. kubelet doesn’t call CNI plugins directly — that job belongs to the container runtime, via the CRI (Container Runtime Interface). When kubelet asks the runtime to start a pod, the runtime creates the pod’s sandbox (the shared network namespace for all containers in the pod), then invokes the configured CNI plugin’s ADD command against that namespace before any application containers start.
How the Pieces Fit Together
At a high level, creating a pod’s network looks like this: the scheduler places a pod on a node, kubelet asks the container runtime to create it, the runtime creates the sandbox network namespace, and then hands that namespace to the CNI plugin to actually wire up.

Everything left of “CNI plugin” is Kubernetes/CRI machinery. Everything at and after it is where the actual plugin — Flannel, Calico, Cilium, whichever you’ve installed — takes over.
Note what’s notably absent from that chain: Kubernetes has no idea what happens inside the CNI plugin box. It doesn’t know if it’s VXLAN, BGP, or something exotic. That ignorance is a feature — it’s what lets CNI plugins innovate independently of Kubernetes release cycles.
Flannel: The Reference-Simple CNI Plugin
Flannel is one of the oldest CNI plugins still in active use, and it’s worth studying specifically because it’s simple — it does one job (give every pod cluster-wide reachability) without trying to also be a network policy engine, a service mesh, or a BGP speaker. That simplicity makes it an excellent teaching example before we look at Calico’s very different approach in Part 3.
Flannel’s architecture has three moving parts on every node:
flanneld, a daemon running on each node. Its job is to allocate a subnet to that node out of the cluster-wide pod CIDR, and to learn the subnet allocations of every other node in the cluster.
A backing store for those subnet leases. In most modern deployments this is the Kubernetes API server itself (Flannel watches a NodeSpec.PodCIDR or a dedicated CRD, depending on version); older deployments used etcd directly. Either way, this store is what turns “I have a subnet” into “everyone in the cluster knows everyone else’s subnet.”
cni0, the Linux bridge on each node that Flannel’s CNI plugin creates and attaches every local pod’s veth pair to — exactly the bridge mechanism from Part 1, just with Flannel doing the setup and IPAM bookkeeping.
When a new node joins the cluster, flanneld requests an unused subnet slice (say, 10.244.5.0/24 out of a cluster-wide 10.244.0.0/16), writes that allocation to the backing store, and configures local routes so anything destined for pods on that node lands on cni0.

The subnet store is what makes this scale cluster-wide: node A doesn’t need to know node B’s topology, only that packets for 10.244.7.0/24 should go to node B’s IP — and the store is where it learns that mapping.
Flannel’s Backends: How Traffic Actually Gets Between Nodes
Where Flannel gets interesting is in its pluggable backends — the mechanism it uses to move a packet from one node’s cni0 bridge to another node’s, once local routing alone isn’t enough (i.e., the destination pod isn’t on this node).
- VXLAN backend — this is the overlay approach from Part 1, applied concretely. Flannel creates a
flannel.1VXLAN interface on each node; when a packet needs to leave the node, it’s encapsulated in a VXLAN/UDP header addressed to the destination node’s IP, sent across the existing physical network, and decapsulated on arrival. This is the default in most cloud and on-prem deployments because it requires zero changes to the underlying network — the physical switches only ever see UDP traffic between known node IPs. - host-gw backend — no encapsulation at all. Flannel simply adds a host route on every node saying “to reach subnet
10.244.7.0/24, send it to node B’s IP as the next hop,” and relies on the underlying network being a flat Layer 2 segment where nodes can reach each other’s IPs directly as a gateway. This is faster (no encapsulation overhead) but only works when all nodes sit on the same L2 network — it won’t cross routed boundaries without additional help. - Other backends (UDP, IPIP, and cloud-specific ones) exist for particular environments, but VXLAN and host-gw are the two you’ll encounter in the overwhelming majority of real clusters.
This backend choice is a direct, practical instance of the overlay-versus-native-routing trade-off from Part 1: VXLAN buys you portability at the cost of encapsulation overhead; host-gw buys you performance at the cost of requiring a flat L2 fabric.
What Flannel Deliberately Doesn’t Do
It’s worth being explicit about Flannel’s scope, because it explains why Calico and Cilium exist as alternatives rather than redundant competitors. Flannel provides pod-to-pod reachability across the cluster — full stop. It has no native support for Network Policies (restricting which pods can talk to which), and it doesn’t participate in BGP peering with your physical network by default. If you need Kubernetes-native network policies, you’d traditionally pair Flannel with a separate policy engine (this used to be a common combination before more integrated CNI options existed), or move to a plugin like Calico that handles both concerns natively.
That’s exactly where we’re headed next.
Try It Yourself: Invoking a Real CNI Plugin by Hand
Everything about the CNI contract above stays somewhat abstract until you’ve actually called a plugin yourself. This lab does exactly that — no Kubernetes involved, just the reference CNI plugin binaries and a small tool called cnitool that plays the role of the container runtime from the sequence diagram earlier in this post. You’re manually performing the two steps that come after “Runtime creates sandbox”: invoking the CNI plugin’s ADD, and watching it delegate to an IPAM plugin.
If you still have the two-VM lab from Part 1 running, you can do this on either VM — it’s independent of the bridge you built by hand there, since this creates its own bridge.
Install the Reference Plugins and cnitool
# The official reference CNI plugin binaries — includes "bridge" and "host-local"
sudo mkdir -p /opt/cni/bin
curl -L https://github.com/containernetworking/plugins/releases/download/v1.5.1/cni-plugins-linux-amd64-v1.5.1.tgz \
| sudo tar -C /opt/cni/bin -xz
# cnitool: a small CLI that calls a CNI plugin exactly like a runtime would
go install github.com/containernetworking/cni/cnitool@latest
sudo cp "$(go env GOPATH)/bin/cnitool" /usr/local/bin/
Write the Network Configuration
This JSON is what a real runtime would read before invoking the plugin — it’s the same “bridge” plugin Flannel itself wraps around, just invoked directly instead of by flanneld:
sudo mkdir -p /etc/cni/net.d
cat <<'EOF' | sudo tee /etc/cni/net.d/10-mynet.conf
{
"cniVersion": "1.0.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni_bridge0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.22.0.0/24",
"routes": [{ "dst": "0.0.0.0/0" }]
}
}
EOF
Create a Sandbox Namespace — the Step Before the CNI Plugin Runs
In a real cluster, this is the container runtime’s job, done just before it calls ADD:
sudo ip netns add mypod
Call ADD — This Is the Actual CNI Invocation
export CNI_PATH=/opt/cni/bin
export NETCONFPATH=/etc/cni/net.d
sudo -E cnitool add mynet /var/run/netns/mypod
You’ll get back a JSON result describing the interface and IP the plugin configured — that’s the ADD response format described earlier in this post, printed to your terminal instead of consumed by a runtime. Behind the scenes, the bridge plugin just did three things: created the cni_bridge0 bridge if it didn’t exist, created a veth pair with one end in mypod, and called out to the host-local IPAM plugin to get an address from 10.22.0.0/24 — the exact ADD → IPAM delegation from the sequence diagram above, just running on your terminal instead of inside kubelet.
Verify What the Plugin Actually Did
# The pod's interface and assigned IP
sudo ip netns exec mypod ip addr show
sudo ip netns exec mypod ip route show
# The bridge the plugin created, and the veth attached to it
ip link show cni_bridge0
ip link show master cni_bridge0
# Connectivity out, via the ipMasq rule the plugin configured
sudo ip netns exec mypod ping -c 3 8.8.8.8
Call DEL — Tearing It Back Down
sudo -E cnitool del mynet /var/run/netns/mypod
sudo ip netns del mypod
This is the other half of the CNI contract: DEL undoes exactly what ADD set up — removing the veth pair and releasing the IP back to the host-local IPAM plugin’s allocation pool.
Once this feels routine, it’s worth reading the bridge plugin’s behavior against Flannel’s architecture section above: flanneld isn’t a competing implementation of what you just did by hand — it’s automation around this exact same bridge-plus-IPAM pattern, adding the subnet-lease coordination and cross-node backend that make it work cluster-wide instead of on one host.
Where This Goes Next
With CNI’s contract and Flannel’s concrete implementation in place, the next posts get progressively more sophisticated:
- Part 3: Calico CNI in depth — BGP-based routing, and native Network Policy enforcement
- Part 4: Kubernetes Services — how ClusterIP, NodePort, and kube-proxy sit on top of everything covered so far
- Part 5: Intro to Network Policies
- Part 6: Calico Network Policies
This post accompanies Part 2 of the Understanding Kubernetes Networking video series: POD Network, CNI, and Flannel CNI Plugin.