BPF filters let you zero in on specific network traffic, but their real power comes when you combine simple conditions into complex expressions.

Let’s watch tcpdump in action. Imagine we want to see only TCP traffic destined for port 80 on our local machine, but only if the SYN flag is set and the packet is larger than 64 bytes.

sudo tcpdump -i eth0 'tcp and dst port 80 and tcp[tcpflags] & tcp-syn != 0 and len > 64'

This command will start capturing packets on eth0. If a packet meets all these criteria, tcpdump will print its summary. You’ll see lines like:

14:32:15.123456 IP 192.168.1.100.54321 > 192.168.1.1.80: Flags [S], seq 1234567890, win 65535, options [mss 1460], length 0

This is a SYN packet, destined for port 80, originating from 192.168.1.100, and it’s not a zero-length packet (though the BPF filter explicitly checks len > 64, so this example might not strictly match if the payload is zero, but the IP/TCP headers add up). The key is that only packets matching all conditions are displayed.

The problem tcpdump’s BPF filters solve is information overload. Without filters, you’re drowning in network noise. BPF lets you become a forensic investigator, pulling out just the specific packets you need for analysis, debugging, or security monitoring. It’s like having a scalpel instead of a sledgehammer for network traffic.

Internally, BPF (Berkeley Packet Filter) is a mini virtual machine that runs within the kernel. When you specify a filter, the kernel compiles it into bytecode for this VM. As network packets arrive, they are passed through this VM. If the VM’s program (your filter) evaluates to true for a packet, the packet is passed up to user space (where tcpdump lives); otherwise, it’s dropped. This kernel-level filtering is incredibly efficient, preventing unnecessary data from even reaching user-space applications.

The core building blocks are simple conditions like tcp, udp, port 80, host 192.168.1.1, or net 192.168.1.0/24. These are great for broad strokes. But the real power comes from combining them.

Boolean Expressions:

  • and (or &&): Both conditions must be true. tcp and port 80 means TCP and port 80.
  • or (or ||): Either condition can be true. udp or tcp means UDP or TCP.
  • not (or !): Negates the condition. not port 22 means any traffic not to port 22.
  • Parentheses (): Used to group conditions and control the order of evaluation, just like in standard algebra. (tcp or udp) and port 53 means either TCP or UDP traffic, and it must be to port 53.

Offset Expressions: These are where things get really granular. They allow you to inspect specific bytes within a packet. The syntax is [offset:size].

  • offset: The starting byte position (0-indexed).
  • size: The number of bytes to check (e.g., 1 for a single byte, 2 for a 16-bit integer, 4 for a 32-bit integer).

You can perform bitwise operations on these bytes. The most common is checking TCP flags. The TCP header starts with a 20-byte fixed part. The flags are located at offset 13 of the TCP header, as a single byte.

  • tcp[13] gives you the byte at offset 13 of the TCP header.
  • tcpflags is a shorthand for tcp[13].

We can then use bitwise AND (&) to check for specific flags. The common TCP flags are defined as constants:

  • tcp-fin (0x01)
  • tcp-syn (0x02)
  • tcp-rst (0x04)
  • tcp-psh (0x08)
  • tcp-ack (0x10)
  • tcp-urg (0x20)
  • tcp-ece (0x40)
  • tcp-cwr (0x80)

So, tcp[tcpflags] & tcp-syn != 0 checks if the SYN flag is set. The != 0 is crucial because if the bitwise AND results in a non-zero value, it means the SYN bit was indeed set in that byte.

You can also check for specific byte values directly. For example, to find ARP packets (EtherType 0x0806): ether proto 0x0806 is the high-level way. But you could also do it with offsets on the Ethernet frame, which is 14 bytes long: ether[12:2] = 0x0806.

Consider checking for packets with a specific IP Protocol number. The IP header’s protocol field is at offset 9 (1 byte). To find only ICMP packets (protocol 1): ip proto 1 is the easy way. The offset way is ip[9] = 1.

You can also check ranges. To find TCP packets with a payload size between 100 and 200 bytes: tcp and len >= 100 and len <= 200. Alternatively, using the len keyword which refers to the total packet length: len > 64 and len < 164.

The tcpdump manual page is your best friend here. It lists all the available primitives and details the offset expressions.

When dealing with complex filters, especially those involving offsets and bitwise operations, it’s easy to make mistakes. Always test your filters on a small, representative sample of traffic before deploying them in a critical monitoring scenario. A typo in an offset or an incorrect bitmask can lead to missing crucial packets or capturing too much irrelevant data.

The next hurdle is often learning how to integrate these BPF filters with other tools, like Wireshark or scripting languages, for more advanced analysis and automation.

Want structured learning?

Take the full Tcpdump course →