dnsproxy: Fix wrong domain len causing crash in memmove
[framework/connectivity/connman.git] / src / dnsproxy.c
index b97b1d4..5c4099b 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
@@ -33,6 +33,7 @@
 #include <sys/socket.h>
 #include <netdb.h>
 #include <resolv.h>
+#include <gweb/gresolv.h>
 
 #include <glib.h>
 
@@ -138,6 +139,7 @@ struct cache_data {
 
 struct cache_entry {
        char *key;
+       int want_refresh;
        int hits;
        struct cache_data *ipv4;
        struct cache_data *ipv6;
@@ -183,6 +185,7 @@ 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 int protocol_offset(int protocol)
 {
@@ -263,6 +266,52 @@ 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 */
@@ -318,8 +367,6 @@ static void update_cached_ttl(unsigned char *buf, int len, int new_ttl)
        }
 }
 
-
-
 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)
@@ -447,53 +494,50 @@ static int append_query(unsigned char *buf, unsigned int size,
                                const char *query, const char *domain)
 {
        unsigned char *ptr = buf;
-       char *offset;
        int len;
 
        DBG("query %s domain %s", query, domain);
 
-       offset = (char *) query;
-       while (offset != NULL) {
-               char *tmp;
+       while (query != NULL) {
+               const char *tmp;
 
-               tmp = strchr(offset, '.');
+               tmp = strchr(query, '.');
                if (tmp == NULL) {
-                       len = strlen(offset);
+                       len = strlen(query);
                        if (len == 0)
                                break;
                        *ptr = len;
-                       memcpy(ptr + 1, offset, len);
+                       memcpy(ptr + 1, query, len);
                        ptr += len + 1;
                        break;
                }
 
-               *ptr = tmp - offset;
-               memcpy(ptr + 1, offset, tmp - offset);
-               ptr += tmp - offset + 1;
+               *ptr = tmp - query;
+               memcpy(ptr + 1, query, tmp - query);
+               ptr += tmp - query + 1;
 
-               offset = tmp + 1;
+               query = tmp + 1;
        }
 
-       offset = (char *) domain;
-       while (offset != NULL) {
-               char *tmp;
+       while (domain != NULL) {
+               const char *tmp;
 
-               tmp = strchr(offset, '.');
+               tmp = strchr(domain, '.');
                if (tmp == NULL) {
-                       len = strlen(offset);
+                       len = strlen(domain);
                        if (len == 0)
                                break;
                        *ptr = len;
-                       memcpy(ptr + 1, offset, len);
+                       memcpy(ptr + 1, domain, len);
                        ptr += len + 1;
                        break;
                }
 
-               *ptr = tmp - offset;
-               memcpy(ptr + 1, offset, tmp - offset);
-               ptr += tmp - offset + 1;
+               *ptr = tmp - domain;
+               memcpy(ptr + 1, domain, tmp - domain);
+               ptr += tmp - domain + 1;
 
-               offset = tmp + 1;
+               domain = tmp + 1;
        }
 
        *ptr++ = 0x00;
@@ -518,7 +562,7 @@ static gboolean cache_check_is_valid(struct cache_data *data,
  */
 static void cache_enforce_validity(struct cache_entry *entry)
 {
-       time_t current_time = time(0);
+       time_t current_time = time(NULL);
 
        if (cache_check_is_valid(entry->ipv4, current_time) == FALSE
                                                        && entry->ipv4) {
@@ -538,11 +582,18 @@ static void cache_enforce_validity(struct cache_entry *entry)
        }
 }
 
-
 static uint16_t cache_check_validity(char *question, uint16_t type,
                                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);
 
@@ -552,14 +603,18 @@ static uint16_t cache_check_validity(char *question, uint16_t type,
                        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)
+                       if (cache_check_is_valid(entry->ipv6, current_time)
+                                       == FALSE && want_refresh == FALSE) {
                                g_hash_table_remove(cache, question);
-
-                       type = 0;
+                               type = 0;
+                       }
                }
                break;
 
@@ -568,10 +623,14 @@ static uint16_t cache_check_validity(char *question, uint16_t type,
                        DBG("cache %s \"%s\" type AAAA", entry->ipv6 ?
                                        "timeout" : "entry missing", question);
 
-                       if (cache_check_is_valid(entry->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;
        }
@@ -991,7 +1050,7 @@ static void cache_cleanup(void)
        struct cache_timeout data;
        int count = 0;
 
-       data.current_time = time(0);
+       data.current_time = time(NULL);
        data.max_timeout = 0;
        data.try_harder = 0;
 
@@ -1036,6 +1095,10 @@ static gboolean cache_invalidate_entry(gpointer key, gpointer 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);
@@ -1049,7 +1112,11 @@ static gboolean cache_invalidate_entry(gpointer key, gpointer value,
                entry->ipv6 = NULL;
        }
 
-       return TRUE;
+       /* keep the entry if we want it refreshed, delete it otherwise */
+       if (entry->want_refresh)
+               return FALSE;
+       else
+               return TRUE;
 }
 
 /*
@@ -1060,11 +1127,59 @@ static gboolean cache_invalidate_entry(gpointer key, gpointer value,
  */
 static void cache_invalidate(void)
 {
-       DBG("Invalidating the DNS cache");
-        g_hash_table_foreach_remove(cache, cache_invalidate_entry,
-                                               NULL);
+       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)
 {
@@ -1083,9 +1198,9 @@ static int reply_query_type(unsigned char *msg, int len)
        /* now the query, which is a name and 2 16 bit words */
        l = dns_name_length(c) + 1;
        c += l;
-       len -= l;
        w = (uint16_t *) c;
        type = ntohs(*w);
+
        return type;
 }
 
@@ -1111,6 +1226,15 @@ static int cache_update(struct server_data *srv, unsigned char *msg,
                        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;
@@ -1163,7 +1287,6 @@ static int cache_update(struct server_data *srv, unsigned char *msg,
                return 0;
 
        qlen = strlen(question);
-       current_time = time(0);
 
        /*
         * If the cache contains already data, check if the
@@ -1186,6 +1309,7 @@ static int cache_update(struct server_data *srv, unsigned char *msg,
 
                entry->key = g_strdup(question);
                entry->ipv4 = entry->ipv6 = NULL;
+               entry->want_refresh = 0;
                entry->hits = 0;
 
                if (type == 1)
@@ -1288,7 +1412,7 @@ static int ns_resolv(struct server_data *server, struct request_data *req,
                        data = entry->ipv6;
 
                if (data) {
-                       ttl_left = data->valid_until - time(0);
+                       ttl_left = data->valid_until - time(NULL);
                        entry->hits++;
                }
 
@@ -1315,6 +1439,8 @@ static int ns_resolv(struct server_data *server, struct request_data *req,
        sk = g_io_channel_unix_get_fd(server->channel);
 
        err = send(sk, request, req->request_len, 0);
+       if (err < 0)
+               return -EIO;
 
        req->numserv++;
 
@@ -1425,13 +1551,13 @@ static int forward_dns_reply(unsigned char *reply, int reply_len, int protocol,
                         */
                        ptr = reply + offset + sizeof(struct domain_hdr);
                        host_len = *ptr;
-                       domain_len = strlen((const char *)ptr) - host_len - 1;
+                       domain_len = strlen((const char *)ptr + host_len + 1);
 
                        /*
-                        * remove the domain name and replaced it by the end
+                        * remove the domain name and replace it by the end
                         * of reply.
                         */
-                       memmove(ptr + host_len + 1,
+                       memcpy(ptr + host_len + 1,
                                ptr + host_len + domain_len + 1,
                                reply_len - (ptr - reply + domain_len));
 
@@ -1499,6 +1625,18 @@ static void cache_element_destroy(gpointer value)
                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;
@@ -1528,8 +1666,16 @@ static void destroy_server(struct server_data *server)
        }
        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);
 }
@@ -2053,6 +2199,7 @@ static void dnsproxy_offline_mode(connman_bool_t enabled)
                        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;
@@ -2094,6 +2241,7 @@ static void dnsproxy_default_changed(struct connman_service *service)
        }
 
        g_free(interface);
+       cache_refresh();
 }
 
 static struct connman_notifier dnsproxy_notifier = {
@@ -2124,7 +2272,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);
@@ -2518,6 +2666,17 @@ static int create_listener(struct listener_data *ifdata)
        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;
@@ -2530,11 +2689,7 @@ static void destroy_listener(struct listener_data *ifdata)
 
                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;
        }
 
@@ -2546,11 +2701,7 @@ static void destroy_listener(struct listener_data *ifdata)
 
                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;
        }
 
@@ -2610,7 +2761,12 @@ void __connman_dnsproxy_remove_listener(const char *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)