<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Linux Networking Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/linux-networking/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/linux-networking/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 06 Jul 2026 03:36:28 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Linux Networking Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/linux-networking/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>TC eBPF — Pod-Level Network Policy Without iptables</title>
		<link>https://linuxcent.com/tc-ebpf-kubernetes-network-policy/</link>
					<comments>https://linuxcent.com/tc-ebpf-kubernetes-network-policy/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sun, 03 May 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Cilium]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux Networking]]></category>
		<category><![CDATA[NetworkPolicy]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[TC eBPF]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1837</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>TC eBPF Kubernetes network policy relies on pod identity Cilium attaches at the veth. Why XDP alone isn't enough, and how to debug stale TC filters.</p>
<p>The post <a href="https://linuxcent.com/tc-ebpf-kubernetes-network-policy/">TC eBPF — Pod-Level Network Policy Without iptables</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>eBPF: From Kernel to Cloud, Episode 8</em><br />
<em><a href="https://linuxcent.com/what-is-ebpf-linux-kubernetes/">What Is eBPF?</a> · <a href="https://linuxcent.com/bpf-verifier-kubernetes-safety/">The BPF Verifier</a> · <a href="https://linuxcent.com/ebpf-vs-kernel-modules-kubernetes/">eBPF vs Kernel Modules</a> · <a href="https://linuxcent.com/ebpf-program-types-kubernetes/">eBPF Program Types</a> · <a href="https://linuxcent.com/ebpf-maps-explained/">eBPF Maps</a> · <a href="https://linuxcent.com/ebpf-co-re-libbpf-portable-programs/">CO-RE and libbpf</a> · <a href="https://linuxcent.com/ebpf-xdp-kubernetes-networking/">XDP</a> · </em><em>TC eBPF</em>**</p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>TC eBPF fires after <code class="" data-line="">sk_buff</code> allocation — it has socket metadata, cgroup ID, and pod identity that XDP lacks<br />
<em>(<code class="" data-line="">sk_buff</code> = the kernel&#8217;s socket buffer, allocated for every packet; TC fires after this allocation, so it can read socket and process identity)</em></li>
<li>Direct action (DA) mode combines filter and action; the program&#8217;s return value is the packet fate</li>
<li>Multiple TC programs chain on the same hook ordered by priority — stale programs from Cilium upgrades cause silent policy conflicts</li>
<li><code class="" data-line="">tc filter show dev &lt;iface&gt; ingress/egress</code> is the primary inspection tool; <code class="" data-line="">bpftool net list</code> shows the full node picture</li>
<li>XDP + TC is the Cilium data path: XDP for pre-stack service load balancing, TC for per-pod identity-based enforcement</li>
<li>TC can modify packet content (<code class="" data-line="">bpf_skb_store_bytes</code>) — the basis for TC-based DNAT and packet mangling</li>
</ul>
<hr />
<p>TC eBPF is where Cilium implements pod-level network policy without iptables — the hook that fires after <code class="" data-line="">sk_buff</code> allocation, where socket and cgroup context exist, making per-pod enforcement possible. The obvious follow-up to XDP is why Cilium doesn&#8217;t use it for everything — pod network policy, egress enforcement, the full NetworkPolicy ruleset. The answer reveals an inherent trade-off built into the Linux data path: XDP&#8217;s speed comes from running before any context exists. At the moment it fires, there is no socket, no cgroup, no way to tell which pod sent the packet. The moment you need pod identity, you need a hook that fires later — and pays for it.</p>
<hr />
<p>A specific pod in production was experiencing intermittent TCP connection failures to an external service. Not all connections — roughly one in fifty. Kubernetes NetworkPolicy showed egress allowed for the namespace. Cilium policy status showed no violations. Running <code class="" data-line="">curl</code> from inside the pod worked fine.</p>
<p>The application logs told a different story: connection timeouts at the 30-second mark, no SYN-ACK received. Not a DNS issue — I verified with <code class="" data-line="">tcpdump</code> inside the pod namespace. SYN packets were leaving the pod network namespace. They weren&#8217;t making it onto the wire.</p>
<p>I ran <code class="" data-line="">bpftool net list</code> on the node and saw two TC egress programs attached to that pod&#8217;s veth interface. One from the current Cilium version (installed six weeks ago). One from the previous version — from before the rolling upgrade. Two programs. Different policy epochs. The older one had a stale block rule that fired intermittently based on connection tuple patterns it was never designed to handle in the new policy model.</p>
<p>Without understanding TC eBPF — what programs attach where, how multiple programs interact, and how to inspect them — I would have kept chasing ghosts in the application layer.</p>
<h2 id="quick-check-are-there-stale-tc-filters-on-your-cluster">Quick Check: Are There Stale TC Filters on Your Cluster?</h2>
<p>The most common TC eBPF issue on production clusters — stale filters left behind by a Cilium upgrade — is a two-command check:</p>
<pre><code class="" data-line=""># SSH into a worker node, then pick any pod&#039;s veth interface:
ip link | grep lxc | head -5
# lxc8a3f21b@if7: ...
# lxc2c9d3e1@if9: ...

# Check TC filters on that interface
tc filter show dev lxc8a3f21b egress
</code></pre>
<p><strong>Healthy output (one filter, one priority):</strong></p>
<pre><code class="" data-line="">filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 cil_to_container direct-action not_in_hw id 44
</code></pre>
<p><strong>Stale filter present (two priorities = problem):</strong></p>
<pre><code class="" data-line="">filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 cil_to_container direct-action not_in_hw id 44
filter protocol all pref 2 bpf chain 0
filter protocol all pref 2 bpf chain 0 handle 0x1 old_cil_to_container direct-action not_in_hw id 17
#                  ^^^^^^ two different priorities = two programs running in sequence
</code></pre>
<p>Two priorities on the same hook means two programs running sequentially. If the older one has a stale DROP rule, packets are being dropped intermittently — and nothing in the application layer will tell you why.</p>
<blockquote>
<p><strong>Not running Cilium?</strong> If you&#8217;re on a non-Cilium CNI (Calico, Flannel, <code class="" data-line="">aws-vpc-cni</code>), you likely won&#8217;t have TC eBPF filters on pod interfaces. Run <code class="" data-line="">tc filter show dev eth0 ingress</code> on the node uplink instead to see if any TC programs are attached at the node level. An empty response is normal for non-Cilium clusters.</p>
</blockquote>
<h2 id="why-tc-not-xdp">Why TC, Not XDP</h2>
<p>EP07 covered XDP: fastest possible hook, fires before <code class="" data-line="">sk_buff</code>, drops at line rate. If XDP is so fast, why doesn&#8217;t Cilium use it for everything?</p>
<p>Because XDP sees only raw packet bytes. No socket. No cgroup. No pod identity.</p>
<p>In Kubernetes, network policy is inherently about identity. &#8220;Allow pod A to connect to pod B on port 8080.&#8221; To enforce this, you need to know which pod a packet is coming from on egress — and which pod it&#8217;s going to on ingress. That mapping lives in the cgroup hierarchy and the socket buffer, neither of which exist at XDP time.</p>
<p>TC fires later in the packet lifecycle, after <code class="" data-line="">sk_buff</code> is allocated and populated:</p>
<pre><code class="" data-line="">Ingress path:
  wire → NIC → [XDP hook] → sk_buff allocated → [TC ingress hook] → netfilter → socket

Egress path:
  socket → IP routing → [TC egress hook] → qdisc → NIC → wire
</code></pre>
<p>At the TC egress hook on a pod&#8217;s veth interface, the <code class="" data-line="">sk_buff</code> carries the socket that created the packet — and from that socket you can read the cgroup ID. The cgroup hierarchy maps container → pod, so the TC program knows which pod this traffic belongs to. That&#8217;s what makes pod-level enforcement possible.</p>
<h2 id="the-linux-traffic-control-architecture">The Linux Traffic Control Architecture</h2>
<p><code class="" data-line="">tc</code> (traffic control) is the Linux subsystem for managing packet queues and scheduling. Most Linux administrators know it as the bandwidth-shaping tool:</p>
<pre><code class="" data-line=""># Classic tc usage — rate limit an interface
tc qdisc add dev eth0 root tbf rate 100mbit burst 32kbit latency 400ms
</code></pre>
<p>The qdisc (queuing discipline) is the primary abstraction. Under the qdisc sits a <strong>filter</strong> layer — and the filter type relevant to eBPF is <code class="" data-line="">cls_bpf</code>, which attaches eBPF programs as packet classifiers.</p>
<blockquote>
<p><strong>qdisc (queuing discipline)</strong> is the kernel&#8217;s packet scheduler for an interface — it controls how packets are buffered and in what order they leave. For eBPF policy enforcement, Cilium uses a special qdisc called <code class="" data-line="">clsact</code> which has no buffering behaviour at all; it purely provides the ingress and egress hook points where eBPF filters attach. If a pod veth doesn&#8217;t have <code class="" data-line="">clsact</code>, Cilium isn&#8217;t enforcing policy on that pod.</p>
</blockquote>
<p>Cilium attaches <code class="" data-line="">cls_bpf</code> filters in <strong>direct action</strong> (DA) mode, which combines classifier and action into a single eBPF program. The program&#8217;s return value is the packet fate directly:</p>
<table>
<thead>
<tr>
<th>Return value</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">TC_ACT_OK</code> (0)</td>
<td>Pass the packet</td>
</tr>
<tr>
<td><code class="" data-line="">TC_ACT_SHOT</code> (2)</td>
<td>Drop the packet</td>
</tr>
<tr>
<td><code class="" data-line="">TC_ACT_REDIRECT</code> (7)</td>
<td>Redirect to another interface</td>
</tr>
<tr>
<td><code class="" data-line="">TC_ACT_PIPE</code> (3)</td>
<td>Pass to the next filter in the chain</td>
</tr>
</tbody>
</table>
<h2 id="tc-context-what-your-program-can-see">TC Context: What Your Program Can See</h2>
<p>TC programs receive a <code class="" data-line="">struct __sk_buff</code> — a safe, BPF-accessible projection of the kernel <code class="" data-line="">sk_buff</code>. Unlike the raw packet bytes in XDP, <code class="" data-line="">__sk_buff</code> includes metadata:</p>
<pre><code class="" data-line="">struct __sk_buff {
    __u32 len;           // packet length
    __u32 pkt_type;      // PACKET_HOST, PACKET_BROADCAST, etc.
    __u32 mark;          // skb-&gt;mark — used by Cilium for pod identity
    __u32 queue_mapping;
    __u32 protocol;      // ETH_P_IP, ETH_P_IPV6, etc.
    __u32 vlan_present;
    __u32 vlan_tci;
    __u32 vlan_proto;
    __u32 priority;
    __u32 ingress_ifindex;
    __u32 ifindex;
    __u32 tc_index;
    __u32 cb[5];
    __u32 hash;
    __u32 tc_classid;
    __u32 data;          // offset to packet data
    __u32 data_end;
    __u32 napi_id;
    __u32 family;
    __u32 remote_ip4;    // source IP (ingress) or dest IP (egress)
    __u32 local_ip4;
    __u32 remote_port;
    __u32 local_port;
    // ...
};
</code></pre>
<p><code class="" data-line="">skb-&gt;mark</code> is how Cilium passes pod identity between its hook points.</p>
<blockquote>
<p><strong><code class="" data-line="">skb-&gt;mark</code></strong> is a 32-bit field in every <code class="" data-line="">sk_buff</code> that any kernel subsystem can read or write. It&#8217;s a general-purpose scratch field — iptables uses it, routing rules use it, and Cilium uses it to carry pod security identity from the socket hook through to TC enforcement. When Cilium stamps a pod&#8217;s identity into <code class="" data-line="">skb-&gt;mark</code> at connection time, every subsequent TC filter on that packet&#8217;s path can read it without another identity lookup. The socket-level cgroup hook (<code class="" data-line="">cgroup_sock_addr</code>) stamps the cgroup-derived pod identity into <code class="" data-line="">skb-&gt;mark</code> when the socket calls <code class="" data-line="">connect()</code>. By the time the packet reaches the TC egress hook, <code class="" data-line="">skb-&gt;mark</code> carries the pod&#8217;s security identity — and the TC program uses it for policy enforcement.</p>
</blockquote>
<h2 id="what-ciliums-tc-filters-actually-do">What Cilium&#8217;s TC Filters Actually Do</h2>
<p>The TC filter on each pod&#8217;s veth is Cilium&#8217;s enforcement point for Kubernetes NetworkPolicy. The mechanism:</p>
<ol>
<li>When a pod opens a connection, a <code class="" data-line="">cgroup_sock_addr</code> hook stamps the pod&#8217;s security identity (derived from its labels + namespace) into <code class="" data-line="">skb-&gt;mark</code></li>
<li>The TC egress filter on the veth reads <code class="" data-line="">skb-&gt;mark</code>, looks up the pod identity + destination in the policy map, and returns <code class="" data-line="">TC_ACT_SHOT</code> (drop) or <code class="" data-line="">TC_ACT_OK</code> (pass)</li>
<li>The TC ingress filter on the receiving pod&#8217;s veth does the same check for inbound traffic</li>
</ol>
<p>The policy map is a BPF LRU hash keyed on <code class="" data-line="">{pod_identity, dst_ip, dst_port, protocol}</code>. This is what <code class="" data-line="">cilium policy get</code> reads from — and what <code class="" data-line="">bpftool map dump</code> shows directly:</p>
<pre><code class="" data-line=""># Find Cilium&#039;s policy maps
bpftool map list | grep -i policy

# Dump the active policy entries for a specific endpoint
# Get endpoint ID from: cilium endpoint list
cilium bpf policy get &lt;endpoint-id&gt;

# Cross-check with raw bpftool dump
bpftool map dump id &lt;POLICY_MAP_ID&gt; | head -30
</code></pre>
<p>The <code class="" data-line="">clsact</code> qdisc is the prerequisite for any TC eBPF filter — it creates the ingress and egress hook points without any queuing behavior. Every pod veth on a Cilium node has one:</p>
<pre><code class="" data-line="">tc qdisc show dev lxcABCDEF
# qdisc clsact ffff: dev lxcABCDEF parent ffff:fff1
# ^^^^^^^^^^^^ this line confirms Cilium&#039;s hook points exist on this pod&#039;s veth
# If this is missing: Cilium is NOT enforcing NetworkPolicy on this pod
</code></pre>
<p>If a pod veth doesn&#8217;t show <code class="" data-line="">clsact</code>, Cilium isn&#8217;t enforcing policy on that pod.</p>
<h2 id="multiple-programs-and-the-filter-chain">Multiple Programs and the Filter Chain</h2>
<p>This is the detail that caused my production incident.</p>
<p>TC supports chaining multiple filters on the same hook, ordered by priority. Lower priority number runs first. When Cilium upgrades, it installs a new filter at a new priority before removing the old one. If the upgrade procedure has any timing gap — or if the removal step fails silently — you end up with two programs running in sequence.</p>
<pre><code class="" data-line=""># Show all TC filters on a pod&#039;s veth — both priorities visible
tc filter show dev lxc12345 egress

# Example output with a stale filter:
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 cil_to_container direct-action not_in_hw id 44
filter protocol all pref 2 bpf chain 0
filter protocol all pref 2 bpf chain 0 handle 0x1 old_cil_to_container direct-action not_in_hw id 17
</code></pre>
<p>Two programs. Pref 1 runs first. Pref 2 runs second — unless pref 1 returned <code class="" data-line="">TC_ACT_SHOT</code>, in which case the packet is already dropped and pref 2 never fires.</p>
<p>In my incident: pref 1 was the current Cilium version with correct policy, returning <code class="" data-line="">TC_ACT_OK</code> for the traffic in question. Pref 2 was the old version with a stale block entry, returning <code class="" data-line="">TC_ACT_SHOT</code> for a subset of connection tuples. Because <code class="" data-line="">TC_ACT_OK</code> passes to the next filter in the chain (<code class="" data-line="">TC_ACT_PIPE</code> would do the same), pref 2 got to run — and intermittently dropped packets.</p>
<p>The fix:</p>
<pre><code class="" data-line=""># Remove the stale filter by priority
tc filter del dev lxc12345 egress pref 2

# Verify only the current filter remains
tc filter show dev lxc12345 egress
</code></pre>
<p>This should be part of any post-upgrade verification for Cilium-managed clusters.</p>
<h2 id="how-cilium-uses-tc-across-the-full-node">How Cilium Uses TC Across the Full Node</h2>
<p>Cilium&#8217;s TC deployment on a node:</p>
<pre><code class="" data-line="">Pod veth (host-side, lxcXXXXX):
  TC ingress: cil_from_container — L3/L4 policy on the pod&#039;s outbound traffic
  TC egress:  cil_to_container   — L3/L4 policy on traffic arriving at the pod

Node uplink (eth0):
  TC ingress: cil_from_netdev    — traffic arriving from outside the node
  TC egress:  cil_to_netdev      — traffic leaving the node

XDP on eth0:
  cil_xdp_entry — pre-stack service load balancing (DNAT for ClusterIP)
</code></pre>
<p>The naming is counterintuitive at first: <code class="" data-line="">cil_from_container</code> is attached to the TC <strong>ingress</strong> hook on the veth.</p>
<blockquote>
<p><strong>Veth direction confusion:</strong> TC ingress/egress is named from the kernel&#8217;s perspective of the interface, not the pod&#8217;s. The host-side veth interface <em>receives</em> traffic that the pod is <em>sending</em> — so TC ingress on the host veth = the pod&#8217;s outbound traffic. This trips up everyone the first time. When debugging, always confirm direction with <code class="" data-line="">tc filter show dev lxcXXX ingress</code> and <code class="" data-line="">egress</code> separately, and check which Cilium program name is attached (<code class="" data-line="">cil_from_container</code> = pod outbound, <code class="" data-line="">cil_to_container</code> = pod inbound). The veth ingress direction from the host perspective is traffic flowing out of the container. Traffic leaving the pod hits the host-side veth ingress, which is <code class="" data-line="">cil_from_container</code>. It enforces egress policy for the pod. Naming follows the kernel&#8217;s perspective of the interface, not the application&#8217;s.</p>
</blockquote>
<p>To see the full picture on a node:</p>
<pre><code class="" data-line=""># All eBPF network programs (XDP and TC) across all interfaces
bpftool net list

# TC-specific view
for iface in $(ip link | grep lxc | awk -F&#039;: &#039; &#039;{print $2}&#039;); do
    echo &quot;=== $iface ===&quot;
    tc filter show dev $iface ingress
    tc filter show dev $iface egress
done
</code></pre>
<h2 id="tc-can-modify-packets-too">TC Can Modify Packets Too</h2>
<p>Unlike XDP, TC programs have full access to the <code class="" data-line="">sk_buff</code> and can modify packet content — headers, payload, and checksums. This is how TC-based DNAT works in Cilium when XDP isn&#8217;t available on the NIC: the program rewrites the destination IP at L3 and updates the IP + transport checksums atomically. The kernel BPF helper handles the checksum recalculation.</p>
<p>From an operational standpoint: if you see a TC program attached but expected traffic is being redirected rather than dropped, the program is likely doing DNAT. <code class="" data-line="">bpftool prog dump xlated id &lt;ID&gt;</code> shows the disassembled instructions and will reveal <code class="" data-line="">bpf_skb_store_bytes</code> calls if packet rewriting is happening.</p>
<h2 id="debugging-tc-programs-in-production">Debugging TC Programs in Production</h2>
<p>Workflow I follow when investigating network issues on Cilium clusters:</p>
<pre><code class="" data-line=""># 1. List all eBPF network programs (see the full picture)
bpftool net list

# 2. Check specific interface for stale TC filters
tc filter show dev lxcABCDEF ingress
tc filter show dev lxcABCDEF egress

# 3. Inspect a specific program
bpftool prog show id 44

# 4. Disassemble a program (last resort for understanding behavior)
bpftool prog dump xlated id 44

# 5. Check Cilium&#039;s view of the same interface
cilium endpoint list
cilium endpoint get &lt;endpoint-id&gt;

# 6. Enable verbose TC program logs (debug builds only)
# Cilium: set CILIUM_DEBUG=true in the deployment
</code></pre>
<h2 id="common-mistakes">Common Mistakes</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Impact</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Not checking for stale TC filters after Cilium upgrades</td>
<td>Conflicting policy programs cause intermittent drops</td>
<td>Run <code class="" data-line="">tc filter show</code> post-upgrade; remove stale by priority</td>
</tr>
<tr>
<td>Confusing ingress/egress direction on veth interfaces</td>
<td>Policy applied to wrong traffic direction</td>
<td>TC ingress on host-side veth = pod&#8217;s outbound traffic</td>
</tr>
<tr>
<td>Attaching TC without <code class="" data-line="">clsact</code> qdisc</td>
<td>Filter attachment fails</td>
<td><code class="" data-line="">tc qdisc add dev &lt;iface&gt; clsact</code> before filter add</td>
</tr>
<tr>
<td>Using <code class="" data-line="">TC_ACT_OK</code> when you want to stop the chain</td>
<td>Subsequent filters still run</td>
<td>Use <code class="" data-line="">TC_ACT_OK</code> knowing the chain continues; use <code class="" data-line="">TC_ACT_REDIRECT</code> or explicit <code class="" data-line="">TC_ACT_SHOT</code> only</td>
</tr>
<tr>
<td>Expecting TC performance equal to XDP</td>
<td>TC has sk_buff overhead — it&#8217;s slower</td>
<td>Right tool: XDP for pre-stack bulk drops, TC for identity-aware policy</td>
</tr>
<tr>
<td>Hardcoding <code class="" data-line="">skb-&gt;mark</code> interpretation</td>
<td>Different tools use mark differently</td>
<td>Document mark field usage clearly; coordinate between Cilium and custom programs</td>
</tr>
</tbody>
</table>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>TC eBPF fires after <code class="" data-line="">sk_buff</code> allocation — it has socket metadata, cgroup ID, and pod identity that XDP lacks</li>
<li>Direct action (DA) mode combines filter and action; the program&#8217;s return value is the packet fate</li>
<li>Multiple TC programs chain on the same hook ordered by priority — stale programs from Cilium upgrades cause silent policy conflicts</li>
<li><code class="" data-line="">tc filter show dev &lt;iface&gt; ingress/egress</code> is the primary inspection tool; <code class="" data-line="">bpftool net list</code> shows the full node picture</li>
<li>XDP + TC is the Cilium data path: XDP for pre-stack service load balancing, TC for per-pod identity-based enforcement</li>
<li>TC can modify packet content (<code class="" data-line="">bpf_skb_store_bytes</code>) — the basis for TC-based DNAT and packet mangling</li>
</ul>
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP08 closes out the kernel machinery arc: program types, maps, CO-RE, XDP, TC. Five episodes on the engine under the tools. EP09 shifts from understanding the machinery to using it interactively.</p>
<p>bpftrace turns kernel knowledge into one-liners you can run on a live production node. Which process is touching this file right now? Where is this latency spike originating in the kernel call stack? Which container is making DNS queries to an unexpected resolver? Under 10 seconds per question — no restart, no sidecar, no instrumentation change.</p>
<p>Every bpftrace one-liner is a complete eBPF program compiled, loaded, run, and cleaned up on the fly. EP09 covers how that works and why it changes the way you investigate production incidents.</p>
<p><em>Next: <a href="/bpftrace-kernel-observability/">bpftrace — kernel answers in one line</a></em></p>
<p>Get EP09 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&amp;linkname=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Ftc-ebpf-kubernetes-network-policy%2F&#038;title=TC%20eBPF%20%E2%80%94%20Pod-Level%20Network%20Policy%20Without%20iptables" data-a2a-url="https://linuxcent.com/tc-ebpf-kubernetes-network-policy/" data-a2a-title="TC eBPF — Pod-Level Network Policy Without iptables"></a></p><p>The post <a href="https://linuxcent.com/tc-ebpf-kubernetes-network-policy/">TC eBPF — Pod-Level Network Policy Without iptables</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/tc-ebpf-kubernetes-network-policy/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1837</post-id>	</item>
		<item>
		<title>XDP — Packets Processed Before the Kernel Knows They Arrived</title>
		<link>https://linuxcent.com/ebpf-xdp-kubernetes-networking/</link>
					<comments>https://linuxcent.com/ebpf-xdp-kubernetes-networking/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Tue, 21 Apr 2026 14:53:14 +0000</pubDate>
				<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Cilium]]></category>
		<category><![CDATA[DDoS mitigation]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux Networking]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[XDP]]></category>
		<guid isPermaLink="false">https://linuxcent.com/ebpf-xdp-kubernetes-networking/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span>XDP eBPF processes packets before the Linux kernel allocates a single byte of memory. How Cilium uses it for load balancing, unlike iptables.</p>
<p>The post <a href="https://linuxcent.com/ebpf-xdp-kubernetes-networking/">XDP — Packets Processed Before the Kernel Knows They Arrived</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 10</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>eBPF: From Kernel to Cloud, Episode 7</em><br />
<em><a href="https://linuxcent.com/what-is-ebpf-linux-kubernetes/">What Is eBPF?</a> · <a href="https://linuxcent.com/bpf-verifier-kubernetes-safety/">The BPF Verifier</a> · <a href="https://linuxcent.com/ebpf-vs-kernel-modules-kubernetes/">eBPF vs Kernel Modules</a> · <a href="https://linuxcent.com/ebpf-program-types-kubernetes/">eBPF Program Types</a> · <a href="https://linuxcent.com/ebpf-maps-explained/">eBPF Maps</a> · <a href="https://linuxcent.com/ebpf-co-re-libbpf-portable-programs/">CO-RE and libbpf</a> · </em><em>XDP</em>**</p>
<p><strong>14 min read</strong></p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>EP01 through EP06 covered what eBPF is, how the verifier keeps it safe, and how the toolchain compiles and loads programs across kernel versions. This episode is where that foundation meets production networking.</p>
<p>XDP — eXpress Data Path — is the earliest hook in the Linux kernel packet path. It fires before <code class="" data-line="">sk_buff</code> allocation, before routing, before netfilter. A DROP decision at XDP costs one bounds check and a return value. Everything else is skipped. At 1 million packets per second, that difference shows up directly as CPU.</p>
<p>This episode explains where XDP sits, what it can and cannot see, how Cilium uses it, and what every Kubernetes operator needs to know about it — even if they never write an eBPF program.</p>
<hr />
<h2 id="table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#tldr">TL;DR</a></li>
<li><a href="#quick-check-is-xdp-running-on-your-cluster">Quick Check: Is XDP Running on Your Cluster?</a></li>
<li><a href="#where-xdp-sits-in-the-kernel-data-path">Where XDP Sits in the Kernel Data Path</a></li>
<li><a href="#xdp-modes">XDP Modes</a></li>
<li><a href="#the-xdp-context-what-your-program-can-see">The XDP Context: What Your Program Can See</a></li>
<li><a href="#what-this-means-on-your-cluster-right-now">What This Means on Your Cluster Right Now</a></li>
<li><a href="#xdp-metadata-cooperating-with-tc">XDP Metadata: Cooperating with TC</a></li>
<li><a href="#how-cilium-uses-xdp">How Cilium Uses XDP</a></li>
<li><a href="#operational-inspection">Operational Inspection</a></li>
<li><a href="#common-mistakes">Common Mistakes</a></li>
<li><a href="#key-takeaways">Key Takeaways</a></li>
</ul>
<hr />
<p style="font-size:0.72em;font-weight:700;letter-spacing:0.12em;color:#f59e0b;text-transform:uppercase;margin:2em 0 0.75em 0;text-align:center;">Architecture Overview</p>
<figure class="wp-block-image size-full" style="margin:0 0 0.5em 0;">
<img fetchpriority="high" decoding="async" width="2400" height="2461" src="https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2.png" alt="XDP Pre-Stack Packet Hook — eBPF kernel data path diagram showing where XDP fires before sk_buff allocation" class="wp-image-2114" style="width:100%;height:auto;display:block;border-radius:8px;" srcset="https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2.png 2400w, https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2-293x300.png 293w, https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2-999x1024.png 999w, https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2-768x788.png 768w, https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2-1498x1536.png 1498w, https://linuxcent.com/wp-content/uploads/2026/05/ep07-xdp-og-2-1997x2048.png 1997w" sizes="(max-width: 2400px) 100vw, 2400px" /><figcaption style="text-align:center;font-size:0.85em;color:#6b7280;margin-top:0.75em;">XDP fires before <code class="" data-line="">sk_buff</code> allocation — the earliest possible kernel hook for zero-copy packet processing.</figcaption></figure>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0.5em 0 2em 0;"/>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>XDP fires before <code class="" data-line="">sk_buff</code> allocation — the earliest possible kernel hook for packet processing<br />
<em>(<code class="" data-line="">sk_buff</code> = the kernel&#8217;s socket buffer — every normal packet requires one to be allocated, which adds up fast at scale)</em></li>
<li>Three modes: native (in-driver, full performance), generic (fallback, no perf gain), offloaded (NIC ASIC)</li>
<li>XDP context is raw packet bytes — no socket, no cgroup, no pod identity; handle non-IP traffic explicitly</li>
<li>Every pointer dereference requires a bounds check against <code class="" data-line="">data_end</code> — the verifier enforces this</li>
<li><code class="" data-line="">BPF_MAP_TYPE_LPM_TRIE</code> is the right map type for IP prefix blocklists — handles /32 hosts and CIDRs together</li>
<li>XDP metadata area enables coordination with TC programs — classify at XDP speed, enforce with pod context at TC</li>
</ul>
<hr />
<h2 id="quick-check-is-xdp-running-on-your-cluster">Quick Check: Is XDP Running on Your Cluster?</h2>
<p>Before the data path walkthrough — a two-command check you can run right now on any cluster node:</p>
<pre><code class="" data-line=""># SSH into a worker node, then:
bpftool net list
</code></pre>
<p>On a Cilium-managed node, you&#8217;ll see something like:</p>
<pre><code class="" data-line="">eth0 (index 2):
        xdpdrv  id 44

lxc8a3f21b (index 7):
        tc ingress id 47
        tc egress  id 48
</code></pre>
<p>Reading the output:<br />
&#8211; <code class="" data-line="">xdpdrv</code> — XDP in <strong>native mode</strong>, running in the NIC driver before <code class="" data-line="">sk_buff</code> (this is what you want)<br />
&#8211; <code class="" data-line="">xdpgeneric</code> instead of <code class="" data-line="">xdpdrv</code> — <strong>generic mode</strong>, runs after <code class="" data-line="">sk_buff</code> allocation, no performance benefit<br />
&#8211; No XDP line at all — XDP not deployed; your CNI uses iptables for service forwarding</p>
<p>If you&#8217;re on <strong>EKS with <code class="" data-line="">aws-vpc-cni</code></strong> or <strong>GKE with <code class="" data-line="">kubenet</code></strong>, you likely won&#8217;t see XDP here — those CNIs use iptables. Understanding this section explains why teams migrating to Cilium see lower node CPU under the same traffic load.</p>
<hr />
<h2 id="where-xdp-sits-in-the-kernel-data-path">Where XDP Sits in the Kernel Data Path</h2>
<p>A client&#8217;s cluster was under a SYN flood — roughly 1 million packets per second from a rotating set of source IPs. We had iptables DROP rules installed within the first ten minutes, blocklist updated every 30 seconds as new source ranges appeared. The flood traffic dropped in volume. But node CPU stayed high. The <code class="" data-line="">%si</code> column in <code class="" data-line="">top</code> — software interrupt time — was sitting at 25–30%.</p>
<blockquote>
<p><strong><code class="" data-line="">%si</code> in <code class="" data-line="">top</code></strong> is the percentage of CPU time spent handling hardware interrupts and kernel-level packet processing — separate from your application&#8217;s CPU usage. On a quiet managed cluster (EKS, GKE) this is usually under 1%. Under a packet flood, high <code class="" data-line="">%si</code> means the kernel is burning cycles just <em>receiving</em> packets, before your workloads run at all. It&#8217;s the metric that tells you the problem is below the application layer.</p>
</blockquote>
<p>The iptables rules were matching. Packets were being dropped. CPU was still burning. The answer is where in the kernel the drop was happening. iptables fires inside the <code class="" data-line="">netfilter</code> framework — after the kernel has already allocated an <code class="" data-line="">sk_buff</code> for the packet, done DMA from the NIC ring buffer, and traversed several netfilter hooks. At 1Mpps, the allocation cost alone is measurable.</p>
<p>XDP fires before any of that.</p>
<p>The standard Linux packet receive path:</p>
<pre><code class="" data-line="">NIC hardware
  ↓
DMA to ring buffer (kernel memory)
  ↓
[XDP hook — fires here, before sk_buff]
  ├── XDP_DROP   → discard, zero further allocation
  ├── XDP_PASS   → continue to kernel network stack
  ├── XDP_TX     → transmit back out the same interface
  └── XDP_REDIRECT → forward to another interface or CPU
  ↓
sk_buff allocated from slab allocator
  ↓
netfilter: PREROUTING
  ↓
IP routing decision
  ↓
netfilter: INPUT or FORWARD
  ↓  [iptables fires somewhere in here]
socket receive queue
  ↓
userspace application
</code></pre>
<p>XDP runs at the driver level, in the NAPI poll context — the same context where the driver is processing received packets off the ring buffer. The program runs before the kernel&#8217;s general networking code gets involved. There&#8217;s no <code class="" data-line="">sk_buff</code>, no reference counting, no slab allocation.</p>
<blockquote>
<p><strong>NAPI</strong> (New API) is how modern Linux receives packets efficiently. Instead of one CPU interrupt per packet (catastrophically expensive at 1Mpps), the NIC fires a single interrupt, then the kernel polls the NIC ring buffer in batches until it&#8217;s drained. XDP runs inside this polling loop — as close to the hardware as software gets without running on the NIC itself.</p>
</blockquote>
<p>At 1Mpps, the difference between XDP_DROP and an iptables DROP is roughly the cost of allocating and then immediately freeing 1 million <code class="" data-line="">sk_buff</code> objects per second — plus netfilter traversal, connection tracking lookup, and the DROP action itself. That&#8217;s the CPU time that was burning.</p>
<p>After moving the blocklist to an XDP program, the <code class="" data-line="">%si</code> on the same traffic load dropped from 28% to 3%.</p>
<hr />
<h2 id="xdp-modes">XDP Modes</h2>
<p>XDP operates in three modes, and which one you get depends on your NIC driver.</p>
<h3 id="native-xdp-xdp_flags_drv_mode">Native XDP (XDP_FLAGS_DRV_MODE)</h3>
<p>The eBPF program runs directly in the NIC driver&#8217;s NAPI poll function — in interrupt context, before <code class="" data-line="">sk_buff</code>. This is the only mode that delivers the full performance benefit.</p>
<p>Driver support is required. The widely supported drivers: <code class="" data-line="">mlx4</code>, <code class="" data-line="">mlx5</code> (Mellanox/NVIDIA), <code class="" data-line="">i40e</code>, <code class="" data-line="">ice</code> (Intel), <code class="" data-line="">bnxt_en</code> (Broadcom), <code class="" data-line="">virtio_net</code> (KVM/QEMU), <code class="" data-line="">veth</code> (containers). Check support:</p>
<pre><code class="" data-line=""># Verify native XDP support on your driver
ethtool -i eth0 | grep driver
# driver: mlx5_core  ← supports native XDP

# Load in native mode
ip link set dev eth0 xdpdrv obj blocklist.bpf.o sec xdp
</code></pre>
<p>The <code class="" data-line="">veth</code> driver supporting native XDP is what makes XDP meaningful inside Kubernetes pods — each pod&#8217;s veth interface can run an XDP program at wire speed.</p>
<h3 id="generic-xdp-xdp_flags_skb_mode">Generic XDP (XDP_FLAGS_SKB_MODE)</h3>
<p>Fallback for drivers that don&#8217;t support native XDP. The program still runs, but it runs after <code class="" data-line="">sk_buff</code> allocation, as a hook in the <code class="" data-line="">netif_receive_skb</code> path. No performance benefit over early netfilter. <code class="" data-line="">sk_buff</code> is still allocated and freed for every packet.</p>
<pre><code class="" data-line=""># Generic mode — development and testing only
ip link set dev eth0 xdpgeneric obj blocklist.bpf.o sec xdp
</code></pre>
<p>Use this for development on a laptop with a NIC that lacks native XDP support. Never benchmark with it and never use it in production expecting performance gains.</p>
<h3 id="offloaded-xdp">Offloaded XDP</h3>
<p>Runs on the NIC&#8217;s own processing unit (SmartNIC). Zero CPU involvement — the XDP decision happens in NIC hardware. Supported by Netronome Agilio NICs. Rare in production, but the theoretical ceiling for XDP performance.</p>
<hr />
<h2 id="the-xdp-context-what-your-program-can-see">The XDP Context: What Your Program Can See</h2>
<p>XDP programs receive one argument: <code class="" data-line="">struct xdp_md</code>.</p>
<pre><code class="" data-line="">struct xdp_md {
    __u32 data;           // offset of first packet byte in the ring buffer page
    __u32 data_end;       // offset past the last byte
    __u32 data_meta;      // metadata area before data (XDP metadata for TC cooperation)
    __u32 ingress_ifindex;
    __u32 rx_queue_index;
};
</code></pre>
<p><code class="" data-line="">data</code> and <code class="" data-line="">data_end</code> are used as follows:</p>
<pre><code class="" data-line="">void *data     = (void *)(long)ctx-&gt;data;
void *data_end = (void *)(long)ctx-&gt;data_end;

// Every pointer dereference must be bounds-checked first
struct ethhdr *eth = data;
if ((void *)(eth + 1) &gt; data_end)
    return XDP_PASS;  // malformed or truncated packet
</code></pre>
<p>The verifier enforces these bounds checks — every pointer derived from <code class="" data-line="">ctx-&gt;data</code> must be validated before use. The error <code class="" data-line="">invalid mem access &#039;inv&#039;</code> means you dereferenced a pointer without checking the bounds. This is the most common cause of XDP program rejection.</p>
<blockquote>
<p><strong>For operators (not writing XDP code):</strong> You&#8217;ll see <code class="" data-line="">invalid mem access &#039;inv&#039;</code> in logs when an eBPF program is rejected at load time — most commonly during a Cilium upgrade or a custom tool deployment on a kernel the tool wasn&#8217;t built for. The fix is in the eBPF source or the tool version, not the cluster config.</p>
</blockquote>
<p>What XDP <strong>cannot</strong> see:<br />
&#8211; Socket state — no socket buffer exists yet<br />
&#8211; Cgroup hierarchy — no pod identity<br />
&#8211; Process information — no PID, no container<br />
&#8211; Connection tracking state (unless you maintain it yourself in a map)</p>
<p>XDP is ingress-only. It fires on packets arriving at an interface, not departing. For egress, TC is the hook.</p>
<hr />
<h2 id="what-this-means-on-your-cluster-right-now">What This Means on Your Cluster Right Now</h2>
<p>Every Cilium-managed node has XDP programs running. Here&#8217;s how to see them:</p>
<pre><code class="" data-line=""># All XDP programs on all interfaces — this is the full picture
bpftool net list
# Sample output on a Cilium node:
#
# eth0 (index 2):
#         xdpdrv  id 44         ← XDP in native mode on the node uplink
#
# lxc8a3f21b (index 7):
#         tc ingress id 47      ← TC enforces NetworkPolicy on pod ingress
#         tc egress  id 48      ← TC enforces NetworkPolicy on pod egress
#
# &quot;xdpdrv&quot;     = native mode (runs in NIC driver, before sk_buff — full performance)
# &quot;xdpgeneric&quot; = fallback mode (after sk_buff — no performance benefit over iptables)

# Which mode is active?
ip link show eth0 | grep xdp
# xdp mode drv  ← native (full performance)
# xdp mode generic  ← fallback (no perf benefit)

# Details on the XDP program ID
bpftool prog show id $(bpftool net show dev eth0 | grep xdp | awk &#039;{print $NF}&#039;)
# Shows: loaded_at, tag, xlated bytes, jited bytes, map IDs
</code></pre>
<p>The <code class="" data-line="">map IDs</code> in that output are the BPF maps the XDP program is using — typically the service VIP table for DNAT, and in security tools, the blocklist or allowlist. To see what&#8217;s in them:</p>
<pre><code class="" data-line=""># List maps used by the XDP program
bpftool prog show id &lt;PROG_ID&gt; | grep map_ids

# Dump the service map (for a Cilium node — this is the load balancer table)
bpftool map dump id &lt;MAP_ID&gt; | head -40
</code></pre>
<p>For a blocklist scenario — like the SYN flood mitigation above — the <code class="" data-line="">BPF_MAP_TYPE_LPM_TRIE</code> is the standard data structure. A lookup for <code class="" data-line="">192.168.1.45</code> hits a <code class="" data-line="">192.168.1.0/24</code> entry in the same map, handling both host /32s and CIDR ranges in one lookup.</p>
<pre><code class="" data-line=""># Count entries in an XDP filter map
bpftool map dump id &lt;BLOCKLIST_MAP_ID&gt; | grep -c &quot;key&quot;

# Verify XDP is active and inspect program details
bpftool net show dev eth0
</code></pre>
<hr />
<h2 id="xdp-metadata-cooperating-with-tc">XDP Metadata: Cooperating with TC</h2>
<p>Think of it as a sticky note attached to the packet. XDP writes the note at line speed (no context about pods or sockets). TC reads it later when full context is available, and acts on it. The packet carries the note between them.</p>
<p>More precisely: XDP can write metadata into the area before <code class="" data-line="">ctx-&gt;data</code> — a small scratch space that survives as the packet moves from XDP to the TC hook. This is the coordination mechanism between the two eBPF layers.</p>
<p>The pattern: XDP classifies at speed (no <code class="" data-line="">sk_buff</code> overhead), TC enforces with pod context (where you have socket identity). XDP writes a classification tag into the metadata area. TC reads it and makes the policy decision.</p>
<p>From an operational standpoint, when you see two eBPF programs on the same interface (one XDP, one TC), this pipeline is the likely explanation:</p>
<pre><code class="" data-line="">bpftool net list
# xdpdrv id 44 on eth0       ← XDP classifier running at line rate
# tc ingress id 47 on eth0   ← TC enforcer reading XDP metadata
</code></pre>
<hr />
<h2 id="how-cilium-uses-xdp">How Cilium Uses XDP</h2>
<blockquote>
<p><strong>Not running Cilium?</strong> On EKS with <code class="" data-line="">aws-vpc-cni</code> or GKE with <code class="" data-line="">kubenet</code>, service forwarding uses iptables NAT rules and <code class="" data-line="">conntrack</code> instead. You can see this with <code class="" data-line="">iptables -t nat -L -n</code> on a node — look for the <code class="" data-line="">KUBE-SVC-*</code> chains. Those chains are what XDP replaces in a Cilium cluster. This is why teams migrating from kube-proxy to Cilium report lower node CPU at high connection rates — it&#8217;s not magic, it&#8217;s hook placement.</p>
</blockquote>
<p>On a Cilium node, XDP handles the load balancing path for ClusterIP services. When a packet arrives at the node destined for a ClusterIP:</p>
<ol>
<li>XDP program checks the destination IP against a BPF LRU hash map of known service VIPs</li>
<li>On a match, it performs DNAT — rewriting the destination IP to a backend pod IP</li>
<li>Returns <code class="" data-line="">XDP_TX</code> or <code class="" data-line="">XDP_REDIRECT</code> to forward directly</li>
</ol>
<p>No iptables NAT rules. No <code class="" data-line="">conntrack</code> state machine. No socket buffer allocation for the routing decision. The lookup is O(1) in a BPF hash map.</p>
<pre><code class="" data-line=""># See Cilium&#039;s XDP program on the node uplink
ip link show eth0 | grep xdp
# xdp  (attached, native mode)

# The XDP program details
bpftool prog show pinned /sys/fs/bpf/cilium/xdp

# Load time, instruction count, JIT-compiled size
bpftool prog show id $(bpftool net list | grep xdp | awk &#039;{print $NF}&#039;)
</code></pre>
<p>At production scale — 500+ nodes, 50k+ services — removing iptables from the service forwarding path with XDP reduces per-node CPU utilization measurably. The effect is most visible on nodes handling high connection rates to cluster services.</p>
<hr />
<h2 id="operational-inspection">Operational Inspection</h2>
<pre><code class="" data-line=""># All XDP programs on all interfaces
bpftool net list

# Check XDP mode (native, generic, offloaded)
ip link show | grep xdp

# Per-interface stats — includes XDP drop/pass counters
cat /sys/class/net/eth0/statistics/rx_dropped

# XDP drop counters exposed via bpftool
bpftool map dump id &lt;stats_map_id&gt;

# Verify XDP is active and show program details
bpftool net show dev eth0
</code></pre>
<hr />
<h2 id="common-mistakes">Common Mistakes</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Impact</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Missing bounds check before pointer dereference</td>
<td>Verifier rejects: &#8220;invalid mem access&#8221;</td>
<td>Always check <code class="" data-line="">ptr + sizeof(*ptr) &gt; data_end</code> before use</td>
</tr>
<tr>
<td>Using generic XDP for performance testing</td>
<td>Misleading numbers — sk_buff still allocated</td>
<td>Test in native mode only; check <code class="" data-line="">ip link</code> output for mode</td>
</tr>
<tr>
<td>Not handling non-IP traffic (ARP, IPv6, VLAN)</td>
<td>ARP breaks, IPv6 drops, VLAN-tagged frames dropped</td>
<td>Check <code class="" data-line="">eth-&gt;h_proto</code> and return <code class="" data-line="">XDP_PASS</code> for non-IP</td>
</tr>
<tr>
<td>XDP for egress or pod identity</td>
<td>No socket context at XDP; XDP is ingress only</td>
<td>Use TC egress for pod-identity-aware egress policy</td>
</tr>
<tr>
<td>Forgetting <code class="" data-line="">BPF_F_NO_PREALLOC</code> on LPM trie</td>
<td>Full memory allocated at map creation for all entries</td>
<td>Always set this flag for sparse prefix tries</td>
</tr>
<tr>
<td>Blocking ARP by accident in a /24 blocklist</td>
<td>Loss of layer-2 reachability within the blocked subnet</td>
<td>Separate ARP handling before the IP blocklist check</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>XDP fires before <code class="" data-line="">sk_buff</code> allocation — the earliest possible kernel hook for packet processing</li>
<li>Three modes: native (in-driver, full performance), generic (fallback, no perf gain), offloaded (NIC ASIC)</li>
<li>XDP context is raw packet bytes — no socket, no cgroup, no pod identity; handle non-IP traffic explicitly</li>
<li>Every pointer dereference requires a bounds check against <code class="" data-line="">data_end</code> — the verifier enforces this</li>
<li><code class="" data-line="">BPF_MAP_TYPE_LPM_TRIE</code> is the right map for IP prefix blocklists — handles /32 hosts and CIDRs together</li>
<li>XDP metadata area enables coordination with TC programs — classify at XDP speed, enforce with pod context at TC</li>
</ul>
<h2 id="whats-next">What&#8217;s Next</h2>
<p>XDP handles ingress at the fastest possible point but has no visibility into which pod sent a packet. EP08 covers TC eBPF — the hook that fires after <code class="" data-line="">sk_buff</code> allocation, where socket and cgroup context exist.</p>
<p>TC is how Cilium implements pod-to-pod network policy without iptables. It&#8217;s also where stale programs from failed Cilium upgrades leave ghost filters that cause intermittent packet drops. Knowing how TC programs chain — and how to find and remove stale ones — is a specific, concrete operational skill.</p>
<p><em>Next: <a href="https://linuxcent.com/tc-ebpf-kubernetes-network-policy/">TC eBPF — pod-level network policy without iptables</a></em></p>
<p>Get EP08 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&amp;linkname=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Febpf-xdp-kubernetes-networking%2F&#038;title=XDP%20%E2%80%94%20Packets%20Processed%20Before%20the%20Kernel%20Knows%20They%20Arrived" data-a2a-url="https://linuxcent.com/ebpf-xdp-kubernetes-networking/" data-a2a-title="XDP — Packets Processed Before the Kernel Knows They Arrived"></a></p><p>The post <a href="https://linuxcent.com/ebpf-xdp-kubernetes-networking/">XDP — Packets Processed Before the Kernel Knows They Arrived</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/ebpf-xdp-kubernetes-networking/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1540</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-22 12:17:14 by W3 Total Cache
-->