Reading DNS
A query and its response share an id. The name, the type, and the response code are the fields worth quoting.
Match a query to its response
DNS in the lab file is usually UDP/53. A query has dns.flags.response == 0. The response has dns.flags.response == 1 and the same dns.id. Filter dns.id == 0x1a2b once you have read an id, and the pair sits together even when other clients are in the file. The time between them is dns.time on the response, which Wireshark fills in when it saw the query.
The name is dns.qry.name. The type is dns.qry.type: 1 is an A record, 28 is AAAA, 5 is CNAME, 16 is TXT. The response code is dns.flags.rcode. Zero is NOERROR, two is SERVFAIL, three is NXDOMAIN. NOERROR with zero answers is a different outcome from NXDOMAIN, and the field is how you keep them apart.
dns.flags.response == 0 && ip.src == 10.8.0.40
Truncation and the TCP retry
A UDP response can set the truncated bit, dns.flags.truncated == 1, when the answer did not fit. Clients then retry the same name over TCP/53. A capture filter that kept only udp port 53 drops that retry on the floor. A display filter of dns still shows both, because the dissector recognizes DNS on either transport.
CNAME chains show up as more than one name in the response. Read the answers in order instead of assuming the first name is the address. The address the application connected to is the A or AAAA at the end of the chain, and the TCP conversation that follows should use that address. If it uses a different one, you have two events, not one.
Worked case: the client asked, and the resolver said no such name
10.8.0.40 is supposed to use the lab resolver 10.8.0.53. You filter queries from the client. Most rows ask for files.lab.example and print.lab.example, type A, and the responses are NOERROR with an address. One name, setup.example.test, comes back with dns.flags.rcode == 3. The id matches a query 2 milliseconds earlier. The client asked once, the resolver answered NXDOMAIN, and no TCP connection to that name follows, because there is no address to connect to.
You write the client, the resolver, the qname, the type, the rcode, and the frame numbers. You also check the destination of the query. A query sent somewhere other than 10.8.0.53 is a different finding from a query the lab resolver answered. The name in the packet is the evidence. A resolved column on the IP address is not a substitute for dns.qry.name.
dns.flags.rcode == 3 && ip.addr == 10.8.0.40
A short note is the whole decode
A DNS finding fits in one line plus the frames: who asked, who answered, the exact name, the type, and the rcode. Add the answer address when there was one. Leave the interpretation of “why this name” to the person who owns the host, unless the lab exercise itself tells you the expected names.
When the response is missing, say the query was observed and the response was not, at this capture point. A resolver can answer on a path you did not record.
Diagram
From a query to the response code you can quote
- Query
Find the client’s questions with the response flag clear.
dns.flags.response == 0 && ip.src == 10.8.0.40
- Id
Match dns.id to the response. Read dns.time when both frames are present.
- Name
Record dns.qry.name and dns.qry.type. Then read dns.flags.rcode.
- Address
If an A or AAAA exists, the later TCP connection should use that address.
Check yourself
1. Which filter shows DNS questions from 10.8.0.40 and hides the answers?
2. dns.flags.rcode is 3. What did the resolver say?
3. How do you pair a query with its response in a busy file?