X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;f=src%2Fdnsproxy.c;h=58d1bfe2c3741f94a8f2c28accededb2a88d8e06;hb=335e7562aafe49d4f91184ebe4d84d1aa99b9ca7;hp=a56e312e82baf22c049eec61f1405734d601aa7c;hpb=bf76335588bb9266f644648ad482f9d0ec7d5e02;p=framework%2Fconnectivity%2Fconnman.git diff --git a/src/dnsproxy.c b/src/dnsproxy.c index a56e312..58d1bfe 100644 --- a/src/dnsproxy.c +++ b/src/dnsproxy.c @@ -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 +#include #include #include #include @@ -32,6 +33,8 @@ #include #include #include +#include +#include #include @@ -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");