Decoding TCPdump DNS Output: Percent Signs and ARCOUNT Anomalies in DNS Query Analysis


4 views

When analyzing DNS traffic with TCPdump, you'll frequently encounter output like this:

21:54:35.391126 IP resolver.7538 > server.domain: 57385% [1au] A? www.domain.de. (42)

The percent sign (%) in the sequence number field (57385%) indicates that the DNS query has the Recursion Desired (RD) flag set. Unlike the plus sign (+) which explicitly shows RD=1, the percent sign is TCPdump's way of displaying this flag when certain conditions are met.

This behavior can be verified by examining raw DNS packets:

// Example DNS header structure
struct dns_header {
    uint16_t id;      // identification number (57385 in our example)
    uint8_t rd :1;    // recursion desired
    uint8_t tc :1;    // truncated message
    uint8_t aa :1;    // authoritative answer
    uint8_t opcode :4; // purpose of message
    uint8_t qr :1;    // query/response flag
    uint8_t rcode :4; // response code
    uint8_t z :3;     // reserved
    uint8_t ra :1;    // recursion available
    uint16_t qdcount; // number of question entries
    uint16_t ancount; // number of answer entries
    uint16_t nscount; // number of authority entries
    uint16_t arcount; // number of resource entries
};

The [1au] notation in TCPdump output refers to the additional record count (ARCOUNT) in the DNS header. The "au" stands for "authenticated data" and indicates the presence of DNSSEC-related information.

This is actually not an anomaly but rather a common modern DNS feature. Many resolvers now include EDNS0 (Extension mechanisms for DNS) options:

// Example EDNS0 OPT pseudo-RR
static void print_edns0(const u_char *opt, u_int len) {
    printf(" EDNS: version: %u, flags: %#x; udp: %u\n",
           *opt, (*opt >> 8) & 0xff, (*opt >> 16) & 0xffff);
}

To better understand these DNS queries, let's examine them with different tools:

# Using dig to see similar output
dig +nocmd +nocomments +nostats www.domain.de

# Using tshark for more detailed analysis
tshark -n -r dns.pcap -Y "dns" -T fields -e dns.id -e dns.flags -e dns.count.add_rr

Breaking down our original example:

  • 57385%: Query ID with RD flag set
  • [1au]: 1 additional record with authenticated data
  • A?: Standard A record query
  • (42): Total packet size in bytes

For developers working with DNS packets programmatically, here's how to parse these flags:

def parse_dns_flags(flags):
    return {
        'qr': (flags & 0x8000) >> 15,
        'opcode': (flags & 0x7800) >> 11,
        'aa': (flags & 0x0400) >> 10,
        'tc': (flags & 0x0200) >> 9,
        'rd': (flags & 0x0100) >> 8,
        'ra': (flags & 0x0080) >> 7,
        'z': (flags & 0x0070) >> 4,
        'rcode': flags & 0x000f
    }

When examining TCPdump output for DNS traffic, you'll occasionally encounter a curious percent sign (%) after the query ID. Let's break down what we're seeing:

21:54:35.391126 IP resolver.7538 > server.domain: 57385% [1au] A? www.domain.de. (42)

Here's the technical breakdown:

  • 57385: The DNS query ID (2-byte identifier)
  • %: Indicates the QR (Query/Response) bit is 0 (query)
  • [1au]: Shows there's 1 additional record in the ARCOUNT section

The ARCOUNT field in DNS headers indicates the number of resource records in the additional records section. The [1au] notation specifically means:

1 - count of additional records
au - indicates these are EDNS options (DNS Extension Mechanisms)

This isn't actually a protocol anomaly despite TCPdump's notation. It's a legitimate EDNS0 extension commonly seen in modern DNS implementations.

Let's compare with DIG output to see the same information presented differently:

$ dig +nocmd +nocomments www.domain.de A
; <<>> DiG 9.16.1 <<>> +nocmd +nocomments www.domain.de A
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57385
;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

Notice how the additional record count (ARCOUNT) matches what we saw in TCPdump.

Here's a Python snippet to parse DNS headers and extract these values:

import struct

def parse_dns_header(data):
    header = struct.unpack('!6H', data[:12])
    return {
        'id': header[0],
        'qr': (header[1] >> 15) & 0x1,
        'qdcount': header[2],
        'ancount': header[3],
        'nscount': header[4],
        'arcount': header[5]
    }

# Example usage with captured DNS packet
dns_packet = b'\xe0\x69\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01'  # sample header
print(parse_dns_header(dns_packet))

Understanding these details is crucial when:

  • Debugging DNS-related issues in applications
  • Implementing custom DNS resolvers
  • Analyzing network traffic for security purposes
  • Optimizing DNS query performance

The additional records often contain EDNS options that can affect how DNS servers process queries, particularly around UDP packet size and DNSSEC.

The additional section typically contains:

  • EDNS options (OPT pseudo-RR)
  • TSIG records for authenticated updates
  • Client subnet information in ECS (EDNS Client Subnet)

Here's how to check for EDNS using dig:

dig +nocmd +edns=0 www.domain.de