Mon 16 Jan 2017

How to do a simple query

This example demonstrates the basics of working with the C API of getdns, the concepts that play a role and the pieces involved:

Setting up a context

All configurable things in getdns are associated with a getdns_context. This includes resolution mode (stub or full recursive), timeout values, root-hints, DNSSEC trust-anchors, search path + how to handle it, namespaces, etc. So to do lookups with getdns, we first have to create a getdns_context:

#include <getdns/getdns_extra.h>
#include <stdio.h>

int main()
{
    getdns_return_t r;
    getdns_context *ctxt = NULL;

    if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    if (ctxt)
        getdns_context_destroy(ctxt);

    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

Almost all functions in getdns return a status code of type getdns_return_t. When the function was successful, it will return GETDNS_RETURN_SUCCESS (which is #defined to be 0). Otherwise r will contain a non-zero value.

getdns_get_errorstr_by_id() returns a textual representation of the returned getdns_return_t status code. This is however a non standard API-spec function, therefore we have to include the getdns/getdns_extra.h header.

getdns_context_create() returns a newly allocated getdns_context * pointer on the first parameter (&ctxt), which is passed by reference. If something went wrong, getdns_context_create() will not touch that parameter. Therefore, the ctxt variable has to be assigned NULL beforehand to be able to test its value to determine whether or not it should be destroyed (like we do in the example).

Doing the query.

Next step; doing the query. In the example below, we query for the addresses of getdnsapi.net.

#include <getdns/getdns_extra.h>
#include <stdio.h>

int main()
{
    getdns_return_t r;
    getdns_context *ctxt = NULL;
    getdns_dict *resp = NULL;

    if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_address_sync(ctxt, "getdnsapi.net.", NULL, &resp)))
        fprintf( stderr, "Unable to do an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));

    if (resp)
        getdns_dict_destroy(resp);
    if (ctxt)
        getdns_context_destroy(ctxt);

    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

The getdns_address_sync() function is the synchronous version of the asynchronous getdns_address() function. In getdns the synchronous functions have the longer name, to indicate that the API considers asynchronous to be the default modus operandi. We will illustrate how to do the query asynchronously later on in this example.

getdns_address_sync() returns a pointer to a newly allocated response dictionary in the last (passed in by reference) parameter. Similar to getdns_context_create(), the parameter will not be touched when something went wrong, so we do have to assign NULL to resp beforehand, to be able to destroy the response dict in the way we do it in the example.

The coding style employed in the examples is just a compromise between keeping things readable (by avoiding nesting) and trying to make it the least error prone.

Alternatively one could keep track of objects to destroy with control flow:

    if (!(r = getdns_context_create(&ctxt))) {
        if (!(r = getdns_address_sync(ctxt, ..., &resp))) {

            getdns_dict_destroy(resp);
        }
        else
            ; /* error handling */

        getdns_context_destroy(ctxt);
    } else
        ; /* error handling */

or by goto-ing to a precise escape point (DON'T TRY THIS AT HOME), i.e.:

    if ((r = getdns_context_create(&ctxt, 1)))
        goto escape;

    if ((r = getdns_address_sync(ctxt, ..., &resp)))
        goto escape_destroy_context;

    if ((r = getdns_something(...)))
        goto escape_destroy_resp;

escape_destroy_resp:
    getdns_dict_destroy(resp);

escape_destroy_context:
    getdns_context_destroy(ctxt);

escape:
    return r ? EXIT_FAILURE : EXIT_SUCCESS;

Note that if it is not important to know exactly in which function something went wrong, it is possible to use a much more concise style; e.g.:

    if (!(r = getdns_context_create(&ctxt, 1))
    &&  !(r = getdns_address_sync(ctxt, ..., &resp))
    &&  !(r = getdns_something(...))
    &&  !(r = getdns_something_else(...))) { 

        /* The happy path */
    } else
        fprintf( stderr, "Something went wrong somewhere: %s\n"
               , getdns_get_errorstr_by_id(r));

    if (resp) getdns_dict_destroy(resp);
    if (ctxt) getdns_dict_destroy(ctxt);

Getting the data

So now we have a response dict. It contains all sorts of information about the query just performed, amongst which the replies that were received to obtain the requested answer.

The response dict is manifested as a "json"-like structure, that can contain lists, integers, binary blob (bindata's) or other dictionaries representing the information.

It can be convenient to have a peek of the information contained in the response dict. The getdns_query tool can be used to print it out for a specific query, or you could do a query on our website. Click on the "do a query" to see the response dictionary for this query.

The response dict contains a "name", just_address_answers which is a list of all the addresses that were in the received replies. If you need just one address, you could use JSON-pointers to get to it quickly:

#include <getdns/getdns_extra.h>
#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    getdns_return_t r;
    getdns_context *ctxt = NULL;
    getdns_dict *resp = NULL;
    getdns_bindata *address;
    char address_str[1024];

    if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_address_sync(ctxt, "getdnsapi.net.", NULL, &resp)))
        fprintf( stderr, "Unable to do an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_dict_get_bindata( resp,
        "/just_address_answers/0/address_data", &address)))
        fprintf( stderr, "Unable to get an address from the response: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if (address->size != 4 && address->size != 16)
        fprintf(stderr, "Unable to determine type of this address\n");

    else if (! inet_ntop( address->size == 4 ? AF_INET : AF_INET6
                        , address->data, address_str, sizeof(address_str)))
        fprintf(stderr, "Could not convert address to string\n");
    else
        printf("An address of getdnsapi.net is: %s\n", address_str);

    if (resp)
        getdns_dict_destroy(resp);
    if (ctxt)
        getdns_context_destroy(ctxt);

    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

Note that getdns returns "network" format IPv4 and IPv6 addresses. In fact, getdns does not convert any data in the returned DNS replies. Addresses in DNS replies are represented by network format (By A and AAAA resource records). The response dict is merely a mechanism to get to the correct places within the DNS replies.

Asynchronous query

In the asynchronous programming style programs are organized around scheduling and responding to events. With getdns this means that scheduling of queries and handling of query responses is decoupled.

#include <getdns/getdns_extra.h>
#include <stdio.h>
#include <arpa/inet.h>

void callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id);

int main()
{
    getdns_return_t r;
    getdns_context *ctxt = NULL;
    getdns_dict *resp = NULL;
    getdns_bindata *address;
    char address_str[1024];

    if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_address(ctxt, "getdnsapi.net.", NULL,
                    NULL, NULL, callback)))
        fprintf( stderr, "Unable to schedule an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));
    else
        getdns_context_run(ctxt);

    if (ctxt)
        getdns_context_destroy(ctxt);
    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

In the code fragment above, the getdns_address() function is used to schedule an address lookup. Alongside the information needed to do the lookup (the name), a callback function is registered. This function will be called with the results when the request is done.

After the request was scheduled, the library is asked to execute all outstanding requests, by a call to the non standard API-spec getdns_context_run() function. Note that this function is in getdns for convenience only! getdns_context_run() uses the "default" event loop which is based on select() and inherits its limitations. The default event loop will cause problems when many queries will be scheduled simultaneously and the underlying file descriptors will be numbered above FD_SETSIZE.

The code snippet below uses an libuv event loop:

#include <getdns/getdns_extra.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <uv.h>
#include <getdns/getdns_ext_libuv.h>

void callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id);


int main()
{
    getdns_return_t r = GETDNS_RETURN_MEMORY_ERROR;
    getdns_context *ctxt = NULL;
    getdns_dict *resp = NULL;
    getdns_bindata *address;
    char address_str[1024];
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));

    if (!loop)
        fprintf( stderr, "Could not allocate event loop\n");

    else if (uv_loop_init(loop))
        fprintf( stderr, "Could not initialize event loop\n");

    else if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_extension_set_libuv_loop(ctxt, loop)))
        fprintf( stderr, "Unable to set the event loop: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_address(ctxt, "getdnsapi.net.", NULL,
                    NULL, NULL, callback)))
        fprintf( stderr, "Unable to schedule an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));
    else
        uv_run(loop, UV_RUN_DEFAULT);

    if (ctxt)
        getdns_context_destroy(ctxt);
    if (loop) {
        uv_loop_close(loop);
        free(loop);
    }
    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

Note that besides including the uv.h header file, you also need to include getdns/getdns_ext_libuv.h for the getdns_extension_set_libuv_loop() function needed to associate the getdns_context with the event loop. Furthermore, the example needs to be linked against libgetdns_ext_uv.so:

$ cc -o async-example async-example.c -lgetdns -lgetdns_ext_uv -luv

Additionally we provide a header file and extension library for libev and libevent.

Note that our library provides functions to integrate with an existing asynchronous event mechanism. A good example are the node.js bindings, that use this mechanism to seamlessly integrate getdns into the node.js execution environment.

Note that when getdns is linked against libunbound version 1.5.9 or later, full recursive queries will also use the event loop associated with the context.

It is also possible to create asynchronous programs using the getdns event loop abstraction layer. In this way you can build libraries performing asynchronous network task, while leaving the actual event loop to use to the user of you library.

These are all advanced topics though and they deserve their own post in the Advanced Examples section.

Showing all addresses

Below an implementation of the callback function, that prints all returned addresses by iterating over the "just_address_answers" list:

void callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id)
{
    getdns_return_t r;
    getdns_list    *jaa;     /* The just_address_answers list */
    size_t          i;       /* Variable to iterate over the jaa list */
    getdns_dict    *ad;      /* A dictionary containing an address */

    if (cb_type != GETDNS_CALLBACK_COMPLETE) 
        fprintf( stderr, "Something went wrong with this query: %s\n"
               , getdns_get_errorstr_by_id(cb_type));

    else if ((r = getdns_dict_get_list(resp, "just_address_answers", &jaa)))
        fprintf( stderr, "No addresses in the response dict: %s\n"
               , getdns_get_errorstr_by_id(r));

    else for (i = 0; !getdns_list_get_dict(jaa, i, &ad); i++) {

        getdns_bindata *address;
        char            address_str[1024];

        if ((r = getdns_dict_get_bindata(ad, "address_data", &address)))
            fprintf( stderr, "Could not get address_data: %s\n"
                   , getdns_get_errorstr_by_id(r));

        else if (address->size != 4 && address->size != 16)
            fprintf(stderr, "Unable to determine address type\n");

        else if (! inet_ntop( address->size == 4 ? AF_INET : AF_INET6,
            address->data, address_str, sizeof(address_str)))
            fprintf(stderr, "Could not convert address to string\n");
        else 
            printf("An address of getdnsapi.net is: %s\n", address_str);
    }
    getdns_dict_destroy(resp); /* Safe, because resp is NULL on error */
}

In this example we process the response dict in the [callback] directly, but we don't have to do so immediately. From here we could start connecting to the IPv6 addresses (in order) and only schedule a connect to the IPv4 addresses when a IPv6 connection wasn't setup within 25 milliseconds (i.e. Happy Eyeballs). For that use case it would be convenient to keep the response dict around until a connection was successfully made, or until all attempts failed.

Doing multiple requests asynchronously

Until now we have scheduled only a single request for the addresses of getdnsapi.net (although two DNS requests are being made simultaneously; one for the IPv4 addresses and one for the IPv6 addresses). This isn't really a good example of where asynchronous processing comes into its own.

As a good asynchronous example we will do the necessary queries to initiate a DANE connection. The code snippet below schedules a query for the addresses and for the TLSA resource records simultaneously.

An application would normally preferably query against the available upstream resolver for this. Per API-spec, the library resolves in full recursion resolution mode by default. In the code snippet below we will configure the getdns_context for stub resolution mode before scheduling the queries.

Besides global configuration in getdns_contexts, getdns also has a mechanism to define a specific setting on a per query basis. This is done by passing along an extension dictionary when scheduling a request.

For the sake of our example, we consider it sufficient to do the address lookup without DNSSEC protection. However, a TLSA may not be used unless it is secure. It MUST be secure.

In the code snippet below, an extension dict is setup that is passed along with the request for the TLSA record. The extension dict has the dnssec_return_only_secure and the non standard API-spec dnssec_roadblock_avoidance extensions set. The first one makes sure an answer is returned only when it was DNSSEC protected, the dnssec_roadblock_avoidance tells the library that it may fallback to full recursion when it could not get a valid DNSSEC answer from the available upstreams in stub resolution mode.

#include <getdns/getdns_extra.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <uv.h>
#include <getdns/getdns_ext_libuv.h>

struct dane_query_st {
    getdns_dict          *addrs_response;
    getdns_transaction_t  addrs_transaction_id;
    getdns_dict          *tlsas_response;
    getdns_transaction_t  tlsas_transaction_id;
};

void addresses_callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id);

void tlsas_callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id);

int main()
{
    getdns_return_t r = GETDNS_RETURN_MEMORY_ERROR;
    getdns_context *ctxt = NULL;
    getdns_dict *resp = NULL;
    getdns_bindata *address;
    char address_str[1024];
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    getdns_dict *ext = getdns_dict_create();
    struct dane_query_st state = { NULL, 0, NULL, 0 };

    if (!ext)
        fprintf( stderr, "Could not allocate extensions dict\n");

    else if (!loop)
        fprintf( stderr, "Could not allocate event loop\n");

    else if (uv_loop_init(loop))
        fprintf( stderr, "Could not initialize event loop\n");

    else if ((r = getdns_context_create(&ctxt, 1)))
        fprintf( stderr, "Could not create context: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_context_set_resolution_type(
                    ctxt, GETDNS_RESOLUTION_STUB)))
        fprintf( stderr, "Could not set stub resolution modus: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_extension_set_libuv_loop(ctxt, loop)))
        fprintf( stderr, "Unable to set the event loop: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_address( ctxt, "getdnsapi.net.", NULL
                                , &state
                                , &state.addrs_transaction_id
                                , addresses_callback)))
        fprintf( stderr, "Unable to schedule an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_dict_set_int(ext, "dnssec_return_only_secure"
                                         , GETDNS_EXTENSION_TRUE))
         ||  (r = getdns_dict_set_int(ext, "dnssec_roadblock_avoidance"
                                         , GETDNS_EXTENSION_TRUE)))
        fprintf( stderr, "Could not popula extensions dict: %s\n"
               , getdns_get_errorstr_by_id(r));

    else if ((r = getdns_general( ctxt, "_443._tcp.getdnsapi.net."
                                , GETDNS_RRTYPE_TLSA, ext
                                , &state
                                , &state.tlsas_transaction_id
                                , tlsas_callback)))
        fprintf( stderr, "Unable to schedule an address lookup: %s\n"
               , getdns_get_errorstr_by_id(r));
    else
        uv_run(loop, UV_RUN_DEFAULT);

    if (ext)
        getdns_dict_destroy(ext);
    if (ctxt)
        getdns_context_destroy(ctxt);
    if (loop) {
        uv_loop_close(loop);
        free(loop);
    }
    return r ? EXIT_FAILURE : EXIT_SUCCESS;
}

We should continue setting up the connection when both lookups succeeded. To track this, we need a bit of state that is shared between the callbacks.

getdns scheduling methods accept an "user argument" pointer, which will be passed to the callback function when the request is finished. The scheduling methods also return a transaction identifier, which can be used with the getdns_cancel_callback() function to cancel an outstanding request.

In the code snippet above, we have a variable state of type struct dane_query_st for which a reference is passed as the user argument to the schedule functions. The state variable carries both the response dictionary as the transaction identifiers of both requests, so it is available from both callback functions.

Below the two callback functions for the requests. Each callback will cancel the other request when it failed itself and then call abort_connection() Each callback will store its response dict in the state variable when it is complete and will then check if the other request has finished yet. If the other request was not finished yet, it will wait. If it was, it will call setup_connection().

void abort_connection(struct dane_query_st *state)
{
    getdns_dict_destroy(state->addrs_response);
    getdns_dict_destroy(state->tlsas_response);
    fprintf(stderr, "DNS failure\n");
}

void setup_connection(struct dane_query_st *state)
{
    uint32_t status;

    if (getdns_dict_get_int(state->tlsas_response, "status", &status)
    ||  status == GETDNS_RESPSTATUS_ALL_BOGUS_ANSWERS) {

        abort_connection(state);
        return;
    }
    printf("DNS lookups were successful!\n");

    /* Schedule opening the TLS connection to the addresses (if any)
     * and verification with the received TLSAs (if any)
     */
}

void addresses_callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id)
{
    struct dane_query_st *state = (struct dane_query_st *)userarg;

    if (cb_type != GETDNS_CALLBACK_COMPLETE) {
        /* Something went wrong,
         * Cancel the TLSA query if it hasn't finished yet.
         * Then abort the connection.
         */
        if (! state->tlsas_response)
            (void) getdns_cancel_callback(
                ctxt, state->tlsas_transaction_id);

        abort_connection(state);
        return;
    }
    state->addrs_response = resp;
    if (state->tlsas_response)
        setup_connection(state);
    else
        ; /* Wait for TLSA lookup to complete */
}

void tlsas_callback(getdns_context *ctxt, getdns_callback_type_t cb_type,
    getdns_dict *resp, void *userarg, getdns_transaction_t trans_id)
{
    struct dane_query_st *state = (struct dane_query_st *)userarg;

    if (cb_type != GETDNS_CALLBACK_COMPLETE) {
        /* Something went wrong,
         * Cancel the TLSA query if it hasn't finished yet.
         * Then abort the connection.
         */
        if (! state->addrs_response)
            (void) getdns_cancel_callback(
                ctxt, state->addrs_transaction_id);

        abort_connection(state);
        return;
    }
    state->tlsas_response = resp;
    if (state->addrs_response)
        setup_connection(state);
    else
        ; /* Wait for address lookup to complete */
}

Upcoming example: DANE

In the next, upcoming, example, I will build upon this example and show how to use getdns to setup a DANE verified TLS connection asynchronously.

Stay tuned!


Related by JSON Pointers

  Enhanced getdns data access
  Wed 03 Feb 2016
  Melinda Shore   JSON Pointers
Motivation, introduction and demonstration of JSON-pointers
  getdns-0.5.0 release
  Thu 29 Oct 2015
  JSON Pointers
No ldns dependency anymore, JSON pointers with dicts and experimental TLS hostname authentication

Other by Willem Toorop

  Living on the Edge
  Sun 04 Feb 2018
  DNS devroom @ FOSDEM'18
  Willem Toorop   end-2-endness
Greatly needed stub resolver capabilities for applications and systems with the getdns library
  Hands on getdns
  Thu 06 Jul 2017
  JCSA17
  Sara Dickinson   Willem Toorop
Tutorial at the JCSA17 in Paris
  How to get a trustworthy DNS Privacy enabling recursive resolver
  Sun 26 Feb 2017
  NDSS2017
  Willem Toorop   Benno Overeinder   Melinda Shore   DNS Privacy
Analysis of authentication mechanisms for DNS Privacy enabling recursive resolvers, presented at the NDSS2017
  Stubby
  Wed 19 Oct 2016
  NANOG68
  Willem Toorop   Stubby
Introducting Stubby at the NANOG68 in Dallas
  DNSSEC for Legacy Applications
  Thu 19 Nov 2015
  DNS-WG @ RIPE71
  Willem Toorop
Presentation about an experimental nsswitch getdns component.
  getdns - A new stub resolver
  Sun 13 Sep 2015
  vBSDcon 2015
  Willem Toorop
Very complete overview presentation at te vBSDcon 2015 in Reston
  getdns API implementation
  Thu 14 May 2015
  OS-WG @ RIPE70
  Willem Toorop
Presentation in the Open Source Working Group at RIPE70 in Amsterdam
  getdns API
  Thu 26 Mar 2015
  Bits-n-Bites @ IETF92
  Sara Dickinson   Gowri Visweswaran   Willem Toorop
Poster presentation at the Bits-n-Bites of the IETF92
  getdns API implementation
  Wed 25 Jun 2014
  DNSSEC-WS @ ICANN50
  Willem Toorop
Presentation at the DNSSEC Workshop at ICANN50 in London
  getdns API implementation
  Wed 14 May 2014
  OS-WG @ RIPE68
  Willem Toorop
Lightning talk at the Open Source Working Group at RIPE 68 in Warsaw
  getdns API implementation
  Sun 11 May 2014
  DNS-OARC 2014 Spring-WS
  Willem Toorop
Presentation at the DNS-OARC Spring Workshop in Warsaw