Query Cache support (#625)

This PR implements a query cache at the lowest possible level, the actual dns request and response messages.  Only successful and `NXDOMAIN` responses are cached. The lowest TTL in the response message determines the cache validity period for the response, and is capped at the configuration value for `qcache_max_ttl`.  For `NXDOMAIN` responses, the SOA record is evaluated.

For a query to match the cache, the opcode, flags, and each question's class, type, and name are all evaluated.  This is to prevent matching a cached entry for a subtly different query (such as if the RD flag is set on one request and not another).

For things like ares_getaddrinfo() or ares_search() that may spawn multiple queries, each individual message received is cached rather than the overarching response.  This makes it possible for one query in the sequence to be purged from the cache while others still return cached results which means there is no chance of ever returning stale data.

We have had a lot of user requests to return TTLs on all the various parsers like `ares_parse_caa_reply()`, and likely this is because they want to implement caching mechanisms of their own, thus this PR should solve those issues as well.

Due to the internal data structures we have these days, this PR is less than 500 lines of new code.

Fixes #608 

Fix By: Brad House (@bradh352)
pull/630/head
Brad House 1 year ago committed by GitHub
parent 36466bb240
commit 4982f76a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 85
      docs/ares_init.3
  2. 144
      docs/ares_init_options.3
  3. 2
      include/ares.h
  4. 1
      src/lib/Makefile.inc
  5. 92
      src/lib/ares__htable.c
  6. 2
      src/lib/ares_destroy.c
  7. 6
      src/lib/ares_dns_private.h
  8. 18
      src/lib/ares_dns_write.c
  9. 14
      src/lib/ares_init.c
  10. 8
      src/lib/ares_options.c
  11. 20
      src/lib/ares_private.h
  12. 6
      src/lib/ares_process.c
  13. 453
      src/lib/ares_qcache.c
  14. 16
      src/lib/ares_send.c
  15. 4
      src/lib/ares_update_servers.c
  16. 54
      test/ares-test-mock.cc
  17. 6
      test/ares-test.h

@ -1,84 +1,3 @@
.\"
.\" Copyright 1998 by the Massachusetts Institute of Technology.
.\" Copyright (C) 2004-2010 by Daniel Stenberg
.\"
.\" Permission to use, copy, modify, and distribute this
.\" software and its documentation for any purpose and without
.\" fee is hereby granted, provided that the above copyright
.\" notice appear in all copies and that both that copyright
.\" notice and this permission notice appear in supporting
.\" documentation, and that the name of M.I.T. not be used in
.\" advertising or publicity pertaining to distribution of the
.\" software without specific, written prior permission.
.\" M.I.T. makes no representations about the suitability of
.\" this software for any purpose. It is provided "as is"
.\" without express or implied warranty.
.\"
.\" Copyright (C) 2023 The c-ares project and its contributors.
.\" SPDX-License-Identifier: MIT
.\"
.TH ARES_INIT 3 "5 March 2010"
.SH NAME
ares_init \- Initialize a resolver channel
.SH SYNOPSIS
.nf
#include <ares.h>
int ares_init(ares_channel_t **\fIchannelptr\fP)
.fi
.SH DESCRIPTION
The \fBares_init(3)\fP function initializes a communications channel for name
service lookups. If it returns successfully, \fBares_init(3)\fP will set the
variable pointed to by \fIchannelptr\fP to a handle used to identify the name
service channel. The caller should invoke \fIares_destroy(3)\fP on the handle
when the channel is no longer needed.
The \fIares_init_options(3)\fP function is provide to offer more init
alternatives.
.SH RETURN VALUES
\fIares_init(3)\fP can return any of the following values:
.TP 14
.B ARES_SUCCESS
Initialization succeeded.
.TP 14
.B ARES_EFILE
A configuration file could not be read.
.TP 14
.B ARES_ENOMEM
The process's available memory was exhausted.
.TP 14
.B ARES_ENOTINITIALIZED
c-ares library initialization not yet performed.
.SH NOTES
When initializing from
.B /etc/resolv.conf,
.BR ares_init (3)
reads the
.I domain
and
.I search
directives to allow lookups of short names relative to the domains
specified. The
.I domain
and
.I search
directives override one another. If more that one instance of either
.I domain
or
.I search
directives is specified, the last occurrence wins. For more information,
please see the
.BR resolv.conf (5)
manual page.
.SH SEE ALSO
.BR ares_init_options (3),
.BR ares_reinit (3),
.BR ares_destroy (3),
.BR ares_dup (3),
.BR ares_library_init (3),
.BR ares_set_servers (3)
.SH AUTHOR
Greg Hudson, MIT Information Systems
.br
Copyright 1998 by the Massachusetts Institute of Technology.
.br
Copyright (C) 2004-2010 by Daniel Stenberg.
.so man3/ares_init_options.3

@ -2,23 +2,11 @@
.\" Copyright 1998 by the Massachusetts Institute of Technology.
.\" Copyright (C) 2004-2010 by Daniel Stenberg
.\"
.\" Permission to use, copy, modify, and distribute this
.\" software and its documentation for any purpose and without
.\" fee is hereby granted, provided that the above copyright
.\" notice appear in all copies and that both that copyright
.\" notice and this permission notice appear in supporting
.\" documentation, and that the name of M.I.T. not be used in
.\" advertising or publicity pertaining to distribution of the
.\" software without specific, written prior permission.
.\" M.I.T. makes no representations about the suitability of
.\" this software for any purpose. It is provided "as is"
.\" without express or implied warranty.
.\"
.\" SPDX-License-Identifier: MIT
.\"
.TH ARES_INIT 3 "5 March 2010"
.TH ARES_INIT_OPTIONS 3 "5 March 2010"
.SH NAME
ares_init_options \- Initialize a resolver channel
ares_init_options, ares_init \- Initialize a resolver channel
.SH SYNOPSIS
.nf
#include <ares.h>
@ -46,13 +34,22 @@ struct ares_options {
char *hosts_path;
int udp_max_queries;
int maxtimeout; /* in milliseconds */
unsigned int qcache_max_ttl; /* in seconds */
};
int ares_init_options(ares_channel_t **\fIchannelptr\fP,
const struct ares_options *\fIoptions\fP,
int \fIoptmask\fP)
int \fIoptmask\fP);
int ares_init(ares_channel_t **\fIchannelptr\fP);
.fi
.SH DESCRIPTION
The \fBares_init(3)\fP function is equivalent to calling
\fBares_init_channel(NULL, 0)\fP. It is recommended to use
\fBares_init_options(3)\fP instead and to set or make configurable the
appropriate options for your application.
The \fBares_init_options(3)\fP function initializes a communications channel
for name service lookups. If it returns successfully,
\fBares_init_options(3)\fP will set the variable pointed to by
@ -66,8 +63,50 @@ by \fIoptions\fP are set, as follows:
.B ARES_OPT_FLAGS
.B int \fIflags\fP;
.br
Flags controlling the behavior of the resolver. See below for a
description of possible flag values.
Flags controlling the behavior of the resolver:
.RS 4
.TP 23
.B ARES_FLAG_USEVC
Always use TCP queries (the "virtual circuit") instead of UDP
queries. Normally, TCP is only used if a UDP query yields a truncated
result.
.TP 23
.B ARES_FLAG_PRIMARY
Only query the first server in the list of servers to query.
.TP 23
.B ARES_FLAG_IGNTC
If a truncated response to a UDP query is received, do not fall back
to TCP; simply continue on with the truncated response.
.TP 23
.B ARES_FLAG_NORECURSE
Do not set the "recursion desired" bit on outgoing queries, so that the name
server being contacted will not try to fetch the answer from other servers if
it doesn't know the answer locally. Be aware that ares will not do the
recursion for you. Recursion must be handled by the application calling ares
if \fIARES_FLAG_NORECURSE\fP is set.
.TP 23
.B ARES_FLAG_STAYOPEN
Do not close communications sockets when the number of active queries
drops to zero.
.TP 23
.B ARES_FLAG_NOSEARCH
Do not use the default search domains; only query hostnames as-is or
as aliases.
.TP 23
.B ARES_FLAG_NOALIASES
Do not honor the HOSTALIASES environment variable, which normally
specifies a file of hostname translations.
.TP 23
.B ARES_FLAG_NOCHECKRESP
Do not discard responses with the SERVFAIL, NOTIMP, or REFUSED
response code or responses whose questions don't match the questions
in the request. Primarily useful for writing clients which might be
used to test or debug name servers.
.TP 23
.B ARES_FLAG_EDNS
Include an EDNS pseudo-resource record (RFC 2671) in generated requests. As of
v1.22, this is on by default if flags are otherwise not set.
.RE
.TP 18
.B ARES_OPT_TIMEOUT
.B int \fItimeout\fP;
@ -222,6 +261,18 @@ The maximum number of udp queries that can be sent on a single ephemeral port
to a given DNS server before a new ephemeral port is assigned. Any value of 0
or less will be considered unlimited, and is the default.
.br
.TP 18
.B ARES_OPT_QUERY_CACHE
.B unsigned int \fIqcache_max_ttl\fP;
.br
Enable the built-in query cache. Will cache queries based on the returned TTL
in the DNS message. Only fully successful and NXDOMAIN query results will be
cached. Fill in the \fIqcache_max_ttl\fP with the maximum number of seconds
a query result may be cached which will override a larger TTL in the response
message. This must be a non-zero value otherwise the cache will be disabled.
Choose a reasonable value for your application such as 300 (5 minutes) or
3600 (1 hour).
.br
.PP
The \fIoptmask\fP parameter also includes options without a corresponding
field in the
@ -236,52 +287,10 @@ for each resolution.
Do not perform round-robin nameserver selection; always use the list of
nameservers in the same order.
.PP
The
.I flags
field should be the bitwise or of some subset of the following values:
.TP 23
.B ARES_FLAG_USEVC
Always use TCP queries (the "virtual circuit") instead of UDP
queries. Normally, TCP is only used if a UDP query yields a truncated
result.
.TP 23
.B ARES_FLAG_PRIMARY
Only query the first server in the list of servers to query.
.TP 23
.B ARES_FLAG_IGNTC
If a truncated response to a UDP query is received, do not fall back
to TCP; simply continue on with the truncated response.
.TP 23
.B ARES_FLAG_NORECURSE
Do not set the "recursion desired" bit on outgoing queries, so that the name
server being contacted will not try to fetch the answer from other servers if
it doesn't know the answer locally. Be aware that ares will not do the
recursion for you. Recursion must be handled by the application calling ares
if \fIARES_FLAG_NORECURSE\fP is set.
.TP 23
.B ARES_FLAG_STAYOPEN
Do not close communications sockets when the number of active queries
drops to zero.
.TP 23
.B ARES_FLAG_NOSEARCH
Do not use the default search domains; only query hostnames as-is or
as aliases.
.TP 23
.B ARES_FLAG_NOALIASES
Do not honor the HOSTALIASES environment variable, which normally
specifies a file of hostname translations.
.TP 23
.B ARES_FLAG_NOCHECKRESP
Do not discard responses with the SERVFAIL, NOTIMP, or REFUSED
response code or responses whose questions don't match the questions
in the request. Primarily useful for writing clients which might be
used to test or debug name servers.
.TP 23
.B ARES_FLAG_EDNS
Include an EDNS pseudo-resource record (RFC 2671) in generated requests. As of
v1.22, this is on by default if flags are otherwise not set.
.SH RETURN VALUES
\fBares_init_options(3)\fP can return any of the following values:
\fBares_init_options(3)\fP and \fBares_init(3)\fP can return any of the
following values:
.TP 14
.B ARES_SUCCESS
Initialization succeeded.
@ -300,15 +309,14 @@ When initializing from
(or, alternatively when specified by the
.I resolvconf_path
path location)
\fBares_init_options(3)\fP reads the \fIdomain\fP and \fIsearch\fP directives
to allow lookups of short names relative to the domains specified. The
\fIdomain\fP and \fIsearch\fP directives override one another. If more than
one instance of either \fIdomain\fP or \fIsearch\fP directives is specified,
the last occurrence wins. For more information, please see the
\fBares_init_options(3)\fP and \fBares_init(3)\fP reads the \fIdomain\fP and
\fIsearch\fP directives to allow lookups of short names relative to the domains
specified. The \fIdomain\fP and \fIsearch\fP directives override one another.
If more than one instance of either \fIdomain\fP or \fIsearch\fP directives is
specified, the last occurrence wins. For more information, please see the
.BR resolv.conf (5)
manual page.
.SH SEE ALSO
.BR ares_init (3),
.BR ares_reinit (3),
.BR ares_destroy (3),
.BR ares_dup (3),

@ -196,6 +196,7 @@ typedef enum {
#define ARES_OPT_HOSTS_FILE (1 << 18)
#define ARES_OPT_UDP_MAX_QUERIES (1 << 19)
#define ARES_OPT_MAXTIMEOUTMS (1 << 20)
#define ARES_OPT_QUERY_CACHE (1 << 21)
/* Nameinfo flag values */
#define ARES_NI_NOFQDN (1 << 0)
@ -307,6 +308,7 @@ struct ares_options {
char *hosts_path;
int udp_max_queries;
int maxtimeout; /* in milliseconds */
unsigned int qcache_max_ttl; /* Maximum TTL for query cache, 0=disabled */
};
struct hostent;

@ -57,6 +57,7 @@ CSOURCES = ares__addrinfo2hostent.c \
ares_parse_uri_reply.c \
ares_platform.c \
ares_process.c \
ares_qcache.c \
ares_query.c \
ares_rand.c \
ares_search.c \

@ -41,6 +41,7 @@ struct ares__htable {
unsigned int seed;
unsigned int size;
size_t num_keys;
size_t num_collisions;
/* NOTE: if we converted buckets into ares__slist_t we could guarantee on
* hash collisions we would have O(log n) worst case insert and search
* performance. (We'd also need to make key_eq into a key_cmp to
@ -158,9 +159,12 @@ static ares__llist_node_t *ares__htable_find(const ares__htable_t *htable,
static ares_bool_t ares__htable_expand(ares__htable_t *htable)
{
ares__llist_t **buckets = NULL;
unsigned int old_size = htable->size;
ares__llist_t **buckets = NULL;
unsigned int old_size = htable->size;
size_t i;
ares__llist_t **prealloc_llist = NULL;
size_t prealloc_llist_len = 0;
ares_bool_t rv = ARES_FALSE;
/* Not a failure, just won't expand */
if (old_size == ARES__HTABLE_MAX_BUCKETS) {
@ -169,15 +173,33 @@ static ares_bool_t ares__htable_expand(ares__htable_t *htable)
htable->size <<= 1;
/* We must do this in 2 passes as we want it to be non-destructive in case
* there is a memory allocation failure. So we will actually use more
* memory doing it this way, but at least we might be able to gracefully
* recover */
/* We must pre-allocate all memory we'll need before moving entries to the
* new hash array. Otherwise if there's a memory allocation failure in the
* middle, we wouldn't be able to recover. */
buckets = ares_malloc_zero(sizeof(*buckets) * htable->size);
if (buckets == NULL) {
goto fail;
goto done;
}
/* The maximum number of new llists we'll need is the number of collisions
* that were recorded */
prealloc_llist_len = htable->num_collisions;
if (prealloc_llist_len) {
prealloc_llist = ares_malloc_zero(sizeof(*prealloc_llist) *
prealloc_llist_len);
if (prealloc_llist == NULL) {
goto done;
}
}
for (i=0; i<prealloc_llist_len; i++) {
prealloc_llist[i] = ares__llist_create(htable->bucket_free);
if (prealloc_llist[i] == NULL) {
goto done;
}
}
/* Iterate across all buckets and move the entries to the new buckets */
htable->num_collisions = 0;
for (i = 0; i < old_size; i++) {
ares__llist_node_t *node;
@ -185,7 +207,7 @@ static ares_bool_t ares__htable_expand(ares__htable_t *htable)
if (htable->buckets[i] == NULL)
continue;
/* Fast past optimization (most likely case), there is likely only a single
/* Fast path optimization (most likely case), there is likely only a single
* entry in both the source and destination, check for this to confirm and
* if so, just move the bucket over */
if (ares__llist_len(htable->buckets[i]) == 1) {
@ -214,27 +236,48 @@ static ares_bool_t ares__htable_expand(ares__htable_t *htable)
break;
}
/* Grab one off our preallocated list */
if (buckets[idx] == NULL) {
buckets[idx] = ares__llist_create(htable->bucket_free);
}
if (buckets[idx] == NULL) {
goto fail;
/* Silence static analysis, this isn't possible but it doesn't know */
if (prealloc_llist == NULL || prealloc_llist_len == 0) {
goto done;
}
buckets[idx] = prealloc_llist[prealloc_llist_len-1];
prealloc_llist_len--;
} else {
/* Collision occurred since the bucket wasn't empty */
htable->num_collisions++;
}
ares__llist_node_move_parent_first(node, buckets[idx]);
}
/* Abandoned bucket, destroy */
if (htable->buckets[i] != NULL) {
ares__llist_destroy(htable->buckets[i]);
htable->buckets[i] = NULL;
}
}
/* Swap out buckets */
ares__htable_buckets_destroy(htable->buckets, old_size, ARES_FALSE);
/* We have guaranteed all the buckets have either been moved or destroyed,
* so we just call ares_free() on the array and swap out the pointer */
ares_free(htable->buckets);
htable->buckets = buckets;
return ARES_TRUE;
buckets = NULL;
rv = ARES_TRUE;
fail:
ares__htable_buckets_destroy(buckets, htable->size, ARES_FALSE);
htable->size = old_size;
done:
ares_free(buckets);
/* destroy any unused preallocated buckets */
ares__htable_buckets_destroy(prealloc_llist, (unsigned int)prealloc_llist_len,
ARES_FALSE);
/* On failure, we need to restore the htable size */
if (rv != ARES_TRUE) {
htable->size = old_size;
}
return ARES_FALSE;
return rv;
}
ares_bool_t ares__htable_insert(ares__htable_t *htable, void *bucket)
@ -282,6 +325,11 @@ ares_bool_t ares__htable_insert(ares__htable_t *htable, void *bucket)
return ARES_FALSE;
}
/* Track collisions for rehash stablility */
if (ares__llist_len(htable->buckets[idx]) > 1) {
htable->num_collisions++;
}
htable->num_keys++;
return ARES_TRUE;
@ -316,6 +364,12 @@ ares_bool_t ares__htable_remove(ares__htable_t *htable, const void *key)
}
htable->num_keys--;
/* Reduce collisions */
if (ares__llist_len(ares__llist_node_parent(node)) > 1) {
htable->num_collisions--;
}
ares__llist_node_destroy(node);
return ARES_TRUE;
}

@ -89,6 +89,8 @@ void ares_destroy(ares_channel_t *channel)
ares__hosts_file_destroy(channel->hf);
ares__qcache_destroy(channel->qcache);
ares_free(channel);
}

@ -46,6 +46,8 @@ ares_status_t ares_dns_rr_set_opt_own(ares_dns_rr_t *dns_rr,
ares_status_t ares_dns_record_rr_prealloc(ares_dns_record_t *dnsrec,
ares_dns_section_t sect, size_t cnt);
ares_bool_t ares_dns_has_opt_rr(const ares_dns_record_t *rec);
void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec,
unsigned int ttl_decrement);
struct ares_dns_qd {
char *name;
@ -209,6 +211,10 @@ struct ares_dns_record {
unsigned short raw_rcode; /*!< Raw rcode, used to ultimately form real
* rcode after reading OPT record if it
* exists */
unsigned int ttl_decrement; /*!< Special case to apply to writing out
* this record, where it will decrement
* the ttl of any resource records by
* this amount. Used for cache */
ares_dns_qd_t *qd;
size_t qdcount;

@ -847,6 +847,7 @@ static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec,
ares_status_t status;
size_t rdlength;
size_t end_length;
unsigned int ttl;
rr = ares_dns_record_rr_get(dnsrec, section, i);
if (rr == NULL) {
@ -880,7 +881,13 @@ static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec,
}
/* TTL */
status = ares__buf_append_be32(buf, ares_dns_rr_get_ttl(rr));
ttl = ares_dns_rr_get_ttl(rr);
if (rr->parent->ttl_decrement > ttl) {
ttl = 0;
} else {
ttl -= rr->parent->ttl_decrement;
}
status = ares__buf_append_be32(buf, ttl);
if (status != ARES_SUCCESS) {
return status;
}
@ -1036,3 +1043,12 @@ done:
*buf = ares__buf_finish_bin(b, buf_len);
return status;
}
void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec,
unsigned int ttl_decrement)
{
if (dnsrec == NULL)
return;
dnsrec->ttl_decrement = ttl_decrement;
}

@ -358,6 +358,14 @@ int ares_init_options(ares_channel_t **channelptr,
goto done;
}
if (channel->qcache_max_ttl > 0) {
status = ares__qcache_create(channel->rand_state, channel->qcache_max_ttl,
&channel->qcache);
if (status != ARES_SUCCESS) {
goto done;
}
}
if (status == ARES_SUCCESS) {
status = ares__init_by_sysconfig(channel);
if (status != ARES_SUCCESS) {
@ -399,6 +407,12 @@ ares_status_t ares_reinit(ares_channel_t *channel)
DEBUGF(fprintf(stderr, "Error: init_by_sysconfig failed: %s\n",
ares_strerror(status)));
}
/* Flush cached queries on reinit */
if (channel->qcache) {
ares__qcache_flush(channel->qcache);
}
return status;
}

@ -221,6 +221,10 @@ int ares_save_options(ares_channel_t *channel, struct ares_options *options,
options->udp_max_queries = (int)channel->udp_max_queries;
}
if (channel->optmask & ARES_OPT_QUERY_CACHE) {
options->qcache_max_ttl = channel->qcache_max_ttl;
}
*optmask = (int)channel->optmask;
return ARES_SUCCESS;
@ -381,6 +385,10 @@ ares_status_t ares__init_by_options(ares_channel_t *channel,
channel->udp_max_queries = (size_t)options->udp_max_queries;
}
if (optmask & ARES_OPT_QUERY_CACHE) {
channel->qcache_max_ttl = options->qcache_max_ttl;
}
/* Initialize the ipv4 servers if provided */
if (optmask & ARES_OPT_SERVERS && options->nservers > 0) {
ares_status_t status;

@ -243,6 +243,9 @@ struct apattern {
unsigned short type;
};
struct ares__qcache;
typedef struct ares__qcache ares__qcache_t;
struct ares_hosts_file;
typedef struct ares_hosts_file ares_hosts_file_t;
@ -264,6 +267,7 @@ struct ares_channeldata {
size_t nsort;
char *lookups;
size_t ednspsz;
unsigned int qcache_max_ttl;
unsigned int optmask;
/* For binding to local devices and/or IP addresses. Leave
@ -318,6 +322,9 @@ struct ares_channeldata {
/* Cache of local hosts file */
ares_hosts_file_t *hf;
/* Query Cache */
ares__qcache_t *qcache;
};
/* Does the domain end in ".onion" or ".onion."? Case-insensitive. */
@ -556,7 +563,18 @@ size_t ares__log2(size_t n);
size_t ares__pow(size_t x, size_t y);
size_t ares__count_digits(size_t n);
size_t ares__count_hexdigits(size_t n);
void ares__qcache_destroy(ares__qcache_t *cache);
ares_status_t ares__qcache_create(ares_rand_state *rand_state,
unsigned int max_ttl,
ares__qcache_t **cache_out);
void ares__qcache_flush(ares__qcache_t *cache);
ares_status_t ares_qcache_insert(ares_channel_t *channel,
struct timeval *now,
struct query *query,
ares_dns_record_t *dnsrec);
ares_status_t ares_qcache_fetch(ares_channel_t *channel, struct timeval *now,
const unsigned char *qbuf, size_t qlen,
unsigned char **abuf, size_t *alen);
# ifdef _MSC_VER
typedef __int64 ares_int64_t;

@ -714,6 +714,12 @@ static ares_status_t process_answer(ares_channel_t *channel,
}
}
/* If cache insertion was successful, it took ownership. We ignore
* other cache insertion failures. */
if (ares_qcache_insert(channel, now, query, rdnsrec) == ARES_SUCCESS) {
rdnsrec = NULL;
}
server_set_good(server);
end_query(channel, query, ARES_SUCCESS, abuf, alen);

@ -0,0 +1,453 @@
/* MIT License
*
* Copyright (c) 2023 Brad House
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#include "ares_setup.h"
#include "ares.h"
#include "ares_private.h"
struct ares__qcache {
ares__htable_strvp_t *cache;
ares__slist_t *expire;
unsigned int max_ttl;
};
typedef struct {
char *key;
ares_dns_record_t *dnsrec;
time_t expire_ts;
time_t insert_ts;
} ares__qcache_entry_t;
static char *ares__qcache_calc_key(const ares_dns_record_t *dnsrec)
{
ares__buf_t *buf = ares__buf_create();
size_t i;
ares_status_t status;
ares_dns_flags_t flags;
if (dnsrec == NULL || buf == NULL) {
return NULL;
}
/* Format is OPCODE|FLAGS[|QTYPE1|QCLASS1|QNAME1]... */
status = ares__buf_append_str(
buf, ares_dns_opcode_tostr(ares_dns_record_get_opcode(dnsrec)));
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_byte(buf, '|');
if (status != ARES_SUCCESS) {
goto fail;
}
flags = ares_dns_record_get_flags(dnsrec);
/* Only care about RD and CD */
if (flags & ARES_FLAG_RD) {
status = ares__buf_append_str(buf, "rd");
if (status != ARES_SUCCESS) {
goto fail;
}
}
if (flags & ARES_FLAG_CD) {
status = ares__buf_append_str(buf, "cd");
if (status != ARES_SUCCESS) {
goto fail;
}
}
for (i = 0; i < ares_dns_record_query_cnt(dnsrec); i++) {
const char *name;
ares_dns_rec_type_t qtype;
ares_dns_class_t qclass;
status = ares_dns_record_query_get(dnsrec, i, &name, &qtype, &qclass);
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_byte(buf, '|');
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_str(buf, ares_dns_rec_type_tostr(qtype));
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_byte(buf, '|');
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_str(buf, ares_dns_class_tostr(qclass));
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_byte(buf, '|');
if (status != ARES_SUCCESS) {
goto fail;
}
status = ares__buf_append_str(buf, name);
if (status != ARES_SUCCESS) {
goto fail;
}
}
return ares__buf_finish_str(buf, NULL);
fail:
ares__buf_destroy(buf);
return NULL;
}
static void ares__qcache_expire(ares__qcache_t *cache, struct timeval *now)
{
ares__slist_node_t *node;
if (cache == NULL) {
return;
}
while ((node = ares__slist_node_first(cache->expire)) != NULL) {
ares__qcache_entry_t *entry = ares__slist_node_val(node);
if (entry->expire_ts > now->tv_sec) {
break;
}
ares__htable_strvp_remove(cache->cache, entry->key);
ares__slist_node_destroy(node);
}
}
void ares__qcache_flush(ares__qcache_t *cache)
{
struct timeval now;
memset(&now, 0, sizeof(now));
ares__qcache_expire(cache, &now);
}
void ares__qcache_destroy(ares__qcache_t *cache)
{
if (cache == NULL) {
return;
}
ares__htable_strvp_destroy(cache->cache);
ares__slist_destroy(cache->expire);
ares_free(cache);
}
static int ares__qcache_entry_sort_cb(const void *arg1, const void *arg2)
{
const ares__qcache_entry_t *entry1 = arg1;
const ares__qcache_entry_t *entry2 = arg2;
if (entry1->expire_ts > entry2->expire_ts) {
return 1;
}
if (entry1->expire_ts < entry2->expire_ts) {
return -1;
}
return 0;
}
static void ares__qcache_entry_destroy_cb(void *arg)
{
ares__qcache_entry_t *entry = arg;
if (entry == NULL) {
return;
}
ares_free(entry->key);
ares_dns_record_destroy(entry->dnsrec);
ares_free(entry);
}
ares_status_t ares__qcache_create(ares_rand_state *rand_state,
unsigned int max_ttl,
ares__qcache_t **cache_out)
{
ares_status_t status = ARES_SUCCESS;
ares__qcache_t *cache;
cache = ares_malloc_zero(sizeof(*cache));
if (cache == NULL) {
status = ARES_ENOMEM;
goto done;
}
cache->cache = ares__htable_strvp_create(NULL);
if (cache->cache == NULL) {
status = ARES_ENOMEM;
goto done;
}
cache->expire = ares__slist_create(rand_state, ares__qcache_entry_sort_cb,
ares__qcache_entry_destroy_cb);
if (cache->expire == NULL) {
status = ARES_ENOMEM;
goto done;
}
cache->max_ttl = max_ttl;
done:
if (status != ARES_SUCCESS) {
*cache_out = NULL;
ares__qcache_destroy(cache);
return status;
}
*cache_out = cache;
return status;
}
static unsigned int ares__qcache_calc_minttl(ares_dns_record_t *dnsrec)
{
unsigned int minttl = 0xFFFFFFFF;
size_t sect;
for (sect = ARES_SECTION_ANSWER; sect <= ARES_SECTION_ADDITIONAL; sect++) {
size_t i;
for (i = 0; i < ares_dns_record_rr_cnt(dnsrec, (ares_dns_section_t)sect);
i++) {
const ares_dns_rr_t *rr =
ares_dns_record_rr_get(dnsrec, (ares_dns_section_t)sect, i);
ares_dns_rec_type_t type = ares_dns_rr_get_type(rr);
unsigned int ttl = ares_dns_rr_get_ttl(rr);
if (type == ARES_REC_TYPE_OPT || type == ARES_REC_TYPE_SOA) {
continue;
}
if (ttl < minttl) {
minttl = ttl;
}
}
}
return minttl;
}
static unsigned int ares__qcache_soa_minimum(ares_dns_record_t *dnsrec)
{
size_t i;
/* RFC 2308 Section 5 says its the minimum of MINIMUM and the TTL of the
* record. */
for (i = 0; i < ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_AUTHORITY); i++) {
const ares_dns_rr_t *rr =
ares_dns_record_rr_get(dnsrec, ARES_SECTION_AUTHORITY, i);
ares_dns_rec_type_t type = ares_dns_rr_get_type(rr);
unsigned int ttl;
unsigned int minimum;
if (type != ARES_REC_TYPE_SOA) {
continue;
}
minimum = ares_dns_rr_get_u32(rr, ARES_RR_SOA_MINIMUM);
ttl = ares_dns_rr_get_ttl(rr);
if (ttl > minimum) {
return minimum;
}
return ttl;
}
return 0;
}
static char *ares__qcache_calc_key_frombuf(const unsigned char *qbuf,
size_t qlen)
{
ares_status_t status;
ares_dns_record_t *dnsrec = NULL;
char *key = NULL;
status = ares_dns_parse(qbuf, qlen, 0, &dnsrec);
if (status != ARES_SUCCESS) {
goto done;
}
key = ares__qcache_calc_key(dnsrec);
done:
ares_dns_record_destroy(dnsrec);
return key;
}
/* On success, takes ownership of dnsrec */
static ares_status_t ares__qcache_insert(ares__qcache_t *qcache,
ares_dns_record_t *dnsrec,
const unsigned char *qbuf,
size_t qlen,
struct timeval *now)
{
ares__qcache_entry_t *entry;
unsigned int ttl;
ares_dns_rcode_t rcode = ares_dns_record_get_rcode(dnsrec);
ares_dns_flags_t flags = ares_dns_record_get_flags(dnsrec);
if (qcache == NULL || dnsrec == NULL) {
return ARES_EFORMERR;
}
/* Only save NOERROR or NXDOMAIN */
if (rcode != ARES_RCODE_NOERROR && rcode != ARES_RCODE_NXDOMAIN) {
return ARES_ENOTIMP;
}
/* Don't save truncated queries */
if (flags & ARES_FLAG_TC) {
return ARES_ENOTIMP;
}
/* Look at SOA for NXDOMAIN for minimum */
if (rcode == ARES_RCODE_NXDOMAIN) {
ttl = ares__qcache_soa_minimum(dnsrec);
} else {
ttl = ares__qcache_calc_minttl(dnsrec);
}
/* Don't cache something that is already expired */
if (ttl == 0) {
return ARES_EREFUSED;
}
if (ttl > qcache->max_ttl) {
ttl = qcache->max_ttl;
}
entry = ares_malloc_zero(sizeof(*entry));
if (entry == NULL) {
goto fail;
}
entry->dnsrec = dnsrec;
entry->expire_ts = now->tv_sec + ttl;
entry->insert_ts = now->tv_sec;
/* We can't guarantee the server responded with the same flags as the
* request had, so we have to re-parse the request in order to generate the
* key for caching, but we'll only do this once we know for sure we really
* want to cache it */
entry->key = ares__qcache_calc_key_frombuf(qbuf, qlen);
if (entry->key == NULL) {
goto fail;
}
if (!ares__htable_strvp_insert(qcache->cache, entry->key, entry)) {
goto fail;
}
if (ares__slist_insert(qcache->expire, entry) == NULL) {
goto fail;
}
return ARES_SUCCESS;
fail:
if (entry != NULL && entry->key != NULL) {
ares__htable_strvp_remove(qcache->cache, entry->key);
ares_free(entry->key);
ares_free(entry);
}
return ARES_ENOMEM;
}
static ares_status_t ares__qcache_fetch(ares__qcache_t *qcache,
ares_dns_record_t *dnsrec,
struct timeval *now,
unsigned char **buf, size_t *buf_len)
{
char *key = NULL;
ares__qcache_entry_t *entry;
ares_status_t status;
if (qcache == NULL || dnsrec == NULL) {
return ARES_EFORMERR;
}
ares__qcache_expire(qcache, now);
key = ares__qcache_calc_key(dnsrec);
if (key == NULL) {
status = ARES_ENOMEM;
goto done;
}
entry = ares__htable_strvp_get_direct(qcache->cache, key);
if (entry == NULL) {
status = ARES_ENOTFOUND;
goto done;
}
ares_dns_record_write_ttl_decrement(
entry->dnsrec, (unsigned int)(now->tv_sec - entry->insert_ts));
status = ares_dns_write(entry->dnsrec, buf, buf_len);
done:
ares_free(key);
return status;
}
ares_status_t ares_qcache_insert(ares_channel_t *channel, struct timeval *now,
struct query *query,
ares_dns_record_t *dnsrec)
{
return ares__qcache_insert(channel->qcache, dnsrec, query->qbuf, query->qlen,
now);
}
ares_status_t ares_qcache_fetch(ares_channel_t *channel, struct timeval *now,
const unsigned char *qbuf, size_t qlen,
unsigned char **abuf, size_t *alen)
{
ares_status_t status;
ares_dns_record_t *dnsrec = NULL;
if (channel->qcache == NULL) {
return ARES_ENOTFOUND;
}
status = ares_dns_parse(qbuf, qlen, 0, &dnsrec);
if (status != ARES_SUCCESS) {
goto done;
}
status = ares__qcache_fetch(channel->qcache, dnsrec, now, abuf, alen);
done:
ares_dns_record_destroy(dnsrec);
return status;
}

@ -54,9 +54,11 @@ ares_status_t ares_send_ex(ares_channel_t *channel, const unsigned char *qbuf,
{
struct query *query;
size_t packetsz;
struct timeval now;
struct timeval now = ares__tvnow();
ares_status_t status;
unsigned short id = generate_unique_qid(channel);
unsigned char *abuf = NULL;
size_t alen = 0;
/* Verify that the query is at least long enough to hold the header. */
if (qlen < HFIXEDSZ || qlen >= (1 << 16)) {
@ -67,6 +69,17 @@ ares_status_t ares_send_ex(ares_channel_t *channel, const unsigned char *qbuf,
callback(arg, ARES_ESERVFAIL, 0, NULL, 0);
return ARES_ESERVFAIL;
}
/* Check query cache */
status = ares_qcache_fetch(channel, &now, qbuf, qlen, &abuf, &alen);
if (status != ARES_ENOTFOUND) {
/* ARES_SUCCESS means we retrieved the cache, anything else is a critical
* failure, all result in termination */
callback(arg, (int)status, 0, abuf, (int)alen);
ares_free(abuf);
return status;
}
/* Allocate space for query and allocated fields. */
query = ares_malloc(sizeof(struct query));
if (!query) {
@ -129,7 +142,6 @@ ares_status_t ares_send_ex(ares_channel_t *channel, const unsigned char *qbuf,
}
/* Perform the first query action. */
now = ares__tvnow();
status = ares__send_query(query, &now);
if (status == ARES_SUCCESS && qid) {

@ -579,6 +579,10 @@ ares_status_t ares__servers_update(ares_channel_t *channel,
/* Save servers as if they were passed in as an option */
channel->optmask |= ARES_OPT_SERVERS;
}
/* Clear any cached query results */
ares__qcache_flush(channel->qcache);
status = ARES_SUCCESS;
done:

@ -287,6 +287,58 @@ TEST_P(MockUDPMaxQueriesTest, GetHostByNameParallelLookups) {
}
}
class CacheQueriesTest
: public MockChannelOptsTest,
public ::testing::WithParamInterface<int> {
public:
CacheQueriesTest()
: MockChannelOptsTest(1, GetParam(), false,
FillOptions(&opts_),
ARES_OPT_QUERY_CACHE) {}
static struct ares_options* FillOptions(struct ares_options * opts) {
memset(opts, 0, sizeof(struct ares_options));
opts->qcache_max_ttl = 3600;
return opts;
}
private:
struct ares_options opts_;
};
TEST_P(CacheQueriesTest, GetHostByNameCache) {
DNSPacket rsp;
rsp.set_response().set_aa()
.add_question(new DNSQuestion("www.google.com", T_A))
.add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5}));
ON_CALL(server_, OnRequest("www.google.com", T_A))
.WillByDefault(SetReply(&server_, &rsp));
// Get notified of new sockets so we can validate how many are created
int rc = ARES_SUCCESS;
ares_set_socket_callback(channel_, SocketConnectCallback, &rc);
sock_cb_count = 0;
HostResult result1;
ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result1);
Process();
std::stringstream ss1;
EXPECT_TRUE(result1.done_);
ss1 << result1.host_;
EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss1.str());
/* Run again, should return cached result */
HostResult result2;
ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result2);
Process();
std::stringstream ss2;
EXPECT_TRUE(result2.done_);
ss2 << result2.host_;
EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss2.str());
EXPECT_EQ(1, sock_cb_count);
}
#define TCPPARALLELLOOKUPS 32
TEST_P(MockTCPChannelTest, GetHostByNameParallelLookups) {
DNSPacket rsp;
@ -1294,6 +1346,8 @@ INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPChannelTest, ::testing::ValuesI
INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPMaxQueriesTest, ::testing::ValuesIn(ares::test::families));
INSTANTIATE_TEST_SUITE_P(AddressFamilies, CacheQueriesTest, ::testing::ValuesIn(ares::test::families));
INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockTCPChannelTest, ::testing::ValuesIn(ares::test::families));
INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockExtraOptsTest, ::testing::ValuesIn(ares::test::families_modes));

6
test/ares-test.h vendored

@ -112,7 +112,11 @@ class DefaultChannelTest : public LibraryTest {
public:
DefaultChannelTest() : channel_(nullptr)
{
EXPECT_EQ(ARES_SUCCESS, ares_init(&channel_));
/* Enable query cache for live tests */
struct ares_options opts = { 0 };
opts.qcache_max_ttl = 300;
int optmask = ARES_OPT_QUERY_CACHE;
EXPECT_EQ(ARES_SUCCESS, ares_init_options(&channel_, &opts, optmask));
EXPECT_NE(nullptr, channel_);
}

Loading…
Cancel
Save