tcpdump Cheatsheet

TCP Flags and Byte Offsets

Use this tcpdump reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The byte-slice syntax

BPF lets you read raw bytes out of a header. That is how you express anything the named primitives do not cover.

proto [ offset : size ]

- proto is the layer whose header start is offset 0: ether, ip, ip6, arp, tcp, udp, icmp. - offset counts bytes from the start of that header. - size is 1, 2, or 4 bytes, defaulting to 1. Multi-byte reads are big-endian, which is network byte order, so no swapping needed.

tcp[13]      # 1 byte  at TCP header offset 13
ip[2:2]      # 2 bytes at IP header offset 2, the total length
tcp[4:4]     # 4 bytes at TCP offset 4, the sequence number

Offsets are relative to the header, and pcap already knows where the IP header ended, so tcp[0] is the TCP header's own first byte regardless of IP options.

TCP header map

OffsetSizeField
02Source port
22Destination port
44Sequence number
84Acknowledgement number
121Data offset in the high nibble, reserved in the low
131Flags
142Window size
162Checksum
182Urgent pointer
20…Options, then payload

So tcp[2:2] > 1023 matches a destination port above 1023, and tcp[14:2] == 0 matches a zero-window advertisement, which is a receiver telling you it has stopped reading.

Byte 13, the flags byte

Byte 13 packs eight one-bit flags. Bit 0 is the least significant.

BitValueHexFlagpcap name
010x01FINtcp-fin
120x02SYNtcp-syn
240x04RSTtcp-rst
380x08PSHtcp-push
4160x10ACKtcp-ack
5320x20URGtcp-urg
6640x40ECEtcp-ece
71280x80CWRtcp-cwr

tcp[tcpflags] is a pcap alias for the literal 13, so these two are the same filter and the named form is the one to write:

tcpdump 'tcp[tcpflags] & tcp-syn != 0'
tcpdump 'tcp[13] & 2 != 0'

The byte math. tcp[13] fetches the flags byte, & masks it against the bit you care about, and != 0 asks whether that bit survived. A SYN-ACK has flags byte 0x12 (SYN 2 + ACK 16 = 18), so 0x12 & 0x02 = 0x02, which is nonzero, so the filter matches.

Set versus exactly-equal

This is where most wrong filters come from.

FilterReads as
tcp[tcpflags] & tcp-syn != 0SYN is set, other flags don't matter
tcp[tcpflags] == tcp-synSYN set and nothing else, a pure SYN
tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-synSYN set and ACK clear
tcp[tcpflags] == tcp-syn|tcp-ackExactly SYN+ACK, the handshake reply
# New connection attempts only, not the SYN-ACK coming back
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'

# Both halves of every handshake
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0'

# Resets, the fastest "who is refusing me" check
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'

# Connection teardown, graceful or not
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-fin|tcp-rst) != 0'

& binds tighter than == here, so tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn parses as (flags & (SYN|ACK)) == SYN. Add the parentheses anyway if it reads clearer to you.

Scan and anomaly patterns:

# Null scan, no flags at all
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] == 0'

# Xmas scan, FIN + PSH + URG
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] == tcp-fin|tcp-push|tcp-urg'

# SYN and FIN together, illegal, always crafted
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin) ==
  (tcp-syn|tcp-fin)'

TCP payload length, the real expression

There is no tcp.len in BPF. You compute it: total IP length, minus the IP header, minus the TCP header.

(ip total length) - (IP header length) - (TCP header length)
   ip[2:2]           (ip[0]&0xf)<<2       (tcp[12]&0xf0)>>2

Both header lengths are stored in 4-byte words, so each needs a shift to become bytes. (ip[0]&0xf) grabs the IHL nibble and <<2 multiplies by 4. (tcp[12]&0xf0) grabs the data-offset nibble already shifted left by 4, so >>2 is a right-shift by 4 then a left-shift by 2, which nets out to multiply by 4.

# Any TCP packet on port 80 that actually carries data
sudo tcpdump -i eth0 -nn 'tcp port 80 and
  (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

This is IPv4 only. IPv6 has a fixed 40-byte header and a payload-length field, so the IHL term disappears, but extension headers make a general IPv6 version impractical in BPF. Filter IPv6 by port and let a display filter (tcp.len > 0) do the rest.

Matching payload bytes

tcp[X:4] at a computed offset lets you match the first bytes of the payload, which is how you find HTTP verbs without an application dissector.

# 'GET ' = 0x47 0x45 0x54 0x20 at the start of the payload
sudo tcpdump -i eth0 -nn \
  'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'

# 'POST' = 0x504f5354
sudo tcpdump -i eth0 -nn \
  'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354'

# 'HTTP' in a response = 0x48545450
sudo tcpdump -i eth0 -nn \
  'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x48545450'

((tcp[12:1] & 0xf0) >> 2) is the TCP header length in bytes, so it is the offset where the payload begins. Handy ASCII values:

TextHex
GET 0x47455420
POST0x504f5354
PUT 0x50555420
HEAD0x48454144
HTTP0x48545450
SSH-0x5353482d

TLS is easier, because the record header sits at a fixed payload offset:

# TLS handshake records, content type 0x16
sudo tcpdump -i eth0 -nn \
  'tcp port 443 and (tcp[((tcp[12:1] & 0xf0) >> 2)] = 0x16)'

Other useful raw offsets

FilterMatches
ip[8] < 5IP TTL below 5, near-expiring packets
ip[1] & 0xfc >> 2 == 46DSCP 46 (EF), voice traffic
ip[6] & 0x40 != 0Don't Fragment bit set
ip[6] & 0x20 != 0More Fragments bit set
ip[6:2] & 0x1fff != 0A non-first fragment
tcp[14:2] == 0Zero window, receiver is stalled
udp[4:2] > 512UDP length above 512 bytes
ether[12:2] == 0x8100802.1Q tagged frame

Verify any byte expression before you trust a capture window to it: tcpdump -y EN10MB -d '<expr>' compiles it and prints the BPF program without needing an interface or root.