<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://stricaud.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://stricaud.github.io/" rel="alternate" type="text/html" /><updated>2026-10-21T13:59:50+00:00</updated><id>https://stricaud.github.io/feed.xml</id><title type="html">Sebastien Tricaud</title><subtitle>Security with tons of tool kits!</subtitle><author><name>Sebastien Tricaud</name></author><entry><title type="html">Write a packet sniffer with libpcapng</title><link href="https://stricaud.github.io/network/write-a-packet-sniffer-with-libpcapng/" rel="alternate" type="text/html" title="Write a packet sniffer with libpcapng" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-20T00:00:00+00:00</updated><id>https://stricaud.github.io/network/write-a-packet-sniffer-with-libpcapng</id><content type="html" xml:base="https://stricaud.github.io/network/write-a-packet-sniffer-with-libpcapng/"><![CDATA[<p>I have been writing packet sniffers with libpcap for most of my career, and I
still have a soft spot for it. One of the tools I keep coming back to is
<a href="https://github.com/stricaud/mqs">mqs</a>, a little MariaDB/MySQL query sniffer I
wrote because I wanted to see the indicators MISP was about to commit to its
database — as they went past on the wire, before the rows landed. Putting a
proxy in front of the database would have been overkill; sniffing <code class="language-plaintext highlighter-rouge">dst port
3306</code> and handing each query to a LuaJIT script that kept only the <code class="language-plaintext highlighter-rouge">INSERT</code>s
was not. It still works, and I still like it.</p>

<p>What mqs also taught me is exactly where libpcap stops. libpcap gives you
bytes. Everything above the link layer is your problem: Ethernet, then IP,
then the TCP stream, then MySQL’s own framing, and only then the query you
actually came for. Most of mqs is that plumbing, not the part I cared about.
And a BPF filter can say <code class="language-plaintext highlighter-rouge">dst port 3306</code>, but it cannot say
<code class="language-plaintext highlighter-rouge">mysql.query contains "INSERT INTO attributes"</code>, because BPF matches byte
offsets in packets, not fields in protocols. The moment you want the second
thing, you are writing a dissector.</p>

<p>So in <a href="https://github.com/stricaud/libpcapng">libpcapng</a> I put the dissector
and the display-filter engine <em>inside the library</em>. Same filter syntax as
Wireshark’s, same field names, applied to a file you just read or to a live
interface. This post builds the smallest useful sniffer on top of that — the
one that prints what each packet <em>is</em> — three times over, in C, Python and Go.</p>

<h2 id="how-the-pieces-fit">How the pieces fit</h2>

<figure class=""><img src="/assets/images/libpcapng-architecture.svg" alt="libpcapng architecture: sources feed a block layer, then the dissector, the field tree and the display filter, with the C, Rust, Python and Go bindings underneath" /><figcaption>
      The library, end to end. Everything in this post uses the left-hand column as its source and stops at the display filter; the rest is there when you need it.

    </figcaption></figure>

<p>The shape worth remembering is the middle column. Bytes arrive from a file, an
interface, or a buffer you already hold; the block layer turns them into
packets with their link type and timestamp resolved; the dissector builds a
field tree; and a compiled display filter walks that tree. The bindings are
thin — each one carries the same C sources, which is why <code class="language-plaintext highlighter-rouge">pip install</code>,
<code class="language-plaintext highlighter-rouge">go get</code> and <code class="language-plaintext highlighter-rouge">cargo add</code> need nothing on the machine first.</p>

<h2 id="what-we-are-building">What we are building</h2>

<p>Fifty-odd lines that turn a capture file into Wireshark’s summary columns:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   1  HTTP   203.0.113.9     -&gt; 10.0.0.5         GET /index.html HTTP/1.1
   2  DNS    203.0.113.9     -&gt; 8.8.8.8          Query 0x1234 A example.com
   3  TLS    203.0.113.9     -&gt; 10.0.0.5         Handshake Client Hello

3 packet(s)
</code></pre></div></div>

<p>…and take an optional display filter as a second argument, so that</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./sniff capture.pcapng <span class="s1">'http.headers.user_agent contains "curl"'</span>
</code></pre></div></div>

<p>prints only the first line. No protocol parsing of our own, in any of the
three languages.</p>

<p>The shape is the same everywhere, and it is three calls:</p>

<ol>
  <li><strong>Load the decoders</strong> — <code class="language-plaintext highlighter-rouge">ensure_protocols()</code>.</li>
  <li><strong>Read packets</strong> — one callback per frame, with its link type and timestamp
already worked out.</li>
  <li><strong>Dissect, then match</strong> — dissect once, test as many compiled filters
against the result as you like.</li>
</ol>

<h2 id="getting-libpcapng">Getting libpcapng</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/stricaud/libpcapng
<span class="nb">cd </span>libpcapng
<span class="nb">mkdir </span>build <span class="o">&amp;&amp;</span> <span class="nb">cd </span>build
cmake .. <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>Release
make <span class="nt">-j</span>
<span class="nb">sudo </span>make <span class="nb">install</span>
</code></pre></div></div>

<p>The bindings come from the same tree: <code class="language-plaintext highlighter-rouge">pip install pycapng</code> for Python,
<code class="language-plaintext highlighter-rouge">go get github.com/stricaud/libpcapng/bindings/go</code> for Go, and
<a href="https://crates.io/crates/libpcapng"><code class="language-plaintext highlighter-rouge">libpcapng</code></a> on crates.io for Rust.</p>

<p>Everything below needs <strong>0.19.7 or later</strong></p>

<h3 id="a-capture-to-test-against">A capture to test against</h3>

<p>Any capture file will do — <code class="language-plaintext highlighter-rouge">tcpdump -w capture.pcap</code>, or whatever Wireshark
last saved. If you want the exact three packets the output above shows, write
them with pycapng:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="n">pycapng</span>

<span class="n">CLIENT</span><span class="p">,</span> <span class="n">SERVER</span> <span class="o">=</span> <span class="sh">"</span><span class="s">02:00:00:00:00:01</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">02:00:00:00:00:02</span><span class="sh">"</span>

<span class="n">p</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nc">PcapNG</span><span class="p">()</span>
<span class="n">p</span><span class="p">.</span><span class="nc">OpenFileLinkType</span><span class="p">(</span><span class="sh">"</span><span class="s">sample.pcapng</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">,</span> <span class="n">pycapng</span><span class="p">.</span><span class="n">LINKTYPE_ETHERNET</span><span class="p">)</span>

<span class="n">p</span><span class="p">.</span><span class="nc">WritePacket</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="nc">BuildTcpPacket</span><span class="p">(</span>
    <span class="n">src_mac</span><span class="o">=</span><span class="n">CLIENT</span><span class="p">,</span> <span class="n">dst_mac</span><span class="o">=</span><span class="n">SERVER</span><span class="p">,</span>
    <span class="n">src_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">203.0.113.9</span><span class="sh">"</span><span class="p">,</span> <span class="n">dst_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">10.0.0.5</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">src_port</span><span class="o">=</span><span class="mi">54321</span><span class="p">,</span> <span class="n">dst_port</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span>
    <span class="n">data</span><span class="o">=</span><span class="sa">b</span><span class="sh">"</span><span class="s">GET /index.html HTTP/1.1</span><span class="se">\r\n</span><span class="sh">"</span>
         <span class="sa">b</span><span class="sh">"</span><span class="s">Host: www.example.com</span><span class="se">\r\n</span><span class="sh">"</span>
         <span class="sa">b</span><span class="sh">"</span><span class="s">User-Agent: curl/8.4.0</span><span class="se">\r\n\r\n</span><span class="sh">"</span><span class="p">))</span>

<span class="n">p</span><span class="p">.</span><span class="nc">WritePacket</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="nc">BuildDnsQuery</span><span class="p">(</span>
    <span class="n">src_mac</span><span class="o">=</span><span class="n">CLIENT</span><span class="p">,</span> <span class="n">dst_mac</span><span class="o">=</span><span class="n">SERVER</span><span class="p">,</span>
    <span class="n">src_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">203.0.113.9</span><span class="sh">"</span><span class="p">,</span> <span class="n">dst_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">8.8.8.8</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">src_port</span><span class="o">=</span><span class="mi">51000</span><span class="p">,</span> <span class="n">dst_port</span><span class="o">=</span><span class="mi">53</span><span class="p">,</span>
    <span class="n">transaction_id</span><span class="o">=</span><span class="mh">0x1234</span><span class="p">,</span> <span class="n">domain</span><span class="o">=</span><span class="sh">"</span><span class="s">example.com</span><span class="sh">"</span><span class="p">))</span>

<span class="n">p</span><span class="p">.</span><span class="nc">WritePacket</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="nc">BuildTlsClientHello</span><span class="p">(</span>
    <span class="n">src_mac</span><span class="o">=</span><span class="n">CLIENT</span><span class="p">,</span> <span class="n">dst_mac</span><span class="o">=</span><span class="n">SERVER</span><span class="p">,</span>
    <span class="n">src_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">203.0.113.9</span><span class="sh">"</span><span class="p">,</span> <span class="n">dst_ip</span><span class="o">=</span><span class="sh">"</span><span class="s">10.0.0.5</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">src_port</span><span class="o">=</span><span class="mi">54322</span><span class="p">,</span> <span class="n">dst_port</span><span class="o">=</span><span class="mi">443</span><span class="p">))</span>

<span class="n">p</span><span class="p">.</span><span class="nc">CloseFile</span><span class="p">()</span>
</code></pre></div></div>

<p>Note <code class="language-plaintext highlighter-rouge">OpenFileLinkType</code> rather than <code class="language-plaintext highlighter-rouge">OpenFile</code>: every <code class="language-plaintext highlighter-rouge">Build*</code> helper returns a
complete Ethernet frame, and the file has to say so.</p>

<p>Ethernet is the default, so <code class="language-plaintext highlighter-rouge">OpenFile</code> is fine there; being explicit still is 
not wasted, because the day you write something that is <em>not</em> Ethernet, 
the file has to say so. A capture whose interface disagrees with its packets 
is the single easiest way to waste an afternoon with this library.</p>

<h2 id="c">C</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* sniff.c — read a capture, say what each packet is. */</span>
<span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libpcapng/reader.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libpcapng/dissect.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;libpcapng/dfilter.h&gt;</span><span class="cp">
</span>
<span class="k">static</span> <span class="n">pcapng_dfilter_t</span> <span class="o">*</span><span class="n">filter</span><span class="p">;</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">on_packet</span><span class="p">(</span><span class="k">const</span> <span class="n">pcapng_packet_t</span> <span class="o">*</span><span class="n">pkt</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">user</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">pcapng_dissection_t</span> <span class="o">*</span><span class="n">d</span><span class="p">;</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">user</span><span class="p">;</span>

    <span class="n">d</span> <span class="o">=</span> <span class="n">pcapng_dissect</span><span class="p">(</span><span class="n">pkt</span><span class="o">-&gt;</span><span class="n">data</span><span class="p">,</span> <span class="n">pkt</span><span class="o">-&gt;</span><span class="n">captured_len</span><span class="p">,</span> <span class="n">pkt</span><span class="o">-&gt;</span><span class="n">captured_len</span><span class="p">,</span>
                       <span class="n">pkt</span><span class="o">-&gt;</span><span class="n">linktype</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">d</span><span class="p">)</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">filter</span> <span class="o">||</span> <span class="n">pcapng_dfilter_match</span><span class="p">(</span><span class="n">filter</span><span class="p">,</span> <span class="n">d</span><span class="o">-&gt;</span><span class="n">root</span><span class="p">))</span>
        <span class="n">printf</span><span class="p">(</span><span class="s">"%4llu  %-6s %-15s -&gt; %-15s  %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
               <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span><span class="n">pkt</span><span class="o">-&gt;</span><span class="n">index</span><span class="p">,</span> <span class="n">d</span><span class="o">-&gt;</span><span class="n">proto</span><span class="p">,</span> <span class="n">d</span><span class="o">-&gt;</span><span class="n">src</span><span class="p">,</span> <span class="n">d</span><span class="o">-&gt;</span><span class="n">dst</span><span class="p">,</span>
               <span class="n">d</span><span class="o">-&gt;</span><span class="n">info</span><span class="p">);</span>

    <span class="n">pcapng_dissection_free</span><span class="p">(</span><span class="n">d</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>   <span class="cm">/* non-zero stops the read */</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">char</span> <span class="n">err</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>
    <span class="kt">long</span> <span class="n">n</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"usage: %s CAPTURE.pcapng ['display filter']</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
                <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
        <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">pcapng_dissect_ensure_protocols</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">filter</span> <span class="o">=</span> <span class="n">pcapng_dfilter_compile</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">err</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">err</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">filter</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"bad filter: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">err</span><span class="p">);</span>
            <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">n</span> <span class="o">=</span> <span class="n">pcapng_read_packets</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">on_packet</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"cannot read %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"</span><span class="se">\n</span><span class="s">%ld packet(s)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">filter</span><span class="p">)</span>
        <span class="n">pcapng_dfilter_free</span><span class="p">(</span><span class="n">filter</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Build and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cc <span class="nt">-o</span> sniff sniff.c <span class="si">$(</span>pkg-config libpcapng <span class="nt">--cflags</span> <span class="nt">--libs</span><span class="si">)</span>
./sniff sample.pcapng
./sniff sample.pcapng <span class="s1">'tls.handshake_type == 1'</span>
</code></pre></div></div>

<p>If you would rather not install the library system-wide, point the compiler at
the tree you just built:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cc <span class="nt">-o</span> sniff sniff.c <span class="nt">-I</span> path/to/libpcapng/lib/include <span class="se">\</span>
       <span class="nt">-L</span> path/to/libpcapng/build/lib <span class="nt">-lpcapng</span>
</code></pre></div></div>

<p>Worth noticing what is <em>not</em> in that file. No Ethernet header struct, no
<code class="language-plaintext highlighter-rouge">ntohs</code>, no IP header length arithmetic, no TCP stream reassembly, no HTTP
request-line parser. <code class="language-plaintext highlighter-rouge">pcapng_read_packets</code> hands over one frame per call with
its link type already resolved from the interface it belongs to and its
timestamp already scaled by that interface’s resolution — the two things that
are easy to get subtly wrong when you walk pcapng blocks yourself, and that
fail silently when you do, because dissecting from four bytes off produces an
answer rather than an error.</p>

<p><code class="language-plaintext highlighter-rouge">pcapng_dissect</code> returns the summary columns and a field tree;
<code class="language-plaintext highlighter-rouge">pcapng_dfilter_match</code> walks that tree. The split matters: compiling a filter
happens once at startup, dissecting a packet costs microseconds, and matching a
compiled filter against the result costs nanoseconds. Thirty filters should
mean one dissection and thirty tree walks.</p>

<h2 id="python">Python</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="sh">"""</span><span class="s">sniff.py — read a capture, say what each packet is.</span><span class="sh">"""</span>
<span class="kn">import</span> <span class="n">sys</span>

<span class="kn">import</span> <span class="n">pycapng</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">:</span>
        <span class="n">sys</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">usage: </span><span class="si">{</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s"> CAPTURE.pcapng [</span><span class="sh">'</span><span class="s">display filter</span><span class="sh">'</span><span class="s">]</span><span class="sh">"</span><span class="p">)</span>

    <span class="n">pycapng</span><span class="p">.</span><span class="nf">ensure_protocols</span><span class="p">()</span>
    <span class="n">want</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nc">Filter</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2</span> <span class="k">else</span> <span class="sh">""</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">each</span><span class="p">(</span><span class="n">pkt</span><span class="p">):</span>
        <span class="n">d</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nf">dissect</span><span class="p">(</span><span class="n">pkt</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="n">pkt</span><span class="p">.</span><span class="n">linktype</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">d</span> <span class="ow">and</span> <span class="n">want</span><span class="p">.</span><span class="nf">matches</span><span class="p">(</span><span class="n">d</span><span class="p">):</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">pkt</span><span class="p">.</span><span class="n">index</span><span class="si">:</span><span class="mi">4</span><span class="si">}</span><span class="s">  </span><span class="si">{</span><span class="n">d</span><span class="p">.</span><span class="n">proto</span><span class="si">:</span><span class="mi">6</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">d</span><span class="p">.</span><span class="n">src</span><span class="si">:</span><span class="mi">15</span><span class="si">}</span><span class="s"> -&gt; </span><span class="si">{</span><span class="n">d</span><span class="p">.</span><span class="n">dst</span><span class="si">:</span><span class="mi">15</span><span class="si">}</span><span class="s">  </span><span class="si">{</span><span class="n">d</span><span class="p">.</span><span class="n">info</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">True</span>          <span class="c1"># False stops the read
</span>
    <span class="n">n</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nf">foreach_packet</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">each</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s"> packet(s)</span><span class="sh">"</span><span class="p">,</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="p">.</span><span class="n">stderr</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 sniff.py sample.pcapng
python3 sniff.py sample.pcapng <span class="s1">'http.headers.user_agent contains "curl"'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Filter("")</code> matches everything, which is why there is no <code class="language-plaintext highlighter-rouge">if want</code> in the
loop. A filter that will not compile raises <code class="language-plaintext highlighter-rouge">ValueError</code> right where it is
written:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ValueError: cannot compile display filter: expected a value after operator
</code></pre></div></div>

<p>That is deliberate. A typo in a filter should fail at startup, not quietly
match nothing on every packet for the rest of the run.</p>

<p>If you want the whole capture in memory rather than a callback,
<code class="language-plaintext highlighter-rouge">pycapng.read_packets(path)</code> returns a list of the same objects, and
<code class="language-plaintext highlighter-rouge">read_packets_mem(buf)</code> does it for bytes you already hold.</p>

<h2 id="go">Go</h2>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"os"</span>

	<span class="n">pcapng</span> <span class="s">"github.com/stricaud/libpcapng/bindings/go"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">)</span> <span class="o">&lt;</span> <span class="m">2</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Stderr</span><span class="p">,</span> <span class="s">"usage: %s CAPTURE.pcapng ['display filter']</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">0</span><span class="p">])</span>
		<span class="n">os</span><span class="o">.</span><span class="n">Exit</span><span class="p">(</span><span class="m">2</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="n">pcapng</span><span class="o">.</span><span class="n">EnsureProtocols</span><span class="p">()</span>

	<span class="k">var</span> <span class="n">want</span> <span class="o">*</span><span class="n">pcapng</span><span class="o">.</span><span class="n">Filter</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">2</span> <span class="p">{</span>
		<span class="n">f</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">pcapng</span><span class="o">.</span><span class="n">NewFilter</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="p">])</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintln</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Stderr</span><span class="p">,</span> <span class="s">"bad filter:"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
			<span class="n">os</span><span class="o">.</span><span class="n">Exit</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
		<span class="p">}</span>
		<span class="k">defer</span> <span class="n">f</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
		<span class="n">want</span> <span class="o">=</span> <span class="n">f</span>
	<span class="p">}</span>

	<span class="n">n</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">pcapng</span><span class="o">.</span><span class="n">ReadPackets</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">1</span><span class="p">],</span> <span class="k">func</span><span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">pcapng</span><span class="o">.</span><span class="n">Packet</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
		<span class="n">d</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">pcapng</span><span class="o">.</span><span class="n">Dissect</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">Data</span><span class="p">,</span> <span class="n">p</span><span class="o">.</span><span class="n">Linktype</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="o">||</span> <span class="n">d</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">return</span> <span class="no">true</span>
		<span class="p">}</span>
		<span class="k">if</span> <span class="n">want</span><span class="o">.</span><span class="n">Match</span><span class="p">(</span><span class="n">d</span><span class="p">)</span> <span class="p">{</span>
			<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"%4d  %-6s %-15s -&gt; %-15s  %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
				<span class="n">p</span><span class="o">.</span><span class="n">Index</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Protocol</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Src</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Dst</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">Info</span><span class="p">)</span>
		<span class="p">}</span>
		<span class="k">return</span> <span class="no">true</span> <span class="c">// false stops the read</span>
	<span class="p">})</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Stderr</span><span class="p">,</span> <span class="s">"</span><span class="se">\n</span><span class="s">%d packet(s)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go mod init sniffdemo
go get github.com/stricaud/libpcapng/bindings/go
go build <span class="nt">-o</span> sniff <span class="nb">.</span>
./sniff sample.pcapng <span class="s1">'udp'</span>
</code></pre></div></div>

<p>The binding vendors the C sources, so <code class="language-plaintext highlighter-rouge">go build</code> compiles the library as part
of your program — CGo and a C compiler are the only requirements, and there is
nothing to install first. A nil <code class="language-plaintext highlighter-rouge">*Filter</code> matches everything, which is the Go
equivalent of the empty filter above.</p>

<p>To build against a checkout rather than whatever the module proxy has — which
is also how you test a fix before it is tagged — point the module at it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go mod edit <span class="nt">-replace</span> github.com/stricaud/libpcapng/bindings/go<span class="o">=</span>../libpcapng/bindings/go
</code></pre></div></div>

<h2 id="the-two-things-that-will-bite-you">The two things that will bite you</h2>

<h3 id="loading-the-decoders-is-not-optional">Loading the decoders is not optional</h3>

<p>Most of libpcapng’s protocol knowledge is not C code. It is
<a href="https://github.com/stricaud/libpcapng/blob/main/doc/posa.md">POSA</a>
definitions — a small declarative language for protocol layouts — compiled
into the library and registered on demand. Until <code class="language-plaintext highlighter-rouge">ensure_protocols()</code> has run,
you get only the built-in C dissectors, and that failure is quiet in the worst
way:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>without <code class="language-plaintext highlighter-rouge">ensure_protocols()</code></th>
      <th>with</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d.proto</code> for an HTTP request</td>
      <td><code class="language-plaintext highlighter-rouge">TCP</code></td>
      <td><code class="language-plaintext highlighter-rouge">HTTP</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">http.uri</code> in the field tree</td>
      <td>absent</td>
      <td><code class="language-plaintext highlighter-rouge">/index.html</code></td>
    </tr>
    <tr>
      <td>filter <code class="language-plaintext highlighter-rouge">http.uri contains "/index"</code></td>
      <td>matches nothing</td>
      <td>matches</td>
    </tr>
  </tbody>
</table>

<p>No error, no warning — just a sniffer that sees less than it should, and a
filter you will spend twenty minutes re-reading because it looks correct. It
is idempotent and cheap, so call it at startup, before you compile a filter.</p>

<h3 id="field-names-are-mostly-wiresharks-not-always">Field names are mostly Wireshark’s, not always</h3>

<p>The names come from the POSA definitions, and they follow Wireshark where it is
natural, but not slavishly. TLS is the one that catches me: Wireshark says
<code class="language-plaintext highlighter-rouge">tls.handshake.type</code>, libpcapng says <code class="language-plaintext highlighter-rouge">tls.handshake_type</code>. A filter naming a
field that does not exist compiles fine and matches nothing — field names come
into existence during dissection, so there is no registry to check a name
against at compile time.</p>

<p>Rather than guess, ask a packet what it has. <code class="language-plaintext highlighter-rouge">flat()</code> collapses the whole
field tree to <code class="language-plaintext highlighter-rouge">{name: value}</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pycapng</span>

<span class="n">pycapng</span><span class="p">.</span><span class="nf">ensure_protocols</span><span class="p">()</span>
<span class="n">pkt</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nf">read_packets</span><span class="p">(</span><span class="sh">"</span><span class="s">sample.pcapng</span><span class="sh">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">pycapng</span><span class="p">.</span><span class="nf">dissect</span><span class="p">(</span><span class="n">pkt</span><span class="p">.</span><span class="n">data</span><span class="p">,</span> <span class="n">pkt</span><span class="p">.</span><span class="n">linktype</span><span class="p">)</span>

<span class="nf">print</span><span class="p">([</span><span class="n">f</span><span class="p">[</span><span class="sh">"</span><span class="s">abbrev</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">d</span><span class="p">.</span><span class="n">fields</span><span class="p">])</span>
<span class="c1"># ['frame', 'eth', 'ip', 'tcp', 'http']
</span>
<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">d</span><span class="p">.</span><span class="nf">flat</span><span class="p">().</span><span class="nf">items</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">name</span><span class="si">:</span><span class="mi">28</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">value</span><span class="si">!r}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>frame                        ''
frame.len                    129
frame.cap_len                129
eth                          ''
eth.dst                      '02:00:00:00:00:02'
eth.src                      '02:00:00:00:00:01'
eth.type                     2048
ip                           ''
ip.version                   4
ip.hdr_len                   20
...
ip.src                       '203.0.113.9'
ip.dst                       '10.0.0.5'
tcp                          ''
tcp.srcport                  54321
tcp.dstport                  80
...
tcp.flags.push               1
tcp.flags.ack                1
...
communityid                  '1:rdljTHDUmQwTTPaAFRaOWNSJraA='
http                         ''
http.method                  'GET'
http.uri                     '/index.html'
http.version                 'HTTP/1.1'
http.headers                 ''
http.headers.host            'www.example.com'
http.headers.user_agent      'curl/8.4.0'
http.body                    ''
</code></pre></div></div>

<p>Two freebies in there worth pointing at: <code class="language-plaintext highlighter-rouge">tcp.flags.push</code> and friends are
broken out as individual fields, so <code class="language-plaintext highlighter-rouge">tcp.flags.syn == 1 and tcp.flags.ack == 0</code>
works as a filter; and <code class="language-plaintext highlighter-rouge">communityid</code> is the
<a href="https://github.com/corelight/community-id-spec">Community ID</a> flow hash,
computed for every packet, which is what you join on when you want to line
these packets up against Zeek or Suricata logs.</p>

<p>That is also the fastest way to learn what a protocol decoder actually
exposes. <code class="language-plaintext highlighter-rouge">d.fields</code> gives you the layers with their children if you want the
structure; <code class="language-plaintext highlighter-rouge">flat()</code> is what you want when you are hunting for one value and do
not care where it sits. Note the types: <code class="language-plaintext highlighter-rouge">tcp.dstport</code> comes back as an <code class="language-plaintext highlighter-rouge">int</code>,
<code class="language-plaintext highlighter-rouge">http.uri</code> as a <code class="language-plaintext highlighter-rouge">str</code>.</p>

<h2 id="from-a-file-to-an-interface">From a file to an interface</h2>

<p>Everything above reads a file, because that is the version you can run on a
laptop without root. Live capture is the same dissect-then-match loop behind a
different source — in Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pycapng</span>

<span class="k">with</span> <span class="n">pycapng</span><span class="p">.</span><span class="nc">Capture</span><span class="p">(</span><span class="sh">"</span><span class="s">eth0</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">cap</span><span class="p">:</span>
    <span class="n">cap</span><span class="p">.</span><span class="nf">set_filter</span><span class="p">(</span><span class="sh">"</span><span class="s">tcp.dstport == 443</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">cap</span><span class="p">.</span><span class="nf">loop</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">pkt</span><span class="p">:</span> <span class="nf">print</span><span class="p">(</span><span class="n">pkt</span><span class="p">.</span><span class="n">captured_len</span><span class="p">,</span> <span class="sh">"</span><span class="s">bytes</span><span class="sh">"</span><span class="p">))</span>
</code></pre></div></div>

<p>The filter attached to a live capture is the <em>same</em> display-filter syntax, not
BPF. Which brings me back to mqs: the query sniffer I would write today would
not parse MySQL framing at all. It would be this loop, with
<code class="language-plaintext highlighter-rouge">mysql.query contains "INSERT"</code> as the filter, and the Lua script would get
fields instead of bytes.</p>

<p>The three programs in this post are in the repository under
<code class="language-plaintext highlighter-rouge">bindings/*/README</code> and <code class="language-plaintext highlighter-rouge">doc/</code>; if you spot something that does not work the
way it is written here, please open an issue.</p>]]></content><author><name>Sebastien Tricaud</name></author><category term="network" /><category term="libpcapng" /><category term="pcap" /><category term="sniffer" /><category term="c" /><category term="python" /><category term="go" /><summary type="html"><![CDATA[A sniffer that prints what each packet *is*, not how many bytes it has — in C, Python and Go, with Wireshark display filters doing the selecting.]]></summary></entry></feed>