dnsproxy: If no request was sent then the TCP server is destroyed
[framework/connectivity/connman.git] / src / dnsproxy.c
index a56e312..58d1bfe 100644 (file)
@@ -2,7 +2,7 @@
  *
  *  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
@@ -24,6 +24,7 @@
 #endif
 
 #include <errno.h>
+#include <stdlib.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdint.h>
@@ -32,6 +33,8 @@
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <resolv.h>
+#include <gweb/gresolv.h>
 
 #include <glib.h>
 
@@ -113,6 +116,7 @@ struct request_data {
        gpointer resp;
        gsize resplen;
        struct listener_data *ifdata;
+       gboolean append_domain;
 };
 
 struct listener_data {
@@ -123,11 +127,70 @@ struct listener_data {
        guint tcp_listener_watch;
 };
 
+struct cache_data {
+       time_t inserted;
+       time_t valid_until;
+       time_t cache_until;
+       int timeout;
+       uint16_t type;
+       uint16_t answers;
+       unsigned int data_len;
+       unsigned char *data; /* contains DNS header + body */
+};
+
+struct cache_entry {
+       char *key;
+       int want_refresh;
+       int hits;
+       struct cache_data *ipv4;
+       struct cache_data *ipv6;
+};
+
+struct domain_question {
+       uint16_t type;
+       uint16_t class;
+} __attribute__ ((packed));
+
+struct domain_rr {
+       uint16_t type;
+       uint16_t class;
+       uint32_t ttl;
+       uint16_t rdlen;
+} __attribute__ ((packed));
+
+/*
+ * We limit how long the cached DNS entry stays in the cache.
+ * By default the TTL (time-to-live) of the DNS response is used
+ * 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
+ * not occupy too much memory. Each cached entry occupies on average
+ * about 100 bytes memory (depending on DNS name length).
+ * Example: caching www.connman.net uses 97 bytes memory.
+ * The value is the max amount of cached DNS responses (count).
+ */
+#define MAX_CACHE_SIZE 256
+
+static int cache_size;
+static GHashTable *cache;
+static int cache_refcount;
 static GSList *server_list = NULL;
 static GSList *request_list = NULL;
 static GSList *request_pending_list = NULL;
-static guint16 request_id = 0x0000;
 static GHashTable *listener_table = NULL;
+static time_t next_refresh;
+
+static guint16 get_id()
+{
+       return random();
+}
 
 static int protocol_offset(int protocol)
 {
@@ -144,6 +207,27 @@ 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;
@@ -187,6 +271,144 @@ static struct server_data *find_server(const char *interface,
        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 ttl)
+{
+       struct domain_hdr *hdr;
+       int err, offset = protocol_offset(protocol);
+
+       if (offset < 0)
+               return;
+
+       if (len < 12)
+               return;
+
+       hdr = (void *) (buf + offset);
+
+       hdr->id = id;
+       hdr->qr = 1;
+       hdr->rcode = 0;
+       hdr->ancount = htons(answers);
+       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 (err < 0) {
+               connman_error("Cannot send cached DNS response: %s",
+                               strerror(errno));
+               return;
+       }
+}
 
 static void send_response(int sk, unsigned char *buf, int len,
                                const struct sockaddr *to, socklen_t tolen,
@@ -200,144 +422,1030 @@ static void send_response(int sk, unsigned char *buf, int len,
        if (offset < 0)
                return;
 
-       if (len < 12)
+       if (len < 12)
+               return;
+
+       hdr = (void *) (buf + offset);
+
+       DBG("id 0x%04x qr %d opcode %d", hdr->id, hdr->qr, hdr->opcode);
+
+       hdr->qr = 1;
+       hdr->rcode = 2;
+
+       hdr->ancount = 0;
+       hdr->nscount = 0;
+       hdr->arcount = 0;
+
+       err = sendto(sk, buf, len, 0, to, tolen);
+       if (err < 0) {
+               connman_error("Failed to send DNS response: %s",
+                               strerror(errno));
+               return;
+       }
+}
+
+static gboolean request_timeout(gpointer user_data)
+{
+       struct request_data *req = user_data;
+       struct listener_data *ifdata;
+
+       DBG("id 0x%04x", req->srcid);
+
+       if (req == NULL)
+               return FALSE;
+
+       ifdata = req->ifdata;
+
+       request_list = g_slist_remove(request_list, req);
+       req->numserv--;
+
+       if (req->resplen > 0 && req->resp != NULL) {
+               int sk, err;
+
+               sk = g_io_channel_unix_get_fd(ifdata->udp_listener_channel);
+
+               err = sendto(sk, req->resp, req->resplen, 0,
+                                               &req->sa, req->sa_len);
+               if (err < 0)
+                       return FALSE;
+       } else if (req->request && req->numserv == 0) {
+               struct domain_hdr *hdr;
+
+               if (req->protocol == IPPROTO_TCP) {
+                       hdr = (void *) (req->request + 2);
+                       hdr->id = req->srcid;
+                       send_response(req->client_sk, req->request,
+                               req->request_len, NULL, 0, IPPROTO_TCP);
+
+               } else if (req->protocol == IPPROTO_UDP) {
+                       int sk;
+
+                       hdr = (void *) (req->request);
+                       hdr->id = req->srcid;
+                       sk = g_io_channel_unix_get_fd(
+                                               ifdata->udp_listener_channel);
+                       send_response(sk, req->request, req->request_len,
+                                       &req->sa, req->sa_len, IPPROTO_UDP);
+               }
+       }
+
+       g_free(req->resp);
+       g_free(req);
+
+       return FALSE;
+}
+
+static int append_query(unsigned char *buf, unsigned int size,
+                               const char *query, const char *domain)
+{
+       unsigned char *ptr = buf;
+       int len;
+
+       DBG("query %s domain %s", query, domain);
+
+       while (query != NULL) {
+               const char *tmp;
+
+               tmp = strchr(query, '.');
+               if (tmp == NULL) {
+                       len = strlen(query);
+                       if (len == 0)
+                               break;
+                       *ptr = len;
+                       memcpy(ptr + 1, query, len);
+                       ptr += len + 1;
+                       break;
+               }
+
+               *ptr = tmp - query;
+               memcpy(ptr + 1, query, tmp - query);
+               ptr += tmp - query + 1;
+
+               query = tmp + 1;
+       }
+
+       while (domain != NULL) {
+               const char *tmp;
+
+               tmp = strchr(domain, '.');
+               if (tmp == NULL) {
+                       len = strlen(domain);
+                       if (len == 0)
+                               break;
+                       *ptr = len;
+                       memcpy(ptr + 1, domain, len);
+                       ptr += len + 1;
+                       break;
+               }
+
+               *ptr = tmp - domain;
+               memcpy(ptr + 1, domain, tmp - domain);
+               ptr += tmp - domain + 1;
+
+               domain = tmp + 1;
+       }
+
+       *ptr++ = 0x00;
+
+       return ptr - buf;
+}
+
+static gboolean cache_check_is_valid(struct cache_data *data,
+                               time_t current_time)
+{
+       if (data == NULL)
+               return FALSE;
+
+       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_entry *entry)
+{
+       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(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(entry->ipv6, current_time)
+                                       == FALSE && want_refresh == FALSE) {
+                               g_hash_table_remove(cache, question);
+                               type = 0;
+                       }
+               }
+               break;
+
+       case 28:        /* IPv6 */
+               if (cache_check_is_valid(entry->ipv6, current_time) == FALSE) {
+                       DBG("cache %s \"%s\" type AAAA", entry->ipv6 ?
+                                       "timeout" : "entry missing", question);
+
+                       if (want_refresh)
+                               entry->want_refresh = 1;
+
+                       if (cache_check_is_valid(entry->ipv4, current_time)
+                                       == FALSE && want_refresh == FALSE) {
+                               g_hash_table_remove(cache, question);
+                               type = 0;
+                       }
+               }
+               break;
+       }
+
+       return type;
+}
+
+static struct cache_entry *cache_check(gpointer request, int *qtype)
+{
+       char *question = request + 12;
+       struct cache_entry *entry;
+       struct domain_question *q;
+       uint16_t type;
+       int offset;
+
+       offset = strlen(question) + 1;
+       q = (void *) (question + offset);
+       type = ntohs(q->type);
+
+       /* We only cache either A (1) or AAAA (28) requests */
+       if (type != 1 && type != 28)
+               return NULL;
+
+       entry = g_hash_table_lookup(cache, question);
+       if (entry == NULL)
+               return NULL;
+
+       type = cache_check_validity(question, type, entry);
+       if (type == 0)
+               return NULL;
+
+       *qtype = type;
+       return entry;
+}
+
+/*
+ * Get a label/name from DNS resource record. The function decompresses the
+ * label if necessary. The function does not convert the name to presentation
+ * form. This means that the result string will contain label lengths instead
+ * of dots between labels. We intentionally do not want to convert to dotted
+ * format so that we can cache the wire format string directly.
+ */
+static int get_name(int counter,
+               unsigned char *pkt, unsigned char *start, unsigned char *max,
+               unsigned char *output, int output_max, int *output_len,
+               unsigned char **end, char *name, int *name_len)
+{
+       unsigned char *p;
+
+       /* Limit recursion to 10 (this means up to 10 labels in domain name) */
+       if (counter > 10)
+               return -EINVAL;
+
+       p = start;
+       while (*p) {
+               if ((*p & NS_CMPRSFLGS) == NS_CMPRSFLGS) {
+                       uint16_t offset = (*p & 0x3F) * 256 + *(p + 1);
+
+                       if (offset >= max - pkt)
+                               return -ENOBUFS;
+
+                       if (*end == NULL)
+                               *end = p + 2;
+
+                       return get_name(counter + 1, pkt, pkt + offset, max,
+                                       output, output_max, output_len, end,
+                                       name, name_len);
+               } else {
+                       unsigned label_len = *p;
+
+                       if (pkt + label_len > max)
+                               return -ENOBUFS;
+
+                       if (*output_len > output_max)
+                               return -ENOBUFS;
+
+                       /*
+                        * We need the original name in order to check
+                        * if this answer is the correct one.
+                        */
+                       name[(*name_len)++] = label_len;
+                       memcpy(name + *name_len, p + 1, label_len + 1);
+                       *name_len += label_len;
+
+                       /* We compress the result */
+                       output[0] = NS_CMPRSFLGS;
+                       output[1] = 0x0C;
+                       *output_len = 2;
+
+                       p += label_len + 1;
+
+                       if (*end == NULL)
+                               *end = p;
+
+                       if (p >= max)
+                               return -ENOBUFS;
+               }
+       }
+
+       return 0;
+}
+
+static int parse_rr(unsigned char *buf, unsigned char *start,
+                       unsigned char *max,
+                       unsigned char *response, unsigned int *response_size,
+                       uint16_t *type, uint16_t *class, int *ttl, int *rdlen,
+                       unsigned char **end,
+                       char *name)
+{
+       struct domain_rr *rr;
+       int err, offset;
+       int name_len = 0, output_len = 0, max_rsp = *response_size;
+
+       err = get_name(0, buf, start, max, response, max_rsp,
+               &output_len, end, name, &name_len);
+       if (err < 0)
+               return err;
+
+       offset = output_len;
+
+       if ((unsigned int) offset > *response_size)
+               return -ENOBUFS;
+
+       rr = (void *) (*end);
+
+       if (rr == NULL)
+               return -EINVAL;
+
+       *type = ntohs(rr->type);
+       *class = ntohs(rr->class);
+       *ttl = ntohl(rr->ttl);
+       *rdlen = ntohs(rr->rdlen);
+
+       if (*ttl < 0)
+               return -EINVAL;
+
+       memcpy(response + offset, *end, sizeof(struct domain_rr));
+
+       offset += sizeof(struct domain_rr);
+       *end += sizeof(struct domain_rr);
+
+       if ((unsigned int) (offset + *rdlen) > *response_size)
+               return -ENOBUFS;
+
+       memcpy(response + offset, *end, *rdlen);
+
+       *end += *rdlen;
+
+       *response_size = offset + *rdlen;
+
+       return 0;
+}
+
+static gboolean check_alias(GSList *aliases, char *name)
+{
+       GSList *list;
+
+       if (aliases != NULL) {
+               for (list = aliases; list; list = list->next) {
+                       int len = strlen((char *)list->data);
+                       if (strncmp((char *)list->data, name, len) == 0)
+                               return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+static int parse_response(unsigned char *buf, int buflen,
+                       char *question, int qlen,
+                       uint16_t *type, uint16_t *class, int *ttl,
+                       unsigned char *response, unsigned int *response_len,
+                       uint16_t *answers)
+{
+       struct domain_hdr *hdr = (void *) buf;
+       struct domain_question *q;
+       unsigned char *ptr;
+       uint16_t qdcount = ntohs(hdr->qdcount);
+       uint16_t ancount = ntohs(hdr->ancount);
+       int err, i;
+       uint16_t qtype, qclass;
+       unsigned char *next = NULL;
+       unsigned int maxlen = *response_len;
+       GSList *aliases = NULL, *list;
+       char name[NS_MAXDNAME + 1];
+
+       if (buflen < 12)
+               return -EINVAL;
+
+       DBG("qr %d qdcount %d", hdr->qr, qdcount);
+
+       /* We currently only cache responses where question count is 1 */
+       if (hdr->qr != 1 || qdcount != 1)
+               return -EINVAL;
+
+       ptr = buf + sizeof(struct domain_hdr);
+
+       strncpy(question, (char *) ptr, qlen);
+       qlen = strlen(question);
+       ptr += qlen + 1; /* skip \0 */
+
+       q = (void *) ptr;
+       qtype = ntohs(q->type);
+
+       /* We cache only A and AAAA records */
+       if (qtype != 1 && qtype != 28)
+               return -ENOMSG;
+
+       qclass = ntohs(q->class);
+
+       ptr += 2 + 2; /* ptr points now to answers */
+
+       err = -ENOMSG;
+       *response_len = 0;
+       *answers = 0;
+
+       /*
+        * We have a bunch of answers (like A, AAAA, CNAME etc) to
+        * A or AAAA question. We traverse the answers and parse the
+        * resource records. Only A and AAAA records are cached, all
+        * the other records in answers are skipped.
+        */
+       for (i = 0; i < ancount; i++) {
+               /*
+                * Get one address at a time to this buffer.
+                * The max size of the answer is
+                *   2 (pointer) + 2 (type) + 2 (class) +
+                *   4 (ttl) + 2 (rdlen) + addr (16 or 4) = 28
+                * for A or AAAA record.
+                * For CNAME the size can be bigger.
+                */
+               unsigned char rsp[NS_MAXCDNAME];
+               unsigned int rsp_len = sizeof(rsp) - 1;
+               int ret, rdlen;
+
+               memset(rsp, 0, sizeof(rsp));
+
+               ret = parse_rr(buf, ptr, buf + buflen, rsp, &rsp_len,
+                       type, class, ttl, &rdlen, &next, name);
+               if (ret != 0) {
+                       err = ret;
+                       goto out;
+               }
+
+               /*
+                * Now rsp contains compressed or uncompressed resource
+                * record. Next we check if this record answers the question.
+                * The name var contains the uncompressed label.
+                * One tricky bit is the CNAME records as they alias
+                * the name we might be interested in.
+                */
+
+               /*
+                * Go to next answer if the class is not the one we are
+                * looking for.
+                */
+               if (*class != qclass) {
+                       ptr = next;
+                       next = NULL;
+                       continue;
+               }
+
+               /*
+                * Try to resolve aliases also, type is CNAME(5).
+                * This is important as otherwise the aliased names would not
+                * be cached at all as the cache would not contain the aliased
+                * question.
+                *
+                * If any CNAME is found in DNS packet, then we cache the alias
+                * IP address instead of the question (as the server
+                * said that question has only an alias).
+                * This means in practice that if e.g., ipv6.google.com is
+                * queried, DNS server returns CNAME of that name which is
+                * ipv6.l.google.com. We then cache the address of the CNAME
+                * but return the question name to client. So the alias
+                * status of the name is not saved in cache and thus not
+                * returned to the client. We do not return DNS packets from
+                * cache to client saying that ipv6.google.com is an alias to
+                * ipv6.l.google.com but we return instead a DNS packet that
+                * says ipv6.google.com has address xxx which is in fact the
+                * address of ipv6.l.google.com. For caching purposes this
+                * should not cause any issues.
+                */
+               if (*type == 5 && strncmp(question, name, qlen) == 0) {
+                       /*
+                        * So now the alias answered the question. This is
+                        * not very useful from caching point of view as
+                        * the following A or AAAA records will not match the
+                        * question. We need to find the real A/AAAA record
+                        * of the alias and cache that.
+                        */
+                       unsigned char *end = NULL;
+                       int name_len = 0, output_len;
+
+                       memset(rsp, 0, sizeof(rsp));
+                       rsp_len = sizeof(rsp) - 1;
+
+                       /*
+                        * Alias is in rdata part of the message,
+                        * and next-rdlen points to it. So we need to get
+                        * the real name of the alias.
+                        */
+                       ret = get_name(0, buf, next - rdlen, buf + buflen,
+                                       rsp, rsp_len, &output_len, &end,
+                                       name, &name_len);
+                       if (ret != 0) {
+                               /* just ignore the error at this point */
+                               ptr = next;
+                               next = NULL;
+                               continue;
+                       }
+
+                       /*
+                        * We should now have the alias of the entry we might
+                        * want to cache. Just remember it for a while.
+                        * We check the alias list when we have parsed the
+                        * A or AAAA record.
+                        */
+                       aliases = g_slist_prepend(aliases, g_strdup(name));
+
+                       ptr = next;
+                       next = NULL;
+                       continue;
+               }
+
+               if (*type == qtype) {
+                       /*
+                        * We found correct type (A or AAAA)
+                        */
+                       if (check_alias(aliases, name) == TRUE ||
+                               (aliases == NULL && strncmp(question, name,
+                                                       qlen) == 0)) {
+                               /*
+                                * We found an alias or the name of the rr
+                                * matches the question. If so, we append
+                                * the compressed label to the cache.
+                                * The end result is a response buffer that
+                                * will contain one or more cached and
+                                * compressed resource records.
+                                */
+                               if (*response_len + rsp_len > maxlen) {
+                                       err = -ENOBUFS;
+                                       goto out;
+                               }
+                               memcpy(response + *response_len, rsp, rsp_len);
+                               *response_len += rsp_len;
+                               (*answers)++;
+                               err = 0;
+                       }
+               }
+
+               ptr = next;
+               next = NULL;
+       }
+
+out:
+       for (list = aliases; list; list = list->next)
+               g_free(list->data);
+       g_slist_free(aliases);
+
+       return err;
+}
+
+struct cache_timeout {
+       time_t current_time;
+       int max_timeout;
+       int try_harder;
+};
+
+static gboolean cache_check_entry(gpointer key, gpointer value,
+                                       gpointer user_data)
+{
+       struct cache_timeout *data = user_data;
+       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->cache_until;
+               if (max_timeout > data->max_timeout)
+                       data->max_timeout = max_timeout;
+
+               if (entry->ipv4->cache_until < data->current_time)
+                       return TRUE;
+       }
+
+       if (entry->ipv6 != NULL && entry->ipv6->timeout > 0) {
+               max_timeout = entry->ipv6->cache_until;
+               if (max_timeout > data->max_timeout)
+                       data->max_timeout = max_timeout;
+
+               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 void cache_cleanup(void)
+{
+       static int max_timeout;
+       struct cache_timeout data;
+       int count = 0;
+
+       data.current_time = time(NULL);
+       data.max_timeout = 0;
+       data.try_harder = 0;
+
+       /*
+        * 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);
+
+       /*
+        * 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);
+
+       if (count == 0)
+               /*
+                * If we could not remove anything, then remember
+                * what is the max timeout and do nothing if we
+                * have not yet reached it. This will prevent
+                * constant traversal of the cache if it is full.
+                */
+               max_timeout = data.max_timeout;
+       else
+               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;
 
-       hdr = (void *) (buf + offset);
+       g_hash_table_foreach_remove(cache, cache_invalidate_entry, NULL);
+}
 
-       DBG("id 0x%04x qr %d opcode %d", hdr->id, hdr->qr, hdr->opcode);
+static void cache_refresh_entry(struct cache_entry *entry)
+{
 
-       hdr->qr = 1;
-       hdr->rcode = 2;
+       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]);
+       }
+}
 
-       hdr->ancount = 0;
-       hdr->nscount = 0;
-       hdr->arcount = 0;
+static void cache_refresh_iterator(gpointer key, gpointer value,
+                                       gpointer user_data)
+{
+       struct cache_entry *entry = value;
 
-       err = sendto(sk, buf, len, 0, to, tolen);
-       if (err < 0) {
-               connman_error("Failed to send DNS response: %s",
-                               strerror(errno));
+       cache_refresh_entry(entry);
+}
+
+static void cache_refresh(void)
+{
+       if (cache == NULL)
                return;
-       }
+
+       g_hash_table_foreach(cache, cache_refresh_iterator, NULL);
 }
 
-static gboolean request_timeout(gpointer user_data)
+static int reply_query_type(unsigned char *msg, int len)
 {
-       struct request_data *req = user_data;
-       struct listener_data *ifdata;
+       unsigned char *c;
+       uint16_t *w;
+       int l;
+       int type;
 
-       DBG("id 0x%04x", req->srcid);
+       /* skip the header */
+       c = msg + sizeof(struct domain_hdr);
+       len -= sizeof(struct domain_hdr);
 
-       if (req == NULL)
-               return FALSE;
+       if (len < 0)
+               return 0;
 
-       ifdata = req->ifdata;
+       /* 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);
 
-       request_list = g_slist_remove(request_list, req);
-       req->numserv--;
+       return type;
+}
 
-       if (req->resplen > 0 && req->resp != NULL) {
-               int sk, err;
+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 = 0, type = 0, class = 0;
+       struct domain_question *q;
+       struct cache_entry *entry;
+       struct cache_data *data;
+       char question[NS_MAXDNAME + 1];
+       unsigned char response[NS_MAXDNAME + 1];
+       unsigned char *ptr;
+       unsigned int rsplen;
+       gboolean new_entry = TRUE;
+       time_t current_time;
+
+       if (cache_size >= MAX_CACHE_SIZE) {
+               cache_cleanup();
+               if (cache_size >= MAX_CACHE_SIZE)
+                       return 0;
+       }
 
-               sk = g_io_channel_unix_get_fd(ifdata->udp_listener_channel);
+       current_time = time(NULL);
 
-               err = sendto(sk, req->resp, req->resplen, 0,
-                                               &req->sa, req->sa_len);
-               if (err < 0)
-                       return FALSE;
-       } else if (req->request && req->numserv == 0) {
-               struct domain_hdr *hdr;
+       /* don't do a cache refresh more than twice a minute */
+       if (next_refresh < current_time) {
+               cache_refresh();
+               next_refresh = current_time + 30;
+       }
 
-               if (req->protocol == IPPROTO_TCP) {
-                       hdr = (void *) (req->request + 2);
-                       hdr->id = req->srcid;
-                       send_response(req->client_sk, req->request,
-                               req->request_len, NULL, 0, IPPROTO_TCP);
 
-               } else if (req->protocol == IPPROTO_UDP) {
-                       int sk;
+       /* Continue only if response code is 0 (=ok) */
+       if (msg[3] & 0x0f)
+               return 0;
 
-                       hdr = (void *) (req->request);
-                       hdr->id = req->srcid;
-                       sk = g_io_channel_unix_get_fd(
-                                               ifdata->udp_listener_channel);
-                       send_response(sk, req->request, req->request_len,
-                                       &req->sa, req->sa_len, IPPROTO_UDP);
+       if (offset < 0)
+               return 0;
+
+       rsplen = sizeof(response) - 1;
+       question[sizeof(question) - 1] = '\0';
+
+       err = parse_response(msg + offset, msg_len - offset,
+                               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;
                }
        }
 
-       g_free(req->resp);
-       g_free(req);
+       if (err < 0 || ttl == 0)
+               return 0;
 
-       return FALSE;
-}
+       qlen = strlen(question);
+
+       /*
+        * If the cache contains already data, check if the
+        * type of the cached data is the same and do not add
+        * to cache if data is already there.
+        * This is needed so that we can cache both A and AAAA
+        * records for the same name.
+        */
+       entry = g_hash_table_lookup(cache, question);
+       if (entry == NULL) {
+               entry = g_try_new(struct cache_entry, 1);
+               if (entry == NULL)
+                       return -ENOMEM;
 
-static int append_query(unsigned char *buf, unsigned int size,
-                               const char *query, const char *domain)
-{
-       unsigned char *ptr = buf;
-       char *offset;
+               data = g_try_new(struct cache_data, 1);
+               if (data == NULL) {
+                       g_free(entry);
+                       return -ENOMEM;
+               }
 
-       DBG("query %s domain %s", query, domain);
+               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;
+       } else {
+               if (type == 1 && entry->ipv4 != NULL)
+                       return 0;
 
-       offset = (char *) query;
-       while (offset != NULL) {
-               char *tmp;
+               if (type == 28 && entry->ipv6 != NULL)
+                       return 0;
 
-               tmp = strchr(offset, '.');
-               if (tmp == NULL) {
-                       if (strlen(offset) == 0)
-                               break;
-                       *ptr = strlen(offset);
-                       memcpy(ptr + 1, offset, strlen(offset));
-                       ptr += strlen(offset) + 1;
-                       break;
-               }
+               data = g_try_new(struct cache_data, 1);
+               if (data == NULL)
+                       return -ENOMEM;
 
-               *ptr = tmp - offset;
-               memcpy(ptr + 1, offset, tmp - offset);
-               ptr += tmp - offset + 1;
+               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;
 
-               offset = tmp + 1;
+               new_entry = FALSE;
        }
 
-       offset = (char *) domain;
-       while (offset != NULL) {
-               char *tmp;
+       if (ttl < MIN_CACHE_TTL)
+               ttl = MIN_CACHE_TTL;
 
-               tmp = strchr(offset, '.');
-               if (tmp == NULL) {
-                       if (strlen(offset) == 0)
-                               break;
-                       *ptr = strlen(offset);
-                       memcpy(ptr + 1, offset, strlen(offset));
-                       ptr += strlen(offset) + 1;
-                       break;
-               }
+       data->inserted = current_time;
+       data->type = type;
+       data->answers = answers;
+       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);
+               g_free(entry);
+               return -ENOMEM;
+       }
+
+       memcpy(ptr, msg, 12);
+       memcpy(ptr + 12, question, qlen + 1); /* copy also the \0 */
 
-               *ptr = tmp - offset;
-               memcpy(ptr + 1, offset, tmp - offset);
-               ptr += tmp - offset + 1;
+       q = (void *) (ptr + 12 + qlen + 1);
+       q->type = htons(type);
+       q->class = htons(class);
+       memcpy(ptr + 12 + qlen + 1 + sizeof(struct domain_question),
+               response, rsplen);
 
-               offset = tmp + 1;
+       if (new_entry == TRUE) {
+               g_hash_table_replace(cache, entry->key, entry);
+               cache_size++;
        }
 
-       *ptr++ = 0x00;
+       DBG("cache %d %squestion \"%s\" type %d ttl %d size %zd",
+               cache_size, new_entry ? "new " : "old ",
+               question, type, ttl,
+               sizeof(*entry) + sizeof(*data) + data->data_len + qlen);
 
-       return ptr - buf;
+       return 0;
 }
 
 static int ns_resolv(struct server_data *server, struct request_data *req,
                                gpointer request, gpointer name)
 {
        GList *list;
-       int sk, err;
+       int sk, err, type = 0;
        char *dot, *lookup = (char *) name;
+       struct cache_entry *entry;
+
+       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");
+               if (type == 1)
+                       data = entry->ipv4;
+               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, ttl_left);
+                       return 1;
+               }
+
+               if (data != NULL && req->protocol == IPPROTO_UDP) {
+                       int sk;
+                       sk = g_io_channel_unix_get_fd(
+                                       req->ifdata->udp_listener_channel);
+
+                       send_cached_response(sk, data->data,
+                               data->data_len, &req->sa, req->sa_len,
+                               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++;
 
@@ -346,6 +1454,9 @@ static int ns_resolv(struct server_data *server, struct request_data *req,
        if (dot != NULL && dot != lookup + strlen(lookup) - 1)
                return 0;
 
+       if (server->domains != NULL && server->domains->data != NULL)
+               req->append_domain = TRUE;
+
        for (list = server->domains; list; list = list->next) {
                char *domain;
                unsigned char alt[1024];
@@ -380,16 +1491,16 @@ static int ns_resolv(struct server_data *server, struct request_data *req,
 
                memcpy(alt + offset + altlen,
                        request + offset + altlen - domlen,
-                               req->request_len - altlen + domlen);
+                               req->request_len - altlen - offset + domlen);
 
                if (server->protocol == IPPROTO_TCP) {
-                       int req_len = req->request_len + domlen - 1;
+                       int req_len = req->request_len + domlen - 2;
 
                        alt[0] = (req_len >> 8) & 0xff;
                        alt[1] = req_len & 0xff;
                }
 
-               err = send(sk, alt, req->request_len + domlen + 1, 0);
+               err = send(sk, alt, req->request_len + domlen, 0);
                if (err < 0)
                        return -EIO;
 
@@ -399,7 +1510,19 @@ static int ns_resolv(struct server_data *server, struct request_data *req,
        return 0;
 }
 
-static int forward_dns_reply(unsigned char *reply, int reply_len, int protocol)
+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 int forward_dns_reply(unsigned char *reply, int reply_len, int protocol,
+                               struct server_data *data)
 {
        struct domain_hdr *hdr;
        struct request_data *req;
@@ -428,6 +1551,48 @@ static int forward_dns_reply(unsigned char *reply, int reply_len, int protocol)
        req->numresp++;
 
        if (hdr->rcode == 0 || req->resp == NULL) {
+
+               /*
+                * If the domain name was append
+                * remove it before forwarding the reply.
+                */
+               if (req->append_domain == TRUE) {
+                       unsigned char *ptr;
+                       uint8_t host_len;
+                       unsigned int domain_len;
+
+                       /*
+                        * ptr points to the first char of the hostname.
+                        * ->hostname.domain.net
+                        */
+                       ptr = reply + offset + sizeof(struct domain_hdr);
+                       host_len = *ptr;
+                       domain_len = strlen((const char *)ptr + host_len + 1);
+
+                       /*
+                        * Remove the domain name and replace it by the end
+                        * of reply. Check if the domain is really there
+                        * before trying to copy the data. The domain_len can
+                        * be 0 because if the original query did not contain
+                        * a domain name, then we are sending two packets,
+                        * first without the domain name and the second packet
+                        * with domain name. The append_domain is set to true
+                        * even if we sent the first packet without domain
+                        * name. In this case we end up in this branch.
+                        */
+                       if (domain_len > 0) {
+                               /*
+                                * Note that we must use memmove() here,
+                                * because the memory areas can overlap.
+                                */
+                               memmove(ptr + host_len + 1,
+                                       ptr + host_len + domain_len + 1,
+                                       reply_len - (ptr - reply + domain_len));
+
+                               reply_len = reply_len - domain_len;
+                       }
+               }
+
                g_free(req->resp);
                req->resplen = 0;
 
@@ -437,6 +1602,8 @@ static int forward_dns_reply(unsigned char *reply, int reply_len, int protocol)
 
                memcpy(req->resp, reply, reply_len);
                req->resplen = reply_len;
+
+               cache_update(data, reply, reply_len);
        }
 
        if (hdr->rcode > 0 && req->numresp < req->numserv)
@@ -463,6 +1630,42 @@ static int forward_dns_reply(unsigned char *reply, int reply_len, int protocol)
        return err;
 }
 
+static void cache_element_destroy(gpointer value)
+{
+       struct cache_entry *entry = value;
+
+       if (entry == NULL)
+               return;
+
+       if (entry->ipv4 != NULL) {
+               g_free(entry->ipv4->data);
+               g_free(entry->ipv4);
+       }
+
+       if (entry->ipv6 != NULL) {
+               g_free(entry->ipv6->data);
+               g_free(entry->ipv6);
+       }
+
+       g_free(entry->key);
+       g_free(entry);
+
+       if (--cache_size < 0)
+               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;
@@ -480,7 +1683,7 @@ static void destroy_server(struct server_data *server)
        g_io_channel_unref(server->channel);
 
        if (server->protocol == IPPROTO_UDP)
-               connman_info("Removing DNS server %s", server->server);
+               DBG("Removing DNS server %s", server->server);
 
        g_free(server->incoming_reply);
        g_free(server->server);
@@ -491,6 +1694,18 @@ static void destroy_server(struct server_data *server)
                g_free(domain);
        }
        g_free(server->interface);
+
+       /*
+        * 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);
 }
 
@@ -499,10 +1714,9 @@ static gboolean udp_server_event(GIOChannel *channel, GIOCondition condition,
 {
        unsigned char buf[4096];
        int sk, err, len;
+       struct server_data *data = user_data;
 
        if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
-               struct server_data *data = user_data;
-
                connman_error("Error with UDP server %s", data->server);
                data->watch = 0;
                return FALSE;
@@ -514,7 +1728,7 @@ static gboolean udp_server_event(GIOChannel *channel, GIOCondition condition,
        if (len < 12)
                return TRUE;
 
-       err = forward_dns_reply(buf, len, IPPROTO_UDP);
+       err = forward_dns_reply(buf, len, IPPROTO_UDP, data);
        if (err < 0)
                return TRUE;
 
@@ -577,6 +1791,7 @@ hangup:
        if ((condition & G_IO_OUT) && !server->connected) {
                GSList *list;
                GList *domains;
+               int no_request_sent = TRUE;
                struct server_data *udp_server;
 
                udp_server = find_server(server->interface, server->server,
@@ -602,20 +1817,48 @@ hangup:
                        server->timeout = 0;
                }
 
-               for (list = request_list; list; list = list->next) {
+               for (list = request_list; list; ) {
                        struct request_data *req = list->data;
+                       int status;
 
-                       if (req->protocol == IPPROTO_UDP)
+                       if (req->protocol == IPPROTO_UDP) {
+                               list = list->next;
                                continue;
+                       }
 
                        DBG("Sending req %s over TCP", (char *)req->name);
 
+                       status = ns_resolv(server, req,
+                                               req->request, req->name);
+                       if (status > 0) {
+                               /*
+                                * A cached result was sent,
+                                * so the request can be released
+                                */
+                               list = list->next;
+                               request_list = g_slist_remove(request_list, req);
+                               destroy_request_data(req);
+                               continue;
+                       }
+
+                       if (status < 0) {
+                               list = list->next;
+                               continue;
+                       }
+
+                       no_request_sent = FALSE;
+
                        if (req->timeout > 0)
                                g_source_remove(req->timeout);
 
                        req->timeout = g_timeout_add_seconds(30,
                                                request_timeout, req);
-                       ns_resolv(server, req, req->request, req->name);
+                       list = list->next;
+               }
+
+               if (no_request_sent == TRUE) {
+                       destroy_server(server);
+                       return FALSE;
                }
 
        } else if (condition & G_IO_IN) {
@@ -671,7 +1914,8 @@ hangup:
                        reply->received += bytes_recv;
                }
 
-               forward_dns_reply(reply->buf, reply->received, IPPROTO_TCP);
+               forward_dns_reply(reply->buf, reply->received, IPPROTO_TCP,
+                                       server);
 
                g_free(reply);
                server->incoming_reply = NULL;
@@ -821,17 +2065,21 @@ static struct server_data *create_server(const char *interface,
                }
        }
 
+       if (__sync_fetch_and_add(&cache_refcount, 1) == 0)
+               cache = g_hash_table_new_full(g_str_hash,
+                                       g_str_equal,
+                                       NULL,
+                                       cache_element_destroy);
+
        if (protocol == IPPROTO_UDP) {
                /* Enable new servers by default */
                data->enabled = TRUE;
-               connman_info("Adding DNS server %s", data->server);
+               DBG("Adding DNS server %s", data->server);
 
                server_list = g_slist_append(server_list, data);
-
-               return data;
        }
 
-       return NULL;
+       return data;
 }
 
 static gboolean resolv(struct request_data *req,
@@ -852,11 +2100,11 @@ static gboolean resolv(struct request_data *req,
                                G_IO_IN | G_IO_NVAL | G_IO_ERR | G_IO_HUP,
                                                udp_server_event, data);
 
-               if (ns_resolv(data, req, request, name) < 0)
-                       continue;
+               if (ns_resolv(data, req, request, name) > 0)
+                       return TRUE;
        }
 
-       return TRUE;
+       return FALSE;
 }
 
 static void append_domain(const char *interface, const char *domain)
@@ -986,11 +2234,14 @@ static void dnsproxy_offline_mode(connman_bool_t enabled)
                struct server_data *data = list->data;
 
                if (enabled == FALSE) {
-                       connman_info("Enabling DNS server %s", data->server);
+                       DBG("Enabling DNS server %s", data->server);
                        data->enabled = TRUE;
+                       cache_invalidate();
+                       cache_refresh();
                } else {
-                       connman_info("Disabling DNS server %s", data->server);
+                       DBG("Disabling DNS server %s", data->server);
                        data->enabled = FALSE;
+                       cache_invalidate();
                }
        }
 }
@@ -1002,6 +2253,9 @@ static void dnsproxy_default_changed(struct connman_service *service)
 
        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);
@@ -1016,15 +2270,16 @@ static void dnsproxy_default_changed(struct connman_service *service)
                struct server_data *data = list->data;
 
                if (g_strcmp0(data->interface, interface) == 0) {
-                       connman_info("Enabling DNS server %s", data->server);
+                       DBG("Enabling DNS server %s", data->server);
                        data->enabled = TRUE;
                } else {
-                       connman_info("Disabling DNS server %s", data->server);
+                       DBG("Disabling DNS server %s", data->server);
                        data->enabled = FALSE;
                }
        }
 
        g_free(interface);
+       cache_refresh();
 }
 
 static struct connman_notifier dnsproxy_notifier = {
@@ -1055,7 +2310,7 @@ static int parse_request(unsigned char *buf, int len,
        if (hdr->qr != 0 || qdcount != 1)
                return -EINVAL;
 
-       memset(name, 0, size);
+       name[0] = '\0';
 
        ptr = buf + sizeof(struct domain_hdr);
        remain = len - sizeof(struct domain_hdr);
@@ -1119,6 +2374,7 @@ static gboolean tcp_listener_event(GIOChannel *channel, GIOCondition condition,
        socklen_t client_addr_len = sizeof(client_addr);
        GSList *list;
        struct listener_data *ifdata = user_data;
+       int waiting_for_connect = FALSE;
 
        DBG("condition 0x%x", condition);
 
@@ -1162,13 +2418,9 @@ static gboolean tcp_listener_event(GIOChannel *channel, GIOCondition condition,
        req->client_sk = client_sk;
        req->protocol = IPPROTO_TCP;
 
-       request_id += 2;
-       if (request_id == 0x0000 || request_id == 0xffff)
-               request_id += 2;
-
        req->srcid = buf[2] | (buf[3] << 8);
-       req->dstid = request_id;
-       req->altid = request_id + 1;
+       req->dstid = get_id();
+       req->altid = get_id();
        req->request_len = len;
 
        buf[2] = req->dstid & 0xff;
@@ -1176,7 +2428,7 @@ static gboolean tcp_listener_event(GIOChannel *channel, GIOCondition condition,
 
        req->numserv = 0;
        req->ifdata = (struct listener_data *) ifdata;
-       request_list = g_slist_append(request_list, req);
+       req->append_domain = FALSE;
 
        for (list = server_list; list; list = list->next) {
                struct server_data *data = list->data;
@@ -1187,33 +2439,8 @@ static gboolean tcp_listener_event(GIOChannel *channel, GIOCondition condition,
 
                server = create_server(data->interface, NULL,
                                        data->server, IPPROTO_TCP);
-
-               /*
-                * If server is NULL, we're not connected yet.
-                * Copy the relevant buffers and continue with
-                * the next nameserver.
-                * The request will actually be sent once we're
-                * properly connected over TCP to this nameserver.
-                */
-               if (server == NULL) {
-                       req->request = g_try_malloc0(req->request_len);
-                       if (req->request == NULL)
-                               return TRUE;
-
-                       memcpy(req->request, buf, req->request_len);
-
-                       req->name = g_try_malloc0(sizeof(query));
-                       if (req->name == NULL) {
-                               g_free(req->request);
-                               return TRUE;
-                       }
-                       memcpy(req->name, query, sizeof(query));
-
+               if (server == NULL)
                        continue;
-               }
-
-               if (req->timeout > 0)
-                       g_source_remove(req->timeout);
 
                for (domains = data->domains; domains;
                                domains = domains->next) {
@@ -1225,9 +2452,42 @@ static gboolean tcp_listener_event(GIOChannel *channel, GIOCondition condition,
                                                g_strdup(dom));
                }
 
-               req->timeout = g_timeout_add_seconds(30, request_timeout, req);
-               ns_resolv(server, req, buf, query);
+               waiting_for_connect = TRUE;
+       }
+
+       if (waiting_for_connect == FALSE) {
+               /* No server is waiting for connect */
+               send_response(client_sk, buf, len, NULL, 0, IPPROTO_TCP);
+               g_free(req);
+               return TRUE;
+       }
+
+       /*
+        * The server is not connected yet.
+        * Copy the relevant buffers.
+        * The request will actually be sent once we're
+        * properly connected over TCP to the nameserver.
+        */
+       req->request = g_try_malloc0(req->request_len);
+       if (req->request == NULL) {
+               send_response(client_sk, buf, len, NULL, 0, IPPROTO_TCP);
+               g_free(req);
+               return TRUE;
+       }
+       memcpy(req->request, buf, req->request_len);
+
+       req->name = g_try_malloc0(sizeof(query));
+       if (req->name == NULL) {
+               send_response(client_sk, buf, len, NULL, 0, IPPROTO_TCP);
+               g_free(req->request);
+               g_free(req);
+               return TRUE;
        }
+       memcpy(req->name, query, sizeof(query));
+
+       req->timeout = g_timeout_add_seconds(30, request_timeout, req);
+
+       request_list = g_slist_append(request_list, req);
 
        return TRUE;
 }
@@ -1275,13 +2535,9 @@ static gboolean udp_listener_event(GIOChannel *channel, GIOCondition condition,
        req->client_sk = 0;
        req->protocol = IPPROTO_UDP;
 
-       request_id += 2;
-       if (request_id == 0x0000 || request_id == 0xffff)
-               request_id += 2;
-
        req->srcid = buf[0] | (buf[1] << 8);
-       req->dstid = request_id;
-       req->altid = request_id + 1;
+       req->dstid = get_id();
+       req->altid = get_id();
        req->request_len = len;
 
        buf[0] = req->dstid & 0xff;
@@ -1289,13 +2545,21 @@ static gboolean udp_listener_event(GIOChannel *channel, GIOCondition condition,
 
        req->numserv = 0;
        req->ifdata = (struct listener_data *) ifdata;
+       req->append_domain = FALSE;
+
+       if (resolv(req, buf, query) == TRUE) {
+               /* a cached result was sent, so the request can be released */
+               g_free(req);
+               return TRUE;
+       }
+
        req->timeout = g_timeout_add_seconds(5, request_timeout, req);
        request_list = g_slist_append(request_list, req);
 
-       return resolv(req, buf, query);
+       return TRUE;
 }
 
-static int create_dns_listener(int protocol, const char *ifname)
+static int create_dns_listener(int protocol, struct listener_data *ifdata)
 {
        GIOChannel *channel;
        const char *proto;
@@ -1307,23 +2571,19 @@ static int create_dns_listener(int protocol, const char *ifname)
        socklen_t slen;
        int sk, type, v6only = 0;
        int family = AF_INET6;
-       struct listener_data *ifdata;
 
-       DBG("interface %s", ifname);
 
-       ifdata = g_hash_table_lookup(listener_table, ifname);
-       if (ifdata == NULL)
-               return -ENODEV;
+       DBG("interface %s", ifdata->ifname);
 
        switch (protocol) {
        case IPPROTO_UDP:
                proto = "UDP";
-               type = SOCK_DGRAM;
+               type = SOCK_DGRAM | SOCK_CLOEXEC;
                break;
 
        case IPPROTO_TCP:
                proto = "TCP";
-               type = SOCK_STREAM;
+               type = SOCK_STREAM | SOCK_CLOEXEC;
                break;
 
        default:
@@ -1342,7 +2602,8 @@ static int create_dns_listener(int protocol, const char *ifname)
        }
 
        if (setsockopt(sk, SOL_SOCKET, SO_BINDTODEVICE,
-                                       ifname, strlen(ifname) + 1) < 0) {
+                                       ifdata->ifname,
+                                       strlen(ifdata->ifname) + 1) < 0) {
                connman_error("Failed to bind %s listener interface", proto);
                close(sk);
                return -EIO;
@@ -1405,15 +2666,9 @@ static int create_dns_listener(int protocol, const char *ifname)
        return 0;
 }
 
-static void destroy_udp_listener(const char *interface)
+static void destroy_udp_listener(struct listener_data *ifdata)
 {
-       struct listener_data *ifdata;
-
-       DBG("interface %s", interface);
-
-       ifdata = g_hash_table_lookup(listener_table, interface);
-       if (ifdata == NULL)
-               return;
+       DBG("interface %s", ifdata->ifname);
 
        if (ifdata->udp_listener_watch > 0)
                g_source_remove(ifdata->udp_listener_watch);
@@ -1421,15 +2676,9 @@ static void destroy_udp_listener(const char *interface)
        g_io_channel_unref(ifdata->udp_listener_channel);
 }
 
-static void destroy_tcp_listener(const char *interface)
+static void destroy_tcp_listener(struct listener_data *ifdata)
 {
-       struct listener_data *ifdata;
-
-       DBG("interface %s", interface);
-
-       ifdata = g_hash_table_lookup(listener_table, interface);
-       if (ifdata == NULL)
-               return;
+       DBG("interface %s", ifdata->ifname);
 
        if (ifdata->tcp_listener_watch > 0)
                g_source_remove(ifdata->tcp_listener_watch);
@@ -1437,34 +2686,31 @@ static void destroy_tcp_listener(const char *interface)
        g_io_channel_unref(ifdata->tcp_listener_channel);
 }
 
-static int create_listener(const char *interface)
+static int create_listener(struct listener_data *ifdata)
 {
        int err;
 
-       err = create_dns_listener(IPPROTO_UDP, interface);
+       err = create_dns_listener(IPPROTO_UDP, ifdata);
        if (err < 0)
                return err;
 
-       err = create_dns_listener(IPPROTO_TCP, interface);
+       err = create_dns_listener(IPPROTO_TCP, ifdata);
        if (err < 0) {
-               destroy_udp_listener(interface);
+               destroy_udp_listener(ifdata);
                return err;
        }
 
-       if (g_strcmp0(interface, "lo") == 0)
+       if (g_strcmp0(ifdata->ifname, "lo") == 0)
                __connman_resolvfile_append("lo", NULL, "127.0.0.1");
 
        return 0;
 }
 
-static void destroy_listener(const char *interface)
+static void destroy_listener(struct listener_data *ifdata)
 {
        GSList *list;
 
-       if (interface == NULL)
-               return;
-
-       if (g_strcmp0(interface, "lo") == 0)
+       if (g_strcmp0(ifdata->ifname, "lo") == 0)
                __connman_resolvfile_remove("lo", NULL, "127.0.0.1");
 
        for (list = request_pending_list; list; list = list->next) {
@@ -1472,11 +2718,7 @@ static void destroy_listener(const char *interface)
 
                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;
        }
 
@@ -1488,19 +2730,15 @@ static void destroy_listener(const char *interface)
 
                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;
        }
 
        g_slist_free(request_list);
        request_list = NULL;
 
-       destroy_tcp_listener(interface);
-       destroy_udp_listener(interface);
+       destroy_tcp_listener(ifdata);
+       destroy_udp_listener(ifdata);
 }
 
 int __connman_dnsproxy_add_listener(const char *interface)
@@ -1523,7 +2761,7 @@ int __connman_dnsproxy_add_listener(const char *interface)
        ifdata->tcp_listener_channel = NULL;
        ifdata->tcp_listener_watch = 0;
 
-       err = create_listener(interface);
+       err = create_listener(ifdata);
        if (err < 0) {
                connman_error("Couldn't create listener for %s err %d",
                                interface, err);
@@ -1537,16 +2775,27 @@ int __connman_dnsproxy_add_listener(const char *interface)
 
 void __connman_dnsproxy_remove_listener(const char *interface)
 {
+       struct listener_data *ifdata;
+
        DBG("interface %s", interface);
 
-       destroy_listener(interface);
+       ifdata = g_hash_table_lookup(listener_table, interface);
+       if (ifdata == NULL)
+               return;
+
+       destroy_listener(ifdata);
 
        g_hash_table_remove(listener_table, interface);
 }
 
 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)
@@ -1555,6 +2804,8 @@ int __connman_dnsproxy_init(void)
 
        DBG("");
 
+       srandom(time(NULL));
+
        listener_table = g_hash_table_new_full(g_str_hash, g_str_equal,
                                                        g_free, g_free);
        err = __connman_dnsproxy_add_listener("lo");