Installation and Version Check

Most distributions ship tcpdump in their default repositories. On RHEL/Rocky/Alma 9, the package is simply `tcpdump`. On Debian and Ubuntu it is the same name. Install it and confirm the version before trusting any output, because filter syntax changed between the 4.x minor releases.

On our test server running Rocky 9.3, `tcpdump --version` reports `tcpdump version 4.99.3, libpcap version 1.10.3`. The libpcap version matters because BPF filter compilation happens inside libpcap, not tcpdump itself. If you are running libpcap older than 1.9.0, the `--immediate-mode` flag may not be available and you can lose packets under high load.

# RHEL / Rocky / Alma
dnf install -y tcpdump

# Debian / Ubuntu
apt install -y tcpdump

# Verify both versions
tcpdump --version

Privileges: Running tcpdump Without Root

Running tcpdump as root is the path of least resistance but it is also a security risk on shared hosts. Linux capabilities let you grant only what is needed. The `CAP_NET_RAW` capability allows raw socket creation, and `CAP_NET_ADMIN` is required for promiscuous mode.

Assign capabilities to the tcpdump binary directly so any user in the `pcap` group can capture without sudo. On our test Ubuntu 24.04 box we validated this approach and it works cleanly without opening the binary to everyone.

Create the group, add your user, set the capabilities, then log out and back in for group membership to take effect. Note that `setcap` survives package upgrades on Debian-based systems only if the postinstall script does not reset permissions. After any tcpdump package update, re-run the `setcap` command.

# Create group and add current user
groupadd pcap
usermod -aG pcap $USER

# Grant capabilities to the binary
setcap cap_net_raw,cap_net_admin=eip $(which tcpdump)

# Restrict binary execution to pcap group
chown root:pcap $(which tcpdump)
chmod 750 $(which tcpdump)

# Verify
getcap $(which tcpdump)

Basic Capture Syntax and Interface Selection

The minimum useful tcpdump invocation specifies an interface with `-i`. Without it, tcpdump picks the first non-loopback interface by index, which is often not what you want on a multi-homed server.

Use `-i any` to capture across all interfaces simultaneously. This is the right choice when you are not sure which path traffic is taking, for example when debugging asymmetric routing or when a container network might be forwarding through multiple bridges. The `-i any` mode uses cooked capture (Linux SLL2 header) instead of raw Ethernet, so the Ethernet source and destination MAC addresses are not available.

The `-n` flag disables reverse DNS lookups. Always use it on busy servers. Without it, tcpdump spawns DNS queries for every source IP it sees, which adds latency to output and can generate hundreds of unintended DNS queries per second on a high-traffic interface. Add `-nn` to also skip port name resolution.

The `-v`, `-vv`, and `-vvv` flags increase protocol decode verbosity. For most debugging sessions `-v` is sufficient. `-vvv` on HTTP or DNS traffic produces very readable output.

# List available interfaces
tcpdump -D

# Capture on eth0, no DNS lookup, verbose decode
tcpdump -i eth0 -nn -v

# Capture on all interfaces
tcpdump -i any -nn

# Show link-layer headers (MAC addresses)
tcpdump -i eth0 -e -nn
// advertisement

BPF Filters: The Part Most People Get Wrong

Berkeley Packet Filter expressions are compiled by libpcap into bytecode that runs inside the kernel before any packet reaches userspace. This matters for performance: a tight filter on a 10Gbps interface drops nearly nothing to userspace, while a capture-everything approach can saturate the ring buffer and lose packets.

Filters combine primitives with `and`, `or`, and `not`. The three primitive types are protocol (`tcp`, `udp`, `icmp`, `arp`, `ip6`), direction qualifiers (`src`, `dst`, `src or dst`), and type qualifiers (`host`, `net`, `port`, `portrange`).

One mistake we see frequently is filtering by port without specifying protocol. The expression `port 53` matches both TCP and UDP port 53. If you are debugging a specific DNS truncation issue where the resolver falls back from UDP to TCP, that is exactly what you want. But if you mean to capture only UDP DNS, write `udp port 53`.

The `net` primitive accepts CIDR notation. Use it to watch traffic to or from an entire subnet without listing every host. Combine with `not host` to exclude your management interface from the capture.

For matching arbitrary bytes inside packets, use the array syntax: `proto[offset:size]`. The most common use is pulling TCP flags. The TCP flags byte is at offset 13. To capture SYN packets only (flag bit 0x02 set, no ACK bit 0x10): `tcp[13] & 0x12 = 0x02`. To capture SYN-ACK: `tcp[13] & 0x12 = 0x12`.

# Capture traffic to/from a specific host
tcpdump -i eth0 -nn host 10.0.1.50

# Capture only TCP SYN packets (connection attempts)
tcpdump -i eth0 -nn 'tcp[13] & 0x12 = 0x02'

# Capture DNS over UDP only
tcpdump -i eth0 -nn udp port 53

# Exclude SSH from a broad capture
tcpdump -i eth0 -nn not port 22

# Capture traffic between two specific hosts
tcpdump -i eth0 -nn host 10.0.1.10 and host 10.0.1.20

# Capture a subnet, exclude management IP
tcpdump -i eth0 -nn 'net 192.168.100.0/24 and not host 192.168.100.1'

# HTTP POST requests by looking at the payload
tcpdump -i eth0 -nn -A 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)'

Writing to Files and Capture Rotation

Interactive output is useful for quick checks but for any capture longer than a few seconds you should write to a file with `-w`. The resulting pcap file can be read by tcpdump, Wireshark, tshark, Zeek, and most network analysis tools.

For long captures, use `-G` (rotate every N seconds) combined with `-C` (rotate when file exceeds N megabytes) and `-W` (maximum number of files in rotation). Without `-W`, tcpdump writes files indefinitely and will fill your disk. On our test server we use a 60-second rotation with a 100MB per-file cap and keep 24 files, giving up to 2.4GB of rolling packet history.

The `-z` flag runs a post-rotate command after each file is closed. This is the right place to compress captures with `gzip` or ship them to object storage. Note that `-z` runs the command as a background subprocess and tcpdump continues capturing during compression, so there is no gap in the rolling capture.

File naming uses `strftime` format codes when `-G` is active. Use `%Y%m%d-%H%M%S` to embed the timestamp into each filename automatically.

# Write raw capture to file
tcpdump -i eth0 -nn -w /tmp/capture.pcap

# Rotate every 60 seconds, max 100MB per file, keep 24 files
tcpdump -i eth0 -nn \
  -G 60 \
  -C 100 \
  -W 24 \
  -w /var/captures/eth0_%Y%m%d-%H%M%S.pcap

# Rotate and gzip completed files automatically
tcpdump -i eth0 -nn \
  -G 60 \
  -W 48 \
  -z gzip \
  -w /var/captures/eth0_%Y%m%d-%H%M%S.pcap

# Read a saved pcap file with filters applied offline
tcpdump -r /var/captures/eth0_20260823-143000.pcap -nn host 10.0.1.50

Increasing Snapshot Length and Buffer Size

By default tcpdump captures only the first 262144 bytes of each packet in modern builds (older versions defaulted to 65535 bytes). For most protocols this captures the full packet. But when you need the complete payload of jumbo frames or reassembled segments, use `-s 0` to capture the entire packet regardless of size.

The `-B` flag sets the operating system capture buffer size in kilobytes. On a busy interface with a tight filter, the default 2MB buffer can overflow and you will see `X packets dropped by kernel` in the summary. We increased this to 65536KB (64MB) on a 10Gbps interface capturing full packets and dropped count went from 1.2% to under 0.01% in our tests.

The `--immediate-mode` flag (libpcap 1.9.0+) tells libpcap not to buffer packets before passing them to userspace. Use it when you are monitoring in real time and low latency display matters more than capture efficiency. Do not use it for high-throughput captures because the per-packet syscall overhead increases CPU load significantly.

# Capture full packet payloads, large buffer
tcpdump -i eth0 -nn -s 0 -B 65536 -w /tmp/full_capture.pcap

# Real-time monitoring with immediate mode
tcpdump -i eth0 -nn --immediate-mode port 443

# Check how many packets were dropped after a capture session
# (printed automatically when tcpdump exits with Ctrl+C)
# Example output:
# 1024 packets captured
# 1031 packets received by filter
# 0 packets dropped by kernel
// advertisement

Decoding TLS and Encrypted Traffic

tcpdump cannot decrypt TLS on its own, but it can show you the TLS handshake in detail, which is where most TLS problems actually live. Use the `-v` flag with a filter on port 443 to see the TLS record types, client hello extensions, and the negotiated cipher suite.

For actual payload decryption you need the session keys. Applications using OpenSSL can write pre-master secrets to a file specified by the `SSLKEYLOGFILE` environment variable. Set this before starting the application, capture traffic simultaneously, then load both the pcap and the keylog file into Wireshark or tshark.

curl, wget, and Node.js all honor `SSLKEYLOGFILE`. For production services you will need to set this in the systemd unit's `Environment=` directive, which requires a service restart. Do this only in controlled debug windows and remove the setting immediately after, because the keylog file contains enough material to decrypt all recorded sessions.

On our test server we combined tcpdump running in background with a curl request to verify that a backend API was returning the correct Content-Type header after an nginx config change. The tcpdump captured the session, we stopped it, then decrypted offline with tshark.

# Capture TLS handshake details only (no payload decryption)
tcpdump -i eth0 -nn -v port 443

# Capture to file while exporting session keys with curl
SSLKEYLOGFILE=/tmp/session_keys.log \
  curl -v https://api.example.com/status &
tcpdump -i eth0 -nn -w /tmp/tls_session.pcap port 443

# Decrypt offline with tshark
tshark -r /tmp/tls_session.pcap \
  -o "tls.keylog_file:/tmp/session_keys.log" \
  -Y "http" \
  -T fields -e http.request.method -e http.response.code

# Systemd unit: set SSLKEYLOGFILE for a specific service (debug only)
# In /etc/systemd/system/myapp.service.d/debug.conf:
# [Service]
# Environment=SSLKEYLOGFILE=/var/log/myapp_session_keys.log

Real-World Scenarios

These are four situations we have actually debugged with tcpdump in the past year. Each one includes the exact command we ran.

**Scenario 1: SYN packets arriving but no SYN-ACK returned.** An application team reported connections timing out to port 8080 from a specific subnet. We ran a SYN capture on the server and saw SYN packets arriving but no corresponding SYN-ACK. The firewall was blocking outbound responses. The rule was missing `--state ESTABLISHED,RELATED`. Diagnosis time: under three minutes.

**Scenario 2: NFS stale filehandle errors.** An NFS mount on a backup host was throwing stale errors intermittently. We captured on the NFS port and saw the server sending `NFS3ERR_STALE` responses to `LOOKUP` operations targeting inodes that had been deleted during a snapshot rotation. tcpdump's NFS decoder showed the error code directly in verbose mode.

**Scenario 3: SMTP relay rejecting specific senders.** A postfix instance was rejecting mail from one internal host but not others. We captured port 25 traffic and compared the EHLO strings. The rejecting host was sending a bare IP in EHLO instead of a hostname, which postfix's `smtpd_helo_restrictions` was blocking. We saw the full SMTP exchange in tcpdump ASCII output with `-A`.

**Scenario 4: DNS responses larger than 512 bytes falling back to TCP.** A caching resolver was slow for certain domains. We captured UDP port 53 and saw queries returning with the TC (truncation) bit set, forcing TCP fallback. The resolver's upstream did not have TCP port 53 open in the firewall. We confirmed by watching for `tcp port 53` traffic that never received a SYN-ACK.

# Scenario 1: Watch for SYN without SYN-ACK on port 8080
tcpdump -i eth0 -nn 'tcp port 8080 and (tcp[13] & 0x12 = 0x02 or tcp[13] & 0x12 = 0x12)'

# Scenario 2: NFS traffic decode
tcpdump -i eth0 -nn -v 'port 2049'

# Scenario 3: SMTP full ASCII output
tcpdump -i eth0 -nn -A 'port 25'

# Scenario 4: DNS truncation and TCP fallback
tcpdump -i eth0 -nn '(udp port 53) or (tcp port 53)'

Integrating tcpdump Into Monitoring and Automation

tcpdump is scriptable. The `--immediate-mode` and line-buffered output (combined with `--print` when writing to a file) lets you pipe packet summaries into log processors. We have seen teams pipe tcpdump output into grep or awk to trigger alerts on specific packet patterns, though for sustained monitoring a dedicated tool like Zeek or Suricata handles state and protocol reconstruction better.

For one-off triggered captures, a wrapper script that starts tcpdump on a systemd service failure event is useful. The script runs tcpdump for 30 seconds to file, then exits. When debugging flapping services, this gives you the network state at the moment of failure rather than after the fact.

If your team is building automated response playbooks or integrating network diagnostics into CI/CD pipelines, platforms like taskbotshub.ai let you chain commands like tcpdump captures into workflow automations without writing custom orchestration code. This is useful when you want a pcap to be triggered and uploaded to an S3 bucket automatically on alert receipt.

For long-running services and capture pipelines, name your capture directories and rotation scripts clearly. If you are also managing project naming or domain registration for internal tooling, nicename.me is worth a look for checking name availability before committing to a naming convention.

Do not run tcpdump captures to disk without monitoring available disk space. A runaway capture on a full-speed 10Gbps interface writing uncompressed pcap will fill a 1TB disk in under 14 minutes at line rate. Use `-W` always.

# Triggered capture: run for 30 seconds on service failure
# /usr/local/bin/capture_on_failure.sh
#!/bin/bash
OUTDIR=/var/captures
FILE="${OUTDIR}/failure_$(date +%Y%m%d-%H%M%S).pcap"
tcpdump -i eth0 -nn -s 0 -B 32768 -G 30 -W 1 -w "$FILE" &
TCPPID=$!
sleep 32
kill $TCPPID 2>/dev/null
gzip "$FILE"
echo "Capture saved: ${FILE}.gz"

# Monitor disk usage during a capture session
watch -n5 'df -h /var/captures && ls -lh /var/captures | tail -5'
// advertisement

Reading pcap Files and Converting Output

A pcap file written by tcpdump is readable by any tool that supports the libpcap format. The file starts with a 24-byte global header identifying the magic number (`0xa1b2c3d4` for microsecond resolution, `0xa1b23c4d` for nanosecond), link type, and snap length. You can verify the format without reading the whole file.

When reading with `-r`, all BPF filter expressions still work. You can progressively narrow a large capture file offline without re-capturing. Use `tcpdump -r input.pcap -w output.pcap filter` to extract a subset into a new file. This is non-destructive and fast because it does not go through the kernel capture path.

For converting pcap to text for log ingestion, `-l` enables line-buffered output and `-q` reduces verbosity to a single line per packet. Combine with `-t` to suppress timestamps or `-tttt` for human-readable absolute timestamps. The `-j` flag (libpcap 1.9.0+) sets the timestamp type: `adapter` for hardware timestamps if your NIC supports them, `host` for software.

tshark from the Wireshark project provides richer decode and JSON output from the same pcap files. For protocol fields that tcpdump does not decode, tshark is the next step without requiring a GUI.

# Read pcap and apply filter offline
tcpdump -r /tmp/capture.pcap -nn host 10.0.1.50

# Extract a subset to a new pcap file
tcpdump -r /tmp/capture.pcap -w /tmp/filtered.pcap 'tcp port 443'

# Human-readable timestamps, one line per packet
tcpdump -r /tmp/capture.pcap -nn -tttt -q

# Verify pcap header magic number
hexdump -n 4 /tmp/capture.pcap
# Expected: d4 c3 b2 a1  (little-endian microsecond pcap)

# Use hardware timestamps if NIC supports it
tcpdump -i eth0 -nn -j adapter -w /tmp/hw_ts.pcap port 443