Capture Packets on wg0 and Other Tunnel Interfaces on Linux

Jacob Pradels
Jacob Pradels··37 min read

Founding engineer at yeet, working on kernel-side observability and the tooling around it. I write about eBPF, Linux internals, and why your telemetry bill looks the way it does.

To capture packets on wg0 and other tunnel interfaces on Linux, stop expecting a MAC header. A WireGuard, GRE, IPIP or tun device carries IP packets, not Ethernet frames, so the first byte of your capture is the start of the IP header. tcpdump says so in its opening line: listening on wg1, link-type RAW (Raw IP), which libpcap defines as LINKTYPE_RAW, value 101, "IPv4 or IPv6 packets with no link-layer header". Ethernet-framed devices, including loopback, veth pairs, bridges and tap, do have one, which is why ARP decodes on those and never appears on a tunnel. The offset is a property of the device you picked, not of the tool you ran, and any tool that hardcodes one is wrong on half your interfaces. pktscope picks per interface: its TCX eBPF tap snaps from the MAC header on Ethernet-framed devices and from the network header on tunl*, ipip, gre, tun and wg.

Two things sent me down this path, and neither was a network problem. Writing eBPF taps means deciding, in C, which byte of a packet counts as byte zero, and that decision has no default. libpcap hands you a link-layer type and the dissector figures it out; a tc program hands you a buffer and a question. The second was watching people read a capture on a tunnel device and conclude the tool was broken, because a decode that starts on an IP header looks exactly like a decode that lost its first fourteen bytes.

Routing, MTU planning and mesh certificate plumbing are somebody else's day job, and the people who run tunnels at scale know things about PMTUD blackholes that I do not. What I have spent time on is the byte offset, on both sides of it: the kernel side that decides where a frame begins, and the terminal side that has to render the result without lying about which byte is which. That offset is the whole subject here, because it is the one thing every capture tool assumes silently and every tunnel device contradicts.

The alternatives: tcpdump, tshark, termshark, Wireshark over ssh, and pktscope

Packet analysis on a Linux box splits on two questions, and picking the wrong tool almost always means answering the second one when your problem was the first. Where do you read the packets, and what does the host have to have installed before you can. A GUI on your laptop is fine for a capture someone already took; it is useless at 02:00 on a host that has tcpdump and nothing else. Everything below is a real answer to one of those questions, and the differences are in the last three columns.

ToolRuns where the traffic isNeeds on the hostLive detail tree and hex paneRaw-IP tunnel devicesWrites packets to disk
pktscopeYes, terminal on the hostThe yeet daemon; a pinned clang/bpftool/esbuild toolchain to buildYes, three panesSnaps from the network header per interfaceNo
tcpdumpYes, terminal on the hostlibpcap and the tcpdump binaryNo, one line per packetYes, reports link-type RAWOnly with -w
tsharkYes, terminal on the hosttshark, dumpcap, libpcapNo, printed dissection onlyYes, via libpcap's link typeOnly with -w
termsharkYes, terminal on the hosttshark newer than v1.10.2Yes, three panesYes, inherits tshark's handlingYes, caches to ~/.cache/termshark/pcaps/
Wireshark with sshdumpNo, remote binary feeds a local GUIA capture executable with capture capabilitiesYes, in the local GUIYes, via the remote tool's link typeOptional
Wireshark over an ssh pipeNo, dumpcap streams to a local GUIdumpcap on the remote hostYes, in the local GUIYes, via dumpcap's link typeOptional
tcpdump -w then scpCapture yes, analysis notcpdump, plus disk for the fileYes, after the file landsYes, link type is stored in the fileYes, that is the method

pktscope: a TCX eBPF tap that picks the snap offset per interface

pktscope is an eBPF packet analyzer for the terminal: it attaches a TCX tap to one interface and renders every frame as a Wireshark-shaped three-pane view in your shell. The tap hooks TCX ingress and egress, streams full frames up to 1536 bytes into a ring buffer, and applies the --port and --proto filters in the kernel so uninteresting traffic never enters the ring at all. TCX itself is the qdisc-less attach point for tc programs merged in Linux 6.6, with bpf_link ownership semantics, and the tap always returns TCX_NEXT, which continues the program chain rather than accepting or dropping the packet.

The part that matters for a tunnel is where the snap starts. On Ethernet-framed devices, veth pairs and bridges and tap included, the capture begins at the MAC header, so link-level frames like ARP arrive and decode in full. On raw-IP tunnel devices matching tunl*, ipip, gre, tun and wg, it begins at the network header, because there is no MAC header on the wire to begin at. Three panes render the result: a packet list colored by what the payload was identified as, a folding detail tree where every field carries the byte range it decodes, and a hex pane where the selected field is highlighted byte for byte. Loopback is the one gap, and it sits on the attach side rather than the framing side: lo is an Ethernet-framed device, but a TCX program cannot attach to it, so pktscope cannot watch loopback traffic at all and tcpdump -i lo remains the answer for same-host traffic. Nothing is written to disk, and the capture lasts exactly as long as the isolate running the script: it ends when that exits, not when your terminal does, so yeet run -d detaches one and yeet attach reconnects to it later.

tcpdump: the baseline, and it tells you the link type in its first line

tcpdump remains the instrument, and on this specific problem it is more honest than its reputation suggests: the banner it prints before the first packet names the link type it negotiated for the interface. That is the answer to "why does this look wrong" sitting above the output the whole time. -L lists the link types an interface supports and exits, -y forces one, and -s sets the snapshot length, documented as "snarf snaplen bytes of data from each packet rather than the default of 262144 bytes". The special any interface "captures packets from all regular network interfaces of the OS", at the cost of a different header again.

What tcpdump does not give you is structure. One line per packet is the correct output for grep and the wrong output for "which field is at offset 34 and why does this TLS record look truncated". The standard move is -w to a file and analysis somewhere else, and libpcap's own manual is blunt about the assumption people carry into that file: "Do NOT assume that the packets for a given capture or 'savefile' will have any given link-layer header type, such as DLT_EN10MB." That sentence is the entire argument of this post, written by the library, years before anybody ran a WireGuard tunnel.

tshark: Wireshark's dissectors without the GUI, still on libpcap

tshark is the command-line member of the Wireshark family, described in its man page as a network protocol analyzer that captures from a live network or reads a saved capture file and prints the decode to standard output. It brings Wireshark's dissector library with it, which is the largest and best-maintained protocol decoder anyone has, and it captures through libpcap with dumpcap doing the privileged work. If the question is "what does Wireshark think this packet is", tshark answers it on the host with no file transfer.

The cost is register rather than capability. A printed dissection of a packet is a wall of text you scroll rather than a tree you fold, and comparing three packets means three walls. There is also a real distinction the man page draws that trips people up mid-incident: display filters have the richer syntax, capture filters are "much more efficient", and "display filters are currently not supported when capturing and saving" at the same time. Knowing which of the two you are typing is half of using tshark under pressure.

termshark: a terminal UI for tshark, panes and all

termshark is the closest comparison in this table and it deserves the credit: it describes itself as "a terminal UI for tshark, inspired by Wireshark", it reads pcap files or sniffs live interfaces, it uses Wireshark's display filters, and its user guide documents exactly the three-pane shape you would want, a packet list on top, "the structure of the packet" in the middle, and "the bytes that the packet comprises" at the bottom. Nobody needs to invent that layout again, and pktscope did not.

The differences are underneath the panes. termshark drives tshark, so tshark must be installed and "newer than v1.10.2 (from approximately 2013)", and capture commands "might require sudo or root access". It also keeps the packets: by default it saves them to ~/.cache/termshark/pcaps/ on Linux, which is a feature when you want to reopen a capture and a policy problem when the host is not supposed to accumulate traffic. And the filter you type changes meaning depending on the source, since "when reading a file, the filter will be interpreted as a display filter. When reading from an interface, the filter is interpreted as a capture filter."

Wireshark with sshdump: a remote capture binary feeding a local GUI

Wireshark ships this route itself, which makes it the supported answer rather than a trick. sshdump is "an extcap tool that allows one to run a remote capture tool over a SSH connection": you point Wireshark at --remote-host, name the capture program with --remote-capture-command, and packets stream back into the local GUI live. Privilege escalation on the far side is a documented option, --remote-priv taking none, sudo or doas, which is a thoughtful detail that the hand-rolled versions of this always miss.

The requirement is the one that decides it: the remote host still needs a capture executable, and "the capture executable must have the capabilities to capture from the wanted interface". Wireshark also has to be installed locally, because sshdump is a Wireshark extension rather than a standalone program. On a laptop with Wireshark and a jump host with tcpdump, this is the best route in the table. Over a VPN, on a hardened bastion, with X forwarding unavailable, it is not a route at all.

Wireshark over an ssh pipe: dumpcap streaming into a local GUI

The hand-rolled version predates sshdump and still works. Wireshark's own wiki page on pipes gives it as wireshark -k -i <(ssh -l root remote-host "dumpcap -P -w - -f 'not tcp port 22'"), with -k starting the capture immediately and -i - reading from an anonymous pipe. The named-pipe variant does the same thing with mkfifo /tmp/sharkfin when process substitution is not available or the password prompt gets in the way.

Two caveats come straight from that page and both are worth internalizing before you paste it. "The main problem is that you have to exclude the traffic generated by the ssh session from capturing", which is why the filter in the example excludes port 22, and forgetting it means capturing your own capture. The second is that "ssh cannot ask for a password on stdin", so this wants ssh-agent or an askpass helper. It is still the fastest way to get Wireshark's full dissector set onto a remote interface when you have a GUI to point at it.

tcpdump -w plus scp: the round trip everyone actually does

The default workflow in practice is three steps and a wait: capture to a file with -w, copy it back with scp, open it locally. Comparitech's walkthrough is a fair statement of the method, capture separately, transfer with scp or pscp, analyze in Wireshark. It has one genuine advantage nothing else in this table has: the file is evidence. You can attach it to a ticket, hand it to a vendor, and open it again next week when the argument about whose side it was on gets reopened.

It has one structural flaw, and it is the reason this row is last. You find out whether your filter was right after the transfer, which means the loop for a wrong filter is capture, copy, open, discover, delete, repeat, with the failure you were chasing possibly not recurring in between. For an intermittent problem that costs you the event, and for a busy production host it costs you disk you may not have budgeted.

Why does tcpdump on wg0 print link-type RAW instead of Ethernet?

Because there is no Ethernet header on a WireGuard interface to report. WireGuard is a network-layer tunnel: its own site describes it as securely encapsulating IP packets over UDP, and says it "works by adding a network interface (or multiple), like eth0 or wlan0, called wg0". What that interface carries is IP packets. The kernel therefore tells libpcap the link type is LINKTYPE_RAW, tcpdump prints it in the banner as link-type RAW (Raw IP), and every byte you get back is packet, starting with the version nibble of the IP header.

This is the sentence worth keeping: nothing is missing and nothing is truncated. A capture on wg0 and a capture on eth0 differ by fourteen bytes at the front because the frames genuinely differ by fourteen bytes at the front. libpcap's link-layer type list makes the two cases explicit side by side, LINKTYPE_ETHERNET value 1 with the DLT_EN10MB alias for "IEEE 802.3 Ethernet", and LINKTYPE_RAW value 101 for "IPv4 or IPv6 packets with no link-layer header". A tool that assumed the first will misread the second, and a tool that reads the link type will not.

The practical consequence lands on anything downstream of the capture rather than on the capture itself. Byte-offset expressions written against an Ethernet frame move. Scripts that skip fourteen bytes to reach the IP version field read into the middle of the IP header instead. Dissectors handed a pcap without checking pcap_datalink() produce confident nonsense. This is exactly the failure the libpcap manual warns about when it says not to assume any given link-layer header type, and it is why the tunnel case feels like a tool bug: the output is plausible for long enough to act on.

Which Linux interfaces carry a MAC header, and which hand you a bare IP packet?

The split follows what the device is emulating, and once you see it that way the list stops needing memorization. Devices that pretend to be Ethernet carry Ethernet headers, and devices that carry routed IP do not. The kernel's own TUN/TAP documentation states the rule in six words: "TUN works with IP frames. TAP works with Ethernet frames", with the flag names spelling it out further, IFF_TUN being a "TUN device (no Ethernet headers)". One driver, two device types, and the difference is precisely the fourteen bytes at the front.

DeviceFramingCarries a MAC headerTypical name
Physical NICEthernetYeseth0, enp3s0
LoopbackEthernet-framedYeslo
veth pairEthernetYesveth0, container side eth0
BridgeEthernetYesbr0, docker0
tap deviceEthernetYestap0, VM taps
tun deviceRaw IPNotun0
WireGuardRaw IPNowg0
GRE, IPIPRaw IPNogre1, tunl0

veth devices are documented as "virtual Ethernet devices" created "in interconnected pairs" that "can act as tunnels between network namespaces", and the word tunnel there misleads: they are Ethernet all the way down, which is why a capture inside a container sees ARP for the bridge and a capture on wg0 never will. pktscope encodes the same split rather than a per-tool guess, snapping from the MAC header on Ethernet-framed devices and from the network header on raw-IP tunnels, and its interface picker reports each device's kind alongside its rates so the choice is visible before the capture starts. The table above is about framing rather than about any one tool, so read one row of it with care: lo is Ethernet-framed, and pktscope still cannot capture on it, because a TCX program will not attach to loopback.

My capture filter with ether or arp matches nothing on tun0. What broke?

Nothing broke. A filter primitive that names a link-layer field has no field to name on a device with no link-layer header, so it cannot match, and a filter that cannot match is indistinguishable from silence on a quiet interface. This is the second-order version of the same rule: the link type does not only change where your bytes start, it changes which parts of the filter language are meaningful. Anything at layer 3 and above behaves identically on both device classes, because those headers are present either way.

The diagnostic habit worth building is to prove the interface is carrying anything at all before you trust a filter's emptiness. Drop the filter entirely, confirm packets arrive, then add terms back one at a time. On a tunnel, reach for addresses and ports rather than MAC addresses and frame types, since the outer encapsulation that carried the frame is on a different interface with its own capture. That is a filter-language distinction rather than a tool distinction, and it applies to tcpdump, tshark and termshark equally because all three sit on the same library.

tcpdump -L -i wg0            # what link type will this interface give me?

ip -d link show wg0          # the device kind, in the kernel's own words

tcpdump -n -i wg0 -c 5       # no filter at all: does anything arrive here?

-L lists the known data link types for the interface and exits, -d on ip link prints the device kind the kernel has registered, and -c 5 stops after five packets so an idle interface fails fast instead of hanging. Those three lines answer, in order, what the framing is, what the device is, and whether traffic exists, which is the whole triage for "my capture shows nothing" before any filter is involved.

Should I capture on wg0 or on eth0 to see a WireGuard problem?

Both, for different problems, and the choice decides whether you see plaintext or ciphertext. Capturing on wg0 gives you the decapsulated IP traffic, the HTTP request and the TLS handshake and the DNS query, with no Ethernet header and no encryption in the way. Capturing on eth0 gives you the WireGuard transport packets themselves, UDP datagrams whose payload is opaque, which is the right view when the question is whether the tunnel is passing traffic at all rather than what the traffic said.

The clearest published demonstration of this is Nick Babcock's walkthrough, which captures on the tunnel interface and gets the banner listening on wg1, link-type RAW (Raw IP), capture size 262144 bytes with plaintext HTTP behind it, then captures on the physical interface with tcpdump -n -X -i eth0 host 100.100.100.100 and gets encrypted payload starting 0400 0000, cross-checked against WireGuard's protocol documentation. Same traffic, two interfaces, and the two captures answer questions that do not overlap.

Pick by symptom. Handshake failing, no packets moving, MTU suspicion: capture on the physical interface, where retransmits and fragmentation are visible on the outer datagrams. Traffic flowing but the application unhappy, resets arriving, a request that never got a response: capture on the tunnel interface, where the packet the application actually sent is in front of you with no encapsulation to strip. pktscope shows the tunnel side with a --proto filter compiled into the tap, and the outer side is a second run against the physical device.

My tcpdump -i any capture has a header I do not recognize. What is Linux cooked capture?

It is a third framing, and it is neither of the two this post has covered so far. The any pseudo-interface "captures packets from all regular network interfaces of the OS", per the tcpdump man page, and since those interfaces do not share a link-layer format the capture cannot use either one. Linux synthesizes a common header instead, which libpcap lists as LINKTYPE_LINUX_SLL, value 113, "Linux 'cooked' capture encapsulation". Every packet then carries a uniform pseudo-header describing which interface it came from and which direction it went.

Cooked capture is genuinely useful and it is also the least intuitive of the three, because the header you are looking at existed nowhere on the wire. It is the correct choice when you do not yet know which interface the traffic is on, which is a real and common state at the start of an incident. It is the wrong choice once you do know, because you have swapped a real frame for a synthesized one and any byte offset you write is now against a format the kernel invented for your convenience. Narrow to a named interface as soon as the answer is available.

The rule that survives all three cases is the one to carry: read the link type before you read the bytes. LINKTYPE_ETHERNET on a NIC or a veth, LINKTYPE_RAW on a tunnel, LINKTYPE_LINUX_SLL on any, and libpcap will tell you which you got if you ask with pcap_datalink() or just read tcpdump's first line. Every capture confusion in this post reduces to somebody skipping that step, including the ones that end in a bug report against a decoder that was decoding correctly.

I can only reach the box over ssh and I want Wireshark's detail tree, not a pcap round trip

Then the decision is where the dissection happens, and there are exactly three shapes. Send bytes to a local Wireshark, either through sshdump or through a dumpcap pipe. Copy a file back and open it. Or dissect on the host in a terminal, which is termshark and pktscope. The first two need a GUI you can reach, which is precisely what an ssh-only host means you do not have, so the third is not a preference here, it is the only shape that fits the constraint.

Between the terminal options the split is what has to be installed. termshark needs tshark on the host, which on a locked-down box is a package request rather than a command. pktscope needs the yeet daemon, and its build pulls clang, bpftool and esbuild from a checksum-pinned toolchain, which means no system C toolchain and no node or npm on the host. Neither is free. The question is which dependency you can actually get approved this afternoon, and that answer is site-specific rather than technical.

make                                                  # compile the tap, bundle the UI

yeet run .                                            # interface picker, kinds and rates

yeet run . -- --iface wg0 --proto tcp --port 443       # straight at one, filters in-kernel

Arguments after -- reach the script as yeet.args, --iface selects the device so the picker is skipped, and --port and --proto become kernel-side filters rather than a userspace match. Once running, / opens a display filter, 1, 2 and 3 toggle the panes, f follows the tail and p pauses, and the mouse wheel scrolls whichever pane the cursor is over, including the bytes pane. The keys and mouse reference is short enough to read once and keep.

Is it safe to run a capture on a production interface at line rate?

Capture cost is real, it has been measured, and the honest answer is that it depends on what you do with the packets after they leave the kernel. Percona's measurement on a busy database host ran sysbench read-only against 8 tables of a million rows with the dataset in a 16GB buffer pool, and compared capture methods. Writing to a binary file with -w produced "the lowest overhead in response time, around 10%", with a caveat that matters more than the number: noticeable stalls during page cache flushes, causing "response times of several seconds".

Streaming those same -w packets off the box to a remote server measured "20-25% overhead in response time, 10-15% drop in throughput, no stalls". Piping packets into a decoder on the host was worse than either. The shape of the result is consistent: the expensive parts are decoding on the host and writing to the host's disk, and the recommendation Percona lands on, "use tcpdump -w option in all cases and decode later", is a direct consequence. Their follow-up on very busy hosts documents the mitigations, a full 65535 snaplen to avoid truncation, forwarding output to another host because "the database host couldn't handle the additional IO or CPU pressure", and a modulo filter on TCP bytes that cut packet loss from 60% to roughly 0.2%.

Notice what every one of those mitigations has in common: they trim on the path to userspace, or after it. That is not a criticism of libpcap, which does filter in the kernel where it can, and says so plainly: "For a live capture, the filtering will be performed in kernel mode, if possible, to avoid copying 'uninteresting' packets from the kernel to user mode." It is a description of where the remaining cost sits once you have filtered, namely the copy, the decode and the disk.

pktscope moves the same three decisions earlier without claiming to have abolished them. --port and --proto compile into the tap, so unmatched frames never enter the ring buffer. Frames are capped at 1536 bytes by construction rather than by a snaplen you remember to set. Nothing is written to disk, so there is no page cache to flush and no file to rotate or clean up, which also means there is no evidence afterwards. No overhead number for pktscope exists, and inventing one to sit next to Percona's would be worthless, so the comparison here is mechanism against mechanism: cost scales with matched frames rather than with total traffic.

How do I filter for one SNI or one user agent without writing a byte-offset expression?

Use a display filter rather than a capture filter, and know which one your tool is giving you. The two languages are genuinely different and Wireshark's filter man page says so outright, that it "does not describe the capture filter syntax, which is different". Display filters address named protocol fields, support eq, ne, gt, lt, ge, le, the logical and, or, xor, not, a contains operator for searching a field's content, and matches for a case-insensitive Perl-compatible regular expression. That is what makes tls.handshake.extensions_server_name a thing you can type instead of a byte offset you calculate.

pktscope takes a narrower language deliberately, and the trade is worth stating. Space-separated terms AND together, ! negates, and anything unrecognized becomes a substring match over the packet's decoded text and printable payload, so a bare ClaudeBot or 10.0.0.7 works without knowing a field name. Named terms cover the identified kinds (tls, http1, http2, dns, text, arp), the L4 protocols, endpoints (port 443, host 10.0.0.1), decoded fields (sni example.com, ua ClaudeBot), TCP flags and direction (syn, rst, rx, tx, data), and length comparison (len>500). There is no regex and no field-path grammar, which costs you precision and buys you a filter you can type correctly on the first attempt at 02:00.

sni example.com !ack        # TLS records for one host, no bare ACKs
tls rst                     # handshakes followed by resets, the mTLS smell
ua ClaudeBot tx             # one user agent, outbound only
len>500 udp                 # large UDP datagrams, the fragmentation suspect

For anything requiring a regular expression, a field path, or a protocol pktscope does not identify, Wireshark's language is better and tshark or termshark is the tool that has it. That boundary is not a hedge: a display filter grammar is a large surface, Wireshark has spent twenty years on theirs, and the full pktscope filter reference fits in six lines because it is meant to.

My eBPF tap loads on my laptop and gets rejected on the 6.6 box. How do I catch that first?

Run the object through the verifier on the kernels you actually deploy to, not just the one under your hands. This is not a tunnel question, but it is the same class of mistake as assuming an Ethernet header: a thing that is true on your machine and false on the machine that matters. make veristat loads the object with veristat on your kernel, needs sudo, and reports whether every program passes the verifier plus per-program complexity, which catches the common case in a second.

The uncommon case needs other kernels, and the pktscope repo runs them in CI: a GitHub Actions workflow builds the object, boots each kernel in its matrix in a VM using cilium's little-vm-helper, and fails if any verifier rejects it. The same matrix runs locally on Linux with KVM through make veristat-matrix KERNELS="6.6-main bpf-next-main". The floor is set by the attach point rather than by the tap's logic. TCX arrived in Linux 6.6, and yeet binds every tc program through TCX with no classic clsact path anywhere in the runtime, so a kernel older than 6.6 does not attach at all rather than falling back to something older.

make veristat                                         # this kernel's verifier, plus complexity

make veristat-matrix KERNELS="6.6-main bpf-next-main"  # kernels you do not have, in VMs

The bottom line: read the link type, then pick the tool that matches the shape of the box

The rule is device-shaped, not tool-shaped. An Ethernet-framed device gives you a MAC header and ARP; a raw-IP tunnel gives you an IP packet and never will; -i any gives you a header Linux made up so the two can share a file. tcpdump prints which one you got before the first packet, tcpdump -L -i <iface> answers it without capturing anything, and libpcap's manual has said "Do NOT assume that the packets for a given capture or 'savefile' will have any given link-layer header type" for as long as anyone has been reading it.

Past that, pick on constraints rather than preference. With a GUI you can reach, Wireshark through sshdump or a dumpcap pipe is the strongest analysis available and nothing here displaces it. With tshark already installed and a pcap you want to keep, termshark gives you the three panes in a terminal and caches the capture for later. With evidence to hand to a vendor, tcpdump -w and scp, still. With ssh, no GUI, no tshark, and a tunnel device whose offset you do not want to think about, pktscope is the one that fits: a TCX tap that picks the snap point per interface, kernel-side --port and --proto filters, a folding detail tree with byte ranges, and nothing left on disk when it exits.

Frequently asked questions

Does pktscope need sudo?

No. yeet run is unprivileged and never takes sudo, because the yeet daemon performs the privileged BPF load on its behalf. That is also why installing the daemon and running a script are separate steps. make veristat is the exception in this repo and it says so: loading an object with veristat to check the verifier does need sudo, since that is a privileged load with no daemon in the middle.

Does pktscope write a pcap I can send to a vendor?

No. Frames go from the tap into a ring buffer and into the panes, and nothing is written to disk, so there is no file to attach to a ticket and nothing to clean up afterwards. When the deliverable is evidence rather than an answer, use tcpdump -w and keep the file. This is the clearest case in the whole comparison where the older tool is the right one and no argument about mechanism changes it.

Can it show me the contents of a TLS connection?

No. Payload is ciphertext at the capture layer, on any interface and with any capture tool that reads the wire. What survives is what travels in the clear, which for TLS means the record structure and the SNI in the client hello, and pktscope identifies and shows both. Reading the plaintext requires hooking the crypto library with a uprobe on SSL_write or SSL_read, which is a different tool entirely.

Does it need tshark, libpcap or a system C toolchain?

None of them. The capture path is a TCX eBPF program rather than a libpcap handle, so there is no tshark or dumpcap in the loop, and the build resolves clang, bpftool and esbuild from a checksum-pinned toolchain lockfile rather than from the host, so no system C toolchain and no node or npm are required. Two dependencies remain, and both are real enough to name: the yeet daemon, and a kernel built with CONFIG_DEBUG_INFO_BTF, without which the object fails at load time rather than at install.

What happens to a jumbo frame?

It gets truncated. The tap streams frames up to 1536 bytes, which covers a standard 1500-byte MTU with room for the Ethernet header, so on an interface running a 9000-byte MTU you see the head of the frame and not the tail (extrapolated, grounding 2 — review). Headers and the start of the payload are intact, which is what protocol identification needs; a full reassembly of a large transfer is not what this tool is for.

Does the tap change traffic, or can it drop packets?

It cannot. The program always returns TCX_NEXT, which continues the TCX program chain rather than accepting or dropping the packet, so it is a passive observer by construction rather than by configuration. There is no enforcement mode and no flag that turns one on. If a packet goes missing while pktscope is running, the tap is not where it went.

Can I run this on many hosts and see them together?

No. One instance watches one interface on one host, there is no aggregation layer, no retention, and no query language over past captures. Packets live in that one isolate's ring buffer and nowhere else, so when the isolate exits they are gone. Fleet-wide traffic questions are what a platform is for, and a per-host terminal analyzer is a bad substitute for one; reach for this when the question is about a specific interface on a specific box.

Why does ARP show up on my container's interface but not on the tunnel?

Because a container's eth0 is one end of a veth pair, and veth devices are virtual Ethernet devices, so ARP is real traffic on them and decodes in full. A WireGuard or GRE device carries routed IP packets with no link layer at all, so there is no ARP to see, not a filter hiding it. Both behaviors are correct at the same time on the same host, which is what makes this confusing the first time.

Is this a replacement for Wireshark?

No. Wireshark has the dissector library, the reassembly, the statistics, the export formats and twenty years of protocol coverage, and this identifies a short list of payload kinds and shows you bytes. What it removes is the round trip, for the cases where the round trip was the expensive part, and it hands the hard analysis back to Wireshark once you have a file worth opening.

Sources

  • Link-layer header types (tcpdump.org / libpcap, 2026) — the authoritative registry: LINKTYPE_ETHERNET is value 1 with DLT name DLT_EN10MB, described as "IEEE 802.3 Ethernet (10Mb, 100Mb, 1000Mb, and up); the 10MB in the DLT_ name is historical"; LINKTYPE_RAW is value 101, DLT_RAW, "IPv4 or IPv6 packets with no link-layer header"; LINKTYPE_LINUX_SLL is 113, "Linux 'cooked' capture encapsulation"; notes that LINKTYPE_ and DLT_ values usually match but differ in some cases "for reasons of binary compatibility".
  • pcap(3PCAP) manual (tcpdump.org / libpcap, 2026) — states "Do NOT assume that the packets for a given capture or 'savefile' will have any given link-layer header type, such as DLT_EN10MB", and points at pcap_datalink() to ask; documents that a snaplen smaller than the packet captures only the first snaplen bytes; and confirms kernel-side filtering, "for a live capture, the filtering will be performed in kernel mode, if possible, to avoid copying 'uninteresting' packets from the kernel to user mode".
  • tcpdump(1) manual (tcpdump.org, 2026) — documents -L as "list the known data link types for the interface, in the specified mode, and exit", -y datalinktype to force one, -s to "snarf snaplen bytes of data from each packet rather than the default of 262144 bytes" with 0 meaning that default, -w to "write the raw packets to file rather than parsing and printing them out", and the any interface as "a special pseudo-interface, which captures packets from all regular network interfaces of the OS".
  • TUN/TAP device driver documentation (docs.kernel.org) — the primary statement of the framing split, in the kernel's own words: "TUN works with IP frames. TAP works with Ethernet frames", with IFF_TUN documented as "TUN device (no Ethernet headers)" against IFF_TAP; instructs that "you have to read/write IP packets when you are using tun and ethernet frames when using tap", which is the same fourteen-byte difference a capture on a tunnel device shows.
  • veth(4) manual (man7.org) — "the veth devices are virtual Ethernet devices", "always created in interconnected pairs", where packets sent on one are "immediately received on the other device"; they "can act as tunnels between network namespaces to create a bridge to a physical network device in another namespace, but can also be used as standalone network devices", which is why container interfaces are Ethernet-framed and decode ARP despite the word tunnel.
  • WireGuard (wireguard.com) — describes WireGuard as securely encapsulating IP packets over UDP, and works "by adding a network interface (or multiple), like eth0 or wlan0, called wg0" that "acts as a tunnel interface"; states that "any combination of IPv4 and IPv6 can be used, for any of the fields", and that it is "fully capable of encapsulating one inside the other"; the network-layer framing is what produces LINKTYPE_RAW on capture.
  • Viewing WireGuard traffic with tcpdump (Nick Babcock, nickb.dev) — records the banner verbatim, "tcpdump: listening on wg1, link-type RAW (Raw IP), capture size 262144 bytes", and demonstrates the inner-versus-outer split: capturing on the tunnel device shows plaintext HTTP, while tcpdump -n -X -i eth0 host 100.100.100.100 shows the encrypted WireGuard payload beginning 0400 0000, cross-referenced against WireGuard's protocol documentation to confirm the setup.
  • Measuring the impact of tcpdump on very busy hosts (Percona, 2019) — the measured incumbent cost, on sysbench read-only with 8 tables of 1,000,000 rows in a 16GB buffer pool, five-minute runs: -w to a local binary file gives "the lowest overhead in response time, around 10%" but stalls during page cache flushes with "response times of several seconds"; -w streamed to a remote server via netcat gives "20-25% overhead in response time, 10-15% drop in throughput, no stalls"; piping decoded packets is worst; the recommendation is "use tcpdump -w option in all cases and decode later".
  • How to use tcpdump on very busy hosts (Percona) — the standard userspace mitigations and their measured effect: tcpdump -i eth0 -s 65535 to stop truncation, forwarding output to another host because "the database host couldn't handle the additional IO or CPU pressure", and a modulo filter, port 3306 and tcp[1] & 7 == 2 and tcp[3] & 7 == 2, that cut packet loss from 60% to roughly 0.2%; notes that even 5% of packets dropped by the kernel skews query analysis.
  • termshark (termshark.io, 2026) — describes itself as "a terminal UI for tshark, inspired by Wireshark", "written in Go - for Linux, macOS, *BSD, Android (termux) and Windows"; the feature list is "read pcap files or sniff live interfaces", "use Wireshark's display filters", "reassemble TCP and UDP streams" and "view conversations by protocol", which is the same UI shape a terminal packet analyzer wants and the reason to concede it rather than claim it.
  • termshark user guide (gcla/termshark, GitHub) — the dependency and capture semantics underneath that UI: tshark "must be newer than v1.10.2 (from approximately 2013)", packet capture commands "might require sudo or root access", packets are saved by default "to ~/.cache/termshark/pcaps/ on Linux", the three panes are a packet list, "the structure of the packet" and "the bytes that the packet comprises", and "when reading a file, the filter will be interpreted as a display filter. When reading from an interface, the filter is interpreted as a capture filter".
  • sshdump(1) manual (Wireshark, 2026) — "an extcap tool that allows one to run a remote capture tool over a SSH connection", invoked through Wireshark with --remote-host and --remote-capture-command; requires that "the capture executable must have the capabilities to capture from the wanted interface" on the remote host, supports tcpdump or dumpcap there, offers --remote-priv with none, sudo or doas for escalation, and needs Wireshark installed locally because it is an extension rather than a standalone tool.
  • Capture setup: pipes (Wireshark wiki) — the hand-rolled remote capture, wireshark -k -i <(ssh -l root remote-host "dumpcap -P -w - -f 'not tcp port 22'"), plus the mkfifo /tmp/sharkfin variant; states the two problems plainly, that "the main problem is that you have to exclude the traffic generated by the ssh session from capturing" and that "ssh cannot ask for a password on stdin", recommending ssh-agent or x-askpass.
  • tshark(1) manual (Wireshark, 2026) — a network protocol analyzer that "lets you capture packet data from a live network, or read packets from a previously saved capture file", printing the decode or writing a file; captures through libpcap with dumpcap doing the capture work; documents that display filters are more powerful while "capture filters are much more efficient", and that "display filters are currently not supported when capturing and saving" simultaneously.
  • wireshark-filter(4) manual (Wireshark, 2026) — the display filter grammar: existence tests on a protocol or field, comparison operators "eq, == Equal; ne, != Not Equal; gt, > Greater Than; lt, < Less Than; ge, >= ; le, <=", logical "not, !", "and, &&", "xor, ^^", "or, ||", a contains operator for searching field content, matches, ~ for a "case-insensitive Perl-compatible regular expression", and slice syntax [i:j]; states outright that it "does not describe the capture filter syntax, which is different".
  • Composable traffic control with TCX links (eunomia-bpf tutorial, GitHub) — TCX was "introduced by Daniel Borkmann and merged in Linux 6.6" and provides "a link-based attachment model for the TC ingress and egress hooks" without qdisc infrastructure; the link "is tied to a file descriptor, auto-detaches when that fd closes, and cannot be removed by other applications"; return values are TCX_NEXT (-1) "continue to the next program in the chain", TCX_PASS (0) "accept the packet; terminate the chain", and TCX_DROP (2).
  • How to capture packets with tcpdump and analyze in Wireshark (Comparitech) — the incumbent round trip stated as a method: capture on the remote host over ssh, copy the .pcap back with scp or pscp, then open it locally in Wireshark for analysis; the walkthrough is a two-step capture-then-transfer process rather than a live pipe, which is what makes a wrong filter cost a full iteration.

Related resources

  • pktscope on GitHub — the source, the three-pane tour, and the one make plus yeet run . path from clone to a live capture.
  • pktscope capture semantics — the per-interface snap offset in the authors' own words, the 1536-byte frame cap, the kernel-side --port and --proto filters, and the TCX_NEXT guarantee.
  • pktscope display filter reference — the full filter language in six lines: identified kinds, L4 protocols, endpoints, decoded fields, TCP flags, direction, length comparison and negation.
  • pktscope kernel testingmake veristat for the verifier on your kernel, and the matrix workflow that boots each target kernel in a VM before the object ships.
  • Capturing 802.11 frames on a connected interface — the same read-it-where-it-already-exists argument one layer down, for a wireless link you do not want to drop to diagnose.
  • How to monitor HTTP traffic on Linux — the layer above this one, for when the packets are fine and the application's own access log is the thing disagreeing with reality.
  • yeet documentation — the runtime underneath, for driving your own compiled tap from JavaScript, binding it and reading its ring buffer, when the ready-made analyzer is the wrong shape.

See your machine the way the kernel sees it.

Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.