Wireshark: DNS capture and display filters
Wireshark filters land in two places. Capture filters use BPF syntax and decide what gets recorded at all; you set them before you press start. Display filters use Wireshark's own syntax and decide what you see in the pane once you have captured. They are not interchangeable. This page covers both, with the filters you reach for when something DNS-shaped is wrong. For a plain-English walk through what a DNS query looks like on the wire, start there first.
Capture filters (BPF): what to record#
BPF is the kernel-level filter language. It is terser, narrower, and case-sensitive. Keep them simple and broad: you can always narrow further with a display filter.
| Goal | Filter |
|---|---|
| All DNS over UDP+TCP | port 53 |
| DoT (TCP 853) and DoQ (UDP 853) | port 853 |
| DoH (one of many TLS streams) | port 443 and host cloudflare-dns.com |
| DNS to/from a specific resolver | host 192.0.2.53 and port 53 |
| DNS to/from a specific client | host 192.0.2.42 and port 53 |
| Any VLAN-tagged DNS traffic | vlan and port 53 |
| DNS or the L2 ARP needed to reach DNS | port 53 or arp |
| Only the resolver leg (drop client noise) | port 53 and host 192.0.2.53 and not host 192.0.2.42 |
Capture filters cannot reference DNS field names. They are packet-shape filters. If you need to filter on
qnameorrcode, do it as a display filter on a broader capture.Two gotchas:
host cloudflare-dns.comresolves the name to its addresses once, when the capture starts, so a DoH service spread across many IPs needs those IPs listed explicitly. And DNS over QUIC (RFC 9250) shares port 853 with DoT but runs over UDP, sotcp port 853isolates DoT alone.
Display filters (Wireshark syntax): what to look at#
This is where the DNS-aware part lives. Wireshark parses every DNS field by name, so you can filter on qname, rcode, flags, EDNS options, and anything else in the DNS wire format.
Sieve everything down to DNS first#
dnsThen layer in the question.
One catch: mDNS (UDP 5353) and LLMNR (UDP 5355) are parsed by the same dissector but register as their own protocols, so the bare
dnsfilter excludes them while field filters likedns.qry.namestill match their chatter. Addudp.port == 53when you only want classic unicast DNS.
By query name#
| Goal | Filter |
|---|---|
| Queries for a specific name | dns.qry.name == "example.com" |
Any name containing a string (contains matches anywhere, not only the end) | dns.qry.name contains "example.com" |
| Queries for a specific TLD | dns.qry.name matches "\\.gov$" |
| Reverse-lookup queries | dns.qry.name contains "in-addr.arpa" or dns.qry.name contains "ip6.arpa" |
By query type (qtype)#
Use the numeric type or the symbolic name. Common ones:
| Qtype | Numeric | Filter |
|---|---|---|
| A | 1 | dns.qry.type == 1 |
| AAAA | 28 | dns.qry.type == 28 |
| CNAME | 5 | dns.qry.type == 5 |
| MX | 15 | dns.qry.type == 15 |
| NS | 2 | dns.qry.type == 2 |
| PTR | 12 | dns.qry.type == 12 |
| SOA | 6 | dns.qry.type == 6 |
| TXT | 16 | dns.qry.type == 16 |
| SRV | 33 | dns.qry.type == 33 |
| CAA | 257 | dns.qry.type == 257 |
| SVCB | 64 | dns.qry.type == 64 |
| HTTPS | 65 | dns.qry.type == 65 |
| DNSKEY | 48 | dns.qry.type == 48 |
| DS | 43 | dns.qry.type == 43 |
| RRSIG | 46 | dns.qry.type == 46 |
| SSHFP | 44 | dns.qry.type == 44 |
| TLSA | 52 | dns.qry.type == 52 |
| ANY | 255 | dns.qry.type == 255 (expect a minimal HINFO answer per RFC 8482, or a NOTIMP from resolvers like Cloudflare that decline ANY, not full record sets) |
By rcode / DNS errors#
| Goal | Filter |
|---|---|
| Only responses | dns.flags.response == 1 |
Only NXDOMAIN (see glossary) | dns.flags.rcode == 3 |
Only SERVFAIL | dns.flags.rcode == 2 |
Only REFUSED | dns.flags.rcode == 5 |
Any non-zero rcode (anything that is not NOERROR) | dns.flags.rcode != 0 and dns.flags.response == 1 |
FORMERR | dns.flags.rcode == 1 |
NOTIMP | dns.flags.rcode == 4 |
By DNS header flags#
| Goal | Filter |
|---|---|
| Recursion-desired queries | dns.flags.recdesired == 1 |
| Authoritative answers | dns.flags.authoritative == 1 |
| DNSSEC-authenticated answers (AD flag) | dns.flags.authenticated == 1 |
| Truncated responses (UDP overflow) | dns.flags.truncated == 1 |
| Recursion-available answers | dns.flags.recavail == 1 |
| Checking-disabled (client opted out of DNSSEC) | dns.flags.checkdisable == 1 |
Field names verified against the official Wireshark DNS display filter reference. The CD field really is
dns.flags.checkdisable, with no trailingd.
By transport / direction#
| Goal | Filter |
|---|---|
| Just UDP DNS | dns and udp |
| Just TCP DNS (large answers, AXFR) | dns and tcp |
| DoT (DNS over TLS) | tcp.port == 853 |
| DoH ClientHello (no TLS keys needed) | tls.handshake.extensions_server_name contains "dns" |
| Outbound queries from a resolver | ip.src == 192.0.2.53 and dns.flags.response == 0 |
| Inbound responses to a resolver | ip.dst == 192.0.2.53 and dns.flags.response == 1 |
DoH rides inside TLS on port 443, so without decryption the most Wireshark can show you is which server the client talked to: the SNI in the ClientHello (absent when ECH encrypts it). To see the queries themselves, capture with an
SSLKEYLOGFILEand point Wireshark at it (Preferences > Protocols > TLS > (Pre)-Master-Secret log filename); once decrypted, the HTTP/2 stream and the DNS inside it dissect normally and the plaindnsfilter works again.
Fragmentation / EDNS#
| Goal | Filter |
|---|---|
| EDNS0 OPT pseudo-records | dns.opt |
| Anything fragmented at IP (v4 or v6) | ip.flags.mf == 1 or ip.frag_offset > 0 or ipv6.fraghdr |
| UDP DNS payloads over the 1232-byte EDNS default | dns and udp.length > 1240 |
udp.lengthcounts the 8-byte UDP header, so a DNS payload over 1232 bytes shows a length over 1240. 1232 bytes is the EDNS buffer size recommended by DNS Flag Day 2020 to avoid IP fragmentation; it is what Cloudflare and most large resolvers advertise today.
Useful column setup#
In Edit > Preferences > Columns, add:
| Column | Field | Why |
|---|---|---|
| Query name | dns.qry.name | The thing being asked |
| Qtype | dns.qry.type | A, AAAA, etc. |
| Rcode | dns.flags.rcode | NOERROR / NXDOMAIN / SERVFAIL at a glance |
| AD flag | dns.flags.authenticated | DNSSEC AD bit set by resolver |
| Client | ip.src (for queries) | Who is asking |
| RTT | dns.time | Resolver latency (appears on responses only) |
Common scenarios: worked filters#
Catch every NXDOMAIN from a single client#
dns.flags.rcode == 3 and ip.dst == 192.0.2.42Filtering on ip.dst because the NXDOMAIN response travels to the client.
Find truncated UDP answers that forced TCP fallback#
dns.flags.truncated == 1If you see many of these, the server is producing answers larger than the client's advertised EDNS UDP buffer (or the classic 512-byte limit when the query carries no EDNS), and clients are retrying over TCP. Common causes: large TXT records, RRSIG inflation, or a small EDNS UDP buffer advertised by the client.
Spot a misconfigured stub resolver#
dns.flags.recdesired == 0 and dns.flags.response == 0Stub clients set RD=1; queries with RD=0 are what a recursive resolver sends while walking the delegation chain. Seeing RD=0 from a resolver toward authoritative servers is normal iteration. Seeing it from an end-user device is not: either the machine thinks it is a resolver, or its stub configuration is broken.
Confirm DNSSEC validation is happening#
dns and dns.flags.authenticated == 1If the AD-flag count is near zero across a long capture, either the resolver is not validating, or your capture point is upstream of where validation happens (you are seeing pre-validation queries to the authoritative server, not post-validation responses to the client). See dnssec.
The AD bit is set by the resolver to signal that it validated the answer. Seeing
AD=0does not always mean validation failed; the resolver may have validated internally without setting the bit for stub clients that did not request DNSSEC data. Usedns.resp.z.do == 1to check whether the client set the DO bit in its EDNS OPT record.
Find the slowest queries#
Add the dns.time column, sort descending. Anything above 500 ms during normal operation is worth investigating. Cold-cache misses for slow authoritative servers make up most of the long tail; repeated slow lookups to the same name point to pathology.
Capturing DNS traffic with tcpdump#
You can capture DNS traffic on any interface with tcpdump and open the result in Wireshark:
sudo tcpdump -i eth0 -nn -w /tmp/dns.pcap 'port 53'Transfer the file to your workstation, then open it in Wireshark:
scp user@capture-host:/tmp/dns.pcap .For VLAN-tagged environments, capture at the trunk interface and let Wireshark decode the 802.1Q headers:
sudo tcpdump -i eth0 -nn -w /tmp/dns-vlan.pcap 'vlan and port 53'dnstap vs. wire capture#
dnstap is a different mechanism: it captures DNS messages at the resolver process level (BIND, Knot, Unbound support it) before or after the resolver acts on them. Wireshark sees the IP packets on the wire; dnstap sees the DNS messages as the resolver sees them, including queries the resolver generated itself for recursive resolution. The two approaches are complementary.