blob: c13a4688eb1d46754e265031fd13e8e7e00360f0 [file] [log] [blame]
/*
*
* DNS Client - provides an asynchronous DNS resolution client.
*
* The client is implemented using the c-ares library and integrated with
* flimflam's glib main event loop. See http://c-ares.haxx.se and
* http://developer.gnome.org/glib for c-ares and glib documentation.
*
* This file originally created by Google, Inc.
* Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/time.h>
#include <ares.h>
#include <glib.h>
#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/assert.h>
#include <connman/device.h>
#include <connman/dns_client.h>
#include <connman/log.h>
#include <connman/resolver.h>
#include "connman.h"
#define _DBG_DNS_CLIENT(fmt, arg...) DBG(DBG_RESOLV, fmt, ## arg)
/* Structure representing a pending asynchronous name resolution request. */
struct ares_request {
char *hostname; /* hostname that we're resolving */
struct connman_device *device; /* outgoing interface for request */
struct timeval timeout; /* caller-specified timeout */
struct timeval start_time; /* time at which request was started */
connman_dns_client_callback_t cb; /* client-provided callback */
void *data; /* user data */
ares_channel channel; /* opaque, used by c-ares library */
GHashTable *ares_watches; /* fds that we're monitoring for c-ares */
guint timeout_source_id; /* glib source id for our ares timeout */
gboolean running; /* stopped requests are eligible for deletion */
};
/*
* Structure representing a file descriptor that we're monitoring within our
* glib event loop for c-ares.
*/
struct ares_watch {
struct ares_request *request; /* backpointer to our owner */
int fd; /* file descriptor that we're watching */
GIOChannel *gio_channel; /* glib IO channel */
GIOCondition gio_condition; /* events in which we're interested */
guint g_source_id; /* glib source id */
};
/*
* List of pending asynchronous name resolution requests. We expect the number
* of pending requests to be small, hence the use of a linked list.
*/
static GList *pending_requests = NULL;
/*
* ares requests are often stopped from within ares callbacks. In these cases,
* we defer deletion of the ares_request struct to the idle loop. This is the
* glib source id associated with the deferred deletion task.
*/
static guint deferred_deletion_g_source_id = 0;
static void reset_ares_timeout(struct ares_request *request,
gboolean destroy_old_source);
static void stop_ares_request(struct ares_request *request);
/*
* Callback invoked when it's time to give control back to c-ares. Controlled by
* the glib source referred to by |timeout_source_id| in struct ares_request.
*/
static gboolean ares_timeout_cb(gpointer data)
{
struct ares_request *request = data;
const gboolean destroy_old_source = FALSE;
_DBG_DNS_CLIENT("request %p: running = %d", request, request->running);
if (!request->running) {
request->timeout_source_id = 0;
return FALSE;
}
ares_process_fd(request->channel, ARES_SOCKET_BAD, ARES_SOCKET_BAD);
/*
* NOTE: We tell reset_ares_timeout not to destroy its old timer source
* because we're calling it from within that source and it will be
* destroyed by glib when we return FALSE below.
*/
reset_ares_timeout(request, destroy_old_source);
/*
* Return FALSE to get rid of our old glib source. We created a new
* one during our call to reset_ares_timeout above.
*/
return FALSE;
}
/*
* Determine how long c-ares is willing to wait until being given control and
* schedule ares_timeout_cb to be invoked at that time. Any existing
* timer is replaced. If |destroy_old_source| is TRUE, the old timer's glib
* source will be destroyed.
*/
static void reset_ares_timeout(struct ares_request *request,
gboolean destroy_old_source)
{
struct timeval ret_tv, now, elapsed, max_tv;
struct timeval *tv;
struct timeval *max = NULL;
guint timeout_interval_msecs = 0;
gboolean timeout_provided = FALSE;
_DBG_DNS_CLIENT("request %p: running = %d", request, request->running);
if (!request->running)
return;
/*
* Compute how much time has elapsed since the request started.
* If the client provided a non-default timeout and we've timed out,
* notify the client and stop the request.
*/
gettimeofday(&now, NULL);
timersub(&now, &request->start_time, &elapsed);
timeout_provided = request->timeout.tv_sec != 0 ||
request->timeout.tv_usec != 0;
if (timeout_provided && timercmp(&elapsed, &request->timeout, >=)) {
request->cb(request->data, CONNMAN_DNS_CLIENT_ERROR_TIMED_OUT,
NULL);
stop_ares_request(request);
return;
}
/*
* Tell c-ares how long we're willing to wait (max) and see if it wants
* to regain control sooner than that.
*/
if (timeout_provided) {
timersub(&request->timeout, &elapsed, &max_tv);
max = &max_tv;
}
if ((tv = ares_timeout(request->channel, max, &ret_tv)) == NULL) {
connman_error("%s: ares_timeout failed", __func__);
return;
}
/*
* Reschedule our timeout to be the sooner of the ares-specified tiemout
* and the client-specified timeout.
*/
if (request->timeout_source_id != 0 && destroy_old_source) {
if (!g_source_remove(request->timeout_source_id))
_DBG_DNS_CLIENT("g_source_remove failed");
}
timeout_interval_msecs = tv->tv_sec * 1000 + tv->tv_usec / 1000;
_DBG_DNS_CLIENT("timeout interval = %u", timeout_interval_msecs);
request->timeout_source_id = g_timeout_add(timeout_interval_msecs,
ares_timeout_cb,
request);
}
/*
* Callback invoked by glib when there is activity on a file descriptor that
* we're monitoring for c-ares.
*/
static gboolean ares_watch_io_cb(GIOChannel *source,
GIOCondition condition,
gpointer data)
{
struct ares_watch *watch = data;
ares_socket_t read_fd = ARES_SOCKET_BAD;
ares_socket_t write_fd = ARES_SOCKET_BAD;
const gboolean destroy_old_source = TRUE;
_DBG_DNS_CLIENT("watch %p (fd %d): condition = 0x%x", watch, watch->fd,
condition);
if (!watch->request->running) {
/* Destroy this source by returning FALSE. */
watch->g_source_id = 0;
return FALSE;
}
if (condition & (G_IO_NVAL | G_IO_HUP | G_IO_ERR)) {
connman_error("%s: error condition on fd %d", __func__,
watch->fd);
watch->g_source_id = 0;
return FALSE;
}
if (condition & G_IO_IN)
read_fd = watch->fd;
if (condition & G_IO_OUT)
write_fd = watch->fd;
/* Give control to c-ares. */
ares_process_fd(watch->request->channel, read_fd, write_fd);
reset_ares_timeout(watch->request, destroy_old_source);
return TRUE;
}
/*
* Destroy an ares_watch structure. We register this as our value destroy
* function when creating the ares_watches table, and it is called by glib
* whenever we remove a value from the table or destroy the table.
*/
static void destroy_ares_watch(gpointer data)
{
struct ares_watch *watch = data;
CONNMAN_ASSERT(!watch->request->running);
_DBG_DNS_CLIENT("watch %p (fd %d)", watch, watch->fd);
if (watch->g_source_id != 0) {
if (!g_source_remove(watch->g_source_id)) {
_DBG_DNS_CLIENT("g_source_remove failed for id %d",
watch->g_source_id);
}
watch->g_source_id = 0;
}
g_io_channel_unref(watch->gio_channel);
g_free(watch);
}
/*
* Create an ares_watch for |fd| and store it in the ares_watches table for
* |request|. Monitor for readability if |read| is TRUE. Monitor for writability
* if |write| is TRUE. If there is already an entry for |fd| in the table,
* update it according to the values of |read| and |write|.
*/
static gboolean init_ares_watch(struct ares_request *request, int fd,
gboolean read, gboolean write)
{
struct ares_watch *watch;
_DBG_DNS_CLIENT("fd = %d, read = %d, write = %d", fd, read, write);
CONNMAN_ASSERT(request->running);
/*
* If there's an old watch in the table, destroy it. We'll replace it
* with a new one below if c-ares is still interested in this fd.
*/
if (g_hash_table_lookup(request->ares_watches, &fd) != NULL) {
/* This removal calls destroy_ares_watch on the old watch. */
g_hash_table_remove(request->ares_watches, &fd);
}
if (!read && !write)
return TRUE;
watch = g_malloc0(sizeof(struct ares_watch));
watch->request = request;
watch->fd = fd;
watch->g_source_id = 0;
watch->gio_condition = G_IO_NVAL | G_IO_HUP | G_IO_ERR;
if (read)
watch->gio_condition |= G_IO_IN;
if (write)
watch->gio_condition |= G_IO_OUT;
watch->gio_channel = g_io_channel_unix_new(fd);
if (watch->gio_channel == NULL) {
connman_error("%s: could not create g_io_channel for fd %d",
__func__, fd);
g_free(watch);
return FALSE;
}
g_io_channel_set_close_on_unref(watch->gio_channel, FALSE);
g_hash_table_insert(request->ares_watches, &fd, watch);
watch->g_source_id = g_io_add_watch(watch->gio_channel,
watch->gio_condition,
ares_watch_io_cb,
watch);
return TRUE;
}
/*
* Destroy an ares_request struct, freeing the resources allocated in
* init_ares_request. |request| must already have been removed from the
* |pending_requests| list and must have been marked not running.
*/
static void destroy_ares_request(struct ares_request *request)
{
_DBG_DNS_CLIENT("request %p", request);
CONNMAN_ASSERT(!request->running);
ares_destroy(request->channel);
g_free(request->hostname);
if (request->timeout_source_id != 0)
g_source_remove(request->timeout_source_id);
/* Hash table destruction calls destroy_ares_watch on all watches. */
g_hash_table_destroy(request->ares_watches);
if (request->device != NULL) {
connman_device_rp_filter_enable(request->device);
connman_device_unref(request->device);
}
g_free(request);
}
/*
* Callback invoked from the main loop to perform deferred deletion of stopped
* ares_request objects. We do deferred deletion to avoid problems when we're in
* an ares callback and want to delete an object that contains context
* associated with that callback.
*/
static gboolean delete_stopped_ares_requests_cb(gpointer data)
{
GList *node, *next;
struct ares_request *request;
guint num_requests_deleted = 0;
_DBG_DNS_CLIENT("pending_requests list has length %u",
g_list_length(pending_requests));
/*
* Inspect each request in |pending_requests| and destroy it if it's
* not running.
*/
for (node = pending_requests; node != NULL; node = next) {
next = g_list_next(node);
request = node->data;
if (!request->running) {
pending_requests = g_list_delete_link(pending_requests,
node);
destroy_ares_request(request);
++num_requests_deleted;
}
}
_DBG_DNS_CLIENT("deleted %u stopped requests", num_requests_deleted);
deferred_deletion_g_source_id = 0;
return FALSE;
}
/*
* Stop an ares_request and schedule the deferred deletion task if it's
* not already running.
*/
static void stop_ares_request(struct ares_request *request)
{
_DBG_DNS_CLIENT("");
request->running = FALSE;
if (deferred_deletion_g_source_id != 0)
return;
deferred_deletion_g_source_id =
g_idle_add(delete_stopped_ares_requests_cb, NULL);
if (deferred_deletion_g_source_id == 0)
connman_error("%s: g_idle_add failed", __func__);
}
/*
* Callback that is invoked by c-ares to tell us which sockets it wants us to
* monitor for readability and writability.
*/
static void ares_socket_state_cb(void *data, int s, int read, int write)
{
struct ares_request *request = (struct ares_request *)data;
_DBG_DNS_CLIENT("");
if (!request->running)
return;
_DBG_DNS_CLIENT("socket %d: read = %d, write = %d", s, read, write);
if (!init_ares_watch(request, s, read, write))
connman_error("%s: couldn't create ares_watch for socket %d",
__func__, s);
}
/*
* Converts a c-ares status code to the corresponding dns_client status code.
* We do this to completely encapsulate c-ares. In theory, we should be able to
* replace it with a different asynchronous DNS library without changing our
* clients.
*/
static connman_dns_client_status_t status_from_ares_status(int ares_status)
{
switch(ares_status) {
case ARES_SUCCESS:
return CONNMAN_DNS_CLIENT_SUCCESS;
case ARES_ENODATA:
return CONNMAN_DNS_CLIENT_ERROR_NO_DATA;
case ARES_EFORMERR:
return CONNMAN_DNS_CLIENT_ERROR_FORM_ERR;
case ARES_ESERVFAIL:
return CONNMAN_DNS_CLIENT_ERROR_SERVER_FAIL;
case ARES_ENOTFOUND:
return CONNMAN_DNS_CLIENT_ERROR_NOT_FOUND;
case ARES_ENOTIMP:
return CONNMAN_DNS_CLIENT_ERROR_NOT_IMP;
case ARES_EREFUSED:
return CONNMAN_DNS_CLIENT_ERROR_REFUSED;
case ARES_EBADQUERY:
case ARES_EBADNAME:
case ARES_EBADFAMILY:
case ARES_EBADRESP:
return CONNMAN_DNS_CLIENT_ERROR_BAD_QUERY;
case ARES_ECONNREFUSED:
return CONNMAN_DNS_CLIENT_ERROR_NET_REFUSED;
case ARES_ETIMEOUT:
return CONNMAN_DNS_CLIENT_ERROR_TIMED_OUT;
default:
return CONNMAN_DNS_CLIENT_ERROR_UNKNOWN;
}
}
/*
* Returns a human-friendly error string corresponding to |status|.
* The strings that we return are intentionally consistent with shill error
* messages.
*/
const char *connman_dns_client_strerror(connman_dns_client_status_t status)
{
switch(status) {
case CONNMAN_DNS_CLIENT_SUCCESS:
return "The query was successful.";
case CONNMAN_DNS_CLIENT_ERROR_NO_DATA:
return "The query response contains no answers.";
case CONNMAN_DNS_CLIENT_ERROR_FORM_ERR:
return "The server says the query is bad.";
case CONNMAN_DNS_CLIENT_ERROR_SERVER_FAIL:
return "The server says it had a failure.";
case CONNMAN_DNS_CLIENT_ERROR_NOT_FOUND:
return "The queried-for domain was not found.";
case CONNMAN_DNS_CLIENT_ERROR_NOT_IMP:
return "The server doesn't implement operation.";
case CONNMAN_DNS_CLIENT_ERROR_REFUSED:
return "The server replied, refused the query.";
case CONNMAN_DNS_CLIENT_ERROR_BAD_QUERY:
return "Locally we could not format a query.";
case CONNMAN_DNS_CLIENT_ERROR_NET_REFUSED:
return "The network connection was refused.";
case CONNMAN_DNS_CLIENT_ERROR_TIMED_OUT:
return "The network connection was timed out.";
case CONNMAN_DNS_CLIENT_ERROR_UNKNOWN:
default:
return "DNS Resolver unknown internal error.";
}
}
/*
* Callback that is invoked by c-ares when an asynchronous name resolution
* request that we have previously initiated is complete.
*/
static void ares_request_cb(void *arg, int ares_status, int timeouts,
struct hostent *hostent)
{
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
size_t addr_length;
void *addr_buffer;
char ip_addr_string[INET6_ADDRSTRLEN];
struct sockaddr *ip_addr;
struct ares_request *request = (struct ares_request *)arg;
_DBG_DNS_CLIENT("");
if (!request->running)
return;
/* Stop the request. It will be deleted later from the idle loop. */
stop_ares_request(request);
if (ares_status != ARES_SUCCESS) {
_DBG_DNS_CLIENT("ares request for '%s' failed: %s",
request->hostname, ares_strerror(ares_status));
/* Notify client. */
request->cb(request->data, status_from_ares_status(ares_status),
NULL);
return;
}
if (hostent->h_addrtype != AF_INET && hostent->h_addrtype != AF_INET6) {
_DBG_DNS_CLIENT("unsupported addrtype: %d",
hostent->h_addrtype);
request->cb(request->data, CONNMAN_DNS_CLIENT_ERROR_NO_DATA,
NULL);
return;
}
if (hostent->h_addrtype == AF_INET) {
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
addr_length = sizeof(sin.sin_addr.s_addr);
addr_buffer = &sin.sin_addr.s_addr;
ip_addr = (struct sockaddr *) &sin;
} else { /* AF_INET6 */
memset(&sin6, 0, sizeof(sin6));
sin6.sin6_family = AF_INET6;
addr_length = sizeof(sin6.sin6_addr.s6_addr);
addr_buffer = &sin6.sin6_addr.s6_addr;
ip_addr = (struct sockaddr *) &sin6;
}
if (hostent->h_length > addr_length) {
_DBG_DNS_CLIENT("address too large: %u bytes",
hostent->h_length);
request->cb(request->data, CONNMAN_DNS_CLIENT_ERROR_NO_DATA,
NULL);
return;
}
memcpy(addr_buffer, hostent->h_addr, hostent->h_length);
if (inet_ntop(hostent->h_addrtype, addr_buffer, ip_addr_string,
sizeof(ip_addr_string)) == NULL) {
_DBG_DNS_CLIENT("could not convert address to string: %s",
strerror(errno));
request->cb(request->data, CONNMAN_DNS_CLIENT_ERROR_NO_DATA,
NULL);
return;
}
_DBG_DNS_CLIENT("ares request for '%s' succeeded with %d timeouts: %s",
request->hostname, timeouts, ip_addr_string);
request->cb(request->data, status_from_ares_status(ares_status),
ip_addr);
}
/* Cancel all in-progress asynchronous name resolution requests. */
static void cancel_all_ares_requests()
{
GList *node;
struct ares_request *request;
_DBG_DNS_CLIENT("");
while ((node = g_list_first(pending_requests)) != NULL) {
request = node->data;
pending_requests = g_list_delete_link(pending_requests, node);
request->running = FALSE; /* don't trip assertion */
destroy_ares_request(request);
}
}
/*
* Retrieve the nameservers that are configured for |device| and set |options|
* and |optmask| accordingly so that c-ares will use these nameservers when
* issuing DNS requests.
*/
static void set_ares_nameserver_options(struct connman_device *device,
struct ares_options *options,
int *optmask) {
const char *interface;
const struct connman_resolver_state *resolver_state;
char **serverp;
struct in_addr addr;
int i;
if (device == NULL)
return;
interface = connman_device_get_interface(device);
if (interface == NULL) {
connman_error("%s: no interface for device %p", __func__,
device);
return;
}
resolver_state = connman_resolver_lookup(interface);
if (resolver_state == NULL) {
connman_error("%s: no resolver state for interface %s",
__func__, interface);
return;
}
/*
* resolver_state doesn't tell us how many server addresses are present,
* so we need to walk the list once to determine its length so that we
* can allocate the correct number of struct in_addrs and a second time
* to convert each element from a string to a struct in_addr.
*/
options->nservers = 0;
for (serverp = resolver_state->servers; *serverp != NULL; ++serverp)
++options->nservers;
_DBG_DNS_CLIENT("%d servers found for interface %s", options->nservers,
interface);
if (options->nservers == 0) {
connman_error("%s: no DNS server addresses for interface %s",
__func__, interface);
return;
}
options->servers = g_malloc0(options->nservers *
sizeof(*options->servers));
/* options->servers is freed in free_ares_nameserver_options. */
i = 0;
for (serverp = resolver_state->servers; *serverp != NULL; ++serverp) {
if (inet_aton(*serverp, &addr) == 0) {
_DBG_DNS_CLIENT("invalid server %s for interface %s",
*serverp, interface);
/* Omit this server from the list. */
--options->nservers;
continue;
}
_DBG_DNS_CLIENT("found nameserver %s for interface %s",
*serverp, interface);
options->servers[i] = addr;
++i;
}
if (options->nservers == 0) {
connman_error("%s: no valid DNS server addrs for interface %s",
__func__, interface);
return;
}
*optmask |= ARES_OPT_SERVERS;
}
/*
* Free memory allocated by set_ares_nameserver_options.
*/
static void free_ares_nameserver_options(struct ares_options *options)
{
g_free(options->servers);
options->servers = NULL;
options->nservers = 0;
}
/* Initiate an asynchronous name resolution request. */
connman_dns_client_request_t
connman_dns_client_submit_request(const char *hostname,
struct connman_device *device,
int timeout_ms,
connman_dns_client_callback_t cb,
void *data)
{
int ares_status;
struct ares_request *request;
struct ares_options options;
int optmask;
const gboolean destroy_old_source = TRUE;
_DBG_DNS_CLIENT("");
if (timeout_ms < 0) {
_DBG_DNS_CLIENT("invalid timeout value of %d ms", timeout_ms);
return NULL;
}
request = g_malloc0(sizeof(struct ares_request));
request->running = TRUE;
request->ares_watches = g_hash_table_new_full(g_int_hash, g_int_equal,
NULL, destroy_ares_watch);
if (request->ares_watches == NULL) {
_DBG_DNS_CLIENT("could not create ares_watches table");
g_free(request);
return NULL;
}
/*
* Init a c-ares channel for this request. We set an option asking
* c-ares to notify us via callback about which sockets it wants to
* monitor for readability and writability. This allows us to
* integrate c-ares activity into our glib main event loop.
*/
memset(&options, 0, sizeof(options));
options.sock_state_cb = ares_socket_state_cb;
options.sock_state_cb_data = request;
optmask = ARES_OPT_SOCK_STATE_CB;
if (timeout_ms > 0) {
options.timeout = timeout_ms;
optmask |= ARES_OPT_TIMEOUTMS;
}
set_ares_nameserver_options(device, &options, &optmask);
ares_status = ares_init_options(&request->channel, &options, optmask);
free_ares_nameserver_options(&options);
if (ares_status != ARES_SUCCESS) {
connman_error("%s: failed to init c-ares channel: %s", __func__,
ares_strerror(ares_status));
request->running = FALSE; /* don't trip assertion */
g_hash_table_destroy(request->ares_watches);
g_free(request);
return NULL;
}
/*
* If the caller has provided a preferred interface, tell c-ares to
* send requests out that interface and disable rp filter for the
* duration of the request so that we can receive incoming responses.
*/
request->device = NULL;
if (device != NULL) {
_DBG_DNS_CLIENT("caller has specified device %s",
connman_device_get_interface(device));
request->device = connman_device_ref(device);
ares_set_local_dev(request->channel,
connman_device_get_interface(device));
connman_device_rp_filter_disable(request->device);
}
request->cb = cb;
request->data = data;
request->hostname = g_strdup(hostname);
request->timeout.tv_sec = timeout_ms / 1000;
request->timeout.tv_usec = (timeout_ms % 1000) * 1000;
gettimeofday(&request->start_time, NULL);
pending_requests = g_list_append(pending_requests, request);
ares_gethostbyname(request->channel, hostname, AF_INET, ares_request_cb,
request);
reset_ares_timeout(request, destroy_old_source);
return request;
}
/* Cancel an in-progress name resolution request. */
void connman_dns_client_cancel_request(connman_dns_client_request_t request)
{
_DBG_DNS_CLIENT("request %p", request);
if (request == NULL || !request->running)
return;
pending_requests = g_list_remove(pending_requests, request);
request->running = FALSE; /* don't trip assertion */
destroy_ares_request(request);
}
/* Intitialize this module. */
int __connman_dns_client_init(void)
{
int ares_status = 0;
_DBG_DNS_CLIENT("");
ares_status = ares_library_init(ARES_LIB_INIT_ALL);
if (ares_status != ARES_SUCCESS) {
connman_error("%s: Failed to init c-ares: %s", __func__,
ares_strerror(ares_status));
return -1;
}
return 0;
}
/* Clean up. */
void __connman_dns_client_cleanup(void)
{
_DBG_DNS_CLIENT("");
if (deferred_deletion_g_source_id != 0) {
g_source_remove(deferred_deletion_g_source_id);
deferred_deletion_g_source_id = 0;
}
cancel_all_ares_requests();
/* We rely on libcurl to call ares_library_cleanup() */
}