<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Marco Cadetg</title>
    <description>The latest articles on Forem by Marco Cadetg (@domcyrus).</description>
    <link>https://forem.com/domcyrus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3521420%2F4778e2da-9816-414b-b5da-5094d969ba59.png</url>
      <title>Forem: Marco Cadetg</title>
      <link>https://forem.com/domcyrus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/domcyrus"/>
    <language>en</language>
    <item>
      <title>More Security With Landlock</title>
      <dc:creator>Marco Cadetg</dc:creator>
      <pubDate>Sat, 06 Dec 2025 17:25:00 +0000</pubDate>
      <link>https://forem.com/domcyrus/more-security-with-landlock-3fni</link>
      <guid>https://forem.com/domcyrus/more-security-with-landlock-3fni</guid>
      <description>&lt;p&gt;Network monitoring tools like &lt;a href="https://github.com/domcyrus/rustnet" rel="noopener noreferrer"&gt;RustNet&lt;/a&gt; process untrusted data from the wire. This makes them vulnerable to protocol exploits and crafted payloads. If an attacker finds a bug in packet parsing, they could read sensitive files, steal data, or open reverse shells. This has happened with Wireshark before, and since we use pcap, we could face the same issues. This got me thinking about defense-in-depth: what if the tool could sandbox itself after initialization?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Privileged Tools
&lt;/h2&gt;

&lt;p&gt;Network capture tools require elevated privileges. On Linux, &lt;code&gt;CAP_NET_RAW&lt;/code&gt; allows creating raw sockets to capture packets. But once you have this capability, you usually keep it for the entire process lifetime—even though you only need it during initialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Traditional approach: run with elevated privileges&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./network-tool &lt;span class="nt"&gt;--interface&lt;/span&gt; eth0
&lt;span class="c"&gt;# Tool now has CAP_NET_RAW for its entire lifetime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a larger attack surface than necessary. If a bug in Deep Packet Inspection (DPI) code is exploited, the attacker inherits all the privileges the process has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Landlock
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://landlock.io/" rel="noopener noreferrer"&gt;Landlock&lt;/a&gt; is a Linux Security Module (LSM) that allows unprivileged sandboxing. Unlike seccomp (which filters syscalls) or namespaces (which need privileges to set up), Landlock lets a process restrict itself. It's been in the kernel since 5.13 (filesystem), and network restrictions were added in 6.4.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;sandbox after initialization, not before&lt;/strong&gt;. We can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open packet capture handles (needs &lt;code&gt;CAP_NET_RAW&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Load eBPF programs (needs &lt;code&gt;CAP_BPF&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Create log files (needs filesystem write access)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt; apply Landlock restrictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt; drop &lt;code&gt;CAP_NET_RAW&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The existing pcap handle remains valid—the kernel doesn't revoke it. But new raw sockets? Blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The Landlock API involves creating a ruleset, adding rules, and enforcing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;apply_sandbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SandboxConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SandboxResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check kernel support&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;abi_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;landlock_create_ruleset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;null&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LANDLOCK_CREATE_RULESET_VERSION&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;abi_version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SandboxResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Landlock not supported"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Create ruleset with desired restrictions&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ruleset_attr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;landlock_ruleset_attr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;handled_access_fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_EXECUTE&lt;/span&gt;
            &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_FILE&lt;/span&gt;
            &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_WRITE_FILE&lt;/span&gt;
            &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="cm"&gt;/* ... more flags ... */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;handled_access_net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_NET_BIND_TCP&lt;/span&gt;
            &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_NET_CONNECT_TCP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ruleset_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;landlock_create_ruleset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ruleset_attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Allow reading /proc (needed for process identification)&lt;/span&gt;
    &lt;span class="nf"&gt;add_path_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruleset_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/proc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Allow writing to configured log paths&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="py"&gt;.allowed_write_paths&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;add_path_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruleset_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_WRITE_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Enforce the ruleset&lt;/span&gt;
    &lt;span class="nf"&gt;landlock_restrict_self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruleset_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SandboxResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We allow &lt;code&gt;/proc&lt;/code&gt; reads (needed for process lookup) but block everything else. Network restrictions block TCP bind/connect entirely. RustNet is a passive monitor, so it doesn't need outbound connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dropping Capabilities
&lt;/h2&gt;

&lt;p&gt;After the sandbox is applied, we drop &lt;code&gt;CAP_NET_RAW&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;drop_cap_net_raw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;CapSet&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Read current capabilities&lt;/span&gt;
    &lt;span class="nf"&gt;capget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Clear CAP_NET_RAW from all sets&lt;/span&gt;
    &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="py"&gt;.effective&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CAP_NET_RAW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="py"&gt;.permitted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CAP_NET_RAW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="py"&gt;.inheritable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CAP_NET_RAW&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Apply&lt;/span&gt;
    &lt;span class="nf"&gt;capset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is This Actually Useful?
&lt;/h2&gt;

&lt;p&gt;How much do we gain from this? I'm honestly not sure.&lt;/p&gt;

&lt;p&gt;The problem is &lt;code&gt;CAP_BPF&lt;/code&gt;. We can't drop it because RustNet uses eBPF for process lookup - mapping network connections to processes with low overhead. We could fall back to procfs scanning, but that's slower and we'd lose some functionality.&lt;/p&gt;

&lt;p&gt;So even after sandboxing, an attacker who exploits RustNet still has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CAP_BPF&lt;/code&gt; - a powerful capability that allows loading eBPF programs&lt;/li&gt;
&lt;li&gt;The open pcap handle - they can still capture packets&lt;/li&gt;
&lt;li&gt;Read access to &lt;code&gt;/proc&lt;/code&gt; - process information is still available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the worst case, Landlock is just another layer that doesn't actually stop a determined attacker. If someone finds a way around the filesystem restrictions, they're back to having most of what they need.&lt;/p&gt;

&lt;p&gt;That said, defense-in-depth is about raising the bar, not building perfect walls. Landlock blocks the easy paths: no writing to arbitrary files, no opening network connections, no executing binaries. An attacker now needs a Landlock bypass on top of their initial exploit. That's harder than just having full access from the start.&lt;/p&gt;

&lt;p&gt;Is it worth the added complexity? I think so, but I'm not completely convinced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful Degradation
&lt;/h2&gt;

&lt;p&gt;Not all environments support Landlock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kernel &amp;lt; 5.13&lt;/strong&gt;: No Landlock support—continue without sandbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel 5.13-6.3&lt;/strong&gt;: Filesystem restrictions only, no network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel 6.4+&lt;/strong&gt;: Full filesystem + network restrictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker/containers&lt;/strong&gt;: seccomp may block &lt;code&gt;landlock_*&lt;/code&gt; syscalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool checks what's available and applies what it can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_sandbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;SandboxStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FullyEnforced&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sandbox fully applied"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;SandboxStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PartiallyEnforced&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Partial sandbox: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.details&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;SandboxStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NotAvailable&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sandboxing unavailable: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="py"&gt;.reason&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Continue running either way—don't fail on missing sandbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-security environments, a &lt;code&gt;--sandbox-strict&lt;/code&gt; flag makes the tool exit if full enforcement isn't possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI Feedback Loop
&lt;/h2&gt;

&lt;p&gt;I wanted to see the sandbox status clearly. RustNet's TUI now shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─Security────────────────────────────────────────┐
│ Landlock: Fully enforced [kernel supported]     |
│ CAP_NET_RAW dropped, FS restricted, Net blocked |
└─────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it clear whether Landlock is active.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>rust</category>
      <category>systems</category>
    </item>
    <item>
      <title>Why Catching Short-Lived Processes Requires eBPF on Linux but Just a Header on macOS</title>
      <dc:creator>Marco Cadetg</dc:creator>
      <pubDate>Mon, 22 Sep 2025 15:56:13 +0000</pubDate>
      <link>https://forem.com/domcyrus/why-catching-short-lived-processes-requires-ebpf-on-linux-but-just-a-header-on-macos-mlo</link>
      <guid>https://forem.com/domcyrus/why-catching-short-lived-processes-requires-ebpf-on-linux-but-just-a-header-on-macos-mlo</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;macOS's PKTAP provides process info directly in packet headers (10 lines of code), while Linux requires eBPF programs hooking into kernel functions (100+ lines). Both solve the problem of identifying which process owns network packets but with very different complexity levels.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;This is Part 1 of my "Building a Network Monitor" series.&lt;/strong&gt;&lt;br&gt;
Coming next: Implementing Process Detection in RustNet: The Code&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Challenge&lt;/li&gt;
&lt;li&gt;macOS: The PKTAP Approach&lt;/li&gt;
&lt;li&gt;Linux: The Powerful but Complex eBPF Route&lt;/li&gt;
&lt;li&gt;The Trade-offs&lt;/li&gt;
&lt;li&gt;Implementation Notes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Traditional approaches like polling &lt;code&gt;/proc/net/*&lt;/code&gt; on Linux or running &lt;code&gt;lsof&lt;/code&gt; in a loop on macOS work well for long-lived connections, but they struggle with short-lived processes. By the time you poll, the process might already be gone, leaving you with orphaned connections whose origins remain a mystery. For example when running curl.&lt;/p&gt;

&lt;p&gt;While working on adding process identification to the network monitoring tool &lt;a href="https://github.com/domcyrus/rustnet" rel="noopener noreferrer"&gt;RustNet&lt;/a&gt;, I discovered interesting differences in how macOS and Linux tackle this challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  How The Data Flows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  macOS PKTAP:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Packet → Kernel → [+Process Info] → PKTAP Header → Your App
         ↑
         Automatic!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linux eBPF:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Packet → Kernel Function → eBPF Hook → Map → Userspace
           ↑                    ↑        ↑
      tcp_connect()     You write this   You poll this
      udp_sendmsg()
      (and 10 more...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  macOS: The PKTAP Approach
&lt;/h2&gt;

&lt;p&gt;macOS provides PKTAP (Packet Tap), where the kernel automatically includes process information in packet headers. This makes implementation very simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From Apple's darwin-xnu (bsd/net/pktap.h)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pktap_header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
    &lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pth_pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Process ID&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;pth_comm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;    &lt;span class="c1"&gt;// Process name (MAXCOMLEN + 1)&lt;/span&gt;
    &lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pth_epid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// Effective process ID&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;pth_ecomm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// Effective command name&lt;/span&gt;
    &lt;span class="c1"&gt;// ... more fields&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You simply read packets and the process info is right there in the header. The kernel handles all the heavy lifting of mapping packets to processes. Want to know which process sent a packet? Just parse the header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_process_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;process_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_process_name_from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pth_comm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pth_epid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pth_epid&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Clean, simple, and it works for most packets. Interestingly, some packet types (like ICMP and ARP) don't always include process information—likely because they're handled differently by the kernel or lack a clear originating process context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux: The Powerful but Complex eBPF Route
&lt;/h2&gt;

&lt;p&gt;Linux doesn't have an equivalent to PKTAP, so one solution involves using eBPF programs that hook into kernel networking functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kprobe/tcp_connect"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_tcp_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;PT_REGS_PARM1_CORE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Extract network info from socket&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saddr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__sk_common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skc_rcv_saddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daddr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__sk_common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skc_daddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Get process info&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bpf_get_current_comm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Store in map for userspace retrieval&lt;/span&gt;
    &lt;span class="n"&gt;bpf_map_update_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;socket_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_ANY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here is where it gets interesting (and complicated):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need separate kprobes for &lt;code&gt;tcp_connect&lt;/code&gt;, &lt;code&gt;inet_csk_accept&lt;/code&gt;, &lt;code&gt;udp_sendmsg&lt;/code&gt;, &lt;code&gt;tcp_v6_connect&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;The comm field is limited to 16 characters—so "Firefox" becomes "Socket Thread"&lt;/li&gt;
&lt;li&gt;You must understand kernel internals—socket structures, CO-RE relocations, BTF&lt;/li&gt;
&lt;li&gt;Build complexity: Requires libelf, clang, LLVM, and kernel headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Trade-offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  macOS PKTAP Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dead simple API&lt;/li&gt;
&lt;li&gt;Works out of the box&lt;/li&gt;
&lt;li&gt;Full process names (when available)&lt;/li&gt;
&lt;li&gt;Zero kernel programming required&lt;/li&gt;
&lt;li&gt;Automatic process-packet association for most traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  macOS PKTAP Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;macOS only (possibly other BSDs)&lt;/li&gt;
&lt;li&gt;Requires special interface setup&lt;/li&gt;
&lt;li&gt;Limited to what Apple exposes&lt;/li&gt;
&lt;li&gt;Some packet types (ICMP, ARP) may lack process info&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Linux eBPF Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Incredibly powerful and flexible&lt;/li&gt;
&lt;li&gt;Can hook into virtually any kernel function&lt;/li&gt;
&lt;li&gt;Lower overhead than polling&lt;/li&gt;
&lt;li&gt;Works on most modern kernels&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Linux eBPF Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Steep learning curve&lt;/li&gt;
&lt;li&gt;Complex build requirements&lt;/li&gt;
&lt;li&gt;16-char process name limit (&lt;code&gt;comm&lt;/code&gt; field)&lt;/li&gt;
&lt;li&gt;Must handle kernel version differences&lt;/li&gt;
&lt;li&gt;More moving parts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;macOS PKTAP&lt;/th&gt;
&lt;th&gt;Linux eBPF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lines of Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;td&gt;~100+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Requires kernel headers, LLVM, libelf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Process Name Limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17 chars&lt;/td&gt;
&lt;td&gt;16 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kernel Programming&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Days/Weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;macOS only&lt;/td&gt;
&lt;td&gt;Linux 4.x+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Implementation Notes
&lt;/h2&gt;

&lt;p&gt;For RustNet, I ended up using libbpf instead of Rust's aya framework specifically to avoid the rust nightly toolchain dependency. While aya offers more idiomatic Rust, libbpf's stability and broader compatibility made it the better choice for this project.&lt;/p&gt;

&lt;p&gt;The contrast really highlights different OS design philosophies: macOS provides high-level, purpose-built APIs versus Linux offering low-level primitives that can be composed into powerful solutions, albeit with significantly more complexity.&lt;/p&gt;

&lt;p&gt;Both approaches solve the same problem effectively, but the developer experience is very different. I wonder if Linux could benefit from higher-level networking APIs like PKTAP, though perhaps that's antithetical to the Unix philosophy of composable tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on the &lt;code&gt;comm&lt;/code&gt; field&lt;/strong&gt;: The 16-character limitation is a kernel constraint where thread names get truncated. Firefox appears as "Socket Thread", Chrome as "ThreadPoolForeg", etc. You can work around it by combining eBPF with selective procfs lookups, but that defeats some of the performance benefits.&lt;/p&gt;




&lt;h2&gt;
  
  
  💭 Discussion Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Have you worked with PKTAP or eBPF?&lt;/strong&gt; What was your experience?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is Linux's complexity justified&lt;/strong&gt; by the flexibility it provides?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should Linux add a simpler API&lt;/strong&gt; like PKTAP for common use cases?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any Windows developers here?&lt;/strong&gt; How does Windows handle this problem?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What other OS-specific networking APIs have you discovered?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔗 See It In Action
&lt;/h2&gt;

&lt;p&gt;This implementation is part of &lt;a href="https://github.com/domcyrus/rustnet" rel="noopener noreferrer"&gt;RustNet&lt;/a&gt;, a cross-platform network monitoring TUI where you can see both approaches in action. The eBPF implementation is available in v0.9.0 as an experimental feature (&lt;code&gt;--features=ebpf&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;If you're interested in network monitoring, packet inspection, or just want to see how different operating systems handle the same problem, check out the project. I'm always looking for feedback and contributions!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://domcyrus.github.io/systems-programming/networking/ebpf/macos/linux/2025/01/18/pktap-vs-ebpf-network-process-identification.html" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. Follow me for more posts about systems programming, Rust, and the interesting quirks of different operating systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>rust</category>
      <category>networking</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
