
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Last updated: October 2026
Quick answer. The capture is the easy half.
tcpdump -i eth0 -w out.pcaphas worked for thirty years and still does. The hard half is that it hands you a binary file an agent cannot reason about without a parser, and writing that parser is where the time goes. The alternative is to capture and decode in one step, so what reaches the agent is already structured: protocol, ports, flags, and decoded fields like a TLS SNI.yeet run gh:yeet-src/pktscopedoes that with a TCX hook on ingress and egress. The tradeoffs worth knowing before you choose: no stream reassembly, no pcap export, and HTTPS payloads stay ciphertext either way.
I build eBPF tooling at yeet, and packet capture is where I most often watch an agent do a lot of work to arrive somewhere a structured feed would have put it immediately. The capture succeeds, the pcap lands, and then the agent writes four hundred lines of parsing to answer a question the decoder already knew.
With tcpdump, which is present on essentially every Linux host and needs no explanation to anyone who will later read your runbook. Three forms cover almost everything, and the differences between them matter more than their similarity suggests:
sudo tcpdump -i eth0 -nn -c 100 # 100 packets, numeric, to the terminal
sudo tcpdump -i eth0 -w capture.pcap # to a file for later analysis
sudo tcpdump -i eth0 'tcp port 443' -nn # filtered with BPF syntax
-nn skips name resolution for both hosts and ports, which matters more than it looks: without it, tcpdump makes DNS queries of its own and those appear in your capture. The filter expression is compiled to a classic BPF program by pcap-filter(7) and runs in the kernel, so unmatched traffic is discarded before it is copied to userspace.
Two things decide whether this is enough. If you want to read the capture yourself in Wireshark, a pcap is exactly right and nothing here improves on it. If something downstream has to interpret the bytes, you have chosen a format that needs a parser, and that choice is the subject of the rest of this post.
Because a pcap is a container for raw frames rather than a description of what happened, so every question about it is a parsing problem before it becomes an analysis problem. That ordering is invisible to a human with Wireshark and expensive for a program.
Ask an agent which hosts a machine talked to and it has to open the file, walk the per-packet records, decode the link layer, decode IP, decode TCP or UDP, and only then aggregate. Each of those steps is well understood and none is free, and the agent is writing them from scratch into a context window rather than calling a decoder that already exists.
The failure modes are predictable. An agent that writes its own parser gets the common path right and the edges wrong: VLAN tags, IPv6 extension headers, truncated frames at the snap length. An agent that shells out to tshark gets a correct decode and then has to parse the text output, which is a second parsing problem wearing a friendlier hat. An agent that asks for a summary from tcpdump with -A gets printable payload bytes with no structure at all.
None of this means pcap is wrong. It means the format was designed for a human with Wireshark, and handing it to a program is a conversion step nobody budgeted for.
.pcap file?Decode in the capture path itself, so the thing that leaves the kernel is already a parsed record rather than a frame waiting for one. The decoding has to happen somewhere, and doing it once in the capture is cheaper than doing it repeatedly in whatever consumes the output:
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/pktscope # live packet list, protocol tree, hex
pktscope attaches TCX hooks on ingress and egress and decodes as it goes, so a TLS handshake reads as TLS Handshake ClientHello · sni=example.com rather than as a hex dump you would have to interpret. The SNI is the clearest example of the difference: it is a field inside a TLS record inside TCP inside IP, and getting to it from a pcap means three decoders deep.
Two filters exist and confusing them is the usual reason a capture looks empty. The in-kernel scope (--port, --proto) is checked inside the BPF program before a ring-buffer reservation, so unmatched traffic costs almost nothing and never reaches userspace. The display filter runs in userspace over packets already captured, so it is instant and reversible and can match decoded fields.
For an agent the consequence is that the first useful question comes immediately rather than after a parser. The decode has already happened, and what is left is the analysis you actually wanted.
Four limits apply, and the first is the one people usually discover after an hour spent trying to read HTTPS out of a capture. None of them is a configuration problem you can work around with a flag.
Encrypted payloads stay encrypted. At the TC layer an HTTPS request is ciphertext, so records identify as TLS ApplicationData and no plaintext parser helps. The SNI from the ClientHello is readable because the handshake is cleartext, which is why you get the hostname and not the request. Reading the plaintext means a uprobe on the TLS library instead, which is a different mechanism with different constraints.
No stream reassembly. Each packet is decoded on its own, so a TLS record or an HTTP message split across segments is reported as spanning segments rather than stitched back together. Anything that needs the whole message needs a tool that reassembles.
Frames are truncated. pktscope captures 1536 bytes from the first captured byte, which covers a full untagged Ethernet frame and not a jumbo frame or a large TSO segment.
HTTP/2 header fields are not decoded. Frame types, stream ids and flags are shown; the header block is HPACK-coded and stays that way.
Those bounds are the reason to be precise about which question you are actually asking before choosing a tool. "Which hosts did this machine reach" is answerable from decoded headers and cheap to get. "What did it send them" usually is not, and no amount of capture configuration changes that.
Both, for different questions, and the choice turns less on capability than on what consumes the output. Framed that way the decision is usually obvious, where framed as a comparison of features it is not.
| Route | What it does better | What it does not cover |
|---|---|---|
tcpdump | Everywhere already, no install, pcap you can hand to someone else | Output is raw frames, so anything downstream needs a parser |
tshark | Correct decode of hundreds of protocols, reads existing pcaps | Prints text, so a program parses prose instead of frames |
pktscope on yeet | Decodes in the capture path, fields like SNI arrive named | Needs a kernel with BTF and TCX; no pcap export, no reassembly |
| A capture library in your own code | Exactly the fields you want, in your own format | You now own a protocol decoder and its edge cases |
tcpdump is the right default when a human will read the result, when you need a pcap for Wireshark or to hand to someone else, or when you are on a host where installing anything is out of the question. It is everywhere, it is well understood, and its filter syntax is the lingua franca.
A decoding capture is the right choice when a program consumes the output, when you want the answer rather than the evidence, or when the field you care about is several decoders deep. It is also the better fit when the capture should run continuously, because a ring of recent packets with no disk spill has a bounded cost that a growing pcap does not.
The limitation on the eBPF side is portability: it needs a kernel with BTF and TCX support, where tcpdump runs on anything. What the TC layer sees covers the vantage point in more detail, and writing a protocol tracer covers building a decoder yourself.
Not usefully, and the reason is the same whichever capture method you pick, because both need a privilege an ordinary process does not have. This is one of the few places where the answer is the same for tcpdump and for eBPF.
Packet capture requires CAP_NET_RAW or CAP_NET_ADMIN, and loading BPF programs requires CAP_BPF with CAP_PERFMON on kernels since 5.8, or CAP_SYS_ADMIN before that. An unprivileged agent can run neither, which is usually the correct outcome: an agent that can read every packet on a host can read other people's traffic on it.
The practical arrangement is that a human grants the capability deliberately, scoped to the investigation, rather than an agent holding it permanently. That is the same shape as any other privileged debugging access, and letting an agent inspect a server safely covers the general case.
Capturing packets on Linux is solved and has been for decades. What is not solved is that the standard output format is a binary container designed for a human with a protocol analyser, so handing it to an agent adds a parsing step that is easy to get subtly wrong. Decoding in the capture path removes that step: the SNI, the ports, the flags and the protocol arrive already identified, and the agent spends its context on the question rather than on the format. Keep tcpdump for the pcap you will open yourself.
sudo tcpdump -i eth0 -nn captures on one interface with numeric hosts and ports. Use -i any for all interfaces, and add a filter expression such as 'tcp port 443' to discard unmatched traffic in the kernel before it is copied to userspace.
Yes, and it costs more than people expect. A pcap is a container of raw frames rather than a description of what happened, so the agent has to decode the link layer, IP and the transport before it can answer even a simple question. In practice it writes a parser into its context window that handles the common path correctly and the edges badly: VLAN tags, IPv6 extension headers, frames truncated at the snap length.
tcpdump captures frames and writes pcap, with filtering compiled to classic BPF in the kernel. An eBPF capture can decode as it captures, so what leaves the kernel is a structured record rather than a frame. The first is better when a human reads the output; the second when a program does.
No. At the TC layer the payload is ciphertext, so records identify as TLS ApplicationData. You can read the SNI hostname because the ClientHello is cleartext, but reading request and response bodies requires attaching to the TLS library inside the process instead.
The SNI lives in the server_name extension of the TLS ClientHello, which is sent in cleartext before encryption begins, so no key is involved in reading it. A decoder that parses the handshake surfaces it directly as a field. Getting there from a pcap means walking the link layer, IP, TCP and then the TLS record layer to reach the extension, which is three decoders deep and the clearest illustration of why format matters here.
Effectively yes, and the answer is the same whichever method you pick. Capturing frames needs CAP_NET_RAW or CAP_NET_ADMIN, and loading BPF programs needs CAP_BPF plus CAP_PERFMON on kernels since 5.8, or CAP_SYS_ADMIN before that. None of those is available to an unprivileged process, which is usually the right outcome: anything that can read every packet on a host can read other people's traffic on it.
Most often a filter mismatch: an in-kernel filter discards traffic before it is ever captured, so a wrong port or protocol produces silence rather than an error. Check the filter before concluding there is no traffic, and confirm you are on the interface the traffic actually uses.
Yes, with the capability and in the right network namespace, and the namespace is the part that catches people. A container with its own network namespace has its own interfaces, so a capture on the host's eth0 shows the host's traffic rather than the container's, and comes back looking empty. Enter the container's namespace, or capture on the veth pair or bridge its traffic actually crosses.
Not meaningfully when the filter is narrow, because in-kernel filtering discards unmatched packets before they are copied to userspace, so the cost tracks matched traffic rather than total traffic. An unfiltered capture on a busy interface is a different matter: every frame is copied out, and on a saturated link that cost is real enough to notice. Filter in the kernel rather than in your eyes.
Only when the whole message fits inside a single captured frame, which for a small GET it often does and for anything with a body it often does not. Without TCP stream reassembly, a request split across segments is reported as spanning segments rather than stitched back together, so the agent sees fragments it would have to reassemble itself. That is the limit most of the other constraints here follow from.
tcpdump manual. Source for the capture flags and filter syntax described here, including what -nn suppresses.pcap-filter(7). The filter expression language, compiled to classic BPF and evaluated in the kernel.pktscope. The decoding capture used here, with its documented limits on reassembly, truncation and HPACK, and the TCX ingress and egress hooks it attaches.server_name extension, the source for SNI travelling in the cleartext ClientHello.