Skip to main content
RunBook Academy

OPNsenseXXXVI · Packet Capture and DiagnosticsBPF filters for firewall traffic

BPF filters for firewall traffic — writing precise capture expressions

Intermediate⏱ ~15 mintcpdumpngrepWireshark (display filter reference)

What you'll learn

  • Write BPF filters for IP, transport, port, and direction
  • Match TCP flags with BPF expressions
  • Use byte offsets for advanced filtering
  • Combine expressions with and, or, not, parentheses

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

A well-written BPF filter captures exactly the slice of traffic the operator needs and nothing else. A poorly written filter either captures too much (drowning the operator in noise) or too little (missing the bug). The discipline is to write the smallest filter that answers the question, and to broaden it when the initial filter shows nothing.

This lesson covers the BPF primitives, TCP flag matching, byte offsets for advanced filtering, and the combination rules.

The BPF primitives

BPF primitives are short keywords the operator combines into an expression:

PrimitiveMatches
host Xframes where source or destination IP is X
src host Xframes where source IP is X
dst host Xframes where destination IP is X
net Xframes where source or destination IP is in network X (CIDR)
src net Xframes where source IP is in network X
dst net Xframes where destination IP is in network X
port Nframes where source or destination port is N (TCP or UDP)
src port Nframes where source port is N
dst port Nframes where destination port is N
tcpTCP frames
udpUDP frames
icmpICMP frames
arpARP frames
ip6IPv6 frames
vlan Nframes tagged with VLAN N
ether host Mframes where source or destination MAC is M
ether src Mframes where source MAC is M
ether dst Mframes where destination MAC is M

Each primitive is a self-contained test. The operator combines them with and, or, not, and parentheses to build the expression.

Read-only / SafeBPF src host and dst port
$ tcpdump -ni igb0 'src host 192.0.2.50 and dst port 443'
12:34:56.789012 aa:bb:cc:11:22:33 > 66:77:88:99:aa:bb, IPv4, length 74: 192.0.2.50.51820 > 203.0.113.5.443: Flags [S], seq 1234567890, win 65535, options [mss 1460,sackOK,ts val 1234567 ecr 0,nop,wscale 7], length 0

Illustrative output

Direction

The src and dst modifiers apply to hosts, networks, and ports. The operator uses them to capture inbound or outbound only:

  • src host X and dst port Y — frames from X to Y (outbound if X is on LAN).
  • dst host X and src port Y — frames to X from Y (inbound if X is on LAN).
  • host X and host Y — frames between X and Y in either direction.

The discipline is to write the direction explicitly. The operator who writes host X and gets traffic in both directions has to filter the output by hand. The operator who writes src host X gets only the relevant slice.

TCP flags

TCP flags are matched with the [tcpflags] qualifier and the named flag macros:

  • tcp-syn — SYN
  • tcp-ack — ACK
  • tcp-fin — FIN
  • tcp-rst — RST
  • tcp-push — PSH
  • tcp-urg — URG

The expression tcp[tcpflags] & tcp-syn != 0 matches any frame with SYN set, regardless of other flags. The expression tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0 matches SYN-only (connection starts, not SYN-ACK).

Read-only / SafeBPF TCP SYN only
$ tcpdump -ni igb0 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' -c 5
12:34:56.789012 aa:bb:cc:11:22:33 > 66:77:88:99:aa:bb, IPv4, length 74: 192.0.2.50.51820 > 203.0.113.5.443: Flags [S], seq ...
12:34:57.123456 aa:bb:cc:11:22:33 > 66:77:88:99:aa:bb, IPv4, length 74: 192.0.2.50.51822 > 203.0.113.5.443: Flags [S], seq ...
12:34:58.234567 aa:bb:cc:11:22:33 > 66:77:88:99:aa:bb, IPv4, length 74: 192.0.2.51.51824 > 203.0.113.5.443: Flags [S], seq ...

Illustrative output

Byte offsets for advanced filtering

BPF supports arbitrary byte-level matching with the bracket notation: proto[offset:length]. For TCP, tcp[13] is the flags byte (offset 13 from the start of the TCP header). The operator can match individual bits with hex masks:

  • tcp[13] & 0x02 != 0 — SYN set.
  • tcp[13] & 0x10 != 0 — ACK set.
  • tcp[13] & 0x01 != 0 — FIN set.
  • tcp[13] & 0x04 != 0 — RST set.

The discipline is to use the named macros (tcp-syn, etc.) where possible and fall back to byte offsets only when the named macros do not cover the case.

For UDP length matching, udp[4:2] is the UDP length field (offset 4, 2 bytes). The expression udp[4:2] > 500 matches UDP datagrams longer than 500 bytes.

For ICMP type and code, icmp[0] is the type byte and icmp[1] is the code byte. icmp[0] == 3 and icmp[1] == 4 matches “destination unreachable, fragmentation needed” — the PMTUD case.

Combining expressions

BPF expressions combine with and, or, not, and parentheses. The precedence is not > and > or. The operator should use parentheses to make intent explicit.

Examples:

  • host 192.0.2.50 and (port 80 or port 443) — web traffic from one host.
  • net 192.0.2.0/24 and not host 192.0.2.1 — traffic from the network excluding the gateway.
  • tcp and not src host 192.0.2.50 — TCP traffic not from a specific host (excluding the noisy host).
  • port 53 and (src host 192.0.2.50 or src host 192.0.2.51) — DNS queries from two hosts.

The discipline is to think about what should and should not be in the capture, and write the filter to include the former and exclude the latter.

Filtering on VLAN and tunnel traffic

VLAN-tagged frames have a 4-byte 802.1Q tag after the source MAC. The BPF vlan N primitive matches VLAN tag N. The operator uses this to filter on a specific VLAN:

  • vlan 10 and host 192.0.2.50 — traffic to/from a host on VLAN 10.

For tunnel traffic (WireGuard, IPsec, OpenVPN), the BPF filter sees the outer header — the encapsulating IP packet, not the inner payload. The operator must filter on the outer tuple. To see the inner payload, the operator captures on the tunnel interface (wg0, ipsec0, etc.) or uses Wireshark which can decrypt some tunnel types.

Common filter recipes

The filters the operator writes most often:

QuestionFilter
One host, all traffichost 192.0.2.50
One host, web onlyhost 192.0.2.50 and (port 80 or port 443)
One host, TCP onlyhost 192.0.2.50 and tcp
All SYN packetstcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0
All RST packetstcp[tcpflags] & tcp-rst != 0
DNS onlyport 53
ICMP onlyicmp
One VLANvlan 10
Exclude a hostnot host 192.0.2.100
Outbound to one destinationsrc net 192.0.2.0/24 and dst host 203.0.113.5

The “broaden when empty” discipline

The most common capture mistake is to write a too-narrow filter that excludes the bug. The operator sees an empty capture, concludes the firewall is not involved, and moves on. The bug is still there.

The discipline: when a filter returns nothing, broaden it. Remove the host restriction; remove the port restriction; capture everything on the interface for 30 seconds. Look for the unexpected packet. The unexpected packet is often the bug.

Read-only / Safebroaden the filter
$ tcpdump -ni igb0 -c 50 'not port 22'
12:34:56.789012 ... 192.0.2.50.51820 > 8.8.8.8.53: UDP, length 42
12:34:57.123456 ... 192.0.2.50.51822 > 8.8.8.8.53: UDP, length 55
12:34:58.234567 ... 192.0.2.51.51824 > 8.8.8.8.53: UDP, length 30
...

Illustrative output

Summary

  • BPF primitives (host, net, port, tcp, udp, etc.) combine with and, or, not, and parentheses.
  • TCP flags are matched with named macros (tcp-syn) or byte offsets (tcp[13] & 0x02).
  • Byte offsets (proto[offset:length]) handle advanced cases — UDP length, ICMP type/code, custom protocols.
  • BPF on encrypted tunnels shows the outer header only; capture on the tunnel interface to see the inner packet.
  • The discipline: write the smallest filter that answers the question, and broaden it when the initial filter shows nothing.

Knowledge check · 4 questions

  1. Q1. You want to capture only TCP SYN packets (connection starts, not SYN-ACK responses). Which BPF filter is correct?

  2. Q2. A BPF filter on the underlying WAN interface can show the inner source and destination of an IPsec-encrypted packet.

  3. Q3. Which of the following BPF filters are syntactically valid? Select all that apply.

  4. Q4. You write a precise filter for the bug you suspect, run the capture, and see no packets. What is the best next step?

Passing score: 75%. Answers are checked in this browser.