Branch data Line data Source code
1 : : /* SPDX-License-Identifier: LGPL-2.1+ */
2 : :
3 : : #include <net/if.h>
4 : :
5 : : #include "af-list.h"
6 : : #include "alloc-util.h"
7 : : #include "dns-domain.h"
8 : : #include "format-util.h"
9 : : #include "resolved-dns-answer.h"
10 : : #include "resolved-dns-cache.h"
11 : : #include "resolved-dns-packet.h"
12 : : #include "string-util.h"
13 : :
14 : : /* Never cache more than 4K entries. RFC 1536, Section 5 suggests to
15 : : * leave DNS caches unbounded, but that's crazy. */
16 : : #define CACHE_MAX 4096
17 : :
18 : : /* We never keep any item longer than 2h in our cache */
19 : : #define CACHE_TTL_MAX_USEC (2 * USEC_PER_HOUR)
20 : :
21 : : /* How long to cache strange rcodes, i.e. rcodes != SUCCESS and != NXDOMAIN (specifically: that's only SERVFAIL for
22 : : * now) */
23 : : #define CACHE_TTL_STRANGE_RCODE_USEC (30 * USEC_PER_SEC)
24 : :
25 : : typedef enum DnsCacheItemType DnsCacheItemType;
26 : : typedef struct DnsCacheItem DnsCacheItem;
27 : :
28 : : enum DnsCacheItemType {
29 : : DNS_CACHE_POSITIVE,
30 : : DNS_CACHE_NODATA,
31 : : DNS_CACHE_NXDOMAIN,
32 : : DNS_CACHE_RCODE, /* "strange" RCODE (effective only SERVFAIL for now) */
33 : : };
34 : :
35 : : struct DnsCacheItem {
36 : : DnsCacheItemType type;
37 : : DnsResourceKey *key;
38 : : DnsResourceRecord *rr;
39 : : int rcode;
40 : :
41 : : usec_t until;
42 : : bool authenticated:1;
43 : : bool shared_owner:1;
44 : :
45 : : int ifindex;
46 : : int owner_family;
47 : : union in_addr_union owner_address;
48 : :
49 : : unsigned prioq_idx;
50 : : LIST_FIELDS(DnsCacheItem, by_key);
51 : : };
52 : :
53 : 0 : static const char *dns_cache_item_type_to_string(DnsCacheItem *item) {
54 [ # # ]: 0 : assert(item);
55 : :
56 [ # # # # : 0 : switch (item->type) {
# ]
57 : :
58 : 0 : case DNS_CACHE_POSITIVE:
59 : 0 : return "POSITIVE";
60 : :
61 : 0 : case DNS_CACHE_NODATA:
62 : 0 : return "NODATA";
63 : :
64 : 0 : case DNS_CACHE_NXDOMAIN:
65 : 0 : return "NXDOMAIN";
66 : :
67 : 0 : case DNS_CACHE_RCODE:
68 : 0 : return dns_rcode_to_string(item->rcode);
69 : : }
70 : :
71 : 0 : return NULL;
72 : : }
73 : :
74 : 0 : static void dns_cache_item_free(DnsCacheItem *i) {
75 [ # # ]: 0 : if (!i)
76 : 0 : return;
77 : :
78 : 0 : dns_resource_record_unref(i->rr);
79 : 0 : dns_resource_key_unref(i->key);
80 : 0 : free(i);
81 : : }
82 : :
83 [ # # ]: 0 : DEFINE_TRIVIAL_CLEANUP_FUNC(DnsCacheItem*, dns_cache_item_free);
84 : :
85 : 0 : static void dns_cache_item_unlink_and_free(DnsCache *c, DnsCacheItem *i) {
86 : : DnsCacheItem *first;
87 : :
88 [ # # ]: 0 : assert(c);
89 : :
90 [ # # ]: 0 : if (!i)
91 : 0 : return;
92 : :
93 : 0 : first = hashmap_get(c->by_key, i->key);
94 [ # # # # : 0 : LIST_REMOVE(by_key, first, i);
# # # # ]
95 : :
96 [ # # ]: 0 : if (first)
97 [ # # ]: 0 : assert_se(hashmap_replace(c->by_key, first->key, first) >= 0);
98 : : else
99 : 0 : hashmap_remove(c->by_key, i->key);
100 : :
101 : 0 : prioq_remove(c->by_expiry, i, &i->prioq_idx);
102 : :
103 : 0 : dns_cache_item_free(i);
104 : : }
105 : :
106 : 0 : static bool dns_cache_remove_by_rr(DnsCache *c, DnsResourceRecord *rr) {
107 : : DnsCacheItem *first, *i;
108 : : int r;
109 : :
110 : 0 : first = hashmap_get(c->by_key, rr->key);
111 [ # # ]: 0 : LIST_FOREACH(by_key, i, first) {
112 : 0 : r = dns_resource_record_equal(i->rr, rr);
113 [ # # ]: 0 : if (r < 0)
114 : 0 : return r;
115 [ # # ]: 0 : if (r > 0) {
116 : 0 : dns_cache_item_unlink_and_free(c, i);
117 : 0 : return true;
118 : : }
119 : : }
120 : :
121 : 0 : return false;
122 : : }
123 : :
124 : 0 : static bool dns_cache_remove_by_key(DnsCache *c, DnsResourceKey *key) {
125 : : DnsCacheItem *first, *i, *n;
126 : :
127 [ # # ]: 0 : assert(c);
128 [ # # ]: 0 : assert(key);
129 : :
130 : 0 : first = hashmap_remove(c->by_key, key);
131 [ # # ]: 0 : if (!first)
132 : 0 : return false;
133 : :
134 [ # # ]: 0 : LIST_FOREACH_SAFE(by_key, i, n, first) {
135 : 0 : prioq_remove(c->by_expiry, i, &i->prioq_idx);
136 : 0 : dns_cache_item_free(i);
137 : : }
138 : :
139 : 0 : return true;
140 : : }
141 : :
142 : 0 : void dns_cache_flush(DnsCache *c) {
143 : : DnsResourceKey *key;
144 : :
145 [ # # ]: 0 : assert(c);
146 : :
147 [ # # ]: 0 : while ((key = hashmap_first_key(c->by_key)))
148 : 0 : dns_cache_remove_by_key(c, key);
149 : :
150 [ # # ]: 0 : assert(hashmap_size(c->by_key) == 0);
151 [ # # ]: 0 : assert(prioq_size(c->by_expiry) == 0);
152 : :
153 : 0 : c->by_key = hashmap_free(c->by_key);
154 : 0 : c->by_expiry = prioq_free(c->by_expiry);
155 : 0 : }
156 : :
157 : 0 : static void dns_cache_make_space(DnsCache *c, unsigned add) {
158 [ # # ]: 0 : assert(c);
159 : :
160 [ # # ]: 0 : if (add <= 0)
161 : 0 : return;
162 : :
163 : : /* Makes space for n new entries. Note that we actually allow
164 : : * the cache to grow beyond CACHE_MAX, but only when we shall
165 : : * add more RRs to the cache than CACHE_MAX at once. In that
166 : : * case the cache will be emptied completely otherwise. */
167 : :
168 : 0 : for (;;) {
169 [ # # ]: 0 : _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
170 : : DnsCacheItem *i;
171 : :
172 [ # # ]: 0 : if (prioq_size(c->by_expiry) <= 0)
173 : 0 : break;
174 : :
175 [ # # ]: 0 : if (prioq_size(c->by_expiry) + add < CACHE_MAX)
176 : 0 : break;
177 : :
178 : 0 : i = prioq_peek(c->by_expiry);
179 [ # # ]: 0 : assert(i);
180 : :
181 : : /* Take an extra reference to the key so that it
182 : : * doesn't go away in the middle of the remove call */
183 : 0 : key = dns_resource_key_ref(i->key);
184 : 0 : dns_cache_remove_by_key(c, key);
185 : : }
186 : : }
187 : :
188 : 0 : void dns_cache_prune(DnsCache *c) {
189 : 0 : usec_t t = 0;
190 : :
191 [ # # ]: 0 : assert(c);
192 : :
193 : : /* Remove all entries that are past their TTL */
194 : :
195 : 0 : for (;;) {
196 : : DnsCacheItem *i;
197 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
198 : :
199 : 0 : i = prioq_peek(c->by_expiry);
200 [ # # ]: 0 : if (!i)
201 : 0 : break;
202 : :
203 [ # # ]: 0 : if (t <= 0)
204 : 0 : t = now(clock_boottime_or_monotonic());
205 : :
206 [ # # ]: 0 : if (i->until > t)
207 : 0 : break;
208 : :
209 : : /* Depending whether this is an mDNS shared entry
210 : : * either remove only this one RR or the whole RRset */
211 [ # # # # ]: 0 : log_debug("Removing %scache entry for %s (expired "USEC_FMT"s ago)",
212 : : i->shared_owner ? "shared " : "",
213 : : dns_resource_key_to_string(i->key, key_str, sizeof key_str),
214 : : (t - i->until) / USEC_PER_SEC);
215 : :
216 [ # # ]: 0 : if (i->shared_owner)
217 : 0 : dns_cache_item_unlink_and_free(c, i);
218 : : else {
219 : 0 : _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
220 : :
221 : : /* Take an extra reference to the key so that it
222 : : * doesn't go away in the middle of the remove call */
223 : 0 : key = dns_resource_key_ref(i->key);
224 : 0 : dns_cache_remove_by_key(c, key);
225 : : }
226 : : }
227 : 0 : }
228 : :
229 : 0 : static int dns_cache_item_prioq_compare_func(const void *a, const void *b) {
230 : 0 : const DnsCacheItem *x = a, *y = b;
231 : :
232 [ # # ]: 0 : return CMP(x->until, y->until);
233 : : }
234 : :
235 : 0 : static int dns_cache_init(DnsCache *c) {
236 : : int r;
237 : :
238 [ # # ]: 0 : assert(c);
239 : :
240 : 0 : r = prioq_ensure_allocated(&c->by_expiry, dns_cache_item_prioq_compare_func);
241 [ # # ]: 0 : if (r < 0)
242 : 0 : return r;
243 : :
244 : 0 : r = hashmap_ensure_allocated(&c->by_key, &dns_resource_key_hash_ops);
245 [ # # ]: 0 : if (r < 0)
246 : 0 : return r;
247 : :
248 : 0 : return r;
249 : : }
250 : :
251 : 0 : static int dns_cache_link_item(DnsCache *c, DnsCacheItem *i) {
252 : : DnsCacheItem *first;
253 : : int r;
254 : :
255 [ # # ]: 0 : assert(c);
256 [ # # ]: 0 : assert(i);
257 : :
258 : 0 : r = prioq_put(c->by_expiry, i, &i->prioq_idx);
259 [ # # ]: 0 : if (r < 0)
260 : 0 : return r;
261 : :
262 : 0 : first = hashmap_get(c->by_key, i->key);
263 [ # # ]: 0 : if (first) {
264 : 0 : _cleanup_(dns_resource_key_unrefp) DnsResourceKey *k = NULL;
265 : :
266 : : /* Keep a reference to the original key, while we manipulate the list. */
267 : 0 : k = dns_resource_key_ref(first->key);
268 : :
269 : : /* Now, try to reduce the number of keys we keep */
270 : 0 : dns_resource_key_reduce(&first->key, &i->key);
271 : :
272 [ # # ]: 0 : if (first->rr)
273 : 0 : dns_resource_key_reduce(&first->rr->key, &i->key);
274 [ # # ]: 0 : if (i->rr)
275 : 0 : dns_resource_key_reduce(&i->rr->key, &i->key);
276 : :
277 [ # # # # ]: 0 : LIST_PREPEND(by_key, first, i);
278 [ # # ]: 0 : assert_se(hashmap_replace(c->by_key, first->key, first) >= 0);
279 : : } else {
280 : 0 : r = hashmap_put(c->by_key, i->key, i);
281 [ # # ]: 0 : if (r < 0) {
282 : 0 : prioq_remove(c->by_expiry, i, &i->prioq_idx);
283 : 0 : return r;
284 : : }
285 : : }
286 : :
287 : 0 : return 0;
288 : : }
289 : :
290 : 0 : static DnsCacheItem* dns_cache_get(DnsCache *c, DnsResourceRecord *rr) {
291 : : DnsCacheItem *i;
292 : :
293 [ # # ]: 0 : assert(c);
294 [ # # ]: 0 : assert(rr);
295 : :
296 [ # # ]: 0 : LIST_FOREACH(by_key, i, hashmap_get(c->by_key, rr->key))
297 [ # # # # ]: 0 : if (i->rr && dns_resource_record_equal(i->rr, rr) > 0)
298 : 0 : return i;
299 : :
300 : 0 : return NULL;
301 : : }
302 : :
303 : 0 : static usec_t calculate_until(DnsResourceRecord *rr, uint32_t nsec_ttl, usec_t timestamp, bool use_soa_minimum) {
304 : : uint32_t ttl;
305 : : usec_t u;
306 : :
307 [ # # ]: 0 : assert(rr);
308 : :
309 : 0 : ttl = MIN(rr->ttl, nsec_ttl);
310 [ # # # # ]: 0 : if (rr->key->type == DNS_TYPE_SOA && use_soa_minimum) {
311 : : /* If this is a SOA RR, and it is requested, clamp to
312 : : * the SOA's minimum field. This is used when we do
313 : : * negative caching, to determine the TTL for the
314 : : * negative caching entry. See RFC 2308, Section
315 : : * 5. */
316 : :
317 [ # # ]: 0 : if (ttl > rr->soa.minimum)
318 : 0 : ttl = rr->soa.minimum;
319 : : }
320 : :
321 : 0 : u = ttl * USEC_PER_SEC;
322 [ # # ]: 0 : if (u > CACHE_TTL_MAX_USEC)
323 : 0 : u = CACHE_TTL_MAX_USEC;
324 : :
325 [ # # ]: 0 : if (rr->expiry != USEC_INFINITY) {
326 : : usec_t left;
327 : :
328 : : /* Make use of the DNSSEC RRSIG expiry info, if we
329 : : * have it */
330 : :
331 [ # # ]: 0 : left = LESS_BY(rr->expiry, now(CLOCK_REALTIME));
332 [ # # ]: 0 : if (u > left)
333 : 0 : u = left;
334 : : }
335 : :
336 : 0 : return timestamp + u;
337 : : }
338 : :
339 : 0 : static void dns_cache_item_update_positive(
340 : : DnsCache *c,
341 : : DnsCacheItem *i,
342 : : DnsResourceRecord *rr,
343 : : bool authenticated,
344 : : bool shared_owner,
345 : : usec_t timestamp,
346 : : int ifindex,
347 : : int owner_family,
348 : : const union in_addr_union *owner_address) {
349 : :
350 [ # # ]: 0 : assert(c);
351 [ # # ]: 0 : assert(i);
352 [ # # ]: 0 : assert(rr);
353 [ # # ]: 0 : assert(owner_address);
354 : :
355 : 0 : i->type = DNS_CACHE_POSITIVE;
356 : :
357 [ # # ]: 0 : if (!i->by_key_prev)
358 : : /* We are the first item in the list, we need to
359 : : * update the key used in the hashmap */
360 : :
361 [ # # ]: 0 : assert_se(hashmap_replace(c->by_key, rr->key, i) >= 0);
362 : :
363 : 0 : dns_resource_record_ref(rr);
364 : 0 : dns_resource_record_unref(i->rr);
365 : 0 : i->rr = rr;
366 : :
367 : 0 : dns_resource_key_unref(i->key);
368 : 0 : i->key = dns_resource_key_ref(rr->key);
369 : :
370 : 0 : i->until = calculate_until(rr, (uint32_t) -1, timestamp, false);
371 : 0 : i->authenticated = authenticated;
372 : 0 : i->shared_owner = shared_owner;
373 : :
374 : 0 : i->ifindex = ifindex;
375 : :
376 : 0 : i->owner_family = owner_family;
377 : 0 : i->owner_address = *owner_address;
378 : :
379 : 0 : prioq_reshuffle(c->by_expiry, i, &i->prioq_idx);
380 : 0 : }
381 : :
382 : 0 : static int dns_cache_put_positive(
383 : : DnsCache *c,
384 : : DnsResourceRecord *rr,
385 : : bool authenticated,
386 : : bool shared_owner,
387 : : usec_t timestamp,
388 : : int ifindex,
389 : : int owner_family,
390 : : const union in_addr_union *owner_address) {
391 : :
392 : 0 : _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
393 : : DnsCacheItem *existing;
394 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
395 : : int r, k;
396 : :
397 [ # # ]: 0 : assert(c);
398 [ # # ]: 0 : assert(rr);
399 [ # # ]: 0 : assert(owner_address);
400 : :
401 : : /* Never cache pseudo RRs */
402 [ # # ]: 0 : if (dns_class_is_pseudo(rr->key->class))
403 : 0 : return 0;
404 [ # # ]: 0 : if (dns_type_is_pseudo(rr->key->type))
405 : 0 : return 0;
406 : :
407 : : /* New TTL is 0? Delete this specific entry... */
408 [ # # ]: 0 : if (rr->ttl <= 0) {
409 : 0 : k = dns_cache_remove_by_rr(c, rr);
410 [ # # # # ]: 0 : log_debug("%s: %s",
411 : : k > 0 ? "Removed zero TTL entry from cache" : "Not caching zero TTL cache entry",
412 : : dns_resource_key_to_string(rr->key, key_str, sizeof key_str));
413 : 0 : return 0;
414 : : }
415 : :
416 : : /* Entry exists already? Update TTL, timestamp and owner */
417 : 0 : existing = dns_cache_get(c, rr);
418 [ # # ]: 0 : if (existing) {
419 : 0 : dns_cache_item_update_positive(
420 : : c,
421 : : existing,
422 : : rr,
423 : : authenticated,
424 : : shared_owner,
425 : : timestamp,
426 : : ifindex,
427 : : owner_family,
428 : : owner_address);
429 : 0 : return 0;
430 : : }
431 : :
432 : : /* Otherwise, add the new RR */
433 : 0 : r = dns_cache_init(c);
434 [ # # ]: 0 : if (r < 0)
435 : 0 : return r;
436 : :
437 : 0 : dns_cache_make_space(c, 1);
438 : :
439 : 0 : i = new0(DnsCacheItem, 1);
440 [ # # ]: 0 : if (!i)
441 : 0 : return -ENOMEM;
442 : :
443 : 0 : i->type = DNS_CACHE_POSITIVE;
444 : 0 : i->key = dns_resource_key_ref(rr->key);
445 : 0 : i->rr = dns_resource_record_ref(rr);
446 : 0 : i->until = calculate_until(rr, (uint32_t) -1, timestamp, false);
447 : 0 : i->authenticated = authenticated;
448 : 0 : i->shared_owner = shared_owner;
449 : 0 : i->ifindex = ifindex;
450 : 0 : i->owner_family = owner_family;
451 : 0 : i->owner_address = *owner_address;
452 : 0 : i->prioq_idx = PRIOQ_IDX_NULL;
453 : :
454 : 0 : r = dns_cache_link_item(c, i);
455 [ # # ]: 0 : if (r < 0)
456 : 0 : return r;
457 : :
458 [ # # ]: 0 : if (DEBUG_LOGGING) {
459 : 0 : _cleanup_free_ char *t = NULL;
460 : : char ifname[IF_NAMESIZE + 1];
461 : :
462 : 0 : (void) in_addr_to_string(i->owner_family, &i->owner_address, &t);
463 : :
464 [ # # # # : 0 : log_debug("Added positive %s%s cache entry for %s "USEC_FMT"s on %s/%s/%s",
# # # # ]
465 : : i->authenticated ? "authenticated" : "unauthenticated",
466 : : i->shared_owner ? " shared" : "",
467 : : dns_resource_key_to_string(i->key, key_str, sizeof key_str),
468 : : (i->until - timestamp) / USEC_PER_SEC,
469 : : i->ifindex == 0 ? "*" : strna(format_ifname(i->ifindex, ifname)),
470 : : af_to_name_short(i->owner_family),
471 : : strna(t));
472 : : }
473 : :
474 : 0 : i = NULL;
475 : 0 : return 0;
476 : : }
477 : :
478 : 0 : static int dns_cache_put_negative(
479 : : DnsCache *c,
480 : : DnsResourceKey *key,
481 : : int rcode,
482 : : bool authenticated,
483 : : uint32_t nsec_ttl,
484 : : usec_t timestamp,
485 : : DnsResourceRecord *soa,
486 : : int owner_family,
487 : : const union in_addr_union *owner_address) {
488 : :
489 : 0 : _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
490 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
491 : : int r;
492 : :
493 [ # # ]: 0 : assert(c);
494 [ # # ]: 0 : assert(key);
495 [ # # ]: 0 : assert(owner_address);
496 : :
497 : : /* Never cache pseudo RR keys. DNS_TYPE_ANY is particularly
498 : : * important to filter out as we use this as a pseudo-type for
499 : : * NXDOMAIN entries */
500 [ # # ]: 0 : if (dns_class_is_pseudo(key->class))
501 : 0 : return 0;
502 [ # # ]: 0 : if (dns_type_is_pseudo(key->type))
503 : 0 : return 0;
504 : :
505 [ # # # # ]: 0 : if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) {
506 [ # # ]: 0 : if (!soa)
507 : 0 : return 0;
508 : :
509 : : /* For negative replies, check if we have a TTL of a SOA */
510 [ # # # # : 0 : if (nsec_ttl <= 0 || soa->soa.minimum <= 0 || soa->ttl <= 0) {
# # ]
511 [ # # ]: 0 : log_debug("Not caching negative entry with zero SOA/NSEC/NSEC3 TTL: %s",
512 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
513 : 0 : return 0;
514 : : }
515 [ # # ]: 0 : } else if (rcode != DNS_RCODE_SERVFAIL)
516 : 0 : return 0;
517 : :
518 : 0 : r = dns_cache_init(c);
519 [ # # ]: 0 : if (r < 0)
520 : 0 : return r;
521 : :
522 : 0 : dns_cache_make_space(c, 1);
523 : :
524 : 0 : i = new0(DnsCacheItem, 1);
525 [ # # ]: 0 : if (!i)
526 : 0 : return -ENOMEM;
527 : :
528 : 0 : i->type =
529 [ # # # # ]: 0 : rcode == DNS_RCODE_SUCCESS ? DNS_CACHE_NODATA :
530 : : rcode == DNS_RCODE_NXDOMAIN ? DNS_CACHE_NXDOMAIN : DNS_CACHE_RCODE;
531 : 0 : i->until =
532 [ # # ]: 0 : i->type == DNS_CACHE_RCODE ? timestamp + CACHE_TTL_STRANGE_RCODE_USEC :
533 : 0 : calculate_until(soa, nsec_ttl, timestamp, true);
534 : 0 : i->authenticated = authenticated;
535 : 0 : i->owner_family = owner_family;
536 : 0 : i->owner_address = *owner_address;
537 : 0 : i->prioq_idx = PRIOQ_IDX_NULL;
538 : 0 : i->rcode = rcode;
539 : :
540 [ # # ]: 0 : if (i->type == DNS_CACHE_NXDOMAIN) {
541 : : /* NXDOMAIN entries should apply equally to all types, so we use ANY as
542 : : * a pseudo type for this purpose here. */
543 : 0 : i->key = dns_resource_key_new(key->class, DNS_TYPE_ANY, dns_resource_key_name(key));
544 [ # # ]: 0 : if (!i->key)
545 : 0 : return -ENOMEM;
546 : :
547 : : /* Make sure to remove any previous entry for this
548 : : * specific ANY key. (For non-ANY keys the cache data
549 : : * is already cleared by the caller.) Note that we
550 : : * don't bother removing positive or NODATA cache
551 : : * items in this case, because it would either be slow
552 : : * or require explicit indexing by name */
553 : 0 : dns_cache_remove_by_key(c, key);
554 : : } else
555 : 0 : i->key = dns_resource_key_ref(key);
556 : :
557 : 0 : r = dns_cache_link_item(c, i);
558 [ # # ]: 0 : if (r < 0)
559 : 0 : return r;
560 : :
561 [ # # ]: 0 : log_debug("Added %s cache entry for %s "USEC_FMT"s",
562 : : dns_cache_item_type_to_string(i),
563 : : dns_resource_key_to_string(i->key, key_str, sizeof key_str),
564 : : (i->until - timestamp) / USEC_PER_SEC);
565 : :
566 : 0 : i = NULL;
567 : 0 : return 0;
568 : : }
569 : :
570 : 0 : static void dns_cache_remove_previous(
571 : : DnsCache *c,
572 : : DnsResourceKey *key,
573 : : DnsAnswer *answer) {
574 : :
575 : : DnsResourceRecord *rr;
576 : : DnsAnswerFlags flags;
577 : :
578 [ # # ]: 0 : assert(c);
579 : :
580 : : /* First, if we were passed a key (i.e. on LLMNR/DNS, but
581 : : * not on mDNS), delete all matching old RRs, so that we only
582 : : * keep complete by_key in place. */
583 [ # # ]: 0 : if (key)
584 : 0 : dns_cache_remove_by_key(c, key);
585 : :
586 : : /* Second, flush all entries matching the answer, unless this
587 : : * is an RR that is explicitly marked to be "shared" between
588 : : * peers (i.e. mDNS RRs without the flush-cache bit set). */
589 [ # # # # : 0 : DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
# # # # #
# # # # #
# # ]
590 [ # # ]: 0 : if ((flags & DNS_ANSWER_CACHEABLE) == 0)
591 : 0 : continue;
592 : :
593 [ # # ]: 0 : if (flags & DNS_ANSWER_SHARED_OWNER)
594 : 0 : continue;
595 : :
596 : 0 : dns_cache_remove_by_key(c, rr->key);
597 : : }
598 : 0 : }
599 : :
600 : 0 : static bool rr_eligible(DnsResourceRecord *rr) {
601 [ # # ]: 0 : assert(rr);
602 : :
603 : : /* When we see an NSEC/NSEC3 RR, we'll only cache it if it is from the lower zone, not the upper zone, since
604 : : * that's where the interesting bits are (with exception of DS RRs). Of course, this way we cannot derive DS
605 : : * existence from any cached NSEC/NSEC3, but that should be fine. */
606 : :
607 [ # # # ]: 0 : switch (rr->key->type) {
608 : :
609 : 0 : case DNS_TYPE_NSEC:
610 [ # # # # ]: 0 : return !bitmap_isset(rr->nsec.types, DNS_TYPE_NS) ||
611 : 0 : bitmap_isset(rr->nsec.types, DNS_TYPE_SOA);
612 : :
613 : 0 : case DNS_TYPE_NSEC3:
614 [ # # # # ]: 0 : return !bitmap_isset(rr->nsec3.types, DNS_TYPE_NS) ||
615 : 0 : bitmap_isset(rr->nsec3.types, DNS_TYPE_SOA);
616 : :
617 : 0 : default:
618 : 0 : return true;
619 : : }
620 : : }
621 : :
622 : 0 : int dns_cache_put(
623 : : DnsCache *c,
624 : : DnsCacheMode cache_mode,
625 : : DnsResourceKey *key,
626 : : int rcode,
627 : : DnsAnswer *answer,
628 : : bool authenticated,
629 : : uint32_t nsec_ttl,
630 : : usec_t timestamp,
631 : : int owner_family,
632 : : const union in_addr_union *owner_address) {
633 : :
634 : 0 : DnsResourceRecord *soa = NULL, *rr;
635 : 0 : bool weird_rcode = false;
636 : : DnsAnswerFlags flags;
637 : : unsigned cache_keys;
638 : : int r, ifindex;
639 : :
640 [ # # ]: 0 : assert(c);
641 [ # # ]: 0 : assert(owner_address);
642 : :
643 : 0 : dns_cache_remove_previous(c, key, answer);
644 : :
645 : : /* We only care for positive replies and NXDOMAINs, on all other replies we will simply flush the respective
646 : : * entries, and that's it. (Well, with one further exception: since some DNS zones (akamai!) return SERVFAIL
647 : : * consistently for some lookups, and forwarders tend to propagate that we'll cache that too, but only for a
648 : : * short time.) */
649 : :
650 [ # # # # ]: 0 : if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) {
651 [ # # ]: 0 : if (dns_answer_size(answer) <= 0) {
652 [ # # ]: 0 : if (key) {
653 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
654 : :
655 [ # # ]: 0 : log_debug("Not caching negative entry without a SOA record: %s",
656 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
657 : : }
658 : 0 : return 0;
659 : : }
660 : :
661 : : } else {
662 : : /* Only cache SERVFAIL as "weird" rcode for now. We can add more later, should that turn out to be
663 : : * beneficial. */
664 [ # # ]: 0 : if (rcode != DNS_RCODE_SERVFAIL)
665 : 0 : return 0;
666 : :
667 : 0 : weird_rcode = true;
668 : : }
669 : :
670 : 0 : cache_keys = dns_answer_size(answer);
671 [ # # ]: 0 : if (key)
672 : 0 : cache_keys++;
673 : :
674 : : /* Make some space for our new entries */
675 : 0 : dns_cache_make_space(c, cache_keys);
676 : :
677 [ # # ]: 0 : if (timestamp <= 0)
678 : 0 : timestamp = now(clock_boottime_or_monotonic());
679 : :
680 : : /* Second, add in positive entries for all contained RRs */
681 [ # # # # : 0 : DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, answer) {
# # # # #
# # # # #
# # # # #
# # # ]
682 [ # # ]: 0 : if ((flags & DNS_ANSWER_CACHEABLE) == 0 ||
683 [ # # ]: 0 : !rr_eligible(rr))
684 : 0 : continue;
685 : :
686 : 0 : r = dns_cache_put_positive(
687 : : c,
688 : : rr,
689 : 0 : flags & DNS_ANSWER_AUTHENTICATED,
690 : 0 : flags & DNS_ANSWER_SHARED_OWNER,
691 : : timestamp,
692 : : ifindex,
693 : : owner_family, owner_address);
694 [ # # ]: 0 : if (r < 0)
695 : 0 : goto fail;
696 : : }
697 : :
698 [ # # ]: 0 : if (!key) /* mDNS doesn't know negative caching, really */
699 : 0 : return 0;
700 : :
701 : : /* Third, add in negative entries if the key has no RR */
702 : 0 : r = dns_answer_match_key(answer, key, NULL);
703 [ # # ]: 0 : if (r < 0)
704 : 0 : goto fail;
705 [ # # ]: 0 : if (r > 0)
706 : 0 : return 0;
707 : :
708 : : /* But not if it has a matching CNAME/DNAME (the negative
709 : : * caching will be done on the canonical name, not on the
710 : : * alias) */
711 : 0 : r = dns_answer_find_cname_or_dname(answer, key, NULL, NULL);
712 [ # # ]: 0 : if (r < 0)
713 : 0 : goto fail;
714 [ # # ]: 0 : if (r > 0)
715 : 0 : return 0;
716 : :
717 : : /* See https://tools.ietf.org/html/rfc2308, which say that a matching SOA record in the packet is used to
718 : : * enable negative caching. We apply one exception though: if we are about to cache a weird rcode we do so
719 : : * regardless of a SOA. */
720 : 0 : r = dns_answer_find_soa(answer, key, &soa, &flags);
721 [ # # ]: 0 : if (r < 0)
722 : 0 : goto fail;
723 [ # # # # ]: 0 : if (r == 0 && !weird_rcode)
724 : 0 : return 0;
725 [ # # ]: 0 : if (r > 0) {
726 : : /* Refuse using the SOA data if it is unsigned, but the key is
727 : : * signed */
728 [ # # # # ]: 0 : if (authenticated && (flags & DNS_ANSWER_AUTHENTICATED) == 0)
729 : 0 : return 0;
730 : : }
731 : :
732 [ # # ]: 0 : if (cache_mode == DNS_CACHE_MODE_NO_NEGATIVE) {
733 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
734 [ # # ]: 0 : log_debug("Not caching negative entry for: %s, cache mode set to no-negative",
735 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
736 : 0 : return 0;
737 : : }
738 : :
739 : 0 : r = dns_cache_put_negative(
740 : : c,
741 : : key,
742 : : rcode,
743 : : authenticated,
744 : : nsec_ttl,
745 : : timestamp,
746 : : soa,
747 : : owner_family, owner_address);
748 [ # # ]: 0 : if (r < 0)
749 : 0 : goto fail;
750 : :
751 : 0 : return 0;
752 : :
753 : 0 : fail:
754 : : /* Adding all RRs failed. Let's clean up what we already
755 : : * added, just in case */
756 : :
757 [ # # ]: 0 : if (key)
758 : 0 : dns_cache_remove_by_key(c, key);
759 : :
760 [ # # # # : 0 : DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
# # # # #
# # # # #
# # ]
761 [ # # ]: 0 : if ((flags & DNS_ANSWER_CACHEABLE) == 0)
762 : 0 : continue;
763 : :
764 : 0 : dns_cache_remove_by_key(c, rr->key);
765 : : }
766 : :
767 : 0 : return r;
768 : : }
769 : :
770 : 0 : static DnsCacheItem *dns_cache_get_by_key_follow_cname_dname_nsec(DnsCache *c, DnsResourceKey *k) {
771 : : DnsCacheItem *i;
772 : : const char *n;
773 : : int r;
774 : :
775 [ # # ]: 0 : assert(c);
776 [ # # ]: 0 : assert(k);
777 : :
778 : : /* If we hit some OOM error, or suchlike, we don't care too
779 : : * much, after all this is just a cache */
780 : :
781 : 0 : i = hashmap_get(c->by_key, k);
782 [ # # ]: 0 : if (i)
783 : 0 : return i;
784 : :
785 : 0 : n = dns_resource_key_name(k);
786 : :
787 : : /* Check if we have an NXDOMAIN cache item for the name, notice that we use
788 : : * the pseudo-type ANY for NXDOMAIN cache items. */
789 : 0 : i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_ANY, n));
790 [ # # # # ]: 0 : if (i && i->type == DNS_CACHE_NXDOMAIN)
791 : 0 : return i;
792 : :
793 [ # # ]: 0 : if (dns_type_may_redirect(k->type)) {
794 : : /* Check if we have a CNAME record instead */
795 : 0 : i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_CNAME, n));
796 [ # # # # ]: 0 : if (i && i->type != DNS_CACHE_NODATA)
797 : 0 : return i;
798 : :
799 : : /* OK, let's look for cached DNAME records. */
800 : : for (;;) {
801 [ # # ]: 0 : if (isempty(n))
802 : 0 : return NULL;
803 : :
804 : 0 : i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_DNAME, n));
805 [ # # # # ]: 0 : if (i && i->type != DNS_CACHE_NODATA)
806 : 0 : return i;
807 : :
808 : : /* Jump one label ahead */
809 : 0 : r = dns_name_parent(&n);
810 [ # # ]: 0 : if (r <= 0)
811 : 0 : return NULL;
812 : : }
813 : : }
814 : :
815 [ # # ]: 0 : if (k->type != DNS_TYPE_NSEC) {
816 : : /* Check if we have an NSEC record instead for the name. */
817 : 0 : i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_NSEC, n));
818 [ # # ]: 0 : if (i)
819 : 0 : return i;
820 : : }
821 : :
822 : 0 : return NULL;
823 : : }
824 : :
825 : 0 : int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, bool clamp_ttl, int *rcode, DnsAnswer **ret, bool *authenticated) {
826 : 0 : _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
827 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
828 : 0 : unsigned n = 0;
829 : : int r;
830 : 0 : bool nxdomain = false;
831 : 0 : DnsCacheItem *j, *first, *nsec = NULL;
832 : 0 : bool have_authenticated = false, have_non_authenticated = false;
833 : : usec_t current;
834 : 0 : int found_rcode = -1;
835 : :
836 [ # # ]: 0 : assert(c);
837 [ # # ]: 0 : assert(key);
838 [ # # ]: 0 : assert(rcode);
839 [ # # ]: 0 : assert(ret);
840 [ # # ]: 0 : assert(authenticated);
841 : :
842 [ # # # # ]: 0 : if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) {
843 : : /* If we have ANY lookups we don't use the cache, so
844 : : * that the caller refreshes via the network. */
845 : :
846 [ # # ]: 0 : log_debug("Ignoring cache for ANY lookup: %s",
847 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
848 : :
849 : 0 : c->n_miss++;
850 : :
851 : 0 : *ret = NULL;
852 : 0 : *rcode = DNS_RCODE_SUCCESS;
853 : 0 : *authenticated = false;
854 : :
855 : 0 : return 0;
856 : : }
857 : :
858 : 0 : first = dns_cache_get_by_key_follow_cname_dname_nsec(c, key);
859 [ # # ]: 0 : if (!first) {
860 : : /* If one question cannot be answered we need to refresh */
861 : :
862 [ # # ]: 0 : log_debug("Cache miss for %s",
863 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
864 : :
865 : 0 : c->n_miss++;
866 : :
867 : 0 : *ret = NULL;
868 : 0 : *rcode = DNS_RCODE_SUCCESS;
869 : 0 : *authenticated = false;
870 : :
871 : 0 : return 0;
872 : : }
873 : :
874 [ # # ]: 0 : LIST_FOREACH(by_key, j, first) {
875 [ # # ]: 0 : if (j->rr) {
876 [ # # ]: 0 : if (j->rr->key->type == DNS_TYPE_NSEC)
877 : 0 : nsec = j;
878 : :
879 : 0 : n++;
880 [ # # ]: 0 : } else if (j->type == DNS_CACHE_NXDOMAIN)
881 : 0 : nxdomain = true;
882 [ # # ]: 0 : else if (j->type == DNS_CACHE_RCODE)
883 : 0 : found_rcode = j->rcode;
884 : :
885 [ # # ]: 0 : if (j->authenticated)
886 : 0 : have_authenticated = true;
887 : : else
888 : 0 : have_non_authenticated = true;
889 : : }
890 : :
891 [ # # ]: 0 : if (found_rcode >= 0) {
892 [ # # ]: 0 : log_debug("RCODE %s cache hit for %s",
893 : : dns_rcode_to_string(found_rcode),
894 : : dns_resource_key_to_string(key, key_str, sizeof(key_str)));
895 : :
896 : 0 : *ret = NULL;
897 : 0 : *rcode = found_rcode;
898 : 0 : *authenticated = false;
899 : :
900 : 0 : c->n_hit++;
901 : 0 : return 1;
902 : : }
903 : :
904 [ # # # # : 0 : if (nsec && !IN_SET(key->type, DNS_TYPE_NSEC, DNS_TYPE_DS)) {
# # ]
905 : : /* Note that we won't derive information for DS RRs from an NSEC, because we only cache NSEC RRs from
906 : : * the lower-zone of a zone cut, but the DS RRs are on the upper zone. */
907 : :
908 [ # # ]: 0 : log_debug("NSEC NODATA cache hit for %s",
909 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
910 : :
911 : : /* We only found an NSEC record that matches our name.
912 : : * If it says the type doesn't exist report
913 : : * NODATA. Otherwise report a cache miss. */
914 : :
915 : 0 : *ret = NULL;
916 : 0 : *rcode = DNS_RCODE_SUCCESS;
917 : 0 : *authenticated = nsec->authenticated;
918 : :
919 [ # # ]: 0 : if (!bitmap_isset(nsec->rr->nsec.types, key->type) &&
920 [ # # ]: 0 : !bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_CNAME) &&
921 [ # # ]: 0 : !bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_DNAME)) {
922 : 0 : c->n_hit++;
923 : 0 : return 1;
924 : : }
925 : :
926 : 0 : c->n_miss++;
927 : 0 : return 0;
928 : : }
929 : :
930 [ # # # # : 0 : log_debug("%s cache hit for %s",
# # ]
931 : : n > 0 ? "Positive" :
932 : : nxdomain ? "NXDOMAIN" : "NODATA",
933 : : dns_resource_key_to_string(key, key_str, sizeof key_str));
934 : :
935 [ # # ]: 0 : if (n <= 0) {
936 : 0 : c->n_hit++;
937 : :
938 : 0 : *ret = NULL;
939 [ # # ]: 0 : *rcode = nxdomain ? DNS_RCODE_NXDOMAIN : DNS_RCODE_SUCCESS;
940 [ # # # # ]: 0 : *authenticated = have_authenticated && !have_non_authenticated;
941 : 0 : return 1;
942 : : }
943 : :
944 : 0 : answer = dns_answer_new(n);
945 [ # # ]: 0 : if (!answer)
946 : 0 : return -ENOMEM;
947 : :
948 [ # # ]: 0 : if (clamp_ttl)
949 : 0 : current = now(clock_boottime_or_monotonic());
950 : :
951 [ # # ]: 0 : LIST_FOREACH(by_key, j, first) {
952 [ # # # ]: 0 : _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
953 : :
954 [ # # ]: 0 : if (!j->rr)
955 : 0 : continue;
956 : :
957 [ # # ]: 0 : if (clamp_ttl) {
958 : 0 : rr = dns_resource_record_ref(j->rr);
959 : :
960 [ # # ]: 0 : r = dns_resource_record_clamp_ttl(&rr, LESS_BY(j->until, current) / USEC_PER_SEC);
961 [ # # ]: 0 : if (r < 0)
962 : 0 : return r;
963 : : }
964 : :
965 [ # # ]: 0 : r = dns_answer_add(answer, rr ?: j->rr, j->ifindex, j->authenticated ? DNS_ANSWER_AUTHENTICATED : 0);
966 [ # # ]: 0 : if (r < 0)
967 : 0 : return r;
968 : : }
969 : :
970 : 0 : c->n_hit++;
971 : :
972 : 0 : *ret = answer;
973 : 0 : *rcode = DNS_RCODE_SUCCESS;
974 [ # # # # ]: 0 : *authenticated = have_authenticated && !have_non_authenticated;
975 : 0 : answer = NULL;
976 : :
977 : 0 : return n;
978 : : }
979 : :
980 : 0 : int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address) {
981 : : DnsCacheItem *i, *first;
982 : 0 : bool same_owner = true;
983 : :
984 [ # # ]: 0 : assert(cache);
985 [ # # ]: 0 : assert(rr);
986 : :
987 : 0 : dns_cache_prune(cache);
988 : :
989 : : /* See if there's a cache entry for the same key. If there
990 : : * isn't there's no conflict */
991 : 0 : first = hashmap_get(cache->by_key, rr->key);
992 [ # # ]: 0 : if (!first)
993 : 0 : return 0;
994 : :
995 : : /* See if the RR key is owned by the same owner, if so, there
996 : : * isn't a conflict either */
997 [ # # ]: 0 : LIST_FOREACH(by_key, i, first) {
998 [ # # # # ]: 0 : if (i->owner_family != owner_family ||
999 : 0 : !in_addr_equal(owner_family, &i->owner_address, owner_address)) {
1000 : 0 : same_owner = false;
1001 : 0 : break;
1002 : : }
1003 : : }
1004 [ # # ]: 0 : if (same_owner)
1005 : 0 : return 0;
1006 : :
1007 : : /* See if there's the exact same RR in the cache. If yes, then
1008 : : * there's no conflict. */
1009 [ # # ]: 0 : if (dns_cache_get(cache, rr))
1010 : 0 : return 0;
1011 : :
1012 : : /* There's a conflict */
1013 : 0 : return 1;
1014 : : }
1015 : :
1016 : 0 : int dns_cache_export_shared_to_packet(DnsCache *cache, DnsPacket *p) {
1017 : 0 : unsigned ancount = 0;
1018 : : Iterator iterator;
1019 : : DnsCacheItem *i;
1020 : : int r;
1021 : :
1022 [ # # ]: 0 : assert(cache);
1023 [ # # ]: 0 : assert(p);
1024 : :
1025 [ # # ]: 0 : HASHMAP_FOREACH(i, cache->by_key, iterator) {
1026 : : DnsCacheItem *j;
1027 : :
1028 [ # # ]: 0 : LIST_FOREACH(by_key, j, i) {
1029 [ # # ]: 0 : if (!j->rr)
1030 : 0 : continue;
1031 : :
1032 [ # # ]: 0 : if (!j->shared_owner)
1033 : 0 : continue;
1034 : :
1035 : 0 : r = dns_packet_append_rr(p, j->rr, 0, NULL, NULL);
1036 [ # # # # ]: 0 : if (r == -EMSGSIZE && p->protocol == DNS_PROTOCOL_MDNS) {
1037 : : /* For mDNS, if we're unable to stuff all known answers into the given packet,
1038 : : * allocate a new one, push the RR into that one and link it to the current one.
1039 : : */
1040 : :
1041 : 0 : DNS_PACKET_HEADER(p)->ancount = htobe16(ancount);
1042 : 0 : ancount = 0;
1043 : :
1044 : 0 : r = dns_packet_new_query(&p->more, p->protocol, 0, true);
1045 [ # # ]: 0 : if (r < 0)
1046 : 0 : return r;
1047 : :
1048 : : /* continue with new packet */
1049 : 0 : p = p->more;
1050 : 0 : r = dns_packet_append_rr(p, j->rr, 0, NULL, NULL);
1051 : : }
1052 : :
1053 [ # # ]: 0 : if (r < 0)
1054 : 0 : return r;
1055 : :
1056 : 0 : ancount++;
1057 : : }
1058 : : }
1059 : :
1060 : 0 : DNS_PACKET_HEADER(p)->ancount = htobe16(ancount);
1061 : :
1062 : 0 : return 0;
1063 : : }
1064 : :
1065 : 0 : void dns_cache_dump(DnsCache *cache, FILE *f) {
1066 : : Iterator iterator;
1067 : : DnsCacheItem *i;
1068 : :
1069 [ # # ]: 0 : if (!cache)
1070 : 0 : return;
1071 : :
1072 [ # # ]: 0 : if (!f)
1073 : 0 : f = stdout;
1074 : :
1075 [ # # ]: 0 : HASHMAP_FOREACH(i, cache->by_key, iterator) {
1076 : : DnsCacheItem *j;
1077 : :
1078 [ # # ]: 0 : LIST_FOREACH(by_key, j, i) {
1079 : :
1080 : 0 : fputc('\t', f);
1081 : :
1082 [ # # ]: 0 : if (j->rr) {
1083 : : const char *t;
1084 : 0 : t = dns_resource_record_to_string(j->rr);
1085 [ # # ]: 0 : if (!t) {
1086 : 0 : log_oom();
1087 : 0 : continue;
1088 : : }
1089 : :
1090 : 0 : fputs(t, f);
1091 : 0 : fputc('\n', f);
1092 : : } else {
1093 : : char key_str[DNS_RESOURCE_KEY_STRING_MAX];
1094 : :
1095 : 0 : fputs(dns_resource_key_to_string(j->key, key_str, sizeof key_str), f);
1096 : 0 : fputs(" -- ", f);
1097 : 0 : fputs(dns_cache_item_type_to_string(j), f);
1098 : 0 : fputc('\n', f);
1099 : : }
1100 : : }
1101 : : }
1102 : : }
1103 : :
1104 : 0 : bool dns_cache_is_empty(DnsCache *cache) {
1105 [ # # ]: 0 : if (!cache)
1106 : 0 : return true;
1107 : :
1108 : 0 : return hashmap_isempty(cache->by_key);
1109 : : }
1110 : :
1111 : 0 : unsigned dns_cache_size(DnsCache *cache) {
1112 [ # # ]: 0 : if (!cache)
1113 : 0 : return 0;
1114 : :
1115 : 0 : return hashmap_size(cache->by_key);
1116 : : }
|