*
* Connection Manager
*
- * Copyright (C) 2007-2010 Intel Corporation. All rights reserved.
+ * Copyright (C) 2007-2012 Intel Corporation. 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
#include <sys/socket.h>
#include <netdb.h>
#include <resolv.h>
+#include <gweb/gresolv.h>
#include <glib.h>
struct cache_data {
time_t inserted;
+ time_t valid_until;
+ time_t cache_until;
int timeout;
uint16_t type;
uint16_t answers;
struct cache_entry {
char *key;
+ int want_refresh;
+ int hits;
struct cache_data *ipv4;
struct cache_data *ipv6;
};
* when setting the cache entry life time. The value is in seconds.
*/
#define MAX_CACHE_TTL (60 * 30)
+/*
+ * Also limit the other end, cache at least for 30 seconds.
+ */
+#define MIN_CACHE_TTL (30)
/*
* We limit the cache size to some sane value so that cached data does
static GSList *request_pending_list = NULL;
static guint16 request_id = 0x0000;
static GHashTable *listener_table = NULL;
+static time_t next_refresh;
static int protocol_offset(int protocol)
{
}
+/*
+ * There is a power and efficiency benefit to have entries
+ * in our cache expire at the same time. To this extend,
+ * we round down the cache valid time to common boundaries.
+ */
+static time_t round_down_ttl(time_t end_time, int ttl)
+{
+ if (ttl < 15)
+ return end_time;
+
+ /* Less than 5 minutes, round to 10 second boundary */
+ if (ttl < 300) {
+ end_time = end_time / 10;
+ end_time = end_time * 10;
+ } else { /* 5 or more minutes, round to 30 seconds */
+ end_time = end_time / 30;
+ end_time = end_time * 30;
+ }
+ return end_time;
+}
+
static struct request_data *find_request(guint16 id)
{
GSList *list;
return NULL;
}
+/* we can keep using the same resolve's */
+static GResolv *ipv4_resolve;
+static GResolv *ipv6_resolve;
+
+static void dummy_resolve_func(GResolvResultStatus status,
+ char **results, gpointer user_data)
+{
+}
+
+/*
+ * Refresh a DNS entry, but also age the hit count a bit */
+static void refresh_dns_entry(struct cache_entry *entry, char *name)
+{
+ int age = 1;
+
+ if (ipv4_resolve == NULL) {
+ ipv4_resolve = g_resolv_new(0);
+ g_resolv_set_address_family(ipv4_resolve, AF_INET);
+ g_resolv_add_nameserver(ipv4_resolve, "127.0.0.1", 53, 0);
+ }
+
+ if (ipv6_resolve == NULL) {
+ ipv6_resolve = g_resolv_new(0);
+ g_resolv_set_address_family(ipv6_resolve, AF_INET6);
+ g_resolv_add_nameserver(ipv6_resolve, "127.0.0.1", 53, 0);
+ }
+
+ if (entry->ipv4 == NULL) {
+ DBG("Refresing A record for %s", name);
+ g_resolv_lookup_hostname(ipv4_resolve, name,
+ dummy_resolve_func, NULL);
+ age = 4;
+ }
+
+ if (entry->ipv6 == NULL) {
+ DBG("Refresing AAAA record for %s", name);
+ g_resolv_lookup_hostname(ipv6_resolve, name,
+ dummy_resolve_func, NULL);
+ age = 4;
+ }
+
+ entry->hits -= age;
+ if (entry->hits < 0)
+ entry->hits = 0;
+}
+
+static int dns_name_length(unsigned char *buf)
+{
+ if ((buf[0] & NS_CMPRSFLGS) == NS_CMPRSFLGS) /* compressed name */
+ return 2;
+ return strlen((char *)buf);
+}
+
+static void update_cached_ttl(unsigned char *buf, int len, int new_ttl)
+{
+ unsigned char *c;
+ uint32_t *i;
+ uint16_t *w;
+ int l;
+
+ /* skip the header */
+ c = buf + 12;
+ len -= 12;
+
+ /* skip the query, which is a name and 2 16 bit words */
+ l = dns_name_length(c);
+ c += l;
+ len -= l;
+ c += 4;
+ len -= 4;
+
+ /* now we get the answer records */
+
+ while (len > 0) {
+ /* first a name */
+ l = dns_name_length(c);
+ c += l;
+ len -= l;
+ if (len < 0)
+ break;
+ /* then type + class, 2 bytes each */
+ c += 4;
+ len -= 4;
+ if (len < 0)
+ break;
+
+ /* now the 4 byte TTL field */
+ i = (uint32_t *)c;
+ *i = htonl(new_ttl);
+ c += 4;
+ len -= 4;
+ if (len < 0)
+ break;
+
+ /* now the 2 byte rdlen field */
+ w = (uint16_t *)c;
+ c += ntohs(*w) + 2;
+ len -= ntohs(*w) + 2;
+ }
+}
+
static void send_cached_response(int sk, unsigned char *buf, int len,
const struct sockaddr *to, socklen_t tolen,
- int protocol, int id, uint16_t answers)
+ int protocol, int id, uint16_t answers, int ttl)
{
struct domain_hdr *hdr;
int err, offset = protocol_offset(protocol);
hdr->nscount = 0;
hdr->arcount = 0;
+ /* if this is a negative reply, we are authorative */
+ if (answers == 0)
+ hdr->aa = 1;
+ else
+ update_cached_ttl(buf, len, ttl);
+
DBG("id 0x%04x answers %d", hdr->id, answers);
err = sendto(sk, buf, len, 0, to, tolen);
if (data == NULL)
return FALSE;
- if (data->inserted + data->timeout < current_time)
+ if (data->cache_until < current_time)
return FALSE;
return TRUE;
}
+/*
+ * remove stale cached entries so that they can be refreshed
+ */
+static void cache_enforce_validity(struct cache_entry *entry)
+{
+ time_t current_time = time(NULL);
+
+ if (cache_check_is_valid(entry->ipv4, current_time) == FALSE
+ && entry->ipv4) {
+ DBG("cache timeout \"%s\" type A", entry->key);
+ g_free(entry->ipv4->data);
+ g_free(entry->ipv4);
+ entry->ipv4 = NULL;
+
+ }
+
+ if (cache_check_is_valid(entry->ipv6, current_time) == FALSE
+ && entry->ipv6) {
+ DBG("cache timeout \"%s\" type AAAA", entry->key);
+ g_free(entry->ipv6->data);
+ g_free(entry->ipv6);
+ entry->ipv6 = NULL;
+ }
+}
+
static uint16_t cache_check_validity(char *question, uint16_t type,
- struct cache_data *ipv4,
- struct cache_data *ipv6)
+ struct cache_entry *entry)
{
- time_t current_time = time(0);
+ time_t current_time = time(NULL);
+ int want_refresh = 0;
+
+ /*
+ * if we have a popular entry, we want a refresh instead of
+ * total destruction of the entry.
+ */
+ if (entry->hits > 2)
+ want_refresh = 1;
+
+ cache_enforce_validity(entry);
switch (type) {
case 1: /* IPv4 */
- if (cache_check_is_valid(ipv4, current_time) == FALSE) {
- DBG("cache %s \"%s\" type A",
- ipv4 ? "timeout" : "entry missing", question);
+ if (cache_check_is_valid(entry->ipv4, current_time) == FALSE) {
+ DBG("cache %s \"%s\" type A", entry->ipv4 ?
+ "timeout" : "entry missing", question);
+
+ if (want_refresh)
+ entry->want_refresh = 1;
/*
* We do not remove cache entry if there is still
* valid IPv6 entry found in the cache.
*/
- if (cache_check_is_valid(ipv6, current_time) == FALSE)
+ if (cache_check_is_valid(entry->ipv6, current_time)
+ == FALSE && want_refresh == FALSE) {
g_hash_table_remove(cache, question);
-
- type = 0;
+ type = 0;
+ }
}
break;
case 28: /* IPv6 */
- if (cache_check_is_valid(ipv6, current_time) == FALSE) {
- DBG("cache %s \"%s\" type AAAA",
- ipv6 ? "timeout" : "entry missing", question);
+ if (cache_check_is_valid(entry->ipv6, current_time) == FALSE) {
+ DBG("cache %s \"%s\" type AAAA", entry->ipv6 ?
+ "timeout" : "entry missing", question);
- if (cache_check_is_valid(ipv4, current_time) == FALSE)
- g_hash_table_remove(cache, question);
+ if (want_refresh)
+ entry->want_refresh = 1;
- type = 0;
+ if (cache_check_is_valid(entry->ipv4, current_time)
+ == FALSE && want_refresh == FALSE) {
+ g_hash_table_remove(cache, question);
+ type = 0;
+ }
}
break;
}
if (entry == NULL)
return NULL;
- type = cache_check_validity(question, type, entry->ipv4, entry->ipv6);
+ type = cache_check_validity(question, type, entry);
if (type == 0)
return NULL;
struct cache_timeout {
time_t current_time;
int max_timeout;
+ int try_harder;
};
static gboolean cache_check_entry(gpointer key, gpointer value,
struct cache_entry *entry = value;
int max_timeout;
+ /* Scale the number of hits by half as part of cache aging */
+
+ entry->hits /= 2;
+
/*
* If either IPv4 or IPv6 cached entry has expired, we
* remove both from the cache.
*/
if (entry->ipv4 != NULL && entry->ipv4->timeout > 0) {
- max_timeout = entry->ipv4->inserted + entry->ipv4->timeout;
+ max_timeout = entry->ipv4->cache_until;
if (max_timeout > data->max_timeout)
data->max_timeout = max_timeout;
- if (entry->ipv4->inserted + entry->ipv4->timeout
- < data->current_time)
+ if (entry->ipv4->cache_until < data->current_time)
return TRUE;
}
if (entry->ipv6 != NULL && entry->ipv6->timeout > 0) {
- max_timeout = entry->ipv6->inserted + entry->ipv6->timeout;
+ max_timeout = entry->ipv6->cache_until;
if (max_timeout > data->max_timeout)
data->max_timeout = max_timeout;
- if (entry->ipv6->inserted + entry->ipv6->timeout
- < data->current_time)
+ if (entry->ipv6->cache_until < data->current_time)
return TRUE;
}
+ /*
+ * if we're asked to try harder, also remove entries that have
+ * few hits
+ */
+ if (data->try_harder && entry->hits < 4)
+ return TRUE;
+
return FALSE;
}
{
static int max_timeout;
struct cache_timeout data;
- int count;
+ int count = 0;
- data.current_time = time(0);
+ data.current_time = time(NULL);
data.max_timeout = 0;
+ data.try_harder = 0;
- if (max_timeout > data.current_time) {
- DBG("waiting %ld secs before cleaning cache",
- max_timeout - data.current_time);
- return;
+ /*
+ * In the first pass, we only remove entries that have timed out.
+ * We use a cache of the first time to expire to do this only
+ * when it makes sense.
+ */
+ if (max_timeout <= data.current_time) {
+ count = g_hash_table_foreach_remove(cache, cache_check_entry,
+ &data);
}
+ DBG("removed %d in the first pass", count);
- count = g_hash_table_foreach_remove(cache, cache_check_entry,
+ /*
+ * In the second pass, if the first pass turned up blank,
+ * we also expire entries with a low hit count,
+ * while aging the hit count at the same time.
+ */
+ data.try_harder = 1;
+ if (count == 0)
+ count = g_hash_table_foreach_remove(cache, cache_check_entry,
&data);
- DBG("removed %d", count);
if (count == 0)
/*
max_timeout = 0;
}
+static gboolean cache_invalidate_entry(gpointer key, gpointer value,
+ gpointer user_data)
+{
+ struct cache_entry *entry = value;
+
+ /* first, delete any expired elements */
+ cache_enforce_validity(entry);
+
+ /* if anything is not expired, mark the entry for refresh */
+ if (entry->hits > 0 && (entry->ipv4 || entry->ipv6))
+ entry->want_refresh = 1;
+
+ /* delete the cached data */
+ if (entry->ipv4) {
+ g_free(entry->ipv4->data);
+ g_free(entry->ipv4);
+ entry->ipv4 = NULL;
+ }
+
+ if (entry->ipv6) {
+ g_free(entry->ipv6->data);
+ g_free(entry->ipv6);
+ entry->ipv6 = NULL;
+ }
+
+ /* keep the entry if we want it refreshed, delete it otherwise */
+ if (entry->want_refresh)
+ return FALSE;
+ else
+ return TRUE;
+}
+
+/*
+ * cache_invalidate is called from places where the DNS landscape
+ * has changed, say because connections are added or we entered a VPN.
+ * The logic is to wipe all cache data, but mark all non-expired
+ * parts of the cache for refresh rather than deleting the whole cache.
+ */
+static void cache_invalidate(void)
+{
+ DBG("Invalidating the DNS cache %p", cache);
+
+ if (cache == NULL)
+ return;
+
+ g_hash_table_foreach_remove(cache, cache_invalidate_entry, NULL);
+}
+
+static void cache_refresh_entry(struct cache_entry *entry)
+{
+
+ cache_enforce_validity(entry);
+
+ if (entry->hits > 2 && entry->ipv4 == NULL)
+ entry->want_refresh = 1;
+ if (entry->hits > 2 && entry->ipv6 == NULL)
+ entry->want_refresh = 1;
+
+ if (entry->want_refresh) {
+ char *c;
+ char dns_name[NS_MAXDNAME + 1];
+ entry->want_refresh = 0;
+
+ /* turn a DNS name into a hostname with dots */
+ strncpy(dns_name, entry->key, NS_MAXDNAME);
+ c = dns_name;
+ while (c && *c) {
+ int jump;
+ jump = *c;
+ *c = '.';
+ c += jump + 1;
+ }
+ DBG("Refreshing %s\n", dns_name);
+ /* then refresh the hostname */
+ refresh_dns_entry(entry, &dns_name[1]);
+ }
+}
+
+static void cache_refresh_iterator(gpointer key, gpointer value,
+ gpointer user_data)
+{
+ struct cache_entry *entry = value;
+
+ cache_refresh_entry(entry);
+}
+
+static void cache_refresh(void)
+{
+ if (cache == NULL)
+ return;
+
+ g_hash_table_foreach(cache, cache_refresh_iterator, NULL);
+}
+
+static int reply_query_type(unsigned char *msg, int len)
+{
+ unsigned char *c;
+ uint16_t *w;
+ int l;
+ int type;
+
+ /* skip the header */
+ c = msg + sizeof(struct domain_hdr);
+ len -= sizeof(struct domain_hdr);
+
+ if (len < 0)
+ return 0;
+
+ /* now the query, which is a name and 2 16 bit words */
+ l = dns_name_length(c) + 1;
+ c += l;
+ w = (uint16_t *) c;
+ type = ntohs(*w);
+
+ return type;
+}
+
static int cache_update(struct server_data *srv, unsigned char *msg,
unsigned int msg_len)
{
int offset = protocol_offset(srv->protocol);
int err, qlen, ttl = 0;
- uint16_t answers, type = 0, class = 0;
+ uint16_t answers = 0, type = 0, class = 0;
struct domain_question *q;
struct cache_entry *entry;
struct cache_data *data;
return 0;
}
+ current_time = time(NULL);
+
+ /* don't do a cache refresh more than twice a minute */
+ if (next_refresh < current_time) {
+ cache_refresh();
+ next_refresh = current_time + 30;
+ }
+
+
/* Continue only if response code is 0 (=ok) */
if (msg[3] & 0x0f)
return 0;
question, sizeof(question) - 1,
&type, &class, &ttl,
response, &rsplen, &answers);
+
+ /*
+ * special case: if we do a ipv6 lookup and get no result
+ * for a record that's already in our ipv4 cache.. we want
+ * to cache the negative response.
+ */
+ if ((err == -ENOMSG || err == -ENOBUFS) &&
+ reply_query_type(msg, msg_len) == 28) {
+ entry = g_hash_table_lookup(cache, question);
+ if (entry && entry->ipv4 && entry->ipv6 == NULL) {
+ data = g_try_new(struct cache_data, 1);
+ if (data == NULL)
+ return -ENOMEM;
+ data->inserted = entry->ipv4->inserted;
+ data->type = type;
+ data->answers = msg[5];
+ data->timeout = entry->ipv4->timeout;
+ data->data_len = msg_len;
+ data->data = ptr = g_malloc(msg_len);
+ data->valid_until = entry->ipv4->valid_until;
+ data->cache_until = entry->ipv4->cache_until;
+ memcpy(data->data, msg, msg_len);
+ entry->ipv6 = data;
+ /*
+ * we will get a "hit" when we serve the response
+ * out of the cache
+ */
+ entry->hits--;
+ if (entry->hits < 0)
+ entry->hits = 0;
+ return 0;
+ }
+ }
+
if (err < 0 || ttl == 0)
return 0;
qlen = strlen(question);
- current_time = time(0);
/*
* If the cache contains already data, check if the
entry->key = g_strdup(question);
entry->ipv4 = entry->ipv6 = NULL;
+ entry->want_refresh = 0;
+ entry->hits = 0;
if (type == 1)
entry->ipv4 = data;
else
entry->ipv6 = data;
+ /*
+ * compensate for the hit we'll get for serving
+ * the response out of the cache
+ */
+ entry->hits--;
+ if (entry->hits < 0)
+ entry->hits = 0;
+
new_entry = FALSE;
}
- /*
- * Restrict the cached DNS record TTL to some sane value
- * in order to prevent data staying in the cache too long.
- */
- if (ttl > MAX_CACHE_TTL)
- ttl = MAX_CACHE_TTL;
+ if (ttl < MIN_CACHE_TTL)
+ ttl = MIN_CACHE_TTL;
data->inserted = current_time;
data->type = type;
data->timeout = ttl;
data->data_len = 12 + qlen + 1 + 2 + 2 + rsplen;
data->data = ptr = g_malloc(data->data_len);
+ data->valid_until = current_time + ttl;
+
+ /*
+ * Restrict the cached DNS record TTL to some sane value
+ * in order to prevent data staying in the cache too long.
+ */
+ if (ttl > MAX_CACHE_TTL)
+ ttl = MAX_CACHE_TTL;
+
+ data->cache_until = round_down_ttl(current_time + ttl, ttl);
+
if (data->data == NULL) {
g_free(entry->key);
g_free(data);
entry = cache_check(request, &type);
if (entry != NULL) {
+ int ttl_left = 0;
struct cache_data *data;
DBG("cache hit %s type %s", lookup, type == 1 ? "A" : "AAAA");
else
data = entry->ipv6;
+ if (data) {
+ ttl_left = data->valid_until - time(NULL);
+ entry->hits++;
+ }
+
if (data != NULL && req->protocol == IPPROTO_TCP) {
send_cached_response(req->client_sk, data->data,
data->data_len, NULL, 0, IPPROTO_TCP,
- req->srcid, data->answers);
+ req->srcid, data->answers, ttl_left);
return 1;
}
send_cached_response(sk, data->data,
data->data_len, &req->sa, req->sa_len,
- IPPROTO_UDP, req->srcid, data->answers);
+ IPPROTO_UDP, req->srcid, data->answers,
+ ttl_left);
return 1;
}
}
sk = g_io_channel_unix_get_fd(server->channel);
err = send(sk, request, req->request_len, 0);
+ if (err < 0)
+ return -EIO;
req->numserv++;
cache_size = 0;
}
+static gboolean try_remove_cache(gpointer user_data)
+{
+ if (__sync_fetch_and_sub(&cache_refcount, 1) == 1) {
+ DBG("No cache users, removing it.");
+
+ g_hash_table_destroy(cache);
+ cache = NULL;
+ }
+
+ return FALSE;
+}
+
static void destroy_server(struct server_data *server)
{
GList *list;
}
g_free(server->interface);
- if (__sync_fetch_and_sub(&cache_refcount, 1) == 1)
- g_hash_table_destroy(cache);
+ /*
+ * We do not remove cache right away but delay it few seconds.
+ * The idea is that when IPv6 DNS server is added via RDNSS, it has a
+ * lifetime. When the lifetime expires we decrease the refcount so it
+ * is possible that the cache is then removed. Because a new DNS server
+ * is usually created almost immediately we would then loose the cache
+ * without any good reason. The small delay allows the new RDNSS to
+ * create a new DNS server instance and the refcount does not go to 0.
+ */
+ g_timeout_add_seconds(3, try_remove_cache, NULL);
g_free(server);
}
if (enabled == FALSE) {
connman_info("Enabling DNS server %s", data->server);
data->enabled = TRUE;
+ cache_invalidate();
+ cache_refresh();
} else {
connman_info("Disabling DNS server %s", data->server);
data->enabled = FALSE;
+ cache_invalidate();
}
}
}
DBG("service %p", service);
+ /* DNS has changed, invalidate the cache */
+ cache_invalidate();
+
if (service == NULL) {
/* When no services are active, then disable DNS proxying */
dnsproxy_offline_mode(TRUE);
}
g_free(interface);
+ cache_refresh();
}
static struct connman_notifier dnsproxy_notifier = {
return 0;
}
+static void destroy_request_data(struct request_data *req)
+{
+ if (req->timeout > 0)
+ g_source_remove(req->timeout);
+
+ g_free(req->resp);
+ g_free(req->request);
+ g_free(req->name);
+ g_free(req);
+}
+
static void destroy_listener(struct listener_data *ifdata)
{
GSList *list;
DBG("Dropping pending request (id 0x%04x -> 0x%04x)",
req->srcid, req->dstid);
-
- g_free(req->resp);
- g_free(req->request);
- g_free(req->name);
- g_free(req);
+ destroy_request_data(req);
list->data = NULL;
}
DBG("Dropping request (id 0x%04x -> 0x%04x)",
req->srcid, req->dstid);
-
- g_free(req->resp);
- g_free(req->request);
- g_free(req->name);
- g_free(req);
+ destroy_request_data(req);
list->data = NULL;
}
static void remove_listener(gpointer key, gpointer value, gpointer user_data)
{
- __connman_dnsproxy_remove_listener(key);
+ const char *interface = key;
+ struct listener_data *ifdata = value;
+
+ DBG("interface %s", interface);
+
+ destroy_listener(ifdata);
}
int __connman_dnsproxy_init(void)