1 /* MIT License
2 *
3 * Copyright (c) 2023 Brad House
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 *
24 * SPDX-License-Identifier: MIT
25 */
26 #include "ares_private.h"
27
28 struct ares_qcache {
29 ares_htable_strvp_t *cache;
30 ares_slist_t *expire;
31 unsigned int max_ttl;
32 };
33
34 typedef struct {
35 char *key;
36 ares_dns_record_t *dnsrec;
37 time_t expire_ts;
38 time_t insert_ts;
39 } ares_qcache_entry_t;
40
ares_qcache_calc_key(const ares_dns_record_t * dnsrec)41 static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
42 {
43 ares_buf_t *buf = ares_buf_create();
44 size_t i;
45 ares_status_t status;
46 ares_dns_flags_t flags;
47
48 if (dnsrec == NULL || buf == NULL) {
49 return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */
50 }
51
52 /* Format is OPCODE|FLAGS[|QTYPE1|QCLASS1|QNAME1]... */
53
54 status = ares_buf_append_str(
55 buf, ares_dns_opcode_tostr(ares_dns_record_get_opcode(dnsrec)));
56 if (status != ARES_SUCCESS) {
57 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
58 }
59
60 status = ares_buf_append_byte(buf, '|');
61 if (status != ARES_SUCCESS) {
62 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
63 }
64
65 flags = ares_dns_record_get_flags(dnsrec);
66 /* Only care about RD and CD */
67 if (flags & ARES_FLAG_RD) {
68 status = ares_buf_append_str(buf, "rd");
69 if (status != ARES_SUCCESS) {
70 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
71 }
72 }
73 if (flags & ARES_FLAG_CD) {
74 status = ares_buf_append_str(buf, "cd");
75 if (status != ARES_SUCCESS) {
76 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
77 }
78 }
79
80 for (i = 0; i < ares_dns_record_query_cnt(dnsrec); i++) {
81 const char *name;
82 size_t name_len;
83 ares_dns_rec_type_t qtype;
84 ares_dns_class_t qclass;
85
86 status = ares_dns_record_query_get(dnsrec, i, &name, &qtype, &qclass);
87 if (status != ARES_SUCCESS) {
88 goto fail; /* LCOV_EXCL_LINE: DefensiveCoding */
89 }
90
91 status = ares_buf_append_byte(buf, '|');
92 if (status != ARES_SUCCESS) {
93 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
94 }
95
96 status = ares_buf_append_str(buf, ares_dns_rec_type_tostr(qtype));
97 if (status != ARES_SUCCESS) {
98 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
99 }
100
101 status = ares_buf_append_byte(buf, '|');
102 if (status != ARES_SUCCESS) {
103 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
104 }
105
106 status = ares_buf_append_str(buf, ares_dns_class_tostr(qclass));
107 if (status != ARES_SUCCESS) {
108 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
109 }
110
111 status = ares_buf_append_byte(buf, '|');
112 if (status != ARES_SUCCESS) {
113 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
114 }
115
116 /* On queries, a '.' may be appended to the name to indicate an explicit
117 * name lookup without performing a search. Strip this since its not part
118 * of a cached response. */
119 name_len = ares_strlen(name);
120 if (name_len && name[name_len - 1] == '.') {
121 name_len--;
122 }
123
124 if (name_len > 0) {
125 status = ares_buf_append(buf, (const unsigned char *)name, name_len);
126 if (status != ARES_SUCCESS) {
127 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
128 }
129 }
130 }
131
132 return ares_buf_finish_str(buf, NULL);
133
134 /* LCOV_EXCL_START: OutOfMemory */
135 fail:
136 ares_buf_destroy(buf);
137 return NULL;
138 /* LCOV_EXCL_STOP */
139 }
140
ares_qcache_expire(ares_qcache_t * cache,const ares_timeval_t * now)141 static void ares_qcache_expire(ares_qcache_t *cache, const ares_timeval_t *now)
142 {
143 ares_slist_node_t *node;
144
145 if (cache == NULL) {
146 return;
147 }
148
149 while ((node = ares_slist_node_first(cache->expire)) != NULL) {
150 const ares_qcache_entry_t *entry = ares_slist_node_val(node);
151
152 /* If now is NULL, we're flushing everything, so don't break */
153 if (now != NULL && entry->expire_ts > now->sec) {
154 break;
155 }
156
157 ares_htable_strvp_remove(cache->cache, entry->key);
158 ares_slist_node_destroy(node);
159 }
160 }
161
ares_qcache_flush(ares_qcache_t * cache)162 void ares_qcache_flush(ares_qcache_t *cache)
163 {
164 ares_qcache_expire(cache, NULL /* flush all */);
165 }
166
ares_qcache_destroy(ares_qcache_t * cache)167 void ares_qcache_destroy(ares_qcache_t *cache)
168 {
169 if (cache == NULL) {
170 return;
171 }
172
173 ares_htable_strvp_destroy(cache->cache);
174 ares_slist_destroy(cache->expire);
175 ares_free(cache);
176 }
177
ares_qcache_entry_sort_cb(const void * arg1,const void * arg2)178 static int ares_qcache_entry_sort_cb(const void *arg1, const void *arg2)
179 {
180 const ares_qcache_entry_t *entry1 = arg1;
181 const ares_qcache_entry_t *entry2 = arg2;
182
183 if (entry1->expire_ts > entry2->expire_ts) {
184 return 1;
185 }
186
187 if (entry1->expire_ts < entry2->expire_ts) {
188 return -1;
189 }
190
191 return 0;
192 }
193
ares_qcache_entry_destroy_cb(void * arg)194 static void ares_qcache_entry_destroy_cb(void *arg)
195 {
196 ares_qcache_entry_t *entry = arg;
197 if (entry == NULL) {
198 return; /* LCOV_EXCL_LINE: DefensiveCoding */
199 }
200
201 ares_free(entry->key);
202 ares_dns_record_destroy(entry->dnsrec);
203 ares_free(entry);
204 }
205
ares_qcache_create(ares_rand_state * rand_state,unsigned int max_ttl,ares_qcache_t ** cache_out)206 ares_status_t ares_qcache_create(ares_rand_state *rand_state,
207 unsigned int max_ttl,
208 ares_qcache_t **cache_out)
209 {
210 ares_status_t status = ARES_SUCCESS;
211 ares_qcache_t *cache;
212
213 cache = ares_malloc_zero(sizeof(*cache));
214 if (cache == NULL) {
215 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
216 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
217 }
218
219 cache->cache = ares_htable_strvp_create(NULL);
220 if (cache->cache == NULL) {
221 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
222 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
223 }
224
225 cache->expire = ares_slist_create(rand_state, ares_qcache_entry_sort_cb,
226 ares_qcache_entry_destroy_cb);
227 if (cache->expire == NULL) {
228 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
229 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
230 }
231
232 cache->max_ttl = max_ttl;
233
234 done:
235 if (status != ARES_SUCCESS) {
236 *cache_out = NULL;
237 ares_qcache_destroy(cache);
238 return status;
239 }
240
241 *cache_out = cache;
242 return status;
243 }
244
ares_qcache_calc_minttl(ares_dns_record_t * dnsrec)245 static unsigned int ares_qcache_calc_minttl(ares_dns_record_t *dnsrec)
246 {
247 unsigned int minttl = 0xFFFFFFFF;
248 size_t sect;
249
250 for (sect = ARES_SECTION_ANSWER; sect <= ARES_SECTION_ADDITIONAL; sect++) {
251 size_t i;
252 for (i = 0; i < ares_dns_record_rr_cnt(dnsrec, (ares_dns_section_t)sect);
253 i++) {
254 const ares_dns_rr_t *rr =
255 ares_dns_record_rr_get(dnsrec, (ares_dns_section_t)sect, i);
256 ares_dns_rec_type_t type = ares_dns_rr_get_type(rr);
257 unsigned int ttl = ares_dns_rr_get_ttl(rr);
258
259 /* TTL is meaningless on these record types */
260 if (type == ARES_REC_TYPE_OPT || type == ARES_REC_TYPE_SOA ||
261 type == ARES_REC_TYPE_SIG) {
262 continue;
263 }
264
265 if (ttl < minttl) {
266 minttl = ttl;
267 }
268 }
269 }
270
271 return minttl;
272 }
273
ares_qcache_soa_minimum(ares_dns_record_t * dnsrec)274 static unsigned int ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
275 {
276 size_t i;
277
278 /* RFC 2308 Section 5 says its the minimum of MINIMUM and the TTL of the
279 * record. */
280 for (i = 0; i < ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_AUTHORITY); i++) {
281 const ares_dns_rr_t *rr =
282 ares_dns_record_rr_get(dnsrec, ARES_SECTION_AUTHORITY, i);
283 ares_dns_rec_type_t type = ares_dns_rr_get_type(rr);
284 unsigned int ttl;
285 unsigned int minimum;
286
287 if (type != ARES_REC_TYPE_SOA) {
288 continue;
289 }
290
291 minimum = ares_dns_rr_get_u32(rr, ARES_RR_SOA_MINIMUM);
292 ttl = ares_dns_rr_get_ttl(rr);
293
294 if (ttl > minimum) {
295 return minimum;
296 }
297 return ttl;
298 }
299
300 return 0;
301 }
302
303 /* On success, takes ownership of dnsrec */
ares_qcache_insert_int(ares_qcache_t * qcache,ares_dns_record_t * qresp,const ares_dns_record_t * qreq,const ares_timeval_t * now)304 static ares_status_t ares_qcache_insert_int(ares_qcache_t *qcache,
305 ares_dns_record_t *qresp,
306 const ares_dns_record_t *qreq,
307 const ares_timeval_t *now)
308 {
309 ares_qcache_entry_t *entry;
310 unsigned int ttl;
311 ares_dns_rcode_t rcode = ares_dns_record_get_rcode(qresp);
312 ares_dns_flags_t flags = ares_dns_record_get_flags(qresp);
313
314 if (qcache == NULL || qresp == NULL) {
315 return ARES_EFORMERR;
316 }
317
318 /* Only save NOERROR or NXDOMAIN */
319 if (rcode != ARES_RCODE_NOERROR && rcode != ARES_RCODE_NXDOMAIN) {
320 return ARES_ENOTIMP;
321 }
322
323 /* Don't save truncated queries */
324 if (flags & ARES_FLAG_TC) {
325 return ARES_ENOTIMP;
326 }
327
328 /* Look at SOA for NXDOMAIN for minimum */
329 if (rcode == ARES_RCODE_NXDOMAIN) {
330 ttl = ares_qcache_soa_minimum(qresp);
331 } else {
332 ttl = ares_qcache_calc_minttl(qresp);
333 }
334
335 if (ttl > qcache->max_ttl) {
336 ttl = qcache->max_ttl;
337 }
338
339 /* Don't cache something that is already expired */
340 if (ttl == 0) {
341 return ARES_EREFUSED;
342 }
343
344 entry = ares_malloc_zero(sizeof(*entry));
345 if (entry == NULL) {
346 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
347 }
348
349 entry->dnsrec = qresp;
350 entry->expire_ts = (time_t)now->sec + (time_t)ttl;
351 entry->insert_ts = (time_t)now->sec;
352
353 /* We can't guarantee the server responded with the same flags as the
354 * request had, so we have to re-parse the request in order to generate the
355 * key for caching, but we'll only do this once we know for sure we really
356 * want to cache it */
357 entry->key = ares_qcache_calc_key(qreq);
358 if (entry->key == NULL) {
359 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
360 }
361
362 if (!ares_htable_strvp_insert(qcache->cache, entry->key, entry)) {
363 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
364 }
365
366 if (ares_slist_insert(qcache->expire, entry) == NULL) {
367 goto fail; /* LCOV_EXCL_LINE: OutOfMemory */
368 }
369
370 return ARES_SUCCESS;
371
372 /* LCOV_EXCL_START: OutOfMemory */
373 fail:
374 if (entry != NULL && entry->key != NULL) {
375 ares_htable_strvp_remove(qcache->cache, entry->key);
376 ares_free(entry->key);
377 ares_free(entry);
378 }
379 return ARES_ENOMEM;
380 /* LCOV_EXCL_STOP */
381 }
382
ares_qcache_fetch(ares_channel_t * channel,const ares_timeval_t * now,const ares_dns_record_t * dnsrec,const ares_dns_record_t ** dnsrec_resp)383 ares_status_t ares_qcache_fetch(ares_channel_t *channel,
384 const ares_timeval_t *now,
385 const ares_dns_record_t *dnsrec,
386 const ares_dns_record_t **dnsrec_resp)
387 {
388 char *key = NULL;
389 ares_qcache_entry_t *entry;
390 ares_status_t status = ARES_SUCCESS;
391
392 if (channel == NULL || dnsrec == NULL || dnsrec_resp == NULL) {
393 return ARES_EFORMERR;
394 }
395
396 if (channel->qcache == NULL) {
397 return ARES_ENOTFOUND;
398 }
399
400 ares_qcache_expire(channel->qcache, now);
401
402 key = ares_qcache_calc_key(dnsrec);
403 if (key == NULL) {
404 status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */
405 goto done; /* LCOV_EXCL_LINE: OutOfMemory */
406 }
407
408 entry = ares_htable_strvp_get_direct(channel->qcache->cache, key);
409 if (entry == NULL) {
410 status = ARES_ENOTFOUND;
411 goto done;
412 }
413
414 ares_dns_record_ttl_decrement(entry->dnsrec,
415 (unsigned int)(now->sec - entry->insert_ts));
416
417 *dnsrec_resp = entry->dnsrec;
418
419 done:
420 ares_free(key);
421 return status;
422 }
423
ares_qcache_insert(ares_channel_t * channel,const ares_timeval_t * now,const ares_query_t * query,ares_dns_record_t * dnsrec)424 ares_status_t ares_qcache_insert(ares_channel_t *channel,
425 const ares_timeval_t *now,
426 const ares_query_t *query,
427 ares_dns_record_t *dnsrec)
428 {
429 return ares_qcache_insert_int(channel->qcache, dnsrec, query->query, now);
430 }
431