SourceXR

C/C++ Cross-Reference Tool

DNS Records Query with res_query

We all know the most usual DNS queries, i.e. mapping a name to an IP address. This is performed through the getaddrinfo() call (replacement of gethostbyname). But the DNS is much more than this mapping! It holds among other records the mail servers address for a given name, etc.

In this short article we write a small program that queries the DNS for the mail server associated to a domain name. It can be easily adapted to query any other DNS record.

Apart from the name/address mapping there is no general library call that given a name and a type (that's the name for a specific DNS record) would return the associated address. That's why we are relying here on the res_query call part of the resolv library.

As this call will only build the request, send it to the DNS and then return the buffer containing the reply, we need to dig a little bit in the DNS reply format. It is described in the RFC 1035.

We will just remind here what is needed for our small program. Please refer to the RFC for the full description.

A reply consists of a header, followed by possibly four blocks:

  • question
  • answers
  • name servers
  • address records

Depending on the header contents, these fields are present or not. The header contains the number of each of these records. In the resolv library, it is represented by the HEADER structure.

The question record consists of a domain name, followed by the query type and the query class.

The three remaining records have a generic format:

  • domain name
  • type (2 bytes)
  • class (2 bytes)
  • TTL (4 bytes)
  • length of data (2 bytes)
  • data

Data are interpreted depending on type and class. For example, for an MX record, data consist of two fields: the preference and the mail server name. The RFC contains the full list of records and their interpretation.

As the name in the record may be compressed, we use the library functions to read and print it.

The program is actually quite simple. First, we make the DNS query using res_query, the generic method that allows us to supply the type of record we are interested in:

    const size_t size = 1024;
    unsigned char buffer[size];

    const char *host = argv[1];

    int r = res_query (host, C_IN, T_MX, buffer, size);
    if (r == -1) {
        std::cerr << h_errno << " " << hstrerror (h_errno) << "\n";
        return 1;
    }
    else {
        if (r == static_cast<int> (size)) {
            std::cerr << "Buffer too small reply truncated\n";
            return 1;
        }
    }

Then we check the reply for errors sent by the server:

    HEADER *hdr = reinterpret_cast<HEADER*> (buffer);

    if (hdr->rcode != NOERROR) {

        std::cerr << "Error: ";
        switch (hdr->rcode) {
        case FORMERR:
            std::cerr << "Format error";
            break;
        case SERVFAIL:
            std::cerr << "Server failure";
            break;
        case NXDOMAIN:
            std::cerr << "Name error";
            break;
        case NOTIMP:
            std::cerr << "Not implemented";
            break;
        case REFUSED:
            std::cerr << "Refused";
            break;
        default:
            std::cerr << "Unknown error";
        }
        return 1;
    }

And we handle now the reply. First we read the header to get the number of each of records:

    int question = ntohs (hdr->qdcount);
    int answers = ntohs (hdr->ancount);
    int nameservers = ntohs (hdr->nscount);
    int addrrecords = ntohs (hdr->arcount);

    std::cout << "Reply: question: " << question << ", answers: " << answers
              << ", nameservers: " << nameservers
              << ", address records: " << addrrecords << "\n";

Then we go through each record in turn: first the question specifically, then the last three using the same parse function. The resolv library supplies several helpers to read the received buffer:

    ns_msg m;
    int k = ns_initparse (buffer, r, &m);
    if (k == -1) {
        std::cerr << errno << " " << strerror (errno) << "\n";
        return 1;
    }

ns_initparse reads the buffer and fills the ns_msg structure with its contents.

We are now ready to read the reply: first the question record using macros to access the fields of the ns_rr data structure and then the remaining records.

    for (int i = 0; i < question; ++i) {
        ns_rr rr;
        int k = ns_parserr (&m, ns_s_qd, i, &rr);
        if (k == -1) {
            std::cerr << errno << " " << strerror (errno) << "\n";
            return 1;
        }
        std::cout << "question " << ns_rr_name (rr) << " "
                  << ns_rr_type (rr) << " " << ns_rr_class (rr) << "\n";
    }
    for (int i = 0; i < answers; ++i) {
        parse_record (buffer, r, "answers", ns_s_an, i, &m);
    }

    for (int i = 0; i < nameservers; ++i) {
        parse_record (buffer, r, "nameservers", ns_s_ns, i, &m);
    }

    for (int i = 0; i < addrrecords; ++i) {
        parse_record (buffer, r, "addrrecords", ns_s_ar, i, &m);
    }

And finally the generic parse record function. It only parses a small subset of the available records (A, NS, and MX). It unpacks the domain name as they may be compressed in the record:

void parse_record (unsigned char *buffer, size_t r,
                   const char *section, ns_sect s,
                   int idx, ns_msg *m) {

    ns_rr rr;
    int k = ns_parserr (m, s, idx, &rr);
    if (k == -1) {
        std::cerr << errno << " " << strerror (errno) << "\n";
        return;
    }

    std::cout << section << " " << ns_rr_name (rr) << " "
              << ns_rr_type (rr) << " " << ns_rr_class (rr)
              << ns_rr_ttl (rr) << " ";

    const size_t size = NS_MAXDNAME;
    unsigned char name[size];
    int t = ns_rr_type (rr);

    const u_char *data = ns_rr_rdata (rr);
    if (t == T_MX) {
        int pref = ns_get16 (data);
        ns_name_unpack (buffer, buffer + r, data + sizeof (u_int16_t),
                        name, size);
        char name2[size];
        ns_name_ntop (name, name2, size);
        std::cout << pref << " " << name2;
    }
    else if (t == T_A) {
        unsigned int addr = ns_get32 (data);
        struct in_addr in;
        in.s_addr = ntohl (addr);
        char *a = inet_ntoa (in);
        std::cout << a;
    }
    else if (t == T_NS) {
        ns_name_unpack (buffer, buffer + r, data, name, size);
        char name2[size];
        ns_name_ntop (name, name2, size);
        std::cout << name2;
    }
    else {
        std::cout << "unhandled record";
    }

    std::cout << "\n";
}

To compile this program, you have to link against the resolv library (-lresolv on your compiler command line).

When running it, you should get the following output:

$ ./a.out google.com
Reply: question: 1, answers: 5, nameservers: 0, address records: 5
question google.com 15 1
answers google.com 15 11660 20 alt1.aspmx.l.google.com
answers google.com 15 11660 40 alt3.aspmx.l.google.com
answers google.com 15 11660 10 aspmx.l.google.com
answers google.com 15 11660 50 alt4.aspmx.l.google.com
answers google.com 15 11660 30 alt2.aspmx.l.google.com
addrrecords alt1.aspmx.l.google.com 1 1250 173.194.69.26
addrrecords alt3.aspmx.l.google.com 1 11660 173.194.79.26
addrrecords aspmx.l.google.com 1 11335 173.194.78.27
addrrecords alt4.aspmx.l.google.com 1 11393 173.194.64.27
addrrecords alt2.aspmx.l.google.com 1 11225 74.125.143.27

Within the resolv library, the res_mkquery() call builds a query but does not make the DNS request. It can be used for example to perform DNS queries in non-blocking mode.

Source Code

The source file of the implementation can be found here.

Comments !