<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ebpf</title>
<link>https://coderlegion.com/tag/ebpf</link>
<description>Powered by Question2Answer</description>
<item>
<title>Writing eBPF Probes in C for Advanced Kubernetes Observability</title>
<link>https://coderlegion.com/30935/writing-ebpf-probes-in-c-for-advanced-kubernetes-observability</link>
<description>&lt;p&gt;Traditional Kubernetes observability usually relies on sidecar proxies or application-level tracing, both of which introduce CPU and memory overhead. eBPF avoids this by running sandboxed C programs directly inside the Linux kernel, enabling zero-instrumentation tracing for syscalls, network events, and file platform operations.&lt;/p&gt;
&lt;h4 id=&quot;tracing-tcp-connect-latency&quot;&gt;Tracing TCP Connect Latency&lt;/h4&gt;
&lt;p&gt;Standard pod metrics usually miss what happens deep in the networking stack. By tracing &lt;code&gt;tcp_v4_connect&lt;/code&gt;, we can measure exact TCP connection setup times across nodes without modifying application code or container images.&lt;/p&gt;
&lt;h4 id=&quot;the-ebpf-c-program&quot;&gt;The eBPF C Program&lt;/h4&gt;
&lt;p&gt;The probe below is written in restricted C and compiled into eBPF bytecode using Clang/LLVM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;vmlinux.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;
#include &amp;lt;bpf/bpf_tracing.h&amp;gt;

// Define a BPF map to store the start time of the connect call, keyed by PID
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, u64);
} start SEC(&quot;.maps&quot;);

// Kprobe triggered on entry to tcp_v4_connect
SEC(&quot;kprobe/tcp_v4_connect&quot;)
int BPF_KPROBE(tcp_v4_connect_enter, struct sock *sk)
{
    u64 ts = bpf_ktime_get_ns();
    u32 pid = bpf_get_current_pid_tgid() &amp;gt;&amp;gt; 32;

    bpf_map_update_elem(&amp;amp;start, &amp;amp;pid, &amp;amp;ts, BPF_ANY);
    return 0;
}

// Kretprobe triggered on exit of tcp_v4_connect
SEC(&quot;kretprobe/tcp_v4_connect&quot;)
int BPF_KRETPROBE(tcp_v4_connect_exit, int ret)
{
    u32 pid = bpf_get_current_pid_tgid() &amp;gt;&amp;gt; 32;
    u64 *tsp, delta_us;

    // Lookup the start time
    tsp = bpf_map_lookup_elem(&amp;amp;start, &amp;amp;pid);
    if (!tsp) {
        return 0; // Missed entry
    }

    // Calculate latency in microseconds
    delta_us = (bpf_ktime_get_ns() - *tsp) / 1000;
    
    // bpf_printk logs to /sys/kernel/debug/tracing/trace_pipe
    bpf_printk(&quot;PID %d TCP connect took %llu us, ret = %d\n&quot;, pid, delta_us, ret);

    bpf_map_delete_elem(&amp;amp;start, &amp;amp;pid);
    return 0;
}

char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-mechanics-amp-kernel-integration&quot;&gt;Key Mechanics &amp;amp; Kernel Integration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;#include &amp;lt;vmlinux.h&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Auto-generated from kernel BTF (BPF Type Format). It provides definitions for internal kernel structs without needing multiple kernel header files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;BPF_MAP_TYPE_HASH&lt;/code&gt;&lt;/strong&gt;: eBPF programs cannot leverage global variables for dynamic state. Hash maps allow entry and exit probes to share timestamps across process IDs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bpf_ktime_get_ns()&lt;/code&gt;&lt;/strong&gt;: Fetches high-resolution monotonic timestamps from the kernel clock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bpf_get_current_pid_tgid()&lt;/code&gt;&lt;/strong&gt;: Returns the thread group ID in the upper 32 bits and thread ID in the lower 32 bits. Right-shifting by 32 extracts the user-space PID.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SEC(&quot;kprobe/...&quot;)&lt;/code&gt; / &lt;code&gt;SEC(&quot;kretprobe/...&quot;)&lt;/code&gt;&lt;/strong&gt;: Macros defining ELF section names. Loaders like &lt;code&gt;libbpf&lt;/code&gt; read these to attach probes to kernel functions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deploying this probe via a Kubernetes DaemonSet alongside a user-space agent (for example, written in Go using &lt;code&gt;cilium/ebpf&lt;/code&gt;) provides low-overhead network latency tracing across all pods on a node.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://anasrhimi.tech/blog/ebpf-probes-kubernetes-observability&quot;&gt;anasrhimi.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30935/writing-ebpf-probes-in-c-for-advanced-kubernetes-observability</guid>
<pubDate>Tue, 13 Oct 2026 15:06:28 +0000</pubDate>
</item>
<item>
<title>Building a Line-Rate Linux Kernel Firewall in Pure Rust with Aya and eBPF/XDP</title>
<link>https://coderlegion.com/29624/building-a-line-rate-linux-kernel-firewall-in-pure-rust-with-aya-and-ebpf-xdp</link>
<description>&lt;p&gt;Volumetric Distributed Denial of Service (DDoS) and transport-layer flooding attacks pose an existential challenge to modern cloud infrastructure, edge gateways, and container workloads. &lt;/p&gt;
&lt;p&gt;Conventional Linux packet filtering utilities such as Netfilter (&lt;code&gt;iptables&lt;/code&gt; and &lt;code&gt;nftables&lt;/code&gt;) evaluate incoming frames only after the kernel has allocated a socket buffer (&lt;code&gt;sk_buff&lt;/code&gt;) descriptor and scheduled softirq processing through the core TCP/IP stack. Under multi-gigabit line-rate flood conditions (millions of packets per second), the operating system spends the vast majority of its CPU cycles simply allocating and freeing &lt;code&gt;sk_buff&lt;/code&gt; memory metadata, triggering kernel starvation and packet drop bottlenecks even before firewall rules are evaluated.&lt;/p&gt;
&lt;p&gt;To address this fundamental OS-level bottleneck, I built &lt;strong&gt;xdp-guard&lt;/strong&gt;: a production-grade, memory-safe in-kernel defense engine and per-IP rate limiter implemented strictly in &lt;strong&gt;Pure Rust&lt;/strong&gt; using the &lt;strong&gt;Aya&lt;/strong&gt; eBPF framework.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-ebpf-xdp-at-the-driver-layer-&quot;&gt;Why eBPF / XDP at the Driver Layer?&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;eXpress Data Path (XDP)&lt;/strong&gt; provides a programmable, bare-metal packet processing execution hook directly inside the Network Interface Card (NIC) driver subsystem before any memory descriptor allocation occurs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Conventional Path:
[NIC RX] -&amp;gt; [IRQ / SoftIRQ] -&amp;gt; [Allocate sk_buff] -&amp;gt; [Netfilter/iptables] -&amp;gt; [Socket / App]
                                      ^
                           Severe CPU Allocation Overhead

xdp-guard Path:
[NIC RX] -&amp;gt; [eBPF / XDP Driver Hook] ---&amp;gt; (Match Blocklist) ---&amp;gt; [ XDP_DROP ] (Near-Zero CPU)
                             |
                      (Match Allowlist)
                             |
                             v
                   [ Pass to OS Stack ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When hostile or unapproved packets are identified, returning &lt;code&gt;XDP_DROP&lt;/code&gt; causes the network driver to immediately recycle the packet ring buffer without allocating a single byte of kernel socket memory.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architectural-highlights&quot;&gt;Architectural Highlights&lt;/h3&gt;
&lt;h4 id=&quot;1-zero-c-dependencies-pure-rust-&quot;&gt;1. Zero C Dependencies (Pure Rust)&lt;/h4&gt;
&lt;p&gt;Traditional eBPF tools rely heavily on C toolchains (&lt;code&gt;clang&lt;/code&gt;, &lt;code&gt;llvm&lt;/code&gt;, &lt;code&gt;libbpf&lt;/code&gt;). &lt;code&gt;xdp-guard&lt;/code&gt; is built entirely in Rust from the user-space management CLI down to the &lt;code&gt;no_std&lt;/code&gt; kernel eBPF bytecode using &lt;strong&gt;Aya&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;2-strict-64-bit-memory-boundary-alignments&quot;&gt;2. Strict 64-Bit Memory Boundary Alignments&lt;/h4&gt;
&lt;p&gt;To ensure safe zero-copy communication across the kernel-userspace boundary, shared structures use explicit padding fields to guarantee 8-byte alignment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct RuleValue {
    pub action: u32,       // 0 = Pass, 1 = Drop
    pub _pad: u32,         // Explicit 4-byte padding for 8-byte boundary
    pub created_at: u64,   // Monotonic timestamp
    pub ttl_secs: u64,     // Time-to-live expiration
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The presence of &lt;code&gt;_pad: u32&lt;/code&gt; is critical: it prevents architecture-specific compiler padding discrepancies and satisfies the Linux Kernel BPF Verifier.&lt;/p&gt;
&lt;h4 id=&quot;3-dynamic-bpf-virtual-filesystem-bpffs-synchronization&quot;&gt;3. Dynamic BPF Virtual Filesystem (bpffs) Synchronization&lt;/h4&gt;
&lt;p&gt;Firewall rules (blocklist and allowlist) are dynamically synchronized using pinned BPF hash maps located in &lt;code&gt;/sys/fs/bpf/xdp_guard&lt;/code&gt;. Rules can be inserted or deleted instantaneously from the CLI without tearing down network interfaces or interrupting traffic flows.&lt;/p&gt;
&lt;h4 id=&quot;4-in-kernel-token-bucket-rate-limiter&quot;&gt;4. In-Kernel Token-Bucket Rate Limiter&lt;/h4&gt;
&lt;p&gt;In addition to static blocklists, &lt;code&gt;xdp-guard&lt;/code&gt; embeds a stateful token-bucket rate limiter directly in kernel space. Monotonic arrival intervals ($\Delta t = t&lt;em&gt;{now} - t&lt;/em&gt;{last}$) are computed using nanosecond &lt;code&gt;bpf_ktime_get_ns()&lt;/code&gt; clocks, discarding over-limit burst floods at line-rate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;empirical-verification-real-linux-kernel-telemetry&quot;&gt;Empirical Verification: Real Linux Kernel Telemetry&lt;/h3&gt;
&lt;p&gt;To validate &lt;code&gt;xdp-guard&lt;/code&gt; under realistic operating conditions, we set up an automated Linux network namespace testbed over virtual ethernet (&lt;code&gt;veth&lt;/code&gt;) pairs (&lt;code&gt;xdp_client&lt;/code&gt; at &lt;code&gt;10.10.0.2&lt;/code&gt; and &lt;code&gt;xdp_server&lt;/code&gt; at &lt;code&gt;10.10.0.1&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Telemetry streamed directly from the kernel &lt;code&gt;STATS_MAP&lt;/code&gt; array demonstrated instantaneous 100.00% packet mitigation upon inserting an attacker IP into the kernel blocklist, followed by zero-loss recovery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TIMESTAMP            PASSED (PKTS)   DROPPED (PKTS)  DROP RATIO     
-----------------------------------------------------------------
09:56:56 | Pass: 1        (30.2 KB) | Drop: 0        (0 B)   | Ratio: 0.00%
[!] Inserting IP 10.10.0.2 into BLOCKLIST_MAP... [SUCCESS]
09:56:57 | Pass: 0        (30.2 KB) | Drop: 1        (98 B)  | Ratio: 100.00%
09:56:58 | Pass: 0        (30.2 KB) | Drop: 1        (196 B) | Ratio: 100.00%
09:56:59 | Pass: 0        (30.2 KB) | Drop: 1        (294 B) | Ratio: 100.00%
...
09:57:55 | Pass: 0        (30.2 KB) | Drop: 1        (5.7 KB)| Ratio: 100.00%
[!] Removing IP 10.10.0.2 from BLOCKLIST_MAP... [SUCCESS]
09:57:56 | Pass: 1        (30.3 KB) | Drop: 0        (5.7 KB)| Ratio: 0.00%
09:57:57 | Pass: 1        (30.4 KB) | Drop: 0        (5.7 KB)| Ratio: 0.00%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every single drop occurred directly within the NIC driver hook, requiring zero kernel TCP/IP stack overhead.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;source-code-amp-research-paper&quot;&gt;Source Code &amp;amp; Research Paper&lt;/h3&gt;
&lt;p&gt;The complete project, documentation, and formal research paper are fully open-source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rahadbhuiya/xdp-guard&quot;&gt;https://github.com/rahadbhuiya/xdp-guard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Citable Zenodo DOI:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.5281/zenodo.23120995&quot;&gt;10.5281/zenodo.23120995&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated CI:&lt;/strong&gt; Continuous testing across Rust Stable and Nightly LLVM targets via GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are working on Linux kernel systems, eBPF, or Rust networking, I would love to hear your thoughts, feedback, or suggestions!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29624/building-a-line-rate-linux-kernel-firewall-in-pure-rust-with-aya-and-ebpf-xdp</guid>
<pubDate>Sat, 03 Oct 2026 13:29:33 +0000</pubDate>
</item>
<item>
<title>Booting ARM64 Linux in my Rust KVM Hypervisor: Getting to the console with help of eBPF</title>
<link>https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</link>
<description>&lt;h3 id=&quot;recap&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous article&lt;/a&gt; I got ARM64 Linux as far&lt;br&gt;
as executing inside my Rust KVM hypervisor. I still couldn't actually interact&lt;br&gt;
with the guest, though: I had no working console.&lt;/p&gt;
&lt;p&gt;This article is about getting from &quot;the PC is moving&quot; to typing &quot;hello&quot; into&lt;br&gt;
Linux and getting &quot;HELLO&quot; back.&lt;/p&gt;
&lt;h3 id=&quot;uart&quot;&gt;UART&lt;/h3&gt;
&lt;p&gt;Last time we learned how to obtain the device tree blob from QEMU's &lt;code&gt;virt&lt;/code&gt;&lt;br&gt;
machine. Now we want to take a look at what's inside. We can decompile it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;dtc -I dtb -O dts -o virt.dts virt.dtb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, a simple search through the file already gives us the clue we need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i uart virt.dts -A5 -B1
        pl011@9000000 {
                clock-names = &quot;uartclk\0apb_pclk&quot;;
                clocks = &amp;lt;0x8000 0x8000&amp;gt;;
                interrupts = &amp;lt;0x00 0x01 0x04&amp;gt;;
                reg = &amp;lt;0x00 0x9000000 0x00 0x1000&amp;gt;;
                compatible = &quot;arm,pl011\0arm,primecell&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, now I had a lead: I needed to emulate the &lt;code&gt;pl011&lt;/code&gt; UART at &lt;code&gt;0x09000000&lt;/code&gt; so&lt;br&gt;
that the existing Linux &lt;code&gt;pl011&lt;/code&gt; driver could communicate with it. This was not a&lt;br&gt;
really interesting part, as most of it was simply reading Linux kernel source&lt;br&gt;
code along with &lt;code&gt;pl011&lt;/code&gt; documentation from ARM. Soon I was able to get output&lt;br&gt;
from Linux and it felt really great:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.000000] Linux version 7.1.3 (docker@stjepan-Aspire-F5-573G) (aarch64-linux-gnu-gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP PREEMPT Thu Jul 16 08:52:24 UTC 2026
[    0.000000] KASLR disabled on command line
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] efi: UEFI not found.
[    0.000000] OF: reserved mem: Reserved memory: No reserved-memory node in the DT
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x0000000047ffffff]
[    0.000000] NODE_DATA(0) allocated [mem 0x47fc10c0-0x47fc37bf]
                  ### omitted further kernel log ###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-custom-initramfs&quot;&gt;Creating custom initramfs&lt;/h3&gt;
&lt;p&gt;Now I needed bidirectional UART support so that I could send input to the guest&lt;br&gt;
as well. To make further investigation easier, I created a very simple binary&lt;br&gt;
written in &lt;code&gt;C&lt;/code&gt; to be run as the Linux init process: it just prints out current&lt;br&gt;
interrupt statistics and asks user for input, echoing it back in all caps. All&lt;br&gt;
we then need is &lt;code&gt;initramfs.txt&lt;/code&gt; file with some simple folders and devices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dir / 0755 0 0
dir /dev 0755 0 0
dir /proc 0555 0 0
dir /sys 0555 0 0
nod /dev/kmsg 0600 0 0 c 1 11
nod /dev/console 0600 0 0 c 5 1
file /init /home/stjepanp/Develop/initramfs/init 0755 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I only needed enough of a filesystem to provide &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/dev/console&lt;/code&gt;, and&lt;br&gt;
access to &lt;code&gt;/proc&lt;/code&gt; for interrupt statistics. Then, in the Linux source code&lt;br&gt;
folder, we generate &lt;code&gt;initramfs&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;./usr/gen_initramfs.sh ../initramfs/initramfs.txt -o initramfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see at which memory address to add &lt;code&gt;initramfs&lt;/code&gt;, we can run ordinary QEMU with&lt;br&gt;
our &lt;code&gt;initramfs&lt;/code&gt; and see what happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt,dumpdtb=virt.dtb                                \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0 nokaslr rdinit=/init&quot;          \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults                                             \
        -initrd &quot;./initramfs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After decompiling &lt;code&gt;virt.dtb&lt;/code&gt; (explained in &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous&lt;/a&gt; article) we can see the location:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~/dtb $ grep initrd virt-initramfs.dts -B2 -A3
        chosen {
                bootargs = &quot;console=ttyAMA0 initramfs_async=0 rdinit=/init nokaslr&quot;;
                linux,initrd-start = &amp;lt;0x0 0x40200000&amp;gt;;
                linux,initrd-end = &amp;lt;0x0 0x40294200&amp;gt;;
                stdout-path = &quot;/pl011@9000000&quot;;
                kaslr-seed = &amp;lt;0x8114b902 0xe815aa93&amp;gt;;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do have to recalculate &lt;code&gt;initrd-end&lt;/code&gt; if size of our &lt;code&gt;initramfs&lt;/code&gt; file changes.&lt;br&gt;
If that happens, we can easily recompile the device tree blob after changing&lt;br&gt;
the value. Also, let's not forget to load &lt;code&gt;initramfs&lt;/code&gt; in Rust code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const LOAD_ADDR: u64 = 0x40_000_000;
const INITRAMFS_OFFS: u64 = 0x200_000;
const KERN_OFFS: u64 = 0x200_000 + INITRAMFS_OFFS;

impl VM {
    pub fn arch_load_linux(&amp;amp;mut self, vcpu: &amp;amp;mut VCPU, args: &amp;amp;Args) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let linux_mem_idx = self.add_mem_region(1024 * 1024 * 1024, LOAD_ADDR)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;dtb_path, 0x0)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;initramfs_path, INITRAMFS_OFFS)?;
// ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setting-up-vgic&quot;&gt;Setting up vGIC&lt;/h3&gt;
&lt;p&gt;Even without having the ability to send keystrokes there was one big problem&lt;br&gt;
with the Linux console &lt;code&gt;stdout&lt;/code&gt;: The kernel messages were reaching the console,&lt;br&gt;
but once execution reached &lt;code&gt;/init&lt;/code&gt;, output stopped behaving as expected. It&lt;br&gt;
took me some time to realize that the ARM architectural timer was also involved.&lt;br&gt;
Its interrupts need to be delivered through the virtual GIC, so I first needed&lt;br&gt;
to create a vGIC. It turned out creating the vGIC was easy. I just followed the&lt;br&gt;
logic from QEMU source code:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&quot;&gt;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Rust this translates to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut create_device: kvm_create_device = unsafe { std::mem::zeroed() };
create_device.type_ = KVM_ARM_DEVICE_VGIC_V2;
create_device.fd = 0xffffffff;
create_device.flags = 0;

unsafe { libc::ioctl(self.fd, KVM_CREATE_DEVICE, &amp;amp;mut create_device); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked. Also note that I have omitted proper error handling to keep the&lt;br&gt;
code short and concise.&lt;/p&gt;
&lt;h4 id=&quot;tracing-irq-injection&quot;&gt;Tracing IRQ injection&lt;/h4&gt;
&lt;p&gt;Now if we run our &lt;code&gt;initramfs&lt;/code&gt; with &lt;code&gt;strace&lt;/code&gt; as before, and we try to type in&lt;br&gt;
some keys, we will see that in our log we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i irq kvm.log | tail -n5
458398 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7fd932dbd0) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db30) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db20) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So our &lt;code&gt;ioctl&lt;/code&gt; for IRQ injection actually takes an address as argument. If we&lt;br&gt;
investigate this, we can find out how &lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; is handled in the Linux&lt;br&gt;
kernel:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Reading this code we see that the relevant function is &lt;code&gt;kvm_vm_ioctl_irq_line&lt;/code&gt;&lt;br&gt;
that takes &lt;code&gt;struct kvm_irq_level&lt;/code&gt; as second argument and this is what we need&lt;br&gt;
to interpret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* for KVM_IRQ_LINE */
struct kvm_irq_level {
/*
 * ACPI gsi notion of irq.
 * For IA-64 (APIC model) IOAPIC0: irq 0-23; IOAPIC1: irq 24-47..
 * For X86 (standard AT mode) PIC0/1: irq 0-15. IOAPIC0: 0-23..
 * For ARM: See Documentation/virt/kvm/api.rst
 */
union {
__u32 irq;
__s32 status;
};
__u32 level;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this purpose, we can treat it as an 8-byte structure containing the IRQ number and level. Therefore, we can easily inspect it again via eBPF when we&lt;br&gt;
run QEMU with our &lt;code&gt;initramfs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kprobe:kvm_vm_ioctl_irq_line { printf(&quot;irq_level=%x\n&quot;, *((uint64 *)arg1)); }'
Attaching 1 probe...
irq_level=1000021
irq_level=1000021
irq_level=1000021
irq_level=1000021
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without wasting much time we can easily reconstruct this behavior in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub fn arch_update_irq(level: u32, vm_fd: libc::c_int) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut irq: kvm_irq_level = unsafe { std::mem::zeroed() };
    irq.level = level;
    irq.__bindgen_anon_1.irq = 0x1000021;

    let ret = unsafe {
        libc::ioctl(vm_fd, KVM_IRQ_LINE, &amp;amp;mut irq)
    };

    if ret &amp;lt; 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that, in essence, &lt;code&gt;0x1000021&lt;/code&gt; is not an arbitrary magic number. On ARM64,&lt;br&gt;
&lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; encodes the interrupt type and interrupt ID in the &lt;code&gt;irq&lt;/code&gt; field. Here &lt;code&gt;0x01&lt;/code&gt; selects an SPI and &lt;code&gt;0x21&lt;/code&gt; is interrupt 33 the UART interrupt from&lt;br&gt;
the device tree.&lt;/p&gt;
&lt;h4 id=&quot;no-such-device-or-address&quot;&gt;No such device or address&lt;/h4&gt;
&lt;p&gt;However, when we run our program, it fails with &quot;No such device or address&quot;. To&lt;br&gt;
get some clue on what is going on, we can try &lt;code&gt;strace&lt;/code&gt; and get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5, KVM_RUN, 0)                    = -1 ENXIO (No such device or address)
Error: Os { code: 6, kind: Uncategorized, message: &quot;No such device or address&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first impulse was to search for &lt;code&gt;ENXIO&lt;/code&gt; in Elixir bootlin, but,&lt;br&gt;
unfortunately, there were simply too many functions and branches that could&lt;br&gt;
return &lt;code&gt;ENXIO&lt;/code&gt;. Rather than trying to follow every possible &lt;code&gt;ENXIO&lt;/code&gt; path&lt;br&gt;
manually, I decided to use a more brute-force approach. I attached &lt;code&gt;kretprobe&lt;/code&gt;s&lt;br&gt;
to the KVM functions and looked for one returning &lt;code&gt;-ENXIO&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:kvm_* /retval == (uint32)(-6)/ { printf(&quot;%s\n&quot;, probe); }'
Attaching 487 probes...
kretprobe:kvm_vgic_map_resources
kretprobe:kvm_arch_vcpu_run_pid_change
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second function here does not seem really relevant, so I tried looking for&lt;br&gt;
&lt;code&gt;kvm_vgic_map_resources&lt;/code&gt; on Elixir:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now taking a look at the function we can clearly see where &lt;code&gt;ENXIO&lt;/code&gt; is returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int vgic_v2_map_resources(struct kvm *kvm)
{
    struct vgic_dist *dist = &amp;amp;kvm-&amp;gt;arch.vgic;
    int ret = 0;

    if (IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_dist_base) ||
        IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_cpu_base)) {
        kvm_debug(&quot;Need to set vgic cpu and dist addresses first\n&quot;);
        return -ENXIO;
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have a hint. We need to set vGIC CPU and distributor addresses first. To&lt;br&gt;
confirm our hypothesis we can try tracing this function specifically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:vgic_v2_map_resources { printf(&quot;ret=%u\n&quot;, retval); }'
Attaching 1 probe...
ret=4294967290
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;finding-out-distributor-address&quot;&gt;Finding out distributor address&lt;/h4&gt;
&lt;p&gt;My first attempt to set up distributor address didn't go well. I wrote the&lt;br&gt;
following code (I extracted distributor address from the device tree and&lt;br&gt;
tried to reconstruct the logic from QEMU source code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let dist_addr: u64 = 0x08_000_000;
let mut dev_attr: kvm_device_attr = unsafe { std::mem::zeroed() };
dev_attr.group = KVM_DEV_ARM_VGIC_GRP_ADDR;
dev_attr.attr = KVM_VGIC_V2_ADDR_TYPE_DIST;
dev_attr.flags = 0;
dev_attr.addr = dist_addr;

unsafe { libc::ioctl(create_device.fd as i32, KVM_SET_DEVICE_ATTR, &amp;amp;mut dev_attr) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I ran the program, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(6, KVM_SET_DEVICE_ATTR, 0x7ffe2696d8) = -1 EFAULT (Bad address)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To debug this, I needed to inspect the values defined in&lt;br&gt;
&lt;code&gt;struct kvm_device_attr&lt;/code&gt;, but it was a lot more complicated: I had to check if&lt;br&gt;
the &lt;code&gt;struct&lt;/code&gt; I was tracing was for &lt;code&gt;KVM_DEV_ARM_VGIC_GRP_ADDR&lt;/code&gt; group and for&lt;br&gt;
&lt;code&gt;KVM_VGIC_V2_ADDR_TYPE_DIST&lt;/code&gt;. So, I decided to write my eBPF program in C with&lt;br&gt;
and called it &lt;code&gt;probe-kvm.bpf.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;linux/bpf.h&amp;gt;
#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;
#include &amp;lt;bpf/bpf_tracing.h&amp;gt;
#include &amp;lt;linux/kvm.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;

SEC(&quot;kprobe/vgic_v2_set_attr&quot;)
int BPF_KPROBE(kprobe_vgic_v2_set_attr, void *dev, struct kvm_device_attr *dev_attr)
{
        struct kvm_device_attr attr;
        __u64 value;

        bpf_probe_read_kernel(&amp;amp;attr, sizeof(attr), dev_attr);
        if (attr.group != KVM_DEV_ARM_VGIC_GRP_ADDR) {
                return 0;
        }

        if ((attr.attr != KVM_VGIC_V2_ADDR_TYPE_DIST) &amp;amp;&amp;amp; (attr.attr != KVM_VGIC_V2_ADDR_TYPE_CPU)) {
                return 0;
        }

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To compile and run it I wrote the following script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

clang -O2 -g -mcpu=v1 -target bpf -I /usr/include/aarch64-linux-gnu -c probe-kvm.bpf.c -D__TARGET_ARCH_arm64 -o probe-kvm.bpf.o
sudo rm -rf /sys/fs/bpf/vgic
sudo bpftool prog loadall probe-kvm.bpf.o /sys/fs/bpf/vgic autoattach
sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, I started ordinary QEMU along my eBPF trace and got this output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./start-probe.sh
           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139387: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fd4d3c450

           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139418: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fd4d3c450
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This really looked like a userspace address while I was supplying a raw address.&lt;br&gt;
To find out what was at that address I modified the last line of the BPF program&lt;br&gt;
above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;        if (bpf_probe_read_user(&amp;amp;value, sizeof(value), (void *)attr.addr) != 0)
                return -1;

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx, *addr=0x%llx\n&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr, value);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this I got what I didn't really expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183145: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fdc806ca0, *addr=0x8010000

           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183177: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fdc806ca0, *addr=0x8000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the &lt;code&gt;addr&lt;/code&gt; field is actually a pointer to the address, and not the address&lt;br&gt;
itself. Note that &lt;code&gt;0x8010000&lt;/code&gt; and &lt;code&gt;0x8000000&lt;/code&gt; are the guest physical&lt;br&gt;
addresses of the GIC CPU interface and distributor taken from the virt machine's&lt;br&gt;
device tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pi@raspberrypi:~/dtb $ grep -i intc virt.dts -A3
        intc@8000000 {
                phandle = &amp;lt;0x8001&amp;gt;;
                reg = &amp;lt;0x00 0x8000000 0x00 0x10000 0x00 0x8010000 0x00 0x10000&amp;gt;;
                compatible = &quot;arm,cortex-a15-gic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also explained the &lt;code&gt;EFAULT&lt;/code&gt;: KVM was trying to dereference&lt;br&gt;
&lt;code&gt;attr.addr&lt;/code&gt; as a userspace pointer. With this small fix, I got a fully working&lt;br&gt;
Linux console:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;dev_attr.addr = &amp;amp;dist_addr as *const u64 as u64;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following snippet shows that my &lt;code&gt;init&lt;/code&gt; process (as part of&lt;br&gt;
&lt;code&gt;initramfs&lt;/code&gt;) was run and it echoed back my input in capital letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.832623] clk: Disabling unused clocks
[    0.834046] PM: genpd: Disabling unused power domains
[    0.845565] Freeing unused kernel memory: 12672K
[    0.847475] Run /init as init process
           CPU0       
 11:        199 GIC-0  27 Level     arch_timer
 13:          0 GIC-0  33 Level     uart-pl011
IPI0:         0       Rescheduling interrupts
IPI1:         0       Function call interrupts
IPI2:         0       CPU stop interrupts
IPI3:         0       CPU stop NMIs
IPI4:         0       Timer broadcast interrupts
IPI5:       168       IRQ work interrupts
IPI6:         0       CPU backtrace interrupts
IPI7:         0       KGDB roundup interrupts
Err:          0

type a string: hello
allcaps: HELLO

[    2.224776] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.227382] CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 7.1.3 #1 PREEMPT 
[    2.229749] Hardware name: linux,dummy-virt (DT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interrupt statistics are also printed from my &lt;code&gt;init&lt;/code&gt;. Also, when &lt;code&gt;init&lt;/code&gt;&lt;br&gt;
exits, we get the (correct) Kernel panic that init process was terminated.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;At this point I finally had what I was looking for: a real Linux userspace&lt;br&gt;
running inside my own KVM-based hypervisor, with a working UART, interrupt&lt;br&gt;
delivery and a minimal &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;More importantly, I now had a way to interact with the guest and inspect what&lt;br&gt;
was happening inside KVM. That gives me a much better foundation for the next&lt;br&gt;
step: implementing more of the virtual hardware needed to turn this into a&lt;br&gt;
useful ARM64 virtual machine.&lt;/p&gt;
&lt;p&gt;You can also check out the repository on my GitHub page and, if you have a&lt;br&gt;
Raspberry Pi 4B, try the VMM yourself:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust&quot;&gt;https://github.com/StjepanPoljak/kvm-rust&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</guid>
<pubDate>Fri, 25 Sep 2026 14:40:06 +0000</pubDate>
</item>
<item>
<title>The Invisible Journey: Tracing Packets Through the Linux Kernel with eBPF and `pwru`</title>
<link>https://coderlegion.com/23470/the-invisible-journey-tracing-packets-through-the-linux-kernel-with-ebpf-and-pwru</link>
<description>&lt;p&gt;The modern developer's landscape is a complex tapestry of microservices, containers, and cloud infrastructure. When an application slows down, or a connection drops, the immediate culprits are often assumed to be the application code itself or the external network. But what about the invisible journey a packet takes &lt;em&gt;inside&lt;/em&gt; the operating system? That often-opaque realm, the Linux kernel, holds the true story of network performance, packet drops, and elusive latency spikes.&lt;/p&gt;
&lt;p&gt;For many developers, the kernel remains a mysterious black box. We interact with it daily through syscalls and network interfaces, but rarely peer into its inner workings. Traditional tools like &lt;code&gt;tcpdump&lt;/code&gt; and &lt;code&gt;netstat&lt;/code&gt; offer glimpses – what leaves our machine, what arrives, and what ports are open – but they often fall short when we need to understand &lt;em&gt;why&lt;/em&gt; a packet was dropped, &lt;em&gt;where&lt;/em&gt; it spent its time, or &lt;em&gt;how&lt;/em&gt; it was processed before it ever reached the application. This is the gap that kernel-level observability tools, powered by eBPF, are rapidly filling.&lt;/p&gt;
&lt;p&gt;Inspired by the journey of a fellow developer learning to navigate the kernel's network stack, this article aims to demystify the process of tracking a packet. We'll explore why deep kernel insight is crucial for modern debugging, introduce the revolutionary technology of eBPF, and then dive hands-on into &lt;code&gt;pwru&lt;/code&gt; – an eBPF-based tool that makes the invisible journey of a packet through the Linux kernel visible and understandable. You don't need to be a kernel developer to unlock these insights; you just need the right tools and a guide to interpret their output.&lt;/p&gt;
&lt;h3 id=&quot;the-invisible-journey-why-deep-packet-tracing-matters&quot;&gt;The Invisible Journey: Why Deep Packet Tracing Matters&lt;/h3&gt;
&lt;p&gt;Imagine a web request taking too long. Your application logs show the request was received, but the response was delayed. Is it the database? The application logic? Or something happening deep within the network stack of your server, or even the client? Without kernel-level visibility, you're often left guessing.&lt;/p&gt;
&lt;h4 id=&quot;beyond-the-wire-the-limitations-of-userspace-tools&quot;&gt;Beyond the Wire: The Limitations of Userspace Tools&lt;/h4&gt;
&lt;p&gt;Tools like &lt;code&gt;tcpdump&lt;/code&gt; are invaluable. They capture packets as they hit or leave a network interface, showing you what's on the wire. &lt;code&gt;netstat&lt;/code&gt; or &lt;code&gt;ss&lt;/code&gt; show you active connections, listening ports, and basic statistics. These are the frontline tools for network debugging, and they serve a critical purpose.&lt;/p&gt;
&lt;p&gt;However, they have inherent limitations when you need to understand the &lt;em&gt;internal processing&lt;/em&gt; of a packet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Black Box Kernel:&lt;/strong&gt; &lt;code&gt;tcpdump&lt;/code&gt; sees packets &lt;em&gt;before&lt;/em&gt; the kernel processes them on ingress and &lt;em&gt;after&lt;/em&gt; the kernel has finished processing them on egress. What happens in between – routing decisions, firewall rules, socket buffer management, TCP stack processing – remains a mystery.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Packet Drops:&lt;/strong&gt; If &lt;code&gt;tcpdump&lt;/code&gt; doesn't show a packet leaving your machine, you know it never made it to the wire. But &lt;em&gt;why&lt;/em&gt;? Was it dropped by a firewall? A full transmit queue? A routing error? &lt;code&gt;tcpdump&lt;/code&gt; can't tell you the precise point of failure within the kernel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency Attribution:&lt;/strong&gt; A high round-trip time could be due to network latency, but it could also be due to significant processing delays within the kernel's network stack or the application's inability to read from the socket quickly enough. Isolating these factors is hard with userspace tools alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; Userspace tools often lack the rich context available inside the kernel, such as the specific kernel function being executed, the CPU core involved, or the network namespace.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;common-scenarios-demanding-kernel-level-insight&quot;&gt;Common Scenarios Demanding Kernel-Level Insight&lt;/h4&gt;
&lt;p&gt;Developers frequently encounter scenarios where kernel-level packet tracing becomes indispensable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mysterious Packet Drops:&lt;/strong&gt; &quot;My service isn't receiving all the packets it expects, but &lt;code&gt;tcpdump&lt;/code&gt; on the interface shows them arriving.&quot; This suggests an internal kernel drop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unexplained Latency:&lt;/strong&gt; &quot;My application is slow, but CPU and memory usage are fine, and network latency to other services seems normal.&quot; Could it be contention within the kernel's network buffers or inefficient processing?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firewall Troubleshooting:&lt;/strong&gt; &quot;I've configured &lt;code&gt;iptables&lt;/code&gt;, but traffic still isn't flowing as expected, or is being unexpectedly blocked.&quot; Pinpointing the exact &lt;code&gt;netfilter&lt;/code&gt; hook where a packet is dropped or modified is crucial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing and Forwarding Issues:&lt;/strong&gt; &quot;Why isn't this packet being forwarded to the correct destination, even though my routing table looks correct?&quot; Tracing the packet's path through the routing decision points can reveal the answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex Network Stacks:&lt;/strong&gt; In environments using technologies like IPVS, BPF-based load balancers, or advanced container networking (e.g., Cilium, Calico), understanding the kernel's internal packet flow becomes paramount.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These challenges highlight the need for a deeper level of observability – one that allows us to peek &lt;em&gt;inside&lt;/em&gt; the kernel and follow a packet's every step.&lt;/p&gt;
&lt;h3 id=&quot;enter-ebpf-programmable-kernel-observability&quot;&gt;Enter eBPF: Programmable Kernel Observability&lt;/h3&gt;
&lt;p&gt;For decades, getting insight into the Linux kernel involved recompiling it, using &lt;code&gt;ftrace&lt;/code&gt;, or relying on limited &lt;code&gt;procfs&lt;/code&gt; entries. These methods were often cumbersome, risky, or lacked the necessary detail. The advent of eBPF (extended Berkeley Packet Filter) changed everything.&lt;/p&gt;
&lt;h4 id=&quot;what-is-ebpf-a-high-level-overview-&quot;&gt;What is eBPF? (A High-Level Overview)&lt;/h4&gt;
&lt;p&gt;At its core, eBPF is a revolutionary technology that allows programs to run safely within the Linux kernel. Think of it as a highly efficient, event-driven virtual machine embedded directly in the kernel. Unlike traditional kernel modules, eBPF programs are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Safe:&lt;/strong&gt; All eBPF programs must pass a rigorous kernel verifier before execution, ensuring they don't crash the kernel, loop indefinitely, or access unauthorized memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event-Driven:&lt;/strong&gt; eBPF programs are attached to various &quot;hook points&quot; within the kernel. These can be:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kprobes/Kretprobes:&lt;/strong&gt; Attach to the entry or exit of almost any kernel function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uprobes/Uretprobes:&lt;/strong&gt; Attach to the entry or exit of userspace functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracepoints:&lt;/strong&gt; Stable, well-defined points intentionally added by kernel developers for tracing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Hooks:&lt;/strong&gt; For filtering and processing network packets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syscall Hooks:&lt;/strong&gt; Intercepting system calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficient:&lt;/strong&gt; They execute in-kernel, avoiding costly context switches between userspace and kernel space. They are typically JIT-compiled to native machine code for maximum performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Programmable:&lt;/strong&gt; Developers write eBPF programs (often in C, then compiled to eBPF bytecode using &lt;code&gt;clang&lt;/code&gt; and &lt;code&gt;llvm&lt;/code&gt;) to perform specific tasks: filter events, collect statistics, modify data, or redirect packets. These programs can then communicate results back to userspace via shared maps or ring buffers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In essence, eBPF provides a powerful, flexible, and safe way to extend kernel functionality and gain unprecedented visibility into its operations without modifying kernel source code or rebooting.&lt;/p&gt;
&lt;h4 id=&quot;the-power-of-ebpf-for-networking&quot;&gt;The Power of eBPF for Networking&lt;/h4&gt;
&lt;p&gt;eBPF's origins are deeply rooted in networking (the original BPF was for packet filtering). Today, its capabilities for network observability and control are immense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Advanced Packet Filtering:&lt;/strong&gt; Beyond what &lt;code&gt;iptables&lt;/code&gt; can do, eBPF programs can filter packets based on arbitrary criteria, even deep into application-layer protocols.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traffic Control:&lt;/strong&gt; Shaping, policing, and load balancing traffic directly in the kernel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security:&lt;/strong&gt; Implementing fine-grained network policies and intrusion detection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability:&lt;/strong&gt; This is where &lt;code&gt;pwru&lt;/code&gt; shines. By attaching eBPF programs to key network functions and tracepoints, we can collect detailed information about every packet as it traverses the kernel's network stack. This allows us to see &lt;em&gt;exactly&lt;/em&gt; what the kernel is doing with each packet, providing unparalleled debugging capabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;pwru-your-guide-through-the-kernel-network-stack&quot;&gt;&lt;code&gt;pwru&lt;/code&gt;: Your Guide Through the Kernel Network Stack&lt;/h3&gt;
&lt;p&gt;While you &lt;em&gt;could&lt;/em&gt; write your own eBPF programs to trace packets, tools like &lt;code&gt;pwru&lt;/code&gt; (Packet Walk Ruler) abstract away the complexity, providing a ready-to-use solution for deep network stack inspection. &lt;code&gt;pwru&lt;/code&gt; leverages eBPF to dynamically attach to numerous kernel functions and tracepoints, giving you a real-time, step-by-step view of a packet's journey.&lt;/p&gt;
&lt;h4 id=&quot;introducing-pwru&quot;&gt;Introducing &lt;code&gt;pwru&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; isn't just another &lt;code&gt;tcpdump&lt;/code&gt;. It's a specialized tool designed to show you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hook Point:&lt;/strong&gt; The specific kernel function or tracepoint the packet is currently passing through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action:&lt;/strong&gt; What the kernel did with the packet at that point (e.g., &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;CONSUMED&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;QUEUE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Packet Metadata:&lt;/strong&gt; Crucial details like source/destination IP/port, protocol, packet length (&lt;code&gt;skb_len&lt;/code&gt;), network namespace, CPU, and more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; Which &lt;code&gt;netfilter&lt;/code&gt; hook, if any, was involved, and the return code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This level of detail allows you to precisely identify where packets are being handled, modified, or dropped within the kernel, making it an indispensable tool for debugging complex network issues.&lt;/p&gt;
&lt;h4 id=&quot;setting-up-pwru-practical-steps-&quot;&gt;Setting Up &lt;code&gt;pwru&lt;/code&gt; (Practical Steps)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; requires a relatively recent Linux kernel (4.9+ for basic eBPF, 5.x+ for full features and stability) and a few development tools to compile its eBPF programs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux Kernel: Version 4.9+ (5.x or newer recommended).&lt;/li&gt;
&lt;li&gt;Build Tools: &lt;code&gt;clang&lt;/code&gt;, &lt;code&gt;llvm&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Kernel Headers: Ensure your kernel headers match your running kernel version. On Debian/Ubuntu, &lt;code&gt;sudo apt install linux-headers-$(uname -r)&lt;/code&gt;. On Fedora/CentOS/RHEL, &lt;code&gt;sudo yum install kernel-devel&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;pwru&lt;/code&gt; is typically installed by cloning its GitHub repository and building it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ensure you have git, clang, llvm, and make installed
sudo apt update &amp;amp;&amp;amp; sudo apt install git clang llvm make   # For Debian/Ubuntu
# sudo yum install git clang llvm make                   # For Fedora/CentOS/RHEL

git clone https://github.com/cilium/pwru.git
cd pwru
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;make install&lt;/code&gt; fails or you prefer not to install globally, you can run &lt;code&gt;pwru&lt;/code&gt; directly from the &lt;code&gt;pwru/bin&lt;/code&gt; directory.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;your-first-packet-trace-with-pwru&quot;&gt;Your First Packet Trace with &lt;code&gt;pwru&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Let's start with a simple example: tracing an HTTP request to a local web server (or any external website).&lt;/p&gt;
&lt;p&gt;Open two terminal windows. In the first, start &lt;code&gt;pwru&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1: Start pwru
# Trace TCP packets on port 80 (HTTP) or 443 (HTTPS)
# We'll customize output fields for clarity
sudo pwru --output-fields=src_ip,dst_ip,proto,port,func,action,skb_len \
          --filter &quot;proto == IPPROTO_TCP &amp;amp;&amp;amp; (dst_port == 80 || dst_port == 443)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the second terminal, make an HTTP request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 2: Make an HTTP request
curl -v http://example.com
# Or, if you have a local web server:
# curl -v http://localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back in Terminal 1, you'll see a flood of output. Let's break down a typical line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src_ip=192.168.1.100 dst_ip=93.184.216.34 proto=TCP port=51234-&amp;gt;80 func=ip_rcv action=PASS skb_len=60
src_ip=192.168.1.100 dst_ip=93.184.216.34 proto=TCP port=51234-&amp;gt;80 func=ip_local_deliver_finish action=PASS skb_len=60
src_ip=192.168.1.100 dst_ip=93.184.216.34 proto=TCP port=51234-&amp;gt;80 func=tcp_v4_rcv action=PASS skb_len=60
... (many more lines) ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What are we seeing? Each line represents a &quot;hook point&quot; – a specific kernel function or tracepoint – that the packet traversed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src_ip&lt;/code&gt;, &lt;code&gt;dst_ip&lt;/code&gt;, &lt;code&gt;proto&lt;/code&gt;, &lt;code&gt;port&lt;/code&gt;: Basic network tuple identifying the packet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func&lt;/code&gt;: The kernel function name (&lt;code&gt;ip_rcv&lt;/code&gt;, &lt;code&gt;tcp_v4_rcv&lt;/code&gt;, etc.). This is the heart of &lt;code&gt;pwru&lt;/code&gt;'s insight.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;action&lt;/code&gt;: What happened at that function. &lt;code&gt;PASS&lt;/code&gt; means it continued its journey. &lt;code&gt;DROP&lt;/code&gt; would indicate it was discarded. &lt;code&gt;CONSUMED&lt;/code&gt; often means it was processed and removed from the general network path (e.g., by the TCP stack).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skb_len&lt;/code&gt;: The size of the &lt;code&gt;sk_buff&lt;/code&gt; (socket buffer) structure carrying the packet data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This output shows the packet moving through different layers and processing stages within the kernel. For an outgoing packet, you'd see functions like &lt;code&gt;ip_output&lt;/code&gt;, &lt;code&gt;dev_queue_xmit&lt;/code&gt;, etc. For an incoming packet destined for a local socket, you'd see &lt;code&gt;ip_rcv&lt;/code&gt;, &lt;code&gt;ip_local_deliver&lt;/code&gt;, &lt;code&gt;tcp_v4_rcv&lt;/code&gt;, and eventually &lt;code&gt;sock_queue_rcv_skb&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;deeper-dives-understanding-the-kernel-network-path&quot;&gt;Deeper Dives: Understanding the Kernel Network Path&lt;/h3&gt;
&lt;p&gt;Interpreting &lt;code&gt;pwru&lt;/code&gt;'s output requires a basic understanding of how the Linux kernel processes network packets. While the full network stack is incredibly complex, we can identify key functions and their roles.&lt;/p&gt;
&lt;h4 id=&quot;key-kernel-hook-points-and-their-significance&quot;&gt;Key Kernel Hook Points and Their Significance&lt;/h4&gt;
&lt;p&gt;The kernel network stack can be broadly divided into ingress (receiving) and egress (transmitting) paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ingress Path (Receiving a Packet):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;__netif_receive_skb_core&lt;/code&gt;: The initial entry point for a packet received from the network driver.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_rcv&lt;/code&gt;: The entry point for IPv4 packets after &lt;code&gt;__netif_receive_skb_core&lt;/code&gt;. It performs basic validation and determines if the packet is for the local host or needs forwarding.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_local_deliver_finish&lt;/code&gt;: If the packet is for the local host, this function is called. It passes the packet to the appropriate transport layer (TCP, UDP, ICMP).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcp_v4_rcv&lt;/code&gt; / &lt;code&gt;udp_rcv&lt;/code&gt;: Entry points for TCP and UDP packets, respectively, after &lt;code&gt;ip_local_deliver_finish&lt;/code&gt;. These functions handle protocol-specific processing (e.g., TCP sequence number checks, ACK generation).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sock_queue_rcv_skb&lt;/code&gt;: The packet is finally queued to the receiving socket's buffer, making it available for a userspace application to read via &lt;code&gt;recvmsg&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Egress Path (Transmitting a Packet):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;tcp_sendmsg&lt;/code&gt; / &lt;code&gt;udp_sendmsg&lt;/code&gt;: Initiated by a userspace application's &lt;code&gt;sendmsg&lt;/code&gt; or &lt;code&gt;write&lt;/code&gt; call. These prepare the packet for transmission at the transport layer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_output&lt;/code&gt;: Handles the IPv4 header creation and prepares the packet for sending.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_finish_output&lt;/code&gt;: Finalizes IP output, including routing decisions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dev_queue_xmit&lt;/code&gt;: Queues the packet to the appropriate network device's transmit queue.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__dev_xmit_skb&lt;/code&gt;: The packet is handed off to the network driver for physical transmission.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; shows you the packet traversing many of these functions (and many more in between), along with &lt;code&gt;netfilter&lt;/code&gt; hooks (&lt;code&gt;NF_HOOK&lt;/code&gt; points) and other processing stages.&lt;/p&gt;
&lt;h4 id=&quot;common-scenarios-amp-pwru-insights&quot;&gt;Common Scenarios &amp;amp; &lt;code&gt;pwru&lt;/code&gt; Insights&lt;/h4&gt;
&lt;p&gt;Let's look at how &lt;code&gt;pwru&lt;/code&gt; can help diagnose specific problems.&lt;/p&gt;
&lt;h5 id=&quot;1-pinpointing-packet-drops&quot;&gt;1. Pinpointing Packet Drops&lt;/h5&gt;
&lt;p&gt;One of the most powerful uses of &lt;code&gt;pwru&lt;/code&gt; is identifying &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt; a packet is dropped. If you suspect packet loss:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1: Watch for dropped packets
sudo pwru --output-fields=func,action,src_ip,dst_ip,proto,port,skb_len --filter &quot;action == DROP&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, try to reproduce the packet loss (e.g., attempt a connection to a non-existent port on your machine, or trigger a firewall rule). You might see output like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func=ip_rcv_finish action=DROP src_ip=192.168.1.1 dst_ip=192.168.1.100 proto=ICMP port=0-&amp;gt;0 skb_len=84
func=nf_hook_slow action=DROP src_ip=192.168.1.1 dst_ip=192.168.1.100 proto=TCP port=12345-&amp;gt;80 skb_len=60
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ip_rcv_finish&lt;/code&gt; with &lt;code&gt;DROP&lt;/code&gt; might indicate issues like a full receive queue or an invalid packet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nf_hook_slow&lt;/code&gt; with &lt;code&gt;DROP&lt;/code&gt; is a strong indicator that a &lt;code&gt;netfilter&lt;/code&gt; (iptables) rule is dropping the packet. This immediately tells you to inspect your firewall configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;2-analyzing-latency-attribution&quot;&gt;2. Analyzing Latency Attribution&lt;/h5&gt;
&lt;p&gt;If &lt;code&gt;curl&lt;/code&gt; to &lt;code&gt;localhost&lt;/code&gt; is slow, where is the time spent? &lt;code&gt;pwru&lt;/code&gt; can show the timestamps for each hook point, though interpreting the exact delta requires careful analysis and often correlation with other tools. For a quick check, you can add &lt;code&gt;ktime&lt;/code&gt; (kernel time in nanoseconds) to your output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1: Trace a local request with timestamps
sudo pwru --output-fields=ktime,func,action,src_ip,dst_ip,proto,port,skb_len \
          --filter &quot;proto == IPPROTO_TCP &amp;amp;&amp;amp; (src_ip == 127.0.0.1 || dst_ip == 127.0.0.1) &amp;amp;&amp;amp; (dst_port == 80 || src_port == 80)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in Terminal 2: &lt;code&gt;curl http://localhost&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By examining the &lt;code&gt;ktime&lt;/code&gt; values, you can see if there are unusually large gaps between consecutive &lt;code&gt;func&lt;/code&gt; calls for the same packet. A long gap between &lt;code&gt;ip_rcv&lt;/code&gt; and &lt;code&gt;ip_local_deliver&lt;/code&gt; could suggest CPU contention or heavy processing before the packet reaches the transport layer. A delay between &lt;code&gt;sock_queue_rcv_skb&lt;/code&gt; and the application's actual read might point to application-level slowness.&lt;/p&gt;
&lt;h5 id=&quot;3-understanding-firewall-netfilter-interaction&quot;&gt;3. Understanding Firewall/Netfilter Interaction&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; can explicitly show &lt;code&gt;netfilter&lt;/code&gt; processing. &lt;code&gt;netfilter&lt;/code&gt; hooks are points where &lt;code&gt;iptables&lt;/code&gt; rules operate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1: Trace packets, specifically looking for netfilter hooks
sudo pwru --output-fields=func,action,src_ip,dst_ip,proto,port,skb_len,hook_name,nf_verdict \
          --filter &quot;proto == IPPROTO_TCP &amp;amp;&amp;amp; (dst_port == 22 || src_port == 22)&quot; # e.g., SSH traffic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll see lines like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func=nf_hook_slow action=PASS hook_name=NF_INET_PRE_ROUTING nf_verdict=NF_ACCEPT src_ip=...
func=nf_hook_slow action=PASS hook_name=NF_INET_LOCAL_IN nf_verdict=NF_ACCEPT src_ip=...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hook_name&lt;/code&gt;: Specifies the &lt;code&gt;netfilter&lt;/code&gt; hook (e.g., &lt;code&gt;NF_INET_PRE_ROUTING&lt;/code&gt;, &lt;code&gt;NF_INET_LOCAL_IN&lt;/code&gt;, &lt;code&gt;NF_INET_FORWARD&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nf_verdict&lt;/code&gt;: The verdict of the &lt;code&gt;netfilter&lt;/code&gt; chain (&lt;code&gt;NF_ACCEPT&lt;/code&gt;, &lt;code&gt;NF_DROP&lt;/code&gt;, &lt;code&gt;NF_STOLEN&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a packet is dropped by &lt;code&gt;netfilter&lt;/code&gt;, you'll see an &lt;code&gt;action=DROP&lt;/code&gt; along with &lt;code&gt;nf_verdict=NF_DROP&lt;/code&gt; at a specific &lt;code&gt;hook_name&lt;/code&gt;, telling you exactly which &lt;code&gt;iptables&lt;/code&gt; chain is responsible.&lt;/p&gt;
&lt;h5 id=&quot;4-tracing-specific-connections&quot;&gt;4. Tracing Specific Connections&lt;/h5&gt;
&lt;p&gt;To focus on a single problematic connection, use detailed filters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1: Trace a specific source IP and destination port
sudo pwru --output-fields=func,action,src_ip,dst_ip,proto,port,skb_len \
          --filter &quot;src_ip == 10.0.0.5 &amp;amp;&amp;amp; dst_port == 8080&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures you only see the relevant packets, reducing noise and making analysis much easier. You can combine filters with &lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;NOT&lt;/code&gt; for very precise targeting.&lt;/p&gt;
&lt;h3 id=&quot;advanced-pwru-techniques-and-trade-offs&quot;&gt;Advanced &lt;code&gt;pwru&lt;/code&gt; Techniques and Trade-offs&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; is a powerful tool, and mastering its capabilities involves more than just basic tracing.&lt;/p&gt;
&lt;h4 id=&quot;filtering-and-output-customization&quot;&gt;Filtering and Output Customization&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex Filters:&lt;/strong&gt; &lt;code&gt;pwru&lt;/code&gt; supports complex logical expressions in its &lt;code&gt;--filter&lt;/code&gt; argument.&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Trace HTTP and HTTPS traffic, but only from a specific source IP, and not if it's an ICMP packet
sudo pwru --filter &quot;proto == IPPROTO_TCP &amp;amp;&amp;amp; (dst_port == 80 || dst_port == 443) &amp;amp;&amp;amp; src_ip == 192.168.1.10 AND NOT proto == IPPROTO_ICMP&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom Output Fields:&lt;/strong&gt; The &lt;code&gt;--output-fields&lt;/code&gt; argument is crucial. You can add fields like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cpu&lt;/code&gt;: Which CPU core processed the packet. Useful for identifying CPU affinity issues.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ktime&lt;/code&gt;: Kernel timestamp (nanoseconds).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netns&lt;/code&gt;: Network namespace ID. Essential in containerized environments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ifindex&lt;/code&gt;: Network interface index.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mark&lt;/code&gt;: &lt;code&gt;skb-&amp;gt;mark&lt;/code&gt; value, often used for policy routing or firewall marking.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tstamp&lt;/code&gt;: Hardware timestamp.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ingress_ifindex&lt;/code&gt;, &lt;code&gt;egress_ifindex&lt;/code&gt;: The interface the packet entered/exited.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reason&lt;/code&gt;: Provides more context for drops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consult &lt;code&gt;pwru --help&lt;/code&gt; for a full list of available fields.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saving Output:&lt;/strong&gt; Redirect &lt;code&gt;pwru&lt;/code&gt;'s output to a file for later analysis, especially for long-running traces.&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo pwru --output-fields=... --filter &quot;...&quot; &amp;gt; pwru_trace.log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;performance-considerations&quot;&gt;Performance Considerations&lt;/h4&gt;
&lt;p&gt;eBPF is designed for low overhead, but &lt;code&gt;pwru&lt;/code&gt; attaches to &lt;em&gt;many&lt;/em&gt; kernel hook points. On a very busy system with high packet rates, tracing every packet can still generate a substantial amount of data and consume CPU cycles.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Precision is Key:&lt;/strong&gt; Always use the most precise filters possible. Instead of &lt;code&gt;--filter &quot;proto == IPPROTO_TCP&quot;&lt;/code&gt;, use &lt;code&gt;proto == IPPROTO_TCP &amp;amp;&amp;amp; (src_port == 12345 || dst_port == 8080)&lt;/code&gt; to focus on relevant traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output Fields:&lt;/strong&gt; Only request the output fields you truly need. Each field adds a small amount of overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Short Bursts:&lt;/strong&gt; For initial diagnosis, run &lt;code&gt;pwru&lt;/code&gt; for short bursts (e.g., 5-10 seconds) to capture a representative sample of traffic during a problem occurrence, rather than letting it run indefinitely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production Caution:&lt;/strong&gt; While generally safe, running &lt;code&gt;pwru&lt;/code&gt; on highly loaded production systems without careful filtering and monitoring should be approached with caution. Test your filters in a staging environment first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;limitations-and-alternatives&quot;&gt;Limitations and Alternatives&lt;/h4&gt;
&lt;p&gt;While &lt;code&gt;pwru&lt;/code&gt; is incredibly powerful, it's not a silver bullet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kernel Knowledge Still Required:&lt;/strong&gt; Interpreting the output, especially the &lt;code&gt;func&lt;/code&gt; names, still requires some understanding of the kernel's network stack. You won't become a kernel expert overnight, but you'll learn key functions relevant to your debugging tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root Privileges:&lt;/strong&gt; &lt;code&gt;pwru&lt;/code&gt; requires root privileges to attach eBPF programs to the kernel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel Version Dependency:&lt;/strong&gt; While &lt;code&gt;pwru&lt;/code&gt; tries to be compatible, specific hook points or data structures can vary slightly between kernel versions, potentially affecting output or functionality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not a Packet Capture Tool:&lt;/strong&gt; &lt;code&gt;pwru&lt;/code&gt; shows kernel events, not the full packet payload. For payload inspection, &lt;code&gt;tcpdump&lt;/code&gt; or Wireshark are still necessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Alternatives and Complementary Tools:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BCC Tools:&lt;/strong&gt; The BPF Compiler Collection (BCC) provides a rich set of eBPF-based tools for various aspects of kernel observability, including networking (e.g., &lt;code&gt;tcplife&lt;/code&gt;, &lt;code&gt;dropwatch&lt;/code&gt;, &lt;code&gt;sockstat&lt;/code&gt;). They are often more specialized than &lt;code&gt;pwru&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bpftrace&lt;/code&gt;:&lt;/strong&gt; If &lt;code&gt;pwru&lt;/code&gt;'s predefined tracing isn't enough, &lt;code&gt;bpftrace&lt;/code&gt; allows you to write custom eBPF one-liners or scripts with a simpler syntax than raw C. This is for when you need to combine specific kernel events or perform custom aggregations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tracepoint-analyzer&lt;/code&gt;:&lt;/strong&gt; Another tool specifically designed to help understand kernel tracepoints.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pwru&lt;/code&gt; serves as an excellent starting point and often provides enough detail for most network debugging scenarios. When you hit its limits, &lt;code&gt;bpftrace&lt;/code&gt; or specialized BCC tools are the next logical step.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The journey of a packet through the Linux kernel is often an invisible one, a black box that can hide the root causes of frustrating network performance issues, mysterious packet drops, and elusive latency. For too long, developers have been forced to debug these problems with limited visibility, relying on guesswork and external network tools that only show part of the picture.&lt;/p&gt;
&lt;p&gt;eBPF has fundamentally changed this landscape, empowering us to safely and efficiently peer into the kernel's inner workings. Tools like &lt;code&gt;pwru&lt;/code&gt; democratize this power, translating complex kernel events into actionable insights that even developers &quot;who barely know the kernel&quot; can understand and leverage.&lt;/p&gt;
&lt;p&gt;By embracing &lt;code&gt;pwru&lt;/code&gt; and its eBPF underpinnings, you gain a superpower: the ability to trace a packet's every step, understand its fate, and pinpoint precisely where things go wrong within the operating system. This deeper understanding&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23470/the-invisible-journey-tracing-packets-through-the-linux-kernel-with-ebpf-and-pwru</guid>
<pubDate>Sun, 26 Jul 2026 04:00:42 +0000</pubDate>
</item>
</channel>
</rss>