<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>networking | Ronak Nathani</title><link>https://ronaknathani.pages.dev/tag/networking/</link><atom:link href="https://ronaknathani.pages.dev/tag/networking/index.xml" rel="self" type="application/rss+xml"/><description>networking</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><copyright>© 2026 Ronak Nathani</copyright><lastBuildDate>Fri, 21 Aug 2020 13:04:27 -0700</lastBuildDate><image><url>https://ronaknathani.pages.dev/img/avatar.jpg</url><title>networking</title><link>https://ronaknathani.pages.dev/tag/networking/</link></image><item><title>How a Kubernetes Pod Gets an IP Address</title><link>https://ronaknathani.pages.dev/blog/2020/08/how-a-kubernetes-pod-gets-an-ip-address/</link><pubDate>Fri, 21 Aug 2020 13:04:27 -0700</pubDate><guid>https://ronaknathani.pages.dev/blog/2020/08/how-a-kubernetes-pod-gets-an-ip-address/</guid><description>&lt;p>One of the core requirements of the
&lt;a href="https://kubernetes.io/docs/concepts/cluster-administration/networking/#the-kubernetes-network-model" target="_blank" rel="noopener">Kubernetes networking model&lt;/a> is that every pod should get its own IP address and that every pod in the cluster should be able to talk to it using this IP address. There are several network providers (flannel, calico, canal, etc.) that implement this networking model.&lt;/p>
&lt;p>As I started working on Kubernetes, it wasn&amp;rsquo;t completely clear to me how every pod is assigned an IP address. I understood how various components worked independently, however, it wasn&amp;rsquo;t clear how these components fit together. For instance, I understood what CNI plugins were, however, I didn&amp;rsquo;t know how they were invoked. So, I wanted to write this post to share what I have learned about various networking components and how they are stitched together in a kubernetes cluster for every pod to receive an IP address.&lt;/p>
&lt;p>There are various ways of setting up networking in kubernetes and various options for a container runtime. For this post, I will use
&lt;a href="https://github.com/coreos/flannel" target="_blank" rel="noopener">Flannel&lt;/a> as the network provider and
&lt;a href="https://github.com/containerd/containerd" target="_blank" rel="noopener">Containerd&lt;/a> as the container runtime. Also, I am going to assume that you know how container networking works and only share a very brief overview below for context.&lt;/p>
&lt;h2 id="some-background-concepts">Some Background Concepts&lt;/h2>
&lt;h3 id="container-networking-a-very-brief-overview">Container Networking: A Very Brief Overview&lt;/h3>
&lt;p>There are some really good posts explaining how container networking works. For context, I will go over a very high level overview here with a single approach that involves linux bridge networking and packet encapsulation. I am skipping details here as container networking deserves a blog post of itself. Some of the posts that I have found to be very educational in this space are
&lt;a href="#container-networking">linked in the references below&lt;/a>.&lt;/p>
&lt;h4 id="containers-on-the-same-host">Containers on the same host&lt;/h4>
&lt;p>One of the ways containers running on the same host can talk to each other via their IP addresses is through a linux bridge. In the kubernetes (and docker) world, a
&lt;a href="https://man7.org/linux/man-pages/man4/veth.4.html" target="_blank" rel="noopener">veth (virtual ethernet)&lt;/a> device is created to achieve this. One end of this veth device is inserted into the container network namespace and the other end is connected to a
&lt;a href="https://wiki.archlinux.org/index.php/Network_bridge" target="_blank" rel="noopener">linux bridge&lt;/a> on the host network. All containers on the same host have one end of this veth pair connected to the linux bridge and they can talk to each other using their IP addresses via the bridge. The linux bridge is also assigned an IP address and it acts as a gateway for egress traffic from pods destined to different nodes.
&lt;img src="bridge-networking.png" alt="bridge networking">&lt;/p>
&lt;h4 id="containers-on-different-hosts">Containers on different hosts&lt;/h4>
&lt;p>One of the ways containers running on different hosts can talk to each other via their IP addresses is by using packet encapsulation. Flannel supports this through
&lt;a href="https://vincent.bernat.ch/en/blog/2017-vxlan-linux" target="_blank" rel="noopener">vxlan&lt;/a> which wraps the original packet inside a UDP packet and sends it to the destination.&lt;/p>
&lt;p>In a kubernetes cluster, flannel creates a vxlan device and some route table entries on each of the nodes. Every packet that&amp;rsquo;s destined for a container on a different host goes through the vxlan device and is encapsulated in a UDP packet. On the destination, the encapsulated packet is retrieved and the packet is routed through to the destined pod.
&lt;img src="flannel-networking.png" alt="flannel networking">&lt;/p>
&lt;p>&lt;em>NOTE: This is just one of the ways how networking between containers can be configured.&lt;/em>&lt;/p>
&lt;h3 id="what-is-cri">What Is CRI?&lt;/h3>
&lt;p>
&lt;a href="https://github.com/kubernetes/cri-api" target="_blank" rel="noopener">CRI (Container Runtime Interface)&lt;/a> is a plugin interface that allows kubelet to use different container runtimes. Various container runtimes implement the CRI API and this allows users to use the container runtime of their choice in their kubernetes installation.&lt;/p>
&lt;h3 id="what-is-cni">What is CNI?&lt;/h3>
&lt;p>
&lt;a href="https://github.com/containernetworking/cni" target="_blank" rel="noopener">CNI project&lt;/a> includes a
&lt;a href="https://github.com/containernetworking/cni/blob/master/SPEC.md" target="_blank" rel="noopener">spec&lt;/a> to provide a generic plugin-based networking solution for linux containers. It also consists of various
&lt;a href="https://github.com/containernetworking/plugins" target="_blank" rel="noopener">plugins&lt;/a> which perform different functions in configuring the pod network. A CNI plugin is an executable that follows the CNI spec and we&amp;rsquo;ll discuss some plugins in the post below.&lt;/p>
&lt;h2 id="assigning-subnets-to-nodes-for-pod-ip-addresses">Assigning Subnets To Nodes For Pod IP Addresses&lt;/h2>
&lt;p>If all pods are required to have an IP address, it&amp;rsquo;s important to ensure that all pods across the entire cluster have a unique IP address. This is achieved by assigning each node a unique subnet from which pods are assigned IP addresses on that node.&lt;/p>
&lt;h3 id="node-ipam-controller">Node IPAM Controller&lt;/h3>
&lt;p>When &lt;code>nodeipam&lt;/code> is passed as an option to the
&lt;a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/" target="_blank" rel="noopener">kube-controller-manager&amp;rsquo;s&lt;/a> &lt;code>--controllers&lt;/code> command line flag, it allocates each node a dedicated subnet (podCIDR) from the cluster CIDR (IP range for the cluster network). Since these podCIDRs are disjoint subnets, it allows assigning each pod a unique IP address.&lt;/p>
&lt;p>A kubernetes node is assigned a podCIDR when the node first registers with the cluster. To change the podCIDR allocated to nodes in a cluster, nodes need to be de-registered and then re-registered with any configuration changes first applied to the kubernetes control plane. &lt;code>podCIDR&lt;/code> for a node can be listed using the following command.&lt;/p>
&lt;pre>&lt;code>$ kubectl get no &amp;lt;nodeName&amp;gt; -o json | jq '.spec.podCIDR'
10.244.0.0/24
&lt;/code>&lt;/pre>
&lt;h2 id="kubelet-container-runtime-and-cni-plugins---how-its-all-stitched-together">Kubelet, Container Runtime and CNI Plugins - how it&amp;rsquo;s all stitched together&lt;/h2>
&lt;p>When a pod is scheduled on a node, a lot of things happen to start up a pod. In this section, I&amp;rsquo;ll only focus on the interactions that relate to configuring network for the pod.&lt;/p>
&lt;p>Once a pod is scheduled on the node, the following interactions result in configuring the network and starting the application container.
&lt;img src="kubelet-cri-cni-flowchart.png" alt="kubelet-cri-cni-flowchart">&lt;/p>
&lt;p>Ref:
&lt;a href="https://github.com/containerd/cri/blob/v1.11.1/docs/architecture.md" target="_blank" rel="noopener">Containerd cri plugin architecture&lt;/a>&lt;/p>
&lt;h2 id="interactions-between-container-runtime-and-cni-plugins">Interactions between Container Runtime and CNI Plugins&lt;/h2>
&lt;p>Every network provider has a CNI plugin which is invoked by the container runtime to configure network for a pod as it&amp;rsquo;s started. With containerd as the container runtime,
&lt;a href="https://github.com/containerd/cri" target="_blank" rel="noopener">Containerd CRI plugin&lt;/a> invokes the CNI plugin. Every network provider also has an agent that&amp;rsquo;s installed on each of the kubernetes node to configure pod networking. When the network provider agent is installed, it either ships with the CNI config or it creates one on the node which is then used by the CRI plugin to figure out which CNI plugin to call.&lt;/p>
&lt;p>The location for the CNI config file is configurable and the default value is &lt;code>/etc/cni/net.d/&amp;lt;config-file&amp;gt;&lt;/code>. CNI plugins need to be shipped on every node by the cluster administrators. The location for CNI plugins is configurable as well and the default value is &lt;code>/opt/cni/bin&lt;/code>.&lt;/p>
&lt;p>In case of containerd as the container runtime, path for CNI configuration and CNI plugin binaries can be specified under &lt;code>[plugins.&amp;quot;io.containerd.grpc.v1.cri&amp;quot;.cni]&lt;/code> section of the
&lt;a href="https://github.com/containerd/cri/blob/master/docs/config.md" target="_blank" rel="noopener">containerd config&lt;/a>.&lt;/p>
&lt;p>Since we are referring to Flannel as the network provider here, I&amp;rsquo;ll talk a little about how Flannel is set up. Flanneld is the Flannel daemon and is typically installed on a kubernetes cluster as a daemonset with &lt;code>install-cni&lt;/code> as an
&lt;a href="https://github.com/coreos/flannel/blob/master/Documentation/kube-flannel.yml#L172" target="_blank" rel="noopener">init container&lt;/a>. The &lt;code>install-cni&lt;/code> container creates the
&lt;a href="https://gist.github.com/ronaknnathani/957a56210bd4fbd8e11120273c6b4ede" target="_blank" rel="noopener">CNI configuration file&lt;/a> - &lt;code>/etc/cni/net.d/10-flannel.conflist&lt;/code> - on each node. Flanneld creates a vxlan device, fetches networking metadata from the apiserver and watches for updates on pods. As pods are created, it distributes routes for all pods across the entire cluster and these routes allow pods to connect to each other via their IP addresses. For details on how flannel works, I recommend the
&lt;a href="#how-flannel-works">linked references below&lt;/a>.&lt;/p>
&lt;p>The interactions between Containerd CRI Plugin and CNI plugins can be visualized as follows:
&lt;img src="kubelet-cri-cni-interactions.png" alt="kubelet-cri-cni-interactions">&lt;/p>
&lt;p>As described above, kubelet calls the Containerd CRI plugin in order to create a pod and Containerd CRI plugin calls the CNI plugin to configure network for the pod. The network provider CNI plugin calls other base CNI plugins to configure the network. The interactions between CNI plugins are described below.&lt;/p>
&lt;h3 id="interactions-between-cni-plugins">Interactions Between CNI Plugins&lt;/h3>
&lt;p>There are various CNI plugins that help configure networking between containers on a host. For this post, we will refer to 3 plugins.&lt;/p>
&lt;h4 id="flannel-cni-plugin">Flannel CNI Plugin&lt;/h4>
&lt;p>When using Flannel as the network provider, the Containerd CRI plugin invokes the
&lt;a href="https://github.com/containernetworking/plugins/tree/master/plugins/meta/flannel" target="_blank" rel="noopener">Flannel CNI plugin&lt;/a> using the CNI configuration file - &lt;code>/etc/cni/net.d/10-flannel.conflist&lt;/code>.&lt;/p>
&lt;pre>&lt;code>$ cat /etc/cni/net.d/10-flannel.conflist
{
&amp;quot;name&amp;quot;: &amp;quot;cni0&amp;quot;,
&amp;quot;plugins&amp;quot;: [
{
&amp;quot;type&amp;quot;: &amp;quot;flannel&amp;quot;,
&amp;quot;delegate&amp;quot;: {
&amp;quot;ipMasq&amp;quot;: false,
&amp;quot;hairpinMode&amp;quot;: true,
&amp;quot;isDefaultGateway&amp;quot;: true
}
}
]
}
&lt;/code>&lt;/pre>
&lt;p>The Fannel CNI plugin works in conjunction with Flanneld. When Flanneld starts up, it fetches the podCIDR and other network related details from the apiserver and stores them in a file - &lt;code>/run/flannel/subnet.env&lt;/code>.&lt;/p>
&lt;pre>&lt;code>FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=false
&lt;/code>&lt;/pre>
&lt;p>The Flannel CNI plugin uses the information in &lt;code>/run/flannel/subnet.env&lt;/code> to configure and invoke the bridge CNI plugin.&lt;/p>
&lt;h4 id="bridge-cni-plugin">Bridge CNI Plugin&lt;/h4>
&lt;p>Flannel CNI plugin calls the Bridge CNI plugin with the following configuration:&lt;/p>
&lt;pre>&lt;code>{
&amp;quot;name&amp;quot;: &amp;quot;cni0&amp;quot;,
&amp;quot;type&amp;quot;: &amp;quot;bridge&amp;quot;,
&amp;quot;mtu&amp;quot;: 1450,
&amp;quot;ipMasq&amp;quot;: false,
&amp;quot;isGateway&amp;quot;: true,
&amp;quot;ipam&amp;quot;: {
&amp;quot;type&amp;quot;: &amp;quot;host-local&amp;quot;,
&amp;quot;subnet&amp;quot;: &amp;quot;10.244.0.0/24&amp;quot;
}
}
&lt;/code>&lt;/pre>
&lt;p>When
&lt;a href="https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge" target="_blank" rel="noopener">Bridge CNI plugin&lt;/a> is invoked for the first time, it creates a linux bridge with the &lt;code>&amp;quot;name&amp;quot;: &amp;quot;cni0&amp;quot;&lt;/code> specified in the config file. For every pod, it then creates a veth pair - one end of the pair is in the container&amp;rsquo;s network namespace and the other end is connected to the linux bridge on the host network. With Bridge CNI plugin, all containers on a host are connected to the linux bridge on the host network.&lt;/p>
&lt;p>After configuring the veth pair, Bridge plugin invokes the host-local IPAM CNI plugin. Which IPAM plugin to use can be configured in the CNI config CRI plugin uses to call the flannel CNI plugin.&lt;/p>
&lt;h4 id="host-local-ipam-cni-plugins">Host-local IPAM CNI plugins&lt;/h4>
&lt;p>The Bridge CNI plugin calls the
&lt;a href="https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local" target="_blank" rel="noopener">host-local IPAM CNI plugin&lt;/a> with the following configuration:&lt;/p>
&lt;pre>&lt;code>{
&amp;quot;name&amp;quot;: &amp;quot;cni0&amp;quot;,
&amp;quot;ipam&amp;quot;: {
&amp;quot;type&amp;quot;: &amp;quot;host-local&amp;quot;,
&amp;quot;subnet&amp;quot;: &amp;quot;10.244.0.0/24&amp;quot;,
&amp;quot;dataDir&amp;quot;: &amp;quot;/var/lib/cni/networks&amp;quot;
}
}
&lt;/code>&lt;/pre>
&lt;p>Host-local IPAM (IP Address Management) plugin returns an IP address for the container from the &lt;code>subnet&lt;/code> and stores the allocated IP locally on the host under the directory specified under &lt;code>dataDir&lt;/code> - &lt;code>/var/lib/cni/networks/&amp;lt;network-name=cni0&amp;gt;/&amp;lt;ip&amp;gt;&lt;/code>. &lt;code>/var/lib/cni/networks/&amp;lt;network-name=cni0&amp;gt;/&amp;lt;ip&amp;gt;&lt;/code> file contains the container ID to which the IP is assigned.&lt;/p>
&lt;p>When invoked, the host-local IPAM plugin returns the following payload&lt;/p>
&lt;pre>&lt;code>{
&amp;quot;ip4&amp;quot;: {
&amp;quot;ip&amp;quot;: &amp;quot;10.244.4.2&amp;quot;,
&amp;quot;gateway&amp;quot;: &amp;quot;10.244.4.3&amp;quot;
},
&amp;quot;dns&amp;quot;: {}
}
&lt;/code>&lt;/pre>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>Kube-controller-manager assigns a podCIDR to each node. Pods on a node are assigned an IP address from the subnet value in podCIDR. Because podCIDRs across all nodes are disjoint subnets, it allows assigning each pod a unique IP address.&lt;/p>
&lt;p>Kubernetes cluster administrator configures and installs kubelet, container runtime, network provider agent and distributes CNI plugins on each node. When network provider agent starts, it generates a CNI config. When a pod is scheduled on a node, kubelet calls the CRI plugin to create the pod. In containerd&amp;rsquo;s case, Containerd CRI plugin then calls the CNI plugin specified in the CNI config to configure the pod network. And all of this results in a pod getting an IP address.&lt;/p>
&lt;hr>
&lt;p>It took me a while to understand all the interactions and the details involved. I hope this helped you in improving your understanding of how kubernetes works. If you think I got something wrong, please let me know via
&lt;a href="https://twitter.com/RonakNathani" target="_blank" rel="noopener">twitter&lt;/a> or email me at
&lt;a href="mailto:hello@ronaknathani.com">hello@ronaknathani.com&lt;/a>. If you&amp;rsquo;d like to discuss something in this post or anything else, feel free to reach out. I&amp;rsquo;d love to hear from you!&lt;/p>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;h3 id="container-networking">Container Networking&lt;/h3>
&lt;ul>
&lt;li>
&lt;a href="https://jvns.ca/blog/2016/12/22/container-networking/" target="_blank" rel="noopener">A container networking overview&lt;/a>&lt;/li>
&lt;li>
&lt;a href="https://blog.mbrt.dev/2017-10-01-demystifying-container-networking/" target="_blank" rel="noopener">Demystifying container networking&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="how-flannel-works">How Flannel Works&lt;/h3>
&lt;ul>
&lt;li>
&lt;a href="https://msazure.club/flannel-networking-demystify/" target="_blank" rel="noopener">Flannel Networking Demistify&lt;/a>&lt;/li>
&lt;li>
&lt;a href="https://medium.com/@anilkreddyr/kubernetes-with-flannel-understanding-the-networking-part-2-78b53e5364c7" target="_blank" rel="noopener">Kubernetes With Flannel - Understanding The Networking&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="cri-and-cni">CRI and CNI&lt;/h3>
&lt;ul>
&lt;li>
&lt;a href="https://github.com/containerd/cri/blob/v1.11.1/docs/architecture.md" target="_blank" rel="noopener">CRI Plugin Architecture&lt;/a>&lt;/li>
&lt;li>
&lt;a href="https://github.com/containernetworking/cni/blob/master/SPEC.md" target="_blank" rel="noopener">CNI Spec&lt;/a>&lt;/li>
&lt;li>
&lt;a href="https://github.com/containernetworking/plugins" target="_blank" rel="noopener">CNI Plugins&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Kubernetes NodePort and iptables rules</title><link>https://ronaknathani.pages.dev/blog/2020/07/kubernetes-nodeport-and-iptables-rules/</link><pubDate>Wed, 08 Jul 2020 14:36:33 -0700</pubDate><guid>https://ronaknathani.pages.dev/blog/2020/07/kubernetes-nodeport-and-iptables-rules/</guid><description>&lt;p>I have been working on kubernetes over the last few months and having fun learning about the underlying systems. When I started using
&lt;a href="%28https://kubernetes.io/docs/concepts/services-networking/service/%29">kubernetes services&lt;/a>, I wanted to learn about the iptables rules that kube-proxy creates to enable them, however, I didn&amp;rsquo;t exactly know where to start. While there are some really good posts explaining
&lt;a href="https://www.stackrox.com/post/2020/01/kubernetes-networking-demystified/" target="_blank" rel="noopener">kubernetes networking&lt;/a> and how the concept of
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#proxy-mode-iptables" target="_blank" rel="noopener">services works&lt;/a>, I couldn&amp;rsquo;t easily find a starting point to explore these iptables except reading the code or trying to make sense of the output of &lt;code>iptables-save&lt;/code> command (which I can&amp;rsquo;t).&lt;/p>
&lt;p>In this post, I share some of what I have learned by digging a little deeper into the iptables rules for NodePort type services and share answers to the following questions I came across while working on kube-proxy:&lt;/p>
&lt;ul>
&lt;li>What happens when a non-kubernetes process starts using a port that&amp;rsquo;s allocated as a NodePort to a service?&lt;/li>
&lt;li>Does the service endpoint continue to route traffic to pods if kube-proxy (configured in iptables mode) process dies on the node?&lt;/li>
&lt;/ul>
&lt;h2 id="some-background">Some background&lt;/h2>
&lt;p>Kubernetes allows a
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types" target="_blank" rel="noopener">few ways to expose applications&lt;/a> to the world outside the kubernetes cluster through the concept of
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/" target="_blank" rel="noopener">services&lt;/a>. In our setup at work, we expose a few of our apps through
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#nodeport" target="_blank" rel="noopener">NodePort&lt;/a> type services. Also,
&lt;a href="https://kubernetes.io/docs/concepts/overview/components/#kube-proxy" target="_blank" rel="noopener">kube-proxy&lt;/a> is the kubernetes component that powers the
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/" target="_blank" rel="noopener">services concept&lt;/a> and we run it in iptables mode (default).&lt;/p>
&lt;h2 id="a-little-about-nodeport">A little about NodePort&lt;/h2>
&lt;p>
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#nodeport" target="_blank" rel="noopener">NodePort&lt;/a> is one of the ways of exposing
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/" target="_blank" rel="noopener">Kubernetes Services&lt;/a> to the world outside the kubernetes cluster. As per Kubernetes
&lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#nodeport" target="_blank" rel="noopener">docs&lt;/a>,&lt;/p>
&lt;blockquote>
&lt;p>If you set the type field to NodePort, the Kubernetes control plane allocates a port from a range specified by &lt;code>--service-node-port-range&lt;/code> flag &lt;code>(default: 30000-32767)&lt;/code>. Each node proxies that port (the same port number on every Node) into your Service.&lt;/p>
&lt;/blockquote>
&lt;p>This means that if we have a &lt;code>service&lt;/code> with NodePort &lt;code>30000&lt;/code>, a request to &lt;code>&amp;lt;kubernetes-node-ip&amp;gt;:30000&lt;/code> will get routed to our app. Under normal circumstances kube-proxy
&lt;a href="https://github.com/kubernetes/kubernetes/blob/7de3f938c82a7c0538bdfac5bb9b3cad50c30eae/pkg/proxy/iptables/proxier.go#L1638" target="_blank" rel="noopener">binds and listens&lt;/a> on all NodePorts to ensure these ports stay reserved and no other processes can use them.&lt;/p>
&lt;h3 id="erra-non-kubernetes-is-using-the-nodeport">err&amp;hellip;a non-kubernetes is using the NodePort!!&lt;/h3>
&lt;p>A few weeks back at work, one of our kubernetes nodes was rebooted and as it was added back to the cluster, we saw errors in kube-proxy logs - &lt;code>bind: address already in use&lt;/code>:&lt;/p>
&lt;pre>&lt;code class="language-bash">I0707 20:57:38.648179 1 proxier.go:701] Syncing iptables rules
E0707 20:57:38.679876 1 proxier.go:1072] can't open &amp;quot;nodePort for default/azure-vote-front:&amp;quot; (:30450/tcp), skipping this nodePort: listen tcp :30450: bind: address already in use
&lt;/code>&lt;/pre>
&lt;p>This error meant that a port allocated as a NodePort to a &lt;code>service&lt;/code> was already in use by another process. We found out that there was a non-kubernetes process using this NodePort as a client port to connect to a remote server. This happened due of a race condition post node reboot. This other process started using the NodePort before kube-proxy could bind and listen on the port to reserve it.&lt;/p>
&lt;h2 id="will-the-nodeport-on-this-node-still-route-traffic-to-the-pods">Will the NodePort on this node still route traffic to the pods?&lt;/h2>
&lt;p>This was the immediate question that popped up in our heads as the NodePort allocated to a &lt;code>service&lt;/code> was now being used by a non-kubernetes process. One of my colleagues,
&lt;a href="https://www.linkedin.com/in/alexanderdudko/" target="_blank" rel="noopener">Alex Dudko&lt;/a> pointed out that kube-proxy creates iptables rules in the &lt;strong>PREROUTING&lt;/strong> chain in &lt;strong>nat&lt;/strong> table.&lt;/p>
&lt;p>Because of these rules, the answer to the above question is - &lt;strong>Yes!&lt;/strong> Traffic sent to the NodePort on this node will still correctly be routed to the backend pods it targets.&lt;/p>
&lt;p>The NodePort would also continue to work if another process was listening on the port as a server and not just using it as a client. Though, would our &lt;code>HTTP GET&lt;/code> requests reach this server? More on this later.&lt;/p>
&lt;h2 id="kube-proxy-iptables-rules">Kube-proxy iptables rules&lt;/h2>
&lt;p>In iptables mode, kube-proxy creates iptables rules for kubernetes services which ensure that the request to the &lt;code>service&lt;/code> gets routed (and load balanced) to the appropriate pods.&lt;/p>
&lt;p>These iptables rules also help answer the second question mentioned above. As long as these iptables rules exist, requests to &lt;code>services&lt;/code> will get routed to the appropriate pods even if kube-proxy process dies on the node. Endpoints for new &lt;code>services&lt;/code> won&amp;rsquo;t work from this node, however, since kube-proxy process won&amp;rsquo;t create the iptables rules for it.&lt;/p>
&lt;h3 id="rules-in-the-prerouting-chain">Rules in the PREROUTING chain&lt;/h3>
&lt;p>As outlined in this
&lt;a href="https://docs.google.com/drawings/d/1MtWL8qRTs6PlnJrW4dh8135_S9e2SaawT410bJuoBPk/preview" target="_blank" rel="noopener">flow chart&lt;/a>, there are rules in the &lt;strong>PREROUTING&lt;/strong> chain of the &lt;strong>nat&lt;/strong> table for kubernetes services. As per
&lt;a href="https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg" target="_blank" rel="noopener">iptables rules evaluation order&lt;/a>, rules in the &lt;strong>PREROUTING&lt;/strong> chain are the first ones to be consulted as a packet enters the linux kernel&amp;rsquo;s networking stack.&lt;/p>
&lt;p>Rules created by kube-proxy in the &lt;strong>PREROUTING&lt;/strong> chain help determine if the packet is meant for a local socket on the node or if it should be forwarded to a pod. It is these rules that ensure that the request to &lt;code>&amp;lt;kubernetes-node-ip&amp;gt;:&amp;lt;NodePort&amp;gt;&lt;/code> continue to get routed to pods even if the NodePort is in use by another process.&lt;/p>
&lt;p>Below is an over-simplified diagram that demonstrates this:&lt;/p>
&lt;p>&lt;img src="prerouting-rule.png" alt="prerouting-rule effect">&lt;/p>
&lt;p>In rest of this post, I walk through a setup that reproduces the above scenario and examine the iptables rules that make this work.&lt;/p>
&lt;h2 id="setting-up-a-kubernetes-cluster">Setting up a Kubernetes cluster&lt;/h2>
&lt;p>I created a kubernetes cluster using
&lt;a href="https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough" target="_blank" rel="noopener">Azure Kubernetes Service &lt;/a>.&lt;/p>
&lt;h3 id="creating-pods-and-a-nodeport-service">Creating pods and a NodePort Service&lt;/h3>
&lt;p>From the
&lt;a href="https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough#run-the-application" target="_blank" rel="noopener">AKS docs&lt;/a>, I also created a
&lt;a href="https://gist.github.com/ronaknnathani/49b54d7147e263d8dd3bb6016614f817" target="_blank" rel="noopener">deployment for an app with a frontend and a backend&lt;/a>. I updated the &lt;strong>azure-vote-front&lt;/strong> to &lt;strong>type: NodePort&lt;/strong> service.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>azure-vote-front&lt;/strong> pod in the cluster&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-bash">$ kubectl get po -l app=azure-vote-front --show-labels -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
azure-vote-front-5bc759676c-x9bwg 1/1 Running 0 24d 10.244.0.11 aks-nodepool1-22391620-vmss000000 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt; app=azure-vote-front,pod-template-hash=5bc759676c
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>azure-vote-front&lt;/strong> service targeting the above pod&lt;/li>
&lt;/ul>
&lt;pre>&lt;code class="language-bash">$ kubectl get svc azure-vote-front -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
azure-vote-front NodePort 10.0.237.3 &amp;lt;none&amp;gt; 80:30450/TCP 24d app=azure-vote-front
&lt;/code>&lt;/pre>
&lt;p>The above output shows that the port &lt;strong>30450&lt;/strong> is assigned as the NodePort to our service. And we can send HTTP requests to the app from host network:&lt;/p>
&lt;pre>&lt;code class="language-bash">$ curl -IX GET localhost:30450
HTTP/1.1 200 OK
Server: nginx/1.13.7
Date: Wed, 08 Jul 2020 00:02:08 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 950
Connection: keep-alive
$ curl -sSL localhost:30450 | grep title
&amp;lt;title&amp;gt;Azure Voting App&amp;lt;/title&amp;gt;
&lt;/code>&lt;/pre>
&lt;h3 id="examining-iptables-rules-for-a-nodeport-service">Examining iptables rules for a NodePort service&lt;/h3>
&lt;p>&lt;em>Note: AKS doesn&amp;rsquo;t allow ssh&amp;rsquo;ing into an AKS worker node directly, instead, a pod needs to be spun up to ssh into the worker node. Instructions
&lt;a href="https://docs.microsoft.com/en-us/azure/aks/ssh#configure-virtual-machine-scale-set-based-aks-clusters-for-ssh-access" target="_blank" rel="noopener">here&lt;/a>.&lt;/em>&lt;/p>
&lt;p>Let&amp;rsquo;s look at the iptables rules for the &lt;strong>azure-vote-front&lt;/strong> service.&lt;/p>
&lt;p>After ssh&amp;rsquo;ing into the kubernetes worker node, let&amp;rsquo;s look at the &lt;strong>PREROUTING&lt;/strong> chain in the &lt;strong>nat&lt;/strong> table. (&lt;code>PREROUTING&lt;/code>
&lt;a href="https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture#which-chains-are-implemented-in-each-table" target="_blank" rel="noopener">chain exists&lt;/a> in &lt;code>raw&lt;/code>, &lt;code>nat&lt;/code> and &lt;code>mangle&lt;/code> tables, however, kube-proxy only creates &lt;code>PREROUTING&lt;/code> chain rules in &lt;code>nat&lt;/code> table)&lt;/p>
&lt;pre>&lt;code class="language-bash">$ sudo iptables -t nat -L PREROUTING | column -t
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
&lt;/code>&lt;/pre>
&lt;p>There&amp;rsquo;s a &lt;code>KUBE-SERVICES&lt;/code> chain in the target that&amp;rsquo;s created by kube-proxy. Let&amp;rsquo;s list the rules in that chain.&lt;/p>
&lt;pre>&lt;code class="language-bash">$ sudo iptables -t nat -L KUBE-SERVICES -n | column -t
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.202.188 /* kube-system/healthmodel-replicaset-service: cluster IP */ tcp dpt:25227
KUBE-SVC-KA44REDDIFNWY4W2 tcp -- 0.0.0.0/0 10.0.202.188 /* kube-system/healthmodel-replicaset-service: cluster IP */ tcp dpt:25227
KUBE-MARK-MASQ udp -- !10.244.0.0/16 10.0.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- 0.0.0.0/0 10.0.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 10.0.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.190.160 /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-SVC-XGLOHA7QRQ3V22RZ tcp -- 0.0.0.0/0 10.0.190.160 /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.195.6 /* kube-system/metrics-server: cluster IP */ tcp dpt:443
KUBE-SVC-LC5QY66VUV2HJ6WZ tcp -- 0.0.0.0/0 10.0.195.6 /* kube-system/metrics-server: cluster IP */ tcp dpt:443
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.129.119 /* default/azure-vote-back: cluster IP */ tcp dpt:6379
KUBE-SVC-JQ4VBJ2YWO22DDZW tcp -- 0.0.0.0/0 10.0.129.119 /* default/azure-vote-back: cluster IP */ tcp dpt:6379
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.237.3 /* default/azure-vote-front: cluster IP */ tcp dpt:80
KUBE-SVC-GHSLGKVXVBRM4GZX tcp -- 0.0.0.0/0 10.0.237.3 /* default/azure-vote-front: cluster IP */ tcp dpt:80
KUBE-MARK-MASQ tcp -- !10.244.0.0/16 10.0.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- 0.0.0.0/0 10.0.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-NODEPORTS all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
&lt;/code>&lt;/pre>
&lt;p>The last terget in the &lt;code>KUBE-SERVICES&lt;/code> chain is the &lt;code>KUBE-NODEPORTS&lt;/code> chain. Since the service we created is of type &lt;code>NodePort&lt;/code>, let&amp;rsquo;s list the rules in &lt;code>KUBE-NODEPORTS&lt;/code> chain.&lt;/p>
&lt;pre>&lt;code class="language-bash">$ sudo iptables -t nat -L KUBE-NODEPORTS -n | column -t
Chain KUBE-NODEPORTS (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/azure-vote-front: */ tcp dpt:30450
KUBE-SVC-GHSLGKVXVBRM4GZX tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/azure-vote-front: */ tcp dpt:30450
&lt;/code>&lt;/pre>
&lt;p>We can see that the above targets are for packets destined to our NodePort &lt;strong>30450&lt;/strong>. The comments also show the namespace/pod name - &lt;code>default/azure-vote-front&lt;/code>. For requests originating from outside the cluster and destined to our app running as a pod, the &lt;code>KUBE-MARK-MASQ&lt;/code> rule marks the packet to be altered later in the &lt;code>POSTROUTING&lt;/code> chain to use SNAT (source network address translation) to rewrite the source IP as the node IP (so that other hosts outside the pod network can reply back).&lt;/p>
&lt;p>Since &lt;code>KUBE-MARK-MASQ&lt;/code> target is to
&lt;a href="https://www.frozentux.net/iptables-tutorial/chunkyhtml/x4422.html" target="_blank" rel="noopener">MASQUERADE&lt;/a> packets later, let&amp;rsquo;s follow the &lt;code>KUBE-SVC-GHSLGKVXVBRM4GZX&lt;/code> chain to further examine our service.&lt;/p>
&lt;pre>&lt;code class="language-bash">$ sudo iptables -t nat -L KUBE-SVC-GHSLGKVXVBRM4GZX -n | column -t
Chain KUBE-SVC-GHSLGKVXVBRM4GZX (2 references)
target prot opt source destination
KUBE-SEP-QXDNOBCCLOXLV7LV all -- 0.0.0.0/0 0.0.0.0/0
$ sudo iptables -t nat -L KUBE-SEP-QXDNOBCCLOXLV7LV -n | column -t
Chain KUBE-SEP-QXDNOBCCLOXLV7LV (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.244.0.11 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.244.0.11:80
&lt;/code>&lt;/pre>
&lt;p>Here we see the
&lt;a href="https://www.frozentux.net/iptables-tutorial/chunkyhtml/x4033.html" target="_blank" rel="noopener">DNAT (Destination Network Address Translation)&lt;/a> target is used to rewrite the destination of the packet destined to port &lt;strong>30450&lt;/strong> to our pod &lt;strong>10.244.0.11:80&lt;/strong>. We can verify the &lt;strong>podIP&lt;/strong> and &lt;strong>containerPort&lt;/strong> of our pod as follows:&lt;/p>
&lt;pre>&lt;code class="language-bash">$ kubectl get po azure-vote-front-5bc759676c-x9bwg -o json | jq -r '[.status.podIP, .spec.containers[0].ports[0].containerPort | tostring] | join(&amp;quot;:&amp;quot;)'
10.244.0.11:80
&lt;/code>&lt;/pre>
&lt;h3 id="verify-kube-proxy-is-listening-on-nodeport">Verify kube-proxy is listening on NodePort&lt;/h3>
&lt;p>Under normal circumstances kube-proxy binds and listens on all NodePorts to ensure these ports stay reserved and no other processes can use them. We can verify this on the above kubernetes node:&lt;/p>
&lt;pre>&lt;code class="language-bash">$ sudo lsof -i:30450
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
hyperkube 11558 root 9u IPv6 95841832 0t0 TCP *:30450 (LISTEN)
$ ps -aef | grep -v grep | grep 11558
root 11558 11539 0 Jul02 ? 00:06:37 /hyperkube kube-proxy --kubeconfig=/var/lib/kubelet/kubeconfig --cluster-cidr=10.244.0.0/16 --feature-gates=ExperimentalCriticalPodAnnotation=true --v=3
&lt;/code>&lt;/pre>
&lt;p>kube-proxy is listening on NodePort &lt;strong>30450&lt;/strong>.&lt;/p>
&lt;h3 id="create-a-non-kubernetes-process-use-the-nodeport">Create a non-kubernetes process use the NodePort&lt;/h3>
&lt;p>Now let&amp;rsquo;s kill kube-proxy process and start a server that listens on this NodePort instead.&lt;/p>
&lt;pre>&lt;code class="language-bash">$ kill -9 11558
$ python -m SimpleHTTPServer 30450 &amp;amp;
[1] 123578
$ sudo lsof -i:30450
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 123578 azureuser 3u IPv4 95854834 0t0 TCP *:30450 (LISTEN)
$ ps -aef | grep -v grep | grep 123578
azureus+ 123578 85107 0 00:49 pts/5 00:00:00 python -m SimpleHTTPServer 30450
&lt;/code>&lt;/pre>
&lt;h3 id="where-will-the-requests-to-nodeport-be-routed---pod-or-the-non-kubernetes-process">Where will the requests to NodePort be routed - pod or the non-kubernetes process?&lt;/h3>
&lt;p>While the python process is listening on port &lt;strong>30450&lt;/strong>, also allocated as a &lt;code>NodePort&lt;/code> to our kubernetes service, the iptables rules in the &lt;strong>PREROUTING&lt;/strong> chain will route all requests to port &lt;strong>30450&lt;/strong> to our pod. We can verify this as below:&lt;/p>
&lt;pre>&lt;code class="language-bash">$ curl -I -XGET localhost:30450
HTTP/1.1 200 OK
Server: nginx/1.13.7
Date: Wed, 08 Jul 2020 00:53:09 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 950
Connection: keep-alive
&lt;/code>&lt;/pre>
&lt;p>As we send a &lt;code>GET&lt;/code> request to port &lt;strong>30450&lt;/strong>, we receive a response from an nginx server hosting &lt;strong>Azure Voting App&lt;/strong>, like we saw when
&lt;a href="#creating-pods-and-a-nodeport-service">kube-proxy was listening on port &lt;strong>30450&lt;/strong>&lt;/a>.&lt;/p>
&lt;pre>&lt;code class="language-bash">$ curl -sSL localhost:30450 | grep title
&amp;lt;title&amp;gt;Azure Voting App&amp;lt;/title&amp;gt;
&lt;/code>&lt;/pre>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;ul>
&lt;li>When kube-proxy is used in iptables mode, routing requests to &lt;strong>services&lt;/strong> continues to work for existing services even when the kube-proxy process dies on the node&lt;/li>
&lt;li>Kube-proxy binds and listens (on all k8s nodes) to all ports allocated as NodePorts to ensure these ports stay reserved and no other processes can use them&lt;/li>
&lt;li>Even if a process starts using NodePort, iptables rules (because they are in &lt;strong>PRESOUTING&lt;/strong> chain) ensure that the traffic sent to the &lt;code>NodePort&lt;/code> gets routed to the pods&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>
&lt;a href="https://twitter.com/thockin" target="_blank" rel="noopener">@thockin&amp;rsquo;s&lt;/a> tweet,
&lt;a href="https://twitter.com/thockin/status/1191766983735296000" target="_blank" rel="noopener">describing kube-proxy iptables rules&lt;/a> in a
&lt;a href="https://docs.google.com/drawings/d/1MtWL8qRTs6PlnJrW4dh8135_S9e2SaawT410bJuoBPk/preview" target="_blank" rel="noopener">flow chart&lt;/a>, is an excellent way to holistically follow a packet&amp;rsquo;s path through various iptables rules before it reaches the destined pod.&lt;/li>
&lt;li>
&lt;a href="https://www.stackrox.com/post/2020/01/kubernetes-networking-demystified/" target="_blank" rel="noopener">Kubernetes Networking Demystified: A Brief Guide&lt;/a> is a great post explaining how kubernetes services and kubernetes networking work.&lt;/li>
&lt;/ul></description></item></channel></rss>