Capturing Packets with tcpdump

tcpdump is the reference implementation of “show me the bytes.” It opens a capture device through libpcap, hands the kernel a compiled Berkeley Packet Filter (BPF) program so that unwanted frames are discarded before they are ever copied to user space, copies at most snaplen bytes of each surviving frame into a shared buffer, and then runs a per-protocol printer over those bytes to produce one line of text per packet (tcpdump(1)). Every design decision in the tool traces back to the architecture McCanne and Jacobson described in The BSD Packet Filter at the 1993 Winter USENIX conference — a kernel “network tap” that decides both whether a packet is accepted and how many bytes of it to save (McCanne & Jacobson 1993). Those two decisions are exactly the -s (snapshot length) and filter-expression knobs you type on the command line thirty years later. This note covers the capture pipeline, the flags that matter, how to read the output field by field, and the privilege model; the filter grammar itself lives in The pcap Capture Filter Language.

As of this writing (July 2026) the current stable releases are tcpdump 4.99.6 and libpcap 1.10.6, both dated 30 December 2025, with tcpdump 5.0 and libpcap 1.11 still in development (tcpdump.org). Every command output shown below was produced on this machine against tcpdump 4.99.6 / libpcap 1.10.6 (built with TPACKET_V3) on Fedora 44.

Uncertain

The man page published at tcpdump.org/manpages/tcpdump.1.html documents the 5.0.0-PRE-GIT development branch, not the 4.99.6 stable release. Verify per-option: a handful of options described there (notably --skip, --print-sampling, and the e flag character for the Accurate ECN bit) may not exist in 4.99.x. Reason: no versioned 4.99 man page is published on tcpdump.org, and this machine has no man pages installed for the package. To resolve: build 4.99.6 from the release tarball and read its shipped tcpdump.1.in, or check tcpdump --help (done here — the 4.99.6 usage line does not list --skip or --print-sampling). #uncertain


Mental Model — A Tap, a Filter, and a Truncating Copy

The single most useful mental model is that tcpdump is not reading the network. It is reading a copy that the kernel makes at the moment a frame crosses the boundary between the device driver and the protocol stack. That copy is gated by a filter program the kernel runs on your behalf, and it is deliberately truncated to a length you choose.

flowchart TB
    WIRE["Physical medium<br/>frames arriving on the wire"]
    NIC["NIC + driver<br/>DMA into a ring, IRQ, NAPI poll"]
    SKB["Kernel allocates a socket buffer<br/>struct sk_buff"]
    TAP["The tap point<br/>net/core/dev.c walks the ptype_all list<br/>before normal L3 delivery"]
    BPF["Kernel BPF filter<br/>the compiled filter program runs<br/>ACCEPT returns a byte count, REJECT returns 0"]
    RING["AF_PACKET receive ring<br/>PACKET_RX_RING, TPACKET_V3<br/>mmap'd, shared with user space"]
    LIBPCAP["libpcap<br/>pcap_activate then pcap_loop<br/>one struct pcap_pkthdr per packet"]
    PRINT["tcpdump printers<br/>per-DLT and per-protocol decoders"]
    OUT["stdout: one line per packet"]
    FILE["savefile: -w out.pcap<br/>raw bytes + pcap_pkthdr"]
    STACK["Normal protocol stack<br/>IP, TCP, sockets<br/>unaffected by capture"]

    WIRE --> NIC --> SKB --> TAP
    TAP --> STACK
    TAP --> BPF
    BPF -->|"accepted, copy min(snaplen, len) bytes"| RING
    BPF -->|"rejected, cost is a few instructions"| DROP["discarded, never copied"]
    RING --> LIBPCAP
    LIBPCAP --> PRINT --> OUT
    LIBPCAP --> FILE

The capture pipeline from wire to terminal. What it shows: the tap sits beside the normal receive path, not in it — capturing does not consume, delay, or modify the packets the host is actually processing (contrast XDP (eXpress Data Path) and The Netfilter Framework and Hooks, which sit in the path and can drop or rewrite). The insight to take: there are exactly two places where data volume is reduced, and both are before the copy to user space — the filter (drop the whole packet) and the snapshot length (keep only the first N bytes). Everything you can do to make a capture survivable on a busy host is one of those two knobs. McCanne and Jacobson’s measurements are the reason: their “reject all” benchmark showed a filter that discards in place costs a constant ~5 µs per packet regardless of packet size, while the competing design that copied first and filtered afterwards grew with packet size to nearly 450 µs (McCanne & Jacobson 1993).

The kernel side of that picture on Linux is a packet socket, AF_PACKET, documented in packet(7). A SOCK_RAW packet socket receives frames including the link-level header; SOCK_DGRAM strips it. libpcap uses SOCK_RAW — which is precisely why -e can show you Ethernet addresses at all. The filter reaches the socket through setsockopt(..., SOL_SOCKET, SO_ATTACH_FILTER, ...), the mechanism the kernel documents as Linux Socket Filtering (LSF) and describes as being the same thing as BPF in a Linux context (kernel networking/filter). Since the eBPF rework, classic BPF programs attached this way are transparently translated into the modern instruction set and executed by the eBPF interpreter or JIT — see Classic BPF vs Extended BPF for that translation, and BPF Instruction Set for the instruction encoding.

The Startup Sequence, Mechanically

tcpdump does not talk to the kernel directly. It drives the libpcap API in a fixed order, and knowing that order explains several otherwise-baffling error messages.

sequenceDiagram
    autonumber
    participant U as tcpdump (main)
    participant L as libpcap
    participant K as kernel (AF_PACKET)

    U->>L: pcap_create("wlp2s0")
    Note over L: allocates a pcap_t, nothing opened yet
    U->>L: pcap_set_snaplen(262144)
    U->>L: pcap_set_promisc(1)  unless -p
    U->>L: pcap_set_timeout(1000)  buffering delay
    U->>L: pcap_set_buffer_size()  if -B
    U->>L: pcap_activate()
    L->>K: socket(AF_PACKET, SOCK_RAW, ...)
    K-->>L: EPERM unless CAP_NET_RAW
    L->>K: setsockopt PACKET_ADD_MEMBERSHIP / PACKET_MR_PROMISC
    L->>K: setsockopt PACKET_RX_RING (TPACKET_V3) + mmap
    U->>U: drop privileges to -Z user (setuid/setgid)
    U->>L: pcap_compile("tcp port 443", optimize, netmask)
    Note over L: filter text becomes a struct bpf_program
    U->>L: pcap_setfilter()
    L->>K: setsockopt SO_ATTACH_FILTER
    loop per packet
        K-->>L: ring slot filled: tpacket header + bytes
        L-->>U: callback(pcap_pkthdr{ts, caplen, len}, data)
        U->>U: dispatch to printer for the link type
    end
    U->>L: pcap_stats() on SIGINT
    L-->>U: ps_recv / ps_drop / ps_ifdrop

The libpcap call sequence tcpdump performs, drawn as a message exchange with the kernel. What it shows: the pcap_createpcap_set_*pcap_activate split introduced in libpcap 1.0 (pcap(3PCAP)), and where in that sequence each command-line flag takes effect. The insight to take: three things happen in an order that matters. The device is opened before privileges are dropped (which is why -Z can work at all), the filter is compiled after the device is open (which is why pcap_compile needs to know the link-layer type, and why tcpdump -d without -i or -r warns that it is assuming Ethernet), and caplen and len are separate fields in every packet header — len is the original wire length, caplen is how much survived the snapshot length (pcap.h).

The Privilege Model — Root, CAP_NET_RAW, and Dropping Down

The libpcap documentation states the requirement plainly: on Linux you need to be root, or the capturing program must be installed setuid-root, unless the kernel supports capability bits such as CAP_NET_RAW (pcap(3PCAP)). The kernel’s own check is unambiguous — packet_create() in net/packet/af_packet.c refuses with -EPERM unless the caller holds CAP_NET_RAW in the user namespace owning the network namespace (Linux v6.12 source), matching the capabilities(7) description of CAP_NET_RAW as the capability that permits use of RAW and PACKET sockets.

On this machine, running tcpdump as an unprivileged user produces exactly that failure, and the tool names the missing capability:

$ tcpdump -i lo -c 1
tcpdump: lo: You don't have permission to perform this capture on that device
(Attempt to create packet socket failed - CAP_NET_RAW may be required)

Notably, listing interfaces does not require the capabilitypcap_findalldevs() walks the interface table, not a packet socket:

$ tcpdump -D
1.wlp2s0 [Up, Running, Wireless, Associated]
9.eth0 [Up, Running, Connected]
10.any (Pseudo-device that captures on all interfaces) [Up, Running]
11.lo [Up, Running, Loopback]

A subtle and frequently mis-stated point: promiscuous mode does not need CAP_NET_ADMIN. The capabilities(7) page does list “set promiscuous mode” under CAP_NET_ADMIN, but that refers to the SIOCSIFFLAGS ioctl path. libpcap instead uses the per-socket PACKET_ADD_MEMBERSHIP option with PACKET_MR_PROMISC (packet(7)), and reading packet_dev_mc() in the v6.12 source shows that path calls dev_set_promiscuity() with no capable() or ns_capable() check at all — the authorisation was already done when the socket was created. So CAP_NET_RAW alone is sufficient for a promiscuous capture. See POSIX Capabilities for the capability model itself.

ApproachWhat you grantBlast radiusWhen to use
sudo tcpdumpFull root for the whole processEnormous — the printers are a large C parser surface running as rootInteractive debugging on a machine you already own
sudo tcpdump -Z nobodyRoot only until the device is open, then droppedModerate — decode bugs run unprivilegedThe default habit; many distros build with --with-user so this happens automatically
setcap cap_net_raw+eip /usr/bin/tcpdumpCAP_NET_RAW only, permanently, to anyone who can run the binaryAnyone on the box can sniffRarely a good idea on a shared host
setcap cap_net_raw,cap_net_admin+eip on a dedicated helperCapture rights to one small programSmall, if the helper is smallWireshark’s model — its wiki gives exactly sudo setcap cap_net_raw,cap_net_admin+eip /usr/sbin/dumpcap and keeps the dissectors unprivileged (Wireshark wiki)
sudo -g pcap / group-gated helperCapture rights to a named groupControlledMulti-user hosts

Capture privilege models compared. The insight to take: the reason Wireshark splits dumpcap out as a separate ~small binary is that packet dissection is where the parser bugs live, and dissection needs no privilege. tcpdump’s equivalent hedge is -Z: it opens the device as root, then calls setuid()/setgid() to the named user before it writes any output file or decodes any byte.

The Fedora 44 package used here is built with a dedicated tcpdump user (uid 72 exists on the system, and the binary contains the literal tcpdump next to its “couldn’t find user” error string), and the binary carries no file capabilities — so on this distro the intended flow is “start as root, drop immediately.” The tcpdump CHANGES file records the related sharp edge fixed in 4.99.0: privilege dropping is skipped when you pass -Z root on a --with-user build (CHANGES).

Snapshot Length — Why the Default Is 262144

The snapshot length (-s, colloquially snaplen) is the number of bytes copied out of each accepted packet. It is the second half of the original BPF design: the filter decides acceptance and how many bytes to save.

libpcap defines the ceiling in pcap-int.h:

#define MAXIMUM_SNAPLEN		262144

The comment there explains the choice is somewhat arbitrary but must be large enough for maximum-size Linux loopback packets (65 549 bytes) and for USB captures taken with USBPcap, which exceed 131 072 bytes, while staying small enough that applications which naively size buffers from the snaplen do not try to allocate absurd amounts of memory (pcap-int.h). tcpdump’s CHANGES dates the move to release 4.6.0 (2 July 2014), describing it as increasing the default snapshot size to 256 K to accommodate USB captures. So the modern default is not about Ethernet at all — it is about non-Ethernet link types whose “packets” are far larger than any frame.

The default is visible in compiled filter output as the accept-return value. Here is a real compilation on this machine:

$ tcpdump -d 'tcp[tcpflags] & tcp-syn != 0'
Warning: assuming Ethernet
(000) ldh      [12]                       # load EtherType (Ethernet offset 12)
(001) jeq      #0x800  jt 2  jf 10        # IPv4? else reject
(002) ldb      [23]                       # IPv4 Protocol field (14 + 9)
(003) jeq      #0x6    jt 4  jf 10        # TCP? else reject
(004) ldh      [20]                       # IPv4 flags+fragment offset
(005) jset     #0x1fff jt 10 jf 6         # non-zero fragment offset -> reject
(006) ldxb     4*([14]&0xf)               # X = IHL*4, the IPv4 header length
(007) ldb      [x + 27]                   # 14 + X + 13 = the TCP flags octet
(008) jset     #0x2    jt 9  jf 10        # test bit 0x02 = SYN
(009) ret      #262144                    # ACCEPT: copy up to 262144 bytes
(010) ret      #0                         # REJECT: copy zero bytes

A compiled BPF filter with line-by-line commentary, produced locally with tcpdump -d. The insight to take: ret #262144 and ret #0 are literally the accept and reject verdicts, and the accept value is the snapshot length — the return value of a BPF filter program is the number of bytes to copy. That is why “reject” and “snaplen 0” are the same instruction with a different immediate. Note also line 006: because the IPv4 header is variable-length, the compiler emits an indirect load using the X register — the capability whose absence McCanne and Jacobson called out as a defect of the older CMU/Stanford Packet Filter, which could only read fixed offsets.

When the snapshot length is too small, tcpdump does not silently print garbage — it prints a truncation indicator. Capturing the same five packets at -s 40 and re-reading them gives:

$ tcpdump -nn -r snap40.pcap
reading from file snap40.pcap, link-type EN10MB (Ethernet), snapshot length 40
10:46:41.000000 IP 192.0.2.10.51234 > 198.51.100.20.443:  [|tcp]
10:46:41.040000 IP 192.0.2.10.41234 > 192.0.2.1.53:  [|udp]

The bracketed [|tcp] means “the decoder ran out of captured bytes while parsing the TCP layer.” Forty bytes covers the 14-octet Ethernet header and the 20-octet IPv4 header with six left over — not enough for the 20-octet TCP header, so ports, flags and sequence numbers are all unavailable. Note the asymmetry: -s is a capture-time parameter. Re-reading an existing savefile with a smaller -s does not re-truncate it; the bytes are already on disk.

SnaplenWhat you getUse when
-s 0Reset to the default (262144) — kept for backward compatibility, per tcpdump(1)Never deliberately; it is a legacy spelling
default 262144Whole packets, including jumbo frames, GRO super-packets and USB transfersDefault; correct unless disk or CPU is the constraint
-s 96-s 128Link + network + transport headers, plus a little payloadHigh-rate captures where you only need headers, flags, and sequence numbers
-s 64Ethernet + IPv4 + a bare TCP header, nothing moreExtremely tight budgets; loses TCP options on SYNs
-s 40 or lessNot even a full TCP header — `[tcp]`

Choosing a snapshot length. The insight to take: the header budget is additive and layer-dependent. A SYN with modern options is 14 + 20 + 40 = 74 octets before a single payload byte, so a -s 64 capture cannot show you the window scale or SACK-permitted options that determine how the connection will behave — see TCP Option Encoding and Common Options.

Reading the Output, Field by Field

Everything below was produced locally from a hand-built five-packet capture, so the values are real, not illustrative.

$ tcpdump -nn -r demo.pcap
reading from file demo.pcap, link-type EN10MB (Ethernet), snapshot length 262144
10:46:41.000000 IP 192.0.2.10.51234 > 198.51.100.20.443: Flags [S], seq 3735928559, win 64240, options [mss 1460,sackOK,TS val 439041101 ecr 0,nop,wscale 7], length 0
10:46:41.035300 IP 198.51.100.20.443 > 192.0.2.10.51234: Flags [S.], seq 287454020, ack 3735928560, win 65160, options [mss 1460,sackOK,TS val 2575857510 ecr 439041101,nop,wscale 7], length 0
10:46:41.035400 IP 192.0.2.10.51234 > 198.51.100.20.443: Flags [.], ack 1, win 502, length 0
10:46:41.036100 IP 192.0.2.10.51234 > 198.51.100.20.443: Flags [P.], seq 1:51, ack 1, win 502, length 50
10:46:41.040000 IP 192.0.2.10.41234 > 192.0.2.1.53: 19756+ A? example.com. (29)

Taking the fourth line apart:

TokenMeaning
10:46:41.036100Timestamp applied by the kernel, hh:mm:ss.frac, microsecond resolution by default. tcpdump(1) warns it reflects kernel clock precision and excludes the delay between the interface receiving the frame and the kernel stamping it
IPThe network-layer decoder that claimed the packet (from the EtherType)
192.0.2.10.51234 >Source address and port — note tcpdump’s house style of a dot, not a colon, before the port
198.51.100.20.443:Destination address and port
Flags [P.]TCP control bits, one character each (see the table below)
seq 1:51Sequence range covered by this segment’s payload, relative to the connection’s initial sequence number
ack 1Next sequence number expected from the peer, also relative
win 502Receive window advertised, before applying the window scale factor
length 50Payload bytes, excluding all headers

Sequence numbers are relative by default. Compare packet 1 (seq 3735928559, absolute, because there is no established conversation state yet) with packet 4 (seq 1:51). Reading tcpdump’s print-tcp.c, when the ACK flag is present and -S was not given, tcpdump keeps a per-conversation hash of initial sequence numbers and subtracts the stored baseline (print-tcp.c). Pass -S when you need to correlate against absolute numbers in another tool’s output.

flowchart LR
    subgraph OCT12["tcp[12] — offset/reserved octet"]
      DO["bits 0-3<br/>Data Offset<br/>header length in 32-bit words"]
      RSV["bits 4-6<br/>Reserved"]
      AE["bit 7 = 0x01<br/>AE (Accurate ECN)<br/>formerly NS"]
    end
    subgraph OCT13["tcp[13] — the flags octet"]
      CWR["0x80 CWR<br/>prints W"]
      ECE["0x40 ECE<br/>prints E"]
      URG["0x20 URG<br/>prints U"]
      ACK["0x10 ACK<br/>prints ."]
      PSH["0x08 PSH<br/>prints P"]
      RST["0x04 RST<br/>prints R"]
      SYN["0x02 SYN<br/>prints S"]
      FIN["0x01 FIN<br/>prints F"]
    end
    OCT12 --> OCT13

The two TCP header octets that carry flags, with tcpdump’s printed character for each bit. What it shows: eight control bits live in the octet at TCP offset 13 in the order CWR, ECE, URG, ACK, PSH, RST, SYN, FIN, most-significant first (RFC 9293 §3.1); the ninth flag, AE, lives in the previous octet at tcp[12] & 0x01RFC 9768 renamed the old ECN-nonce (NS) bit to Accurate ECN. The insight to take: the ACK bit prints as a bare dot ., which is why [S.] means SYN+ACK and [.] means a pure acknowledgement — and why tcp[13] alone can never see the AE flag. tcpdump’s tcp_flag_values table maps TH_ACK to the string ., TH_CWR to W, TH_ECNECHO to E, and (on the development branch) TH_AE to e (print-tcp.c). This diagram is the bridge to The pcap Capture Filter Language: tcp[13] in a filter reads exactly the octet drawn on the right.

The Flags That Matter

Name resolution: -n and -nn

-n stops address-to-name conversion; -nn additionally stops port-to-service-name conversion. This is not cosmetic. On an unresolvable or slow-DNS host, name lookups can stall output for seconds per packet and, worse, generate DNS traffic that your own capture then records. Compare the default rendering 198.51.100.20.https with -nn’s 198.51.100.20.443. Make -nn a reflex.

Verbosity: -v, -vv, -vvv

Verbosity adds decoded header fields, not more packets. At -vv the IPv4 header is expanded and checksums are verified:

$ tcpdump -nnvv -r demo.pcap -c 1
10:46:41.000000 IP (tos 0x0, ttl 64, id 7238, offset 0, flags [DF], proto TCP (6), length 60)
    192.0.2.10.51234 > 198.51.100.20.443: Flags [S], cksum 0xa2c9 (correct), seq 3735928559, win 64240, options [mss 1460,sackOK,TS val 439041101 ecr 0,nop,wscale 7], length 0

Every field on the first line maps onto The Internet Protocol Version 4 Header Field by Field: tos, ttl, id, offset, flags [DF], proto, length. The cksum ... (correct) annotation is the one to be careful about — see Failure Modes.

Hex and ASCII: -x, -xx, -X, -XX

Four related flags, distinguished by two independent choices: hex-only versus hex-plus-ASCII, and excluding versus including the link-layer header.

Hex onlyHex + ASCII
Payload from the network layer up-x-X
Including the link-layer header-xx-XX

-XX is the one to use when you are mapping bytes onto header diagrams, because offset 0x0000 is then the first octet of the Ethernet frame:

$ tcpdump -nn -XX -r demo.pcap -c 1
10:46:41.000000 IP 192.0.2.10.51234 > 198.51.100.20.443: Flags [S], seq 3735928559, ...
	0x0000:  5254 00ab cdef 5254 0012 3456 0800 4500  RT....RT..4V..E.
	0x0010:  003c 1c46 4000 4006 3224 c000 020a c633  .<.F@.@.2$.....3
	0x0020:  6414 c822 01bb dead beef 0000 0000 a002  d.."............
	0x0030:  faf0 a2c9 0000 0204 05b4 0402 080a 1a2b  ...............+
	0x0040:  3c4d 0000 0000 0103 0307                 <M........

Walking it against the diagrams:

  • 0x00000x0005 = 52:54:00:ab:cd:ef, the destination MAC address (The Ethernet II Frame Format).
  • 0x00060x000b = 52:54:00:12:34:56, the source MAC.
  • 0x000c0x000d = 0800, the EtherType for IPv4.
  • 0x000e = 45: IP version 4 in the high nibble, IHL = 5 (five 32-bit words = 20 octets) in the low nibble. This is the byte a filter reads as ip[0].
  • 0x00220x0023 = c822 = 51234, the TCP source port. TCP therefore starts at offset 0x22 = 34 = 14 + 20.
  • 0x002e = a0: tcp[12]. High nibble a = 10, so the TCP header is 10 × 4 = 40 octets — 20 fixed plus 20 of options.
  • 0x002f = 02: tcp[13], the flags octet. 0x02 is SYN and nothing else. This single byte is the entire subject of the accuracy note in The pcap Capture Filter Language.
  • 0x0034 onward = the TCP options: 02 04 05b4 (MSS = 1460), 04 02 (SACK-permitted), 08 0a ... (Timestamps), 01 (NOP), 03 03 07 (Window Scale 7).

Timestamps: the -t family

FlagOutputGood for
(default)10:46:41.036100Everyday reading
-t(omitted)Diffing two captures textually
-ttSeconds since the Unix epoch with fractionFeeding another program
-tttDelta since the previous packetSpotting a stall or a retransmission timer
-tttt2026-03-20 10:46:41.035300 — date and timeCorrelating a capture with application logs, which is the reason to reach for it
-tttttDelta since the first packetMeasuring a handshake or a request/response

--time-stamp-precision=micro|nano selects the resolution libpcap requests; the available sources are enumerated in pcap-tstamp(7) as PCAP_TSTAMP_HOST, HOST_LOWPREC, HOST_HIPREC, HOST_HIPREC_UNSYNCED, ADAPTER and ADAPTER_UNSYNCED, selected with -j. The ADAPTER* types are NIC hardware timestamps — the only ones that avoid the interrupt-to-kernel latency the man page warns about.

-e prepends the link-level header, which is how you see MAC addresses, EtherType and the on-wire frame length. Combined with -tttt and -nn:

$ tcpdump -nn -tttt -e -r demo.pcap -c 1
2026-03-20 10:46:41.000000 52:54:00:12:34:56 > 52:54:00:ab:cd:ef, ethertype IPv4 (0x0800), length 74: 192.0.2.10.51234 > 198.51.100.20.443: Flags [S], ...

length 74 here is the frame length (14 + 60), whereas the trailing length 0 on the TCP line is the payload length. Two different length tokens on one line meaning different things is a genuine readability trap.

-A prints packet contents as ASCII with the link header excluded — useful only for text protocols, and actively misleading for anything encrypted. It is the fastest way to eyeball an HTTP/1.1 request; it is useless against TLS.

Volume control: -c, -w, -r

-c count exits after that many matched packets — the cheapest safety net on a production box. -w file writes the raw bytes plus each struct pcap_pkthdr to a savefile instead of decoding them; -r file reads one back. Writing raw and decoding later is almost always right: it moves the expensive per-packet decoding off the capturing host, and it preserves bytes that a decoder bug would otherwise hide. The file format itself is the subject of The pcap and pcapng Capture File Formats.

Promiscuous Mode Versus Monitor Mode

These are routinely conflated and are not the same mechanism, the same layer, or the same consequence.

flowchart TB
    subgraph NORMAL["Default (no promiscuous mode)"]
      N1["NIC hardware filter accepts:<br/>own unicast MAC + broadcast<br/>+ joined multicast groups"]
      N1 --> N2["Everything else dropped in hardware"]
    end
    subgraph PROMISC["Promiscuous (-p disables it; on by default)"]
      P1["PACKET_ADD_MEMBERSHIP with PACKET_MR_PROMISC<br/>calls dev_set_promiscuity()"]
      P1 --> P2["NIC accepts every frame it sees<br/>still a fully associated, normal interface"]
      P2 --> P3["On a switched LAN you mostly see<br/>your own traffic + broadcast + flooded frames"]
    end
    subgraph MONITOR["Monitor mode (-I, 802.11 only)"]
      M1["Radio is put into RFMON<br/>a different, driver-level mode"]
      M1 --> M2["Captures raw 802.11 frames:<br/>beacons, management, control, other BSSIDs"]
      M2 --> M3["Adapter may DISASSOCIATE<br/>you lose network access on that radio"]
    end

Three interface reception modes. What it shows: promiscuous mode is a filtering change on an otherwise normal interface; monitor mode is a radio mode change unique to IEEE 802.11 Wi-Fi. The insight to take: the man page’s caveat on -I is the operational one — the adapter may disassociate from the network it is on, so you cannot use that wireless network while monitoring (tcpdump(1)). Monitor mode gives you the 802.11 headers, beacons and other stations’ frames that promiscuous mode never shows. Two further cautions: tcpdump(1) notes that -p is not a guarantee the interface is non-promiscuous, since something else may have already put it there, and that -p is not a substitute for an explicit ether host filter.

Long Captures — Ring Buffers with -C, -W, -G

The failure mode of an unattended tcpdump -w is filling the disk and taking the host down with it. The rotation flags exist for exactly this.

  • -C file_size closes the current savefile and opens a new one once it exceeds file_size, expressed in millions of bytes (not mebibytes). Files get a numeric suffix.
  • -W filecount caps the number of files. Combined with -C, this creates a genuine ring buffer: file N+1 overwrites file 1, and disk usage is bounded at roughly file_size × filecount.
  • -G rotate_seconds rotates on a time interval instead; the filename is passed through strftime, so -w 'cap-%Y%m%d-%H%M%S.pcap' produces self-describing names. With -G, -W limits how many files are produced before exiting rather than wrapping.
  • -z postrotate-command runs a command against each closed file — the hook for compressing or shipping it.
stateDiagram-v2
    [*] --> Writing1: tcpdump -w cap.pcap -C 100 -W 4
    Writing1: cap.pcap1 (up to 100 MB)
    Writing2: cap.pcap2
    Writing3: cap.pcap3
    Writing4: cap.pcap4
    Writing1 --> Writing2: size limit reached
    Writing2 --> Writing3: size limit reached
    Writing3 --> Writing4: size limit reached
    Writing4 --> Writing1: wraps — oldest file overwritten
    note right of Writing4
        Bounded at ~400 MB total.
        The incident you care about
        must occur within the window.
    end note

A four-file, 100-MB ring buffer as a state machine. The insight to take: a ring buffer converts “how much disk will this eat” (unbounded, dangerous) into “how far back can I see” (bounded, a capacity-planning question). Size the ring from your traffic rate and your detection latency: at 20 MB/s of captured bytes, a 400 MB ring holds only twenty seconds. Pair a wide ring with a tight snaplen — headers-only at -s 96 typically cuts captured volume by an order of magnitude on payload-heavy traffic.

Two more flags belong in the same conversation. -B size sets the kernel capture buffer in KiB — raise it when pcap_stats() reports drops. -U (packet-buffered) and --immediate-mode trade throughput for latency: the former flushes the output after each packet, the latter asks the kernel to deliver packets as they arrive rather than batching them (tcpdump(1)). Use them when piping into another program; avoid them at high packet rates.

Failure Modes

“0 packets captured” with a filter that looks right. Almost always either the wrong interface (check -D, and remember -i any exists) or a direction/VLAN issue. Verify the filter compiles to what you meant with -d before blaming the network.

Packets are being dropped and you did not notice. On SIGINT, tcpdump prints the pcap_stats() counters: packets received by filter, packets dropped by kernel, and packets dropped by interface (ps_recv, ps_drop, ps_ifdrop in struct pcap_stat). A non-zero kernel drop count means your capture is lying by omission — the missing packets look exactly like packets that were never sent. Fix by narrowing the filter, shrinking the snaplen, or raising -B.

Checksums are “incorrect” on locally generated traffic. This is the single most common false alarm. The NIC computes the checksum after the kernel hands off the packet, and the capture tap runs before that — so an outbound packet in your own capture legitimately carries a placeholder. This is checksum offload, covered in Checksum Offloads; -K disables tcpdump’s verification entirely (tcpdump(1)).

Impossibly large “frames.” Seeing a 30 000-byte TCP segment on a 1500-byte-MTU link is generic receive offload, not a miracle; see Segmentation Offloads GSO TSO and Maximum Transmission Unit and Path MTU Discovery. A local capture shows the aggregated super-packet, not what crossed the wire.

[|tcp] everywhere. Snaplen too small. Re-capture; you cannot recover the bytes from the file.

The capture itself perturbs the system. Decoding is expensive and DNS resolution is worse. On a loaded host: -nn, -w to a file, a tight filter, a small snaplen, and always -c or -W.

The tool is a parser running on hostile input. tcpdump’s printers are a large body of C that parses attacker-controlled bytes. That is the whole argument for -Z and for capturing to a file on the production host and decoding it somewhere else.

Alternatives and When to Choose Them

ToolWhere it runsChoose it when
tcpdumpUser space over libpcap/AF_PACKETDefault. Ubiquitous, tiny dependency footprint, present on hosts where nothing else is
tsharkUser space over libpcap, with Wireshark’s dissectorsYou need application-layer decoding, TLS decryption via a key-log file, or field extraction — see Dissecting a Capture with tshark
Wireshark GUIWorkstation, on a savefileInteractive analysis, following streams, flow graphs
dumpcapPrivileged helperYou want capture rights isolated from dissection code
eBPF / tcpdump-like BPF programsIn-kernel, at a TC or XDP hookYou need in-kernel aggregation rather than per-packet copies — see tc and cls_bpf (Traffic Control Hooks), XDP (eXpress Data Path)
AF_XDPZero-copy user-space ringsLine-rate capture where AF_PACKET’s copy is the bottleneck — AF_XDP Zero-Copy Sockets
SPAN / TAP + dedicated capture applianceOff-hostYou must not perturb the host at all, or you need the wire truth rather than the pre-offload truth

The last row deserves emphasis. A host-local capture is taken inside the host, after or before the offload engines have done their work. If the question is “what did the NIC actually put on the fibre,” only an external tap answers it.

Production Notes

A capture on a production host is an intervention, and the discipline is the same as any other: bound it in advance. The pattern that has aged well is a bounded ring with headers only, written raw and analysed elsewhere:

sudo tcpdump -i eth0 -nn -s 128 -w '/var/tmp/inc-%Y%m%d-%H%M%S.pcap' \
     -G 60 -W 30 -Z tcpdump 'host 198.51.100.20 and tcp port 443'

Line by line: -nn suppresses lookups that would both slow the loop and pollute the capture; -s 128 keeps link, network and transport headers plus TCP options while discarding payload; -w with a strftime pattern and -G 60 -W 30 produces a rolling thirty-minute window in one-minute files; -Z tcpdump drops privileges after the device is open; and the filter is as narrow as the question allows, so the kernel discards everything else before it is ever copied.

Three habits worth internalising. First, always check the drop counters when the capture ends — an analysis built on a capture with kernel drops is an analysis built on a biased sample. Second, write raw and decode elsewhere; every decoding decision is reversible if you kept the bytes and irreversible if you did not. Third, capture on both ends when the question is “who dropped it”: a packet present in the sender’s capture and absent from the receiver’s localises the loss to the network, and a packet present in both localises it to the application — a diagnosis no single capture can make.

Finally, note where the tap sits relative to the rest of the stack. Because the tap runs before IP processing, tcpdump sees packets that The Netfilter Framework and Hooks will later drop; because it runs after the driver, it does not see frames the NIC’s hardware filter rejected. For the receive path itself — NAPI polling, sk_buff allocation, the ptype_all walk — see The Network Receive Path, NAPI and Polled Receive and struct sk_buff.

See Also