ACTION adaptor: dns


DNS queries are probably not going to be terribly central to most workflow, but as I was working on extending the SMTP notification adaptor and needed to do some DNS queries as part of that, it occurred to me that they are central to a lot of what I want to do with it. So I decided to expose them as an adaptor.

But while doing that, I realized two things: first, there's a real need for scripting language adaptors and Python will probably be the first one. Second, even for C adaptors that are static linked, there's a real need for a better system of organizing them. So those will be a couple of topics we should address sometime....

At any rate, this current code is Unix-specific. If you need it under Windows, you're on your own, Sparky.
 
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <xmlobj.h>
#include "wftk_session.h"
#include 
#include 
#include 
#include 
The adaptor_info structure is used to pass adaptor info (duh) back to the config module when it's building an adaptor instance. Here's what it contains:
 
static char *names[] = 
{
   "init",
   "free",
   "info",
   "do"
};

XML * ACTION_dns_init (WFTK_ADAPTOR * ad, va_list args);
XML * ACTION_dns_free (WFTK_ADAPTOR * ad, va_list args);
XML * ACTION_dns_info (WFTK_ADAPTOR * ad, va_list args);
XML * ACTION_dns_do   (WFTK_ADAPTOR * ad, va_list args);

static WFTK_API_FUNC vtab[] = 
{
   ACTION_dns_init,
   ACTION_dns_free,
   ACTION_dns_info,
   ACTION_dns_do
};

static struct wftk_adaptor_info _ACTION_dns_info =
{
   4,
   names,
   vtab
};
Cool. So here's the incredibly complex function which returns a pointer to that:
 
struct wftk_adaptor_info * ACTION_dns_get_info ()
{
   return & _ACTION_dns_info;
}
Thus concludes the communication with the config module. Now on with the actual implementation of functionality. It's not complicated.
 
XML * ACTION_dns_init (WFTK_ADAPTOR * ad, va_list args) {
   xml_set (ad->parms, "spec", "wftk");
   return (XML *) 0;
}
XML * ACTION_dns_free (WFTK_ADAPTOR * ad, va_list args) { return (XML *) 0; }
Next up is the info call, which builds and returns a little XML telling the caller about the adaptor. If the adaptor itself is NULL, then it just returns info about the installed adaptor handler; otherwise it's free to elaborate on the adaptor instance.
 
XML * ACTION_dns_info (WFTK_ADAPTOR * ad, va_list args) {
   XML * info;

   info = xml_create ("info");
   xml_set (info, "type", "action");
   xml_set (info, "name", "dns");
   xml_set (info, "ver", "1.0.0");
   xml_set (info, "compiled", __TIME__ " " __DATE__);
   xml_set (info, "author", "Michael Roberts");
   xml_set (info, "contact", "wftk@vivtek.com");
   xml_set (info, "extra_functions", "0");

   return (info);
}

Now that the administration has been taken care of, we come to the core of any action adaptor, the "do" action. The action specification for the DNS adaptor looks at the "action" attribute to figure out what to do. Currently defined actions are "lookup" (which does either forward or reverse resollution) or "mx" (which does an MX query and interprets the results.)
<action handler="dns" action="lookup">
  <field id="ip">12.34.56.78</field>
</action>
Something we'll be using a lot is our list of recognized DNS record types:
 
struct type_str_match
{
    char*  str;
    int type;
};
struct type_str_match types[] =
{
    {"A", T_A},
    {"NS", T_NS},
    {"MD", T_MD},
    {"MF", T_MF},
    {"CNAME", T_CNAME},
    {"SOA", T_SOA},
    {"MB", T_MB},
    {"MG", T_MG},
    {"MR", T_MR},
    {"NULL", T_NULL},
    {"WKS", T_WKS},
    {"PTR", T_PTR},
    {"HINFO", T_HINFO},
    {"MINFO", T_MINFO},
    {"MX", T_MX},
    {"TXT", T_TXT},
    {"RP", T_RP},
    {"AFSDBB", T_AFSDB},
    {"X25", T_X25},
    {"ISDN", T_ISDN},
    {"RT", T_RT},
    {"NSAP", T_NSAP},
    {"NSAP_PTR", T_NSAP_PTR},
    {"SIG", T_SIG},
    {"KEY", T_KEY},
    {"PX", T_PX},
    {"GPOS", T_GPOS},
    {"AAAA", T_AAAA},
    {"LOC", T_LOC},
    {"AXFR", T_AXFR},
    {"MAILB", T_MAILB},
    {"MAILA", T_MAILA},
    {"ANY", T_ANY},
    {"SRV", 33}  // From RFC 2052
};
static const char * lookup_type (int type) {
    int i;

    for (i = 0; i < (sizeof(types) / sizeof(struct type_str_match)); i++) {
        if (types[i].type == type) return types[i].str;
    }

    return "unknown";
}
static int lookup_typestr (const char * type) {
    int i;

    for (i = 0; i < (sizeof(types) / sizeof(struct type_str_match)); i++) {
#ifdef WINDOWS
        if (!stricmp (type, types[i].str)) return types[i].type;
#else
        if (!strcasecmp (type, types[i].str)) return types[i].type;
#endif
    }

    return T_ANY;
}
static const char * lookup_class (int class) {
    switch(class) {
        case 1: return "IN";
    }

    return "unknown";
}
And some header structures:
 
typedef struct
{
    u_int16_t   type;
    u_int16_t   class;
    u_int32_t   ttl;
    u_int16_t   length;

} rr_header;


typedef struct
{
    u_int16_t   priority;
    u_int16_t   weight;
    u_int16_t   port;

} srv_header;
So let's get down to business.
 
static void unpack_answer (XML * action, char * answer, int len);
XML * ACTION_dns_do  (WFTK_ADAPTOR * ad, va_list args)
{
   XML * action = (XML *) 0;
   XML * datasheet = NULL;
   XML * mark;
   char * answer;
   int len;
   int type;
   long retcode;

   if (args) action = va_arg (args, XML *);
   if (!action) {
      xml_set (ad->parms, "error", "No action given.");
      return (XML *) 0;
   }
   datasheet = va_arg (args, XML *);

   if (!strcmp (xml_attrval (action, "action"), "mx")) {
      xml_set  (action, "action", "lookup");
      xml_set  (action, "type", "mx");
   }
   if (!strcmp (xml_attrval (action, "action"), "lookup")) {
      type = lookup_typestr (xml_attrval (action, "type"));
      res_init ();
      xml_set_nodup (action, "lookup_name", xmlobj_get (action, NULL, "name"));
      answer = (char *) malloc (512);
      len = res_query (xml_attrval (action, "lookup_name"), C_IN, type, answer, 512);
      if (len > 512) {
         free ((void *)answer);
         answer = malloc (len);
         len = res_query (xml_attrval (action, "lookup_name"), C_IN, type, answer, len);
      }
      if (len == -1) {
         xml_set (action, "status", "fail");
         xml_set (action, "status.reason", "Domain does not exist.");
         free ((void *)answer);
         return 0;
      }

      xml_setnum (action, "answerlen", len); /* Little sanity check. */

      unpack_answer (action, answer, len);
   } else {
      xml_set (action, "status", "fail");
      xml_setf (action, "status.reason", "Unknown action %s", xml_attrval (action, "action"));
   }

   return 0;
}
OK, now unpacking the answer returned from a DNS server query is ... complicated. Really complicated. And documentation is unbelievably sparse; it looks frankly like nobody does this, and above all, of those who do it, nobody does it correctly. So be happy, Dear Reader, that I am here to figure it out for you. We start by unpacking the overall record, which consists of a header followed by questions, followed by answers and other records, as follows.
 
static int unpack_name(XML * action, XML * subanswer, const char * where, char** location, char* buffer, int len);
static int unpack_question(XML * action, XML * subanswer, char** location, char* buffer, int len);
static int unpack_rr(XML * action, XML * subanswer, char** location, char* buffer, int len);
static void unpack_answer (XML * action, char * buffer, int len) {
    HEADER * hdr = (HEADER *) buffer;
    char   * loc = buffer + sizeof (HEADER);
    XML * subanswer;
    int  i = 0;

    if (len < sizeof(HEADER))
    {
        xml_set (action, "status", "fail");
        xml_set (action, "status.reason", "Faulty response received from DNS server: packet truncated.");
        return;
    }

    xmlobj_setnum (action, NULL, "id", hdr->id);
    xmlobj_setnum (action, NULL, "opcode", hdr->opcode);
    if (hdr->qr) xmlobj_set (action, NULL, "flag.response", "yes");
    if (hdr->qr) xmlobj_setnum (action, NULL, "rcode", hdr->rcode);
    if (hdr->aa) xmlobj_set (action, NULL, "flag.authoritative", "yes");
    if (hdr->tc) xmlobj_set (action, NULL, "flag.truncated", "yes");
    if (hdr->rd) xmlobj_set (action, NULL, "flag.recursion_desired", "yes");
    if (hdr->ra) xmlobj_set (action, NULL, "flag.recursion_available", "yes");

    xmlobj_setnum (action, NULL, "qdcount", hdr->qdcount/256);
    xmlobj_setnum (action, NULL, "ancount", hdr->ancount/256);
    xmlobj_setnum (action, NULL, "nscount", hdr->nscount/256);
    xmlobj_setnum (action, NULL, "arcount", hdr->arcount/256);

    for (i = 0; i < hdr->qdcount/256; i++)
    {
        subanswer = xml_create ("qd");
        xml_append_pretty (action, subanswer);
        if (unpack_question(action, subanswer, &loc, buffer, len) != 0)
            return;
    }

    for (i = 0; i < hdr->ancount/256; i++)
    {
        subanswer = xml_create ("an");
        xml_append_pretty (action, subanswer);
        if (unpack_rr(action, subanswer, &loc, buffer, len) != 0)
            return;
    }

    for (i = 0; i < hdr->nscount/256; i++)
    {
        subanswer = xml_create ("ns");
        xml_append_pretty (action, subanswer);
        if (unpack_rr(action, subanswer, &loc, buffer, len) != 0)
            return;
    }

    for (i = 0; i < hdr->arcount/256; i++)
    {
        subanswer = xml_create ("ar");
        xml_append_pretty (action, subanswer);
        if (unpack_rr(action, subanswer, &loc, buffer, len) != 0)
            return;
    }
}
Once the main record is unpacked, we can unpack the entries in it. First, the response contains the query we originally sent it, so we unpack that question first.
 
static int unpack_question(XML * action, XML * subanswer, char** location, char* buffer, int len)
{
    if (*location >= buffer + len || *location < buffer)
    {
        xml_set (action, "status", "fail");
        xml_set (action, "status.reason", "Malformed DNS query response [1]");
        return -1;
    }

    if (unpack_name(action, subanswer, "name", location, buffer, len) != 0) return -2;

    if (*location + 2 * sizeof (u_int16_t) > buffer + len)
    {
        xml_set (action, "status", "fail");
        xml_set (action, "status.reason", "Malformed DNS query response [2]");
        return -4;
    }

    xmlobj_set (subanswer, NULL, "type", lookup_type ((* (u_int16_t *) * location)/256));
    xml_setnum (subanswer, "type_num", (* (u_int16_t *) * location)/256);
    *location += sizeof(u_int16_t);
    xmlobj_set (subanswer, NULL, "class", lookup_class ((* (u_int16_t *) * location)/256));
    xml_setnum (subanswer, "class_num", (* (u_int16_t *) * location)/256);
    *location += sizeof(u_int16_t);

    return 0;
}
To unpack the question, we need to know how to unpack names, but that's fortunately already implemented for us in libresolv.
 
static int unpack_name(XML * action, XML * subanswer, const char * where, char** location, char* buffer, int len)
{
   char name[512];
   int retval;

   retval = dn_expand (buffer, buffer + len, *location, name, sizeof(name));
   if (retval < 1) {
      xml_set (action, "status", "fail");
      xml_set (action, "status.reason", "Malformed DNS query response [7]");
      return -1;
   }
   *location += retval;
   xmlobj_set (subanswer, NULL, where, name);
   return 0;
}
The rest of the response consists of a series of "rr" subrecords, in three separate lists (answers, nameservers, and "additional" data, which is where nice nameservers will already have looked up the IP addresses for things in answers like MX records and the nameservers.) Each of these records consists of a name, some additional stuff like TTL counter and type and such, and some optional data which depends on the type.
 
static int unpack_rr(XML * action, XML * subanswer, char** location, char* buffer, int len)
{
    rr_header* header;
    char buf[16];
    char * name;
    
    if (*location >= buffer + len || *location < buffer)
    {
        xml_set (action, "status", "fail");
        xml_set (action, "status.reason", "Malformed DNS query response [5]");
        return -1;
    }

    if (unpack_name(action, subanswer, "name", location, buffer, len) != 0) return -2;

    header = (rr_header*)*location;

    if (*location + sizeof(rr_header) >= buffer + len)
    {
        xml_set (action, "status", "fail");
        xml_set (action, "status.reason", "Malformed DNS query response [6]");
        return -3;
    }
    
    *location += sizeof(rr_header) - 2; 

    
    xmlobj_set (subanswer, NULL, "type", lookup_type (ntohs(header->type)));
    xml_setnum (subanswer, "type_num", ntohs(header->type));
    xmlobj_set (subanswer, NULL, "class", lookup_class (ntohs(header->class)));
    xml_setnum (subanswer, "class_num", ntohs(header->class));
    xmlobj_setnum (subanswer, NULL, "ttl", ntohl(header->ttl));
    xmlobj_setnum (subanswer, NULL, "length", ntohs(header->length));

    switch(ntohs(header->type))
    {
        case T_A:
        {
            xmlobj_set (subanswer, NULL, "data", (char *) inet_ntop(AF_INET, *location, &buf[0], sizeof(buf)));
            *location += ntohs(header->length);
            break;
        }
        case T_MX:
        {
            xmlobj_setnum (subanswer, NULL, "prio", (int) (*(u_int16_t *)(*location))/256);
            *location += sizeof (u_int16_t);
        }
        case T_NS:
        case T_CNAME:
        case T_PTR:
        {
            if (unpack_name(action, subanswer, "data", location, buffer, len) != 0) return -2;
            break;
        }
        case 33: // SRV
        {
            srv_header * srv = (srv_header *)*location;
    
            xmlobj_setnum (subanswer, NULL, "priority", srv->priority);
            xmlobj_setnum (subanswer, NULL, "weight", srv->weight);
            xmlobj_setnum (subanswer, NULL, "port", srv->port);
        
            *location += sizeof (srv_header);
            unpack_name(action, subanswer, "target", location, buffer, len);
            break;
        }
        default:
            /*printf("  data    :\n");
            unpack_hex(*location, ntohs(header->length));*/
            *location += ntohs(header->length);
            break;
    }

    return 0;
}

This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2005, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license. This presentation was prepared with LPML. Try literate programming. You'll like it.