skip to content

What does `tcpdump -d` print for a filter such as `tcp port 25`, and how do you read its loads, jumps and ret lines to confirm what it matches?

level: seniorimportance: nice to knowfreq 8%

answer

  1. one instruction per line, then exit
  2. ld reads bytes at fixed offsets
  3. jt and jf are line numbers
  4. ret is accept length or zero

basics

~20 s

tcpdump -d compiles the filter for the link type and prints one classic BPF instruction per line, then exits. Loads read header bytes, jeq and jset branch to absolute line numbers, ret #262144 accepts up to the snapshot length and ret #0 drops.

solid answer

~50 s

`tcpdump -d` compiles the expression with libpcap and prints the **classic BPF** program in human-readable form, one numbered instruction per line, then stops without capturing. Read it as a decision tree: `ldh [12]` loads two bytes at offset 12 (the Ethernet type), `ldb` loads one byte, `ldxb 4*([14]&0xf)` puts the IPv4 header length into the X register, and `jeq`, `jgt`, `jge` or `jset` compare the accumulator and jump to the instruction numbers after `jt` (true) and `jf` (false). Every path ends at a `ret`: `ret #262144` accepts the packet and keeps up to that many bytes, the snapshot length, and `ret #0` drops it. `-dd` prints the same program as a C array fragment, and `-ddd` as decimal numbers preceded by a count. The code depends on the link type: tcpdump uses the `-r` file's or the `-i` interface's, and with neither, Ethernet unless `-y` says otherwise.

code

bash · 6 lines
bash
tcpdump -d 'tcp port 25'
tcpdump -dd 'tcp port 25'
tcpdump -ddd 'tcp port 25'
tcpdump -d -O 'tcp port 25'
tcpdump -d -r incident-0930.pcap 'tcp[tcpflags] & tcp-syn != 0'
tcpdump -d -y LINUX_SLL2 'tcp port 25'

go deeper

for a junior

Recall that tcpdump -d prints the compiled filter and exits, and that ret #0 means the packet is dropped.

for a middle

Walk through loads, compares and jumps, and explain how jt and jf lead to the accept or drop ret lines.

for a senior

Use -d to prove a filter's coverage before a long capture: IPv6 branches, fragment checks, VLAN offsets and the link type it was compiled for.

for a principal

Make compiled-filter review part of approving high-volume or long-running captures, since a wrong filter costs evidence that cannot be recaptured.

## Why read the compiled filter tcpdump hands its expression to libpcap, which compiles it into a **classic BPF (cBPF)** program: a short list of instructions for a small virtual machine with an accumulator (A), an index register (X) and scratch memory. `tcpdump -d` prints that program and exits instead of capturing. Reading it answers questions the expression hides: does this filter handle IPv6, are VLAN offsets applied where I expect, did my grouping produce the logic I meant? Where the kernel runs the program is a separate subject; this one is about reading what was compiled. ## A real listing, annotated This is the optimized program libpcap's own test suite expects for `tcp port 25` on Ethernet; `tcpdump -d 'tcp port 25'` prints this shape: ``` (000) ldh [12] (001) jeq #0x86dd jt 2 jf 8 (002) ldb [20] (003) jeq #0x6 jt 4 jf 19 (004) ldh [54] (005) jeq #0x19 jt 18 jf 6 (006) ldh [56] (007) jeq #0x19 jt 18 jf 19 (008) jeq #0x800 jt 9 jf 19 (009) ldb [23] (010) jeq #0x6 jt 11 jf 19 (011) ldh [20] (012) jset #0x1fff jt 19 jf 13 (013) ldxb 4*([14]&0xf) (014) ldh [x + 14] (015) jeq #0x19 jt 18 jf 16 (016) ldh [x + 16] (017) jeq #0x19 jt 18 jf 19 (018) ret #262144 (019) ret #0 ``` Reading it top to bottom: 1. **000-001** load the EtherType and test for 0x86dd, IPv6. 2. **002-003** IPv6 path: byte 20 is the Next Header field (14-byte Ethernet header plus 6); it must be 6, TCP. 3. **004-007** the ports sit at fixed bytes 54 and 56 (14 + 40), so this path assumes **no extension headers**. 0x19 is 25. 4. **008** not IPv6, so test the same loaded EtherType for 0x800, IPv4. 5. **009-010** byte 23 is the IPv4 protocol field; it must be 6. 6. **011-012** the flags and fragment-offset field; `jset #0x1fff` jumps to reject if the fragment offset is non-zero, because later fragments have no TCP header. 7. **013** `ldxb 4*([14]&0xf)` loads the IPv4 header length, which varies with options, into X. 8. **014-017** the ports are read relative to X. 9. **018** `ret #262144` accepts, keeping up to 262144 bytes, the default snapshot length; **019** `ret #0` drops. ## The instruction vocabulary | Mnemonic | Meaning | |---|---| | `ld`, `ldh`, `ldb` | load 4, 2 or 1 bytes into A from an absolute offset | | `ldh [x + 14]` | load relative to the X register | | `ldxb 4*([14]&0xf)` | X = IPv4 header length in bytes | | `jeq`, `jgt`, `jge` | compare A with a constant | | `jset` | jump true if A AND constant is non-zero | | `and`, `or`, `add`, `lsh` | arithmetic on A | | `ret #n` | accept n bytes, or drop when n is 0 | In `-d` output, the numbers after `jt` and `jf` are **absolute instruction numbers**, printed as the current line plus one plus the stored offset. ## The other dump formats and the link type - **`-dd`** prints the same program as a C program fragment, one `{ code, jt, jf, k }` entry per line, for example `{ 0x28, 0, 0, 0x0000000c },` for `ldh [12]`. Here `jt` and `jf` are the raw **relative** offsets stored in the instruction. - **`-ddd`** prints the instruction count, then each instruction as four decimal numbers. - **`-O`** turns off the optimizer; the unoptimized program is longer and is mainly useful if you suspect an optimizer bug. Compilation is always specific to a link type (DLT). With `-r`, tcpdump compiles for the file's link type; with `-i`, for the interface's; with `-y`, for the type you name. With neither `-r` nor `-i`, `-d` does not guess an interface and compiles for Ethernet (EN10MB). A filter compiled for Ethernet has different offsets from one compiled for Linux cooked capture on `-i any`, so compile for the capture you will actually run. ## What to check in practice - **Address families:** a `jeq #0x86dd` branch means IPv6 is handled; a `tcp[...]` test shows only `jeq #0x800`. - **Grouping:** follow the `jf` paths; if a branch you meant to be required can be skipped on the way to the accepting `ret`, the expression is grouped wrongly. - **VLAN offsets:** after a `vlan` test, loads move 4 bytes further in. - **Snapshot length:** the accept value tracks the snapshot length in force when the filter was compiled.

  • Why can the same filter compile to different `tcpdump -d` output on two hosts?
    Compilation is link-type specific. On an Ethernet interface the IPv4 header starts at byte 14; on Linux cooked capture it starts after the cooked header, so every offset changes. Linux kernels that expose VLAN metadata also get extra `vlanp` checks for `vlan` filters. Compile with the same `-i`, `-r` or `-y` you will capture with.
  • In `tcpdump -d` output for `tcp port 25`, why do the IPv6 port loads use fixed offsets while the IPv4 ones use `[x + 14]`?
    The IPv6 header is a fixed 40 bytes, so the TCP ports start at 14 + 40 = 54. The IPv4 header length varies with options, so the program first loads it into X with `ldxb 4*([14]&0xf)` and indexes from there. The fixed IPv6 offset is also why the port test only matches IPv6 packets without extension headers.

saying these in an interview costs you the question

  • Reads ret #262144 as a rule number or packet count
  • Treats jt and jf in -d output as relative offsets
  • Assumes -d output is the same whatever the link type
  • Thinks tcpdump -d starts a capture as well as printing code
  • Believes -O makes the filter run faster