Kubernetes Networking Explained (2026): CNI, Services, kube-proxy in 1.37
Most Kubernetes networking problems I get asked about are not really networking problems. They are a label that does not match, a port number typed twice in two different places, or a Pod that never became ready. But you cannot tell which one you have unless you know what the layers are and who owns each of them, and Kubernetes networking has more owners than people expect: the CNI plugin, kube-proxy (or its replacement), the API server's EndpointSlice controller, and CoreDNS all touch a single request. This post explains Kubernetes networking as it stands in November 2026, on Kubernetes v1.37.2: the pod network model, what CNI plugins such as Cilium, Calico and Flannel actually do, the Service types, the kube-proxy modes (including the IPVS deprecation and the coming nftables default), EndpointSlices, DNS, and eBPF replacements for kube-proxy.
What is the Kubernetes networking model?
Kubernetes makes three promises and leaves the implementation to someone else. The networking concepts page states them plainly: each Pod in a cluster gets its own unique cluster-wide IP address; all Pods can talk to all other Pods, on the same node or a different one, directly, without proxies or address translation (NAT); and agents on a node, such as the kubelet, can reach the Pods on that node.
That is the whole contract. It is deliberately flat: there is no port mapping like docker run -p 8080:80, and a container listening on port 80 in one Pod does not collide with a container on port 80 in another, because they have different IPs. Containers inside one Pod share a network namespace, so they talk to each other on localhost.
What Kubernetes itself does not do is assign those IPs or move packets between nodes. Out of the box, a fresh kubeadm cluster has nodes that stay NotReady until you install a network plugin. That plugin is the CNI.
On top of the flat pod network, Kubernetes adds a second, virtual layer: Services. A Service gives a set of Pods one stable virtual IP and DNS name, because Pod IPs change every time a Pod is rescheduled. Something on every node (kube-proxy by default) has to turn that virtual IP into a real Pod IP. Keep those two layers separate in your head and most of the rest follows.
What does a CNI plugin actually do?
CNI (Container Network Interface) is a small specification, currently spec version 1.1.0, with library releases up to v1.3.1 in September 2026. When the container runtime creates a Pod sandbox, it calls the configured CNI plugin binary with ADD; the plugin creates the Pod's network interface (usually one end of a veth pair), assigns an IP from the node's range (IPAM), and sets up routes. On Pod deletion it gets DEL. Everything else, such as how traffic crosses between nodes, NetworkPolicy enforcement, encryption and observability, is the plugin's own business.
That is why "which CNI?" is a bigger decision than it sounds. Three projects cover most self-managed clusters:
| CNI | Latest release (checked 10 Nov 2026) | Data plane | NetworkPolicy | Notes | Source |
|---|---|---|---|---|---|
| Cilium | v1.20.4 (23 Oct 2026) | eBPF | Yes, plus L7 and its own CRDs | Can replace kube-proxy entirely; powers GKE Dataplane V2 | release, GKE docs |
| Calico | v3.33.0 (1 Oct 2026) | iptables, nftables, eBPF or VPP | Yes, plus its own policy CRDs | 3.33 added netkit devices (Linux 6.7+), nftables flowtable offload, single quay.io/calico/calico image |
release notes |
| Flannel | v0.28.10 (8 Oct 2026) | VXLAN and other backends | Not natively; Helm chart option netpol.enabled adds kube-network-policies |
Simple layer 3 overlay; one subnet lease per node | README, release |
| Amazon VPC CNI | v1.23.2-eksbuild.8 on EKS 1.31 to 1.37 | Native VPC routing via ENIs | Via add-on settings | Pods get real VPC IPs; the only CNI EKS supports on AWS-hosted nodes | EKS docs |
Two practical points that bite people. First, a CNI without NetworkPolicy support will accept your NetworkPolicy objects and silently enforce nothing; the Flannel README is explicit that flanneld does not enforce policies itself. Second, overlay (VXLAN, Geneve) versus native routing changes your MTU and your debugging: with an overlay, a packet capture on the node shows encapsulated traffic between node IPs, not Pod IPs. If you plan to write policies, read my NetworkPolicy guide after picking a plugin that enforces them.
Kubernetes Service types: ClusterIP vs NodePort vs LoadBalancer
A Service selects Pods by label and gives them a stable front door. The Service docs define four types, each building on the last:
- ClusterIP (the default): a virtual IP reachable only inside the cluster. This is what almost every internal call uses.
- NodePort: a ClusterIP plus a port opened on every node, allocated from
--service-node-port-range, default 30000-32767. - LoadBalancer: a NodePort plus an external load balancer provisioned by a cloud controller (or MetalLB, or anything you name in
spec.loadBalancerClass). - ExternalName: no proxying at all, just a DNS CNAME to an outside name.
There is also the headless Service (clusterIP: None), which skips the virtual IP and returns the Pod IPs straight from DNS. StatefulSets use it so each replica gets its own stable name.
The field that causes the most confusion is the port mapping. port is what clients connect to on the Service IP; targetPort is where the traffic lands on the Pod, and it can be a number or the name of a container port. containerPort in the Pod spec is informational; the process listens wherever it listens. Get targetPort wrong and you get connection refused, which is exactly the case the paired guide, How to Fix a Kubernetes Service Connection Refused or Timeout, walks through cause by cause.
Two newer Service fields are worth knowing. internalTrafficPolicy: Local and externalTrafficPolicy: Local keep traffic on the node it arrived at (the external one also preserves the client source IP). And spec.trafficDistribution, whose PreferSameZone and PreferSameNode values went GA in v1.35, tells the proxy to prefer nearby endpoints, which is the cheapest way to cut cross-zone data transfer charges. The older PreferClose value is now deprecated as an alias for PreferSameZone.
EndpointSlices: how a Service finds its Pods
A Service object never contains Pod IPs. The EndpointSlice controller watches Pods that match the selector and writes their IPs, ports and readiness into EndpointSlice objects labelled kubernetes.io/service-name=<service>. By default each slice holds at most 100 endpoints, configurable with --max-endpoints-per-slice on the controller manager, so a large Service is split across several slices and an update touches one small object instead of one huge one.
Each endpoint carries conditions: ready, serving and terminating. A Pod failing its readiness probe stays in the slice but with ready: false, and kube-proxy stops sending it new traffic. That is why "the Service has no endpoints" so often turns out to be a readiness problem rather than a networking one (my probes explainer covers why).
The old v1 Endpoints API still works, but v1.33 deprecated it. The changelog says it will not be removed, but all users should move to EndpointSlices, so anything you write today should read those instead.
# Kubernetes v1.37 client and cluster assumed.
# List the slices behind a Service and show each endpoint's IP and readiness.
kubectl get endpointslices -l kubernetes.io/service-name=web \
-o custom-columns='SLICE:.metadata.name,IPS:.endpoints[*].addresses[*],READY:.endpoints[*].conditions.ready'
# Which proxy mode is kube-proxy actually running? (kubeadm clusters)
kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep '^mode'
# Pin the mode explicitly in the KubeProxyConfiguration, as the docs now advise:
# apiVersion: kubeproxy.config.k8s.io/v1alpha1
# kind: KubeProxyConfiguration
# mode: "nftables" # needs Linux kernel 5.13 or later
# nodePortAddresses: ["primary"]
kube-proxy modes: iptables vs IPVS vs nftables
kube-proxy runs on every node, watches Services and EndpointSlices, and programs the kernel so that a packet to a ClusterIP gets rewritten (DNAT) to a real Pod IP. How it programs the kernel is the "mode", and this is the part of Kubernetes networking that changed most in the last two years.
iptables is still the default on Linux in 1.37. It creates a few rules per Service and per endpoint, and picks a backend with random-probability rules. The virtual IPs reference is candid that with tens of thousands of Pods and Services you get tens of thousands of rules and slow updates. It also has a detail worth memorising: when a Service has no endpoints at all, kube-proxy installs a REJECT rule in the filter table with the comment "has no endpoints" (source), so clients get an immediate connection refused rather than a hang.
IPVS was the performance answer for large clusters. It is now on its way out. The docs say the kernel IPVS API "turned out to be a bad match" for the Services API and never implemented every edge case. It was deprecated in v1.35, v1.37 added the KubeProxyIPVS feature gate, support will be disabled by default from v1.40 and the mode fully removed in v1.43, per the virtual IPs page.
nftables went GA in v1.33, needs kernel 5.13 or later, and is the documented replacement for both. It processes endpoint changes faster and uses maps and sets instead of long linear rule chains. In v1.37, kube-proxy warns if you start it without an explicit mode, because the Linux default will switch from iptables to nftables in a future release, and kubeadm now writes mode: iptables explicitly when you leave it empty. Before switching, read the migration notes: nftables mode only listens for NodePorts on the node's primary IPs by default, does not add firewall-accept rules for NodePorts, and does not install the old conntrack workaround for kernels before 6.1.
eBPF kube-proxy replacement: do you still need kube-proxy?
Not necessarily. An eBPF data plane can implement Services itself by attaching programs to the socket layer and network devices, translating the Service IP before the packet ever reaches netfilter. Cilium's kube-proxy-free mode is installed with --set kubeProxyReplacement=true plus the API server address (k8sServiceHost, k8sServicePort), because without kube-proxy the agent cannot rely on the kubernetes Service ClusterIP to find the API server. You check it with kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep KubeProxyReplacement. Calico's eBPF data plane offers the same. On managed clusters you may already be running this: GKE Dataplane V2 is implemented with Cilium, does not use kube-proxy for Services, and is on by default for new Autopilot clusters.
The trade is the usual one. You get fewer moving parts per packet and better visibility (Hubble flow logs are genuinely useful), and in exchange iptables-save stops telling you anything about Services and your debugging muscle memory has to change. My eBPF explainer covers the kernel side. For a team that does not already know eBPF, nftables kube-proxy plus a policy-capable CNI is a perfectly respectable 2026 choice.
How Kubernetes DNS and service discovery work
The last owner is DNS. CoreDNS runs as a Deployment behind a Service (traditionally kube-dns in kube-system), and the kubelet writes that Service IP into each Pod's /etc/resolv.conf with search domains for its namespace. A Service called web in namespace shop resolves as web from inside shop, as web.shop from elsewhere, and fully as web.shop.svc.cluster.local. Headless Services return A records for each ready Pod instead of one ClusterIP.
Two things to know. The default ndots:5 means a name such as api.example.com gets tried against every search domain before going upstream, which multiplies DNS queries. And DNS failures look like connection failures to the application: a timeout in your logs can be a resolver that never answered rather than a Service that never accepted. The CoreDNS troubleshooting guide has the commands. North-south HTTP traffic is yet another layer (Ingress or Gateway API), covered in Ingress NGINX Is Retired.
FAQ
What is the difference between a CNI plugin and kube-proxy?
The CNI plugin builds the pod network: it gives each Pod an interface and IP and makes Pod IPs reachable across nodes. kube-proxy implements Services on top of that network by translating Service virtual IPs to Pod IPs. Some CNIs, such as Cilium, can do both, in which case you run without kube-proxy.
Is IPVS mode deprecated in Kubernetes?
Yes. IPVS mode was deprecated in v1.35. The Kubernetes docs say it will be disabled by default from v1.40 (re-enabled with the KubeProxyIPVS feature gate) and removed in v1.43, and they recommend nftables mode as the replacement.
Which CNI should I choose in 2026?
On a managed cloud, start with the provider default (VPC CNI on EKS, Dataplane V2 on GKE) unless you need something it lacks. For self-managed clusters, Cilium suits teams who want eBPF, kube-proxy replacement and flow visibility; Calico suits teams who want mature policy and BGP; Flannel suits small clusters and labs, with a separate policy controller if you need NetworkPolicy.
Should I switch kube-proxy to nftables now?
If your nodes run kernel 5.13 or later and you are on v1.33+, it is GA and the documented direction. Test NodePort behaviour first, since nftables mode listens only on primary node IPs by default and adds no firewall rules. Whatever you choose, set mode explicitly so a future default change does not surprise you.
This quarter, on any cluster I am responsible for, I would do three small things: set the kube-proxy mode explicitly in config, move any IPVS cluster to nftables on a canary node pool well before v1.40, and replace every script that reads kubectl get endpoints with EndpointSlices. None of that is exciting work. It is the kind of platform hygiene that means the next upgrade is boring, which is the only kind of upgrade I want. (I run PandaStack, a Firecracker microVM cloud, and its control plane depends on exactly this sort of unglamorous networking plumbing.)
Related: How to Write Kubernetes NetworkPolicies: Default Deny to Egress (2026), eBPF Explained (2026): Verifier, CO-RE, Cilium, Tetragon and sched_ext and Service Mesh in 2026: Istio Ambient vs Linkerd vs Cilium, Do You Need One?.
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
Load Balancers Explained (2026): L4 vs L7, ALB vs NLB vs GWLB, LCU Costs
How load balancers work in 2026: L4 vs L7, proxy vs passthrough, algorithms, health check defaults, TLS and idle timeouts, AWS ALB vs NLB vs GWLB with GCP and Azure equivalents, Kubernetes LoadBalancer Services, the AWS Load Balancer Controller v3.6, and how LCU billing works.
12 minOct 31, 2026Docker Compose in Production (2026): Is v5.6 Enough, or Do You Need K8s?
Is Docker Compose good enough for production in 2026? What Compose v5 changed (pre_start init containers, reconciliation, unsupported-key warnings), what a single host still cannot do, how Kamal, Coolify and Swarm compare, and when to move to Kubernetes.
13 minNov 10, 2026Is Ansible Still Relevant in 2026? ansible-core 2.22 vs Terraform
Is Ansible still relevant in 2026? What ansible-core 2.22 and Ansible 14 changed, how Ansible compares with Terraform, cloud-init and Kubernetes, where configuration management still wins, and what Event-Driven Ansible and Lightspeed actually do.
11 min