OPNsenseXXXVI · Packet Capture and DiagnosticsBPF filters for firewall traffic
BPF filters for firewall traffic — writing precise capture expressions
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
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:
| Primitive | Matches |
|---|---|
host X | frames where source or destination IP is X |
src host X | frames where source IP is X |
dst host X | frames where destination IP is X |
net X | frames where source or destination IP is in network X (CIDR) |
src net X | frames where source IP is in network X |
dst net X | frames where destination IP is in network X |
port N | frames where source or destination port is N (TCP or UDP) |
src port N | frames where source port is N |
dst port N | frames where destination port is N |
tcp | TCP frames |
udp | UDP frames |
icmp | ICMP frames |
arp | ARP frames |
ip6 | IPv6 frames |
vlan N | frames tagged with VLAN N |
ether host M | frames where source or destination MAC is M |
ether src M | frames where source MAC is M |
ether dst M | frames 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.
$ 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 0Illustrative 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— SYNtcp-ack— ACKtcp-fin— FINtcp-rst— RSTtcp-push— PSHtcp-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).
$ tcpdump -ni igb0 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' -c 512: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:
| Question | Filter |
|---|---|
| One host, all traffic | host 192.0.2.50 |
| One host, web only | host 192.0.2.50 and (port 80 or port 443) |
| One host, TCP only | host 192.0.2.50 and tcp |
| All SYN packets | tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0 |
| All RST packets | tcp[tcpflags] & tcp-rst != 0 |
| DNS only | port 53 |
| ICMP only | icmp |
| One VLAN | vlan 10 |
| Exclude a host | not host 192.0.2.100 |
| Outbound to one destination | src 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.
$ 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 withand,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
Q1. You want to capture only TCP SYN packets (connection starts, not SYN-ACK responses). Which BPF filter is correct?
Q2. A BPF filter on the underlying WAN interface can show the inner source and destination of an IPsec-encrypted packet.
Q3. Which of the following BPF filters are syntactically valid? Select all that apply.
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.