ares_qcache.c (12143B)
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 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 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 162 void ares_qcache_flush(ares_qcache_t *cache) 163 { 164 ares_qcache_expire(cache, NULL /* flush all */); 165 } 166 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 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 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 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 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 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 */ 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 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 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 }