📑 Daftar Isi
- What's Actually Wrong With Your Capture Filter
- The Fix, Step by Step
- Step 1: Fix the Syntax and Get Precedence Right
- Step 2: Fix Permissions and Capabilities
- Step 3: Confirm the Interface and Traffic Direction
- Step 4: Verify What Your Filter Actually Compiles To
- Step 5: Keep the Capture Fast and Stable
- Handy BPF Quick Reference
- Quick Troubleshooting Table
- Related Reading
- Q: Why does tcpdump need root for almost everything?
- Q: My filter parses fine but captures zero packets. What's most likely?
- Q: What does "dropped by kernel" mean in the summary line?
- Q: Can I run capture filters non-interactively in scripts or cron jobs?
- Q: I filtered by port 53 but saw no DNS response. Why?
How to Fix TCPDump Capture Filter on Linux – Step-by-Step BPF Troubleshooting Guide
Straight to it. You typed a tcpdump command that looked perfectly correct, hit Enter, and got either a “syntax error” or something worse: dead silence. Zero packets, no output, no explanation. If you’re inside an incident right now, skip the backstory. Work through the steps below in order, and you’ll land on the fix quickly.
Let me be real with you. After building and fixing tcpdump capture filter problems on Linux production boxes more times than I’d like to count, I can tell you tcpdump is almost never the problem. The filter is. And filters fail four ways: they won’t parse, they’re not allowed to run, they target the wrong interface or direction, or they’re slow enough to drop the packets you actually care about. Each failure has a quick, mechanical fix.
What’s Actually Wrong With Your Capture Filter
Understanding the mechanism helps you stop guessing. Every time tcpdump starts, it compiles your filter expression into a small BPF program and attaches it directly to the kernel. Packets are matched before your process ever sees them. That’s what makes line-rate capture possible, and it’s also what makes the syntax unforgiving. A filter that parses cleanly but asks for the wrong thing will quietly return nothing while the wire is flooded with traffic. That silent failure is the dangerous one, because it looks like “nothing is happening.” People panic, restart services, flush firewall rules, and burn the very evidence window they were trying to open.
So make the diagnosis structured, not emotional.
In my experience, three causes account for roughly 95% of broken filters found in production: BPF syntax and precedence mistakes, missing permissions or capabilities, and the wrong interface, VLAN, or traffic direction. The remaining few percent are performance issues – DNS resolution slowing the capture to a crawl, or a kernel buffer that keeps dropping packets. Every single one is quick to check, and most take under two minutes to verify once you know what to look for.
Symptoms You’ll See
- tcpdump: syntax error – the expression was rejected before capture even started.
- You don’t have permission… – your user can’t open a raw socket.
- Zero packets despite obvious traffic – wrong interface, VLAN, or direction.
- Slow capture or a summary line full of “dropped by kernel”.
The Fix, Step by Step
Work these in order. Each step narrows the problem by elimination, so don’t skip ahead just because you think you know the culprit. I’ve been wrong about that too many times to recommend it.
Step 1: Fix the Syntax and Get Precedence Right
The number one offender is an incomplete primitive. Look at this classic:
tcpdump -i eth0 port 80 or 443
Which produces:
tcpdump: syntax error
Why? Because 443 is not a complete filter primitive. In BPF terms, every chunk needs a qualifier: port 443, host 10.0.0.1, net 192.168.1.0/24. A bare number means nothing to the compiler, so it refuses the whole expression. The fix is boring and correct:
tcpdump -i eth0 'port 80 or port 443'
Then there’s operator precedence, the silent trap. BPF follows a strict rule: not binds tighter than and, and and binds tighter than or. Consider this one:
tcpdump -i eth0 'host 192.168.1.10 and not port 22 or port 443'
It reads as (host 192.168.1.10 and not port 22) or port 443. Almost certainly not what you meant. Force the grouping you actually want with parentheses:
tcpdump -i eth0 'host 192.168.1.10 and not (port 22 or port 443)'
Now it means exactly what it says: traffic from that host, except anything on port 22 or port 443.
Quote Your Filters. Always. Here’s Why.
This is the smallest fix and the one people dismiss first. Wrap every BPF expression in single quotes. Real filters are full of characters the shell wants to reinterpret: the exclamation mark in ! (negation) triggers history expansion in interactive bash, a dollar sign gets expanded, and things like [13] or & cause all kinds of confusion. For example, a legitimate filter that checks the TCP SYN flag:
tcpdump -i eth0 'tcp[13] & 2 != 0'
Leave that unquoted and bash will mangle it before tcpdump ever sees it. Single quotes hand the expression to tcpdump untouched. Make it muscle memory.
One more tool while we’re here. If a filter is long and you reuse it constantly, read it from a file instead:
tcpdump -i eth0 -F /etc/tcpdump/filter.txt
The file contains the raw BPF expression, no quotes. Note that when -F is used, any filter expression on the command line is ignored. Pick one source of truth.

Step 2: Fix Permissions and Capabilities
The second most common wall is this error:
tcpdump: eth0: You don't have permission to perform this capture on that device
The filter isn’t the problem this time. tcpdump needs raw access to the network socket, which on Linux translates to the CAP_NET_RAW capability (CAP_NET_ADMIN for some cases). A regular user doesn’t have it. The straightforward fix is sudo:
sudo tcpdump -i eth0 -c 20 'tcp port 443'
But if your team runs captures constantly, typing sudo every few minutes gets old fast. The alternative is to grant the tcpdump binary capabilities so a specific user can capture without root:
sudo setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump
getcap /usr/sbin/tcpdump
A healthy getcap output looks like this:
/usr/sbin/tcpdump cap_net_admin,cap_net_raw=eip
After that, non-root users can run tcpdump. But treat this as a tradeoff, not a free win. Capabilities like this mean anyone who can execute tcpdump can inspect live traffic. On systems handling sensitive data, prefer sudo plus a dedicated group over global capability grants.
Step 3: Confirm the Interface and Traffic Direction
Now the annoying case: the filter is valid, but zero packets. First question to ask yourself: is the interface right? This is a more common trap than you’d expect, especially on servers with a pile of interfaces. List what tcpdump can actually capture on:
tcpdump -D
Cross-check the output against ip -br link to verify the interface exists and is up. If traffic actually rides a bridge while you’re bound to a physical interface, or you’re just unsure, use the catch-all:
tcpdump -i any 'tcp port 443'
-i any captures across every interface at once. Great for initial triage before you lock onto a specific interface. So here’s the deal: when in doubt, start broad, then narrow.
Next, VLAN. If your server receives tagged traffic from a trunk, ordinary BPF filters tend to miss it because the matcher doesn’t automatically look through the VLAN header. Add the vlan keyword up front:
tcpdump -i eth0 'vlan and host 10.0.0.5'
Then direction. A plain port 443 filter matches port 443 on either the source or destination side. If you only want one direction, say so explicitly:
tcpdump -i eth0 'tcp dst port 443' # outbound
tcpdump -i eth0 'tcp src port 443' # inbound response
And check your IP family. If the traffic you care about is IPv6, an IPv4-based host 10.0.0.x filter sits there in silence. Prefix the host match with the protocol you expect:
tcpdump -i eth0 'ip host 10.0.0.5'
tcpdump -i eth0 'ip6 host 2001:db8::5'
Key takeaway for this step: a filter that compiles without error is not proof that it matches anything. Verify the interface, the VLAN, and the direction before you doubt the switch or the firewall.
Step 4: Verify What Your Filter Actually Compiles To
If you still suspect the filter itself, tcpdump will show you the compiled result. Use the -d flag:
tcpdump -d 'tcp dst port 443'
You’ll see the raw BPF assembly, something like:
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 4
(002) jeq #0x800 jt 5 jf 6
You don’t need to memorize BPF assembly. The point is diagnostic: if -d compiles without error, your syntax is valid. If it errors here, the problem is the filter, full stop. There are also -dd (C code form) and -ddd (decimal form) if you’re embedding the filter into a program. This is the single most underused debugging tool in the whole toolchain.
Step 5: Keep the Capture Fast and Stable
Two small settings make a disproportionate difference. First, -nn disables name and service resolution:
tcpdump -i eth0 -nn -c 50 'tcp port 443'
Without -nn, tcpdump tries to reverse-resolve every IP to a hostname and every port to a service name. That means DNS lookups, which are slow and can stall your output while a lookup times out. On production, use -nn by default.
Second, the capture buffer. When the summary line ends with something like:
10 packets captured
52 packets received by filter
41 packets dropped by kernel
the kernel is discarding packets because the buffer filled faster than you consumed it. Raise the buffer size:
tcpdump -i eth0 -nn -B 4096 -w /tmp/capture.pcap 'tcp port 443'
-B 4096 allocates a 4 MB buffer. For longer captures, combine -w to write to a file so you don’t flood your terminal, then analyze later with tcpdump -r or a GUI tool. That combination alone fixes most of the “slow capture” complaints.
Handy BPF Quick Reference
| Filter | Meaning |
|---|---|
| host 203.0.113.5 | Packets to or from 203.0.113.5 |
| src host 203.0.113.5 | Only packets originating from that host |
| dst host 203.0.113.5 | Only packets destined to that host |
| net 10.0.0.0/8 | Packets involving that subnet |
| tcp port 443 | TCP traffic with port 443 on either side |
| udp port 53 | UDP traffic with port 53 (DNS) |
| ip6 host 2001:db8::5 | IPv6 traffic to or from that host |
| vlan and host 10.1.1.5 | That host even if traffic is VLAN-tagged |
| ether proto 0x0806 | ARP frames |
Quick Troubleshooting Table
| Symptom | Likely Cause | Quick Fix |
|---|---|---|
| tcpdump: syntax error | Incomplete primitive, e.g. port 80 or 443 | Quote it and write port 80 or port 443 |
| You don’t have permission… | Missing CAP_NET_RAW | Use sudo, or setcap cap_net_raw,cap_net_admin=eip |
| Zero packets despite traffic | Wrong interface, VLAN, or src/dst direction | Use tcpdump -D, -i any, prepend vlan, be explicit with dst/src |
| Strange port names in output | Service lookup via /etc/services | Add -nn |
| High “dropped by kernel” | Capture buffer too small | Use -B 4096 or higher |
| Valid filter, wrong results | Precedence of and/or surprises you | Add parentheses, confirm with -d |
Related Reading
- Berkeley Packet Filter: what your filter actually compiles to
- Linux network troubleshooting checklist for NOC teams
- tcpdump vs Wireshark: when to use which
- Linux capabilities and sudo: secure packet capture
Q: Why does tcpdump need root for almost everything?
Capturing raw packets requires CAP_NET_RAW (and often CAP_NET_ADMIN), which regular users don’t hold. Rather than running everything as root, grant the tcpdump binary capabilities with setcap. Just remember that this lets anyone who can execute tcpdump inspect traffic, so weigh the tradeoff on sensitive hosts.
Q: My filter parses fine but captures zero packets. What’s most likely?
Almost always the interface, VLAN header, or direction. Confirm the interface with tcpdump -D against ip -br link, start with -i any if unsure, add the vlan keyword for tagged traffic, and be explicit with src or dst port so you capture the direction you actually need.
Q: What does “dropped by kernel” mean in the summary line?
The kernel accepted packets faster than the capture buffer could hold them and discarded the excess before you read them. Shrink the loss by raising the buffer with -B (e.g. -B 4096) and disabling live DNS resolution with -nn, especially during high-throughput captures.
Q: Can I run capture filters non-interactively in scripts or cron jobs?
Yes, but keep the expression in single quotes inside your script so the shell doesn’t reinterpret it, and store complex filters in a file with -F. Write output to a pcap with -w so the capture survives even if your script’s stdout goes nowhere.
Q: I filtered by port 53 but saw no DNS response. Why?
A plain port 53 filter matches both TCP and UDP, but if you wrote tcp port 53, UDP-based DNS traffic is excluded. DNS traditionally runs over UDP, so use udp port 53 for queries and responses, or just port 53 if you want both protocols.
Work through these steps in the order they’re written and check your output after each one. If the filter still won’t cooperate, dump the compiled program with -d and compare it against what you actually want to see. Then capture a few packets to a pcap file, replay it with tcpdump -r, and confirm the fix before calling the ticket closed. Done.