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
| Offset | Size | Field |
|---|---|---|
| 0 | 2 | Source port |
| 2 | 2 | Destination port |
| 4 | 4 | Sequence number |
| 8 | 4 | Acknowledgement number |
| 12 | 1 | Data offset in the high nibble, reserved in the low |
| 13 | 1 | Flags |
| 14 | 2 | Window size |
| 16 | 2 | Checksum |
| 18 | 2 | Urgent 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.
| Bit | Value | Hex | Flag | pcap name |
|---|---|---|---|---|
| 0 | 1 | 0x01 | FIN | tcp-fin |
| 1 | 2 | 0x02 | SYN | tcp-syn |
| 2 | 4 | 0x04 | RST | tcp-rst |
| 3 | 8 | 0x08 | PSH | tcp-push |
| 4 | 16 | 0x10 | ACK | tcp-ack |
| 5 | 32 | 0x20 | URG | tcp-urg |
| 6 | 64 | 0x40 | ECE | tcp-ece |
| 7 | 128 | 0x80 | CWR | tcp-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.
| Filter | Reads as |
|---|---|
tcp[tcpflags] & tcp-syn != 0 | SYN is set, other flags don't matter |
tcp[tcpflags] == tcp-syn | SYN set and nothing else, a pure SYN |
tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn | SYN set and ACK clear |
tcp[tcpflags] == tcp-syn|tcp-ack | Exactly 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:
| Text | Hex |
|---|---|
GET | 0x47455420 |
POST | 0x504f5354 |
PUT | 0x50555420 |
HEAD | 0x48454144 |
HTTP | 0x48545450 |
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
| Filter | Matches |
|---|---|
ip[8] < 5 | IP TTL below 5, near-expiring packets |
ip[1] & 0xfc >> 2 == 46 | DSCP 46 (EF), voice traffic |
ip[6] & 0x40 != 0 | Don't Fragment bit set |
ip[6] & 0x20 != 0 | More Fragments bit set |
ip[6:2] & 0x1fff != 0 | A non-first fragment |
tcp[14:2] == 0 | Zero window, receiver is stalled |
udp[4:2] > 512 | UDP length above 512 bytes |
ether[12:2] == 0x8100 | 802.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.