Coverage Report

Created: 2025-11-24 06:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/bind9/lib/dns/unreachcache.c
Line
Count
Source
1
/*
2
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
3
 *
4
 * SPDX-License-Identifier: MPL-2.0
5
 *
6
 * This Source Code Form is subject to the terms of the Mozilla Public
7
 * License, v. 2.0. If a copy of the MPL was not distributed with this
8
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
9
 *
10
 * See the COPYRIGHT file distributed with this work for additional
11
 * information regarding copyright ownership.
12
 */
13
14
/*! \file */
15
16
#include <inttypes.h>
17
#include <stdbool.h>
18
19
#include <isc/async.h>
20
#include <isc/buffer.h>
21
#include <isc/hash.h>
22
#include <isc/log.h>
23
#include <isc/loop.h>
24
#include <isc/mem.h>
25
#include <isc/mutex.h>
26
#include <isc/rwlock.h>
27
#include <isc/sockaddr.h>
28
#include <isc/spinlock.h>
29
#include <isc/stdtime.h>
30
#include <isc/string.h>
31
#include <isc/thread.h>
32
#include <isc/time.h>
33
#include <isc/urcu.h>
34
#include <isc/util.h>
35
36
#include <dns/fixedname.h>
37
#include <dns/name.h>
38
#include <dns/rdatatype.h>
39
#include <dns/types.h>
40
#include <dns/unreachcache.h>
41
42
typedef struct dns_ucentry dns_ucentry_t;
43
44
typedef struct dns_uckey {
45
  const isc_sockaddr_t *remote;
46
  const isc_sockaddr_t *local;
47
} dns__uckey_t;
48
49
struct dns_unreachcache {
50
  unsigned int magic;
51
  isc_mem_t *mctx;
52
  uint16_t expire_min_s;
53
  uint16_t expire_max_s;
54
  uint16_t backoff_eligible_s;
55
  struct cds_lfht *ht;
56
  struct cds_list_head *lru;
57
  uint32_t nloops;
58
};
59
60
0
#define UNREACHCACHE_MAGIC    ISC_MAGIC('U', 'R', 'C', 'a')
61
#define VALID_UNREACHCACHE(m) ISC_MAGIC_VALID(m, UNREACHCACHE_MAGIC)
62
63
0
#define UNREACHCACHE_INIT_SIZE (1 << 4) /* Must be power of 2 */
64
0
#define UNREACHCACHE_MIN_SIZE  (1 << 5) /* Must be power of 2 */
65
66
struct dns_ucentry {
67
  isc_loop_t *loop;
68
  isc_stdtime_t expire;
69
  unsigned int exp_backoff_n;
70
  uint16_t wait_time;
71
  bool confirmed;
72
73
  struct cds_lfht_node ht_node;
74
  struct rcu_head rcu_head;
75
  struct cds_list_head lru_head;
76
77
  isc_sockaddr_t remote;
78
  isc_sockaddr_t local;
79
};
80
81
static void
82
ucentry_destroy(struct rcu_head *rcu_head);
83
84
static bool
85
ucentry_alive(struct cds_lfht *ht, dns_ucentry_t *unreach, isc_stdtime_t now,
86
        bool alive_or_waiting);
87
88
dns_unreachcache_t *
89
dns_unreachcache_new(isc_mem_t *mctx, const uint16_t expire_min_s,
90
         const uint16_t expire_max_s,
91
0
         const uint16_t backoff_eligible_s) {
92
0
  REQUIRE(expire_min_s > 0);
93
0
  REQUIRE(expire_min_s <= expire_max_s);
94
95
0
  uint32_t nloops = isc_loopmgr_nloops();
96
0
  dns_unreachcache_t *uc = isc_mem_get(mctx, sizeof(*uc));
97
0
  *uc = (dns_unreachcache_t){
98
0
    .magic = UNREACHCACHE_MAGIC,
99
0
    .expire_min_s = expire_min_s,
100
0
    .expire_max_s = expire_max_s,
101
0
    .backoff_eligible_s = backoff_eligible_s,
102
0
    .nloops = nloops,
103
0
  };
104
105
0
  uc->ht = cds_lfht_new(UNREACHCACHE_INIT_SIZE, UNREACHCACHE_MIN_SIZE, 0,
106
0
            CDS_LFHT_AUTO_RESIZE | CDS_LFHT_ACCOUNTING, NULL);
107
0
  INSIST(uc->ht != NULL);
108
109
0
  uc->lru = isc_mem_cget(mctx, uc->nloops, sizeof(uc->lru[0]));
110
0
  for (size_t i = 0; i < uc->nloops; i++) {
111
0
    CDS_INIT_LIST_HEAD(&uc->lru[i]);
112
0
  }
113
114
0
  isc_mem_attach(mctx, &uc->mctx);
115
116
0
  return uc;
117
0
}
118
119
void
120
0
dns_unreachcache_destroy(dns_unreachcache_t **ucp) {
121
0
  REQUIRE(ucp != NULL && *ucp != NULL);
122
0
  REQUIRE(VALID_UNREACHCACHE(*ucp));
123
0
  dns_unreachcache_t *uc = *ucp;
124
0
  *ucp = NULL;
125
0
  uc->magic = 0;
126
127
0
  dns_ucentry_t *unreach = NULL;
128
0
  struct cds_lfht_iter iter;
129
0
  cds_lfht_for_each_entry(uc->ht, &iter, unreach, ht_node) {
130
0
    INSIST(!cds_lfht_del(uc->ht, &unreach->ht_node));
131
0
    ucentry_destroy(&unreach->rcu_head);
132
0
  }
133
0
  RUNTIME_CHECK(!cds_lfht_destroy(uc->ht, NULL));
134
135
0
  isc_mem_cput(uc->mctx, uc->lru, uc->nloops, sizeof(uc->lru[0]));
136
137
0
  isc_mem_putanddetach(&uc->mctx, uc, sizeof(dns_unreachcache_t));
138
0
}
139
140
static int
141
0
ucentry_match(struct cds_lfht_node *ht_node, const void *key0) {
142
0
  const dns__uckey_t *key = key0;
143
0
  dns_ucentry_t *unreach = caa_container_of(ht_node, dns_ucentry_t,
144
0
              ht_node);
145
146
0
  return isc_sockaddr_equal(&unreach->remote, key->remote) &&
147
0
         isc_sockaddr_equal(&unreach->local, key->local);
148
0
}
149
150
static uint32_t
151
0
ucentry_hash(const dns__uckey_t *key) {
152
0
  return isc_sockaddr_hash(key->remote, false) ^
153
0
         isc_sockaddr_hash(key->local, false);
154
0
}
155
156
static dns_ucentry_t *
157
0
ucentry_lookup(struct cds_lfht *ht, uint32_t hashval, dns__uckey_t *key) {
158
0
  struct cds_lfht_iter iter;
159
160
0
  cds_lfht_lookup(ht, hashval, ucentry_match, key, &iter);
161
162
0
  return cds_lfht_entry(cds_lfht_iter_get_node(&iter), dns_ucentry_t,
163
0
            ht_node);
164
0
}
165
166
static dns_ucentry_t *
167
ucentry_new(isc_loop_t *loop, const isc_sockaddr_t *remote,
168
      const isc_sockaddr_t *local, const isc_stdtime_t expire,
169
0
      const isc_stdtime_t wait_time) {
170
0
  isc_mem_t *mctx = isc_loop_getmctx(loop);
171
0
  dns_ucentry_t *unreach = isc_mem_get(mctx, sizeof(*unreach));
172
0
  *unreach = (dns_ucentry_t){
173
0
    .remote = *remote,
174
0
    .local = *local,
175
0
    .expire = expire,
176
0
    .wait_time = wait_time,
177
0
    .loop = isc_loop_ref(loop),
178
0
    .lru_head = CDS_LIST_HEAD_INIT(unreach->lru_head),
179
0
  };
180
181
0
  return unreach;
182
0
}
183
184
static void
185
0
ucentry_destroy(struct rcu_head *rcu_head) {
186
0
  dns_ucentry_t *unreach = caa_container_of(rcu_head, dns_ucentry_t,
187
0
              rcu_head);
188
0
  isc_loop_t *loop = unreach->loop;
189
0
  isc_mem_t *mctx = isc_loop_getmctx(loop);
190
191
0
  isc_mem_put(mctx, unreach, sizeof(*unreach));
192
0
  isc_loop_unref(loop);
193
0
}
194
195
static void
196
0
ucentry_evict_async(void *arg) {
197
0
  dns_ucentry_t *unreach = arg;
198
199
0
  RUNTIME_CHECK(unreach->loop == isc_loop());
200
201
0
  cds_list_del(&unreach->lru_head);
202
0
  call_rcu(&unreach->rcu_head, ucentry_destroy);
203
0
}
204
205
static void
206
0
ucentry_evict(struct cds_lfht *ht, dns_ucentry_t *unreach) {
207
0
  if (!cds_lfht_del(ht, &unreach->ht_node)) {
208
0
    if (unreach->loop == isc_loop()) {
209
0
      ucentry_evict_async(unreach);
210
0
      return;
211
0
    }
212
0
    isc_async_run(unreach->loop, ucentry_evict_async, unreach);
213
0
  }
214
0
}
215
216
static bool
217
ucentry_alive(struct cds_lfht *ht, dns_ucentry_t *unreach, isc_stdtime_t now,
218
0
        bool alive_or_waiting) {
219
0
  if (cds_lfht_is_node_deleted(&unreach->ht_node)) {
220
0
    return false;
221
0
  } else if (unreach->expire < now) {
222
0
    bool is_waiting = unreach->expire + unreach->wait_time >= now;
223
224
0
    if (is_waiting) {
225
      /*
226
       * Wait some minimum time before evicting an expired
227
       * entry so we can support exponential backoff for
228
       * nodes which enter again shortly after expiring.
229
       *
230
       * The return value depends on whether the caller is
231
       * interested to know if the node is in either active or
232
       * waiting state (i.e. not eviceted), or is interested
233
       * only if it's still alive (i.e. not expired).
234
       */
235
0
      return alive_or_waiting;
236
0
    }
237
238
    /* The entry is already expired, evict it before returning. */
239
0
    ucentry_evict(ht, unreach);
240
0
    return false;
241
0
  }
242
243
0
  return true;
244
0
}
245
246
static void
247
ucentry_purge(struct cds_lfht *ht, struct cds_list_head *lru,
248
0
        isc_stdtime_t now) {
249
0
  size_t count = 10;
250
0
  dns_ucentry_t *unreach;
251
0
  cds_list_for_each_entry_rcu(unreach, lru, lru_head) {
252
0
    if (ucentry_alive(ht, unreach, now, true)) {
253
0
      break;
254
0
    }
255
0
    if (--count == 0) {
256
0
      break;
257
0
    }
258
0
  }
259
0
}
260
261
static void
262
ucentry_backoff(const dns_unreachcache_t *uc, const isc_stdtime_t now,
263
0
    dns_ucentry_t *new, const dns_ucentry_t *old) {
264
  /*
265
   * Perform exponential backoff if this is an expired entry wating to be
266
   * evicted. Otherwise it's a duplicate entry and no backoff is required
267
   * as we will just update the cache with a new entry that has the same
268
   * expiration time as the old one, but calculated freshly, based on the
269
   * current time.
270
   */
271
0
  if (old->expire < now) {
272
0
    new->exp_backoff_n = old->exp_backoff_n + 1;
273
0
  } else {
274
0
    new->exp_backoff_n = old->exp_backoff_n;
275
0
  }
276
0
  for (size_t i = 0; i < new->exp_backoff_n; i++) {
277
0
    new->expire += uc->expire_min_s;
278
0
    if (new->expire > now + uc->expire_max_s) {
279
0
      new->expire = now + uc->expire_max_s;
280
0
      break;
281
0
    }
282
0
  }
283
0
}
284
285
void
286
dns_unreachcache_add(dns_unreachcache_t *uc, const isc_sockaddr_t *remote,
287
0
         const isc_sockaddr_t *local) {
288
0
  REQUIRE(VALID_UNREACHCACHE(uc));
289
0
  REQUIRE(remote != NULL);
290
0
  REQUIRE(local != NULL);
291
292
0
  isc_loop_t *loop = isc_loop();
293
0
  isc_tid_t tid = isc_tid();
294
0
  struct cds_list_head *lru = &uc->lru[tid];
295
0
  isc_stdtime_t now = isc_stdtime_now();
296
0
  isc_stdtime_t expire = now + uc->expire_min_s;
297
0
  bool exp_backoff_activated = false;
298
299
0
  rcu_read_lock();
300
0
  struct cds_lfht *ht = rcu_dereference(uc->ht);
301
0
  INSIST(ht != NULL);
302
303
0
  dns__uckey_t key = {
304
0
    .remote = remote,
305
0
    .local = local,
306
0
  };
307
0
  uint32_t hashval = ucentry_hash(&key);
308
309
0
  dns_ucentry_t *unreach = ucentry_new(loop, remote, local, expire,
310
0
               uc->backoff_eligible_s);
311
0
  struct cds_lfht_node *ht_node;
312
0
  do {
313
0
    ht_node = cds_lfht_add_unique(ht, hashval, ucentry_match, &key,
314
0
                &unreach->ht_node);
315
0
    if (ht_node != &unreach->ht_node) {
316
      /* The entry already exists, get it. */
317
0
      dns_ucentry_t *found = caa_container_of(
318
0
        ht_node, dns_ucentry_t, ht_node);
319
320
      /*
321
       * Consider unreachability as confirmed only if
322
       * an entry is submitted at least twice, i.e. there
323
       * was an older entry (which is exactly this case).
324
       */
325
0
      unreach->confirmed = true;
326
327
      /*
328
       * Recalculate the expire time of the new entry based
329
       * on the old entry's exponential backoff value.
330
       */
331
0
      if (!exp_backoff_activated) {
332
0
        exp_backoff_activated = true;
333
0
        ucentry_backoff(uc, now, unreach, found);
334
0
      }
335
336
      /*
337
       * Evict the old entry, so we can try to insert the new
338
       * one again.
339
       */
340
0
      ucentry_evict(ht, found);
341
0
    }
342
0
  } while (ht_node != &unreach->ht_node);
343
344
  /* No locking, instead we are using per-thread lists */
345
0
  cds_list_add_tail_rcu(&unreach->lru_head, lru);
346
347
0
  ucentry_purge(ht, lru, now);
348
349
0
  rcu_read_unlock();
350
0
}
351
352
isc_result_t
353
dns_unreachcache_find(dns_unreachcache_t *uc, const isc_sockaddr_t *remote,
354
0
          const isc_sockaddr_t *local) {
355
0
  REQUIRE(VALID_UNREACHCACHE(uc));
356
0
  REQUIRE(remote != NULL);
357
0
  REQUIRE(local != NULL);
358
359
0
  isc_result_t result = ISC_R_NOTFOUND;
360
0
  isc_stdtime_t now = isc_stdtime_now();
361
362
0
  rcu_read_lock();
363
0
  struct cds_lfht *ht = rcu_dereference(uc->ht);
364
0
  INSIST(ht != NULL);
365
366
0
  dns__uckey_t key = {
367
0
    .remote = remote,
368
0
    .local = local,
369
0
  };
370
0
  uint32_t hashval = ucentry_hash(&key);
371
372
0
  dns_ucentry_t *found = ucentry_lookup(ht, hashval, &key);
373
0
  if (found != NULL && found->confirmed &&
374
0
      ucentry_alive(ht, found, now, false))
375
0
  {
376
0
    result = ISC_R_SUCCESS;
377
0
  }
378
379
0
  isc_tid_t tid = isc_tid();
380
0
  struct cds_list_head *lru = &uc->lru[tid];
381
0
  ucentry_purge(ht, lru, now);
382
383
0
  rcu_read_unlock();
384
385
0
  return result;
386
0
}
387
388
void
389
dns_unreachcache_remove(dns_unreachcache_t *uc, const isc_sockaddr_t *remote,
390
0
      const isc_sockaddr_t *local) {
391
0
  REQUIRE(VALID_UNREACHCACHE(uc));
392
0
  REQUIRE(remote != NULL);
393
0
  REQUIRE(local != NULL);
394
395
0
  isc_stdtime_t now = isc_stdtime_now();
396
397
0
  rcu_read_lock();
398
0
  struct cds_lfht *ht = rcu_dereference(uc->ht);
399
0
  INSIST(ht != NULL);
400
401
0
  dns__uckey_t key = {
402
0
    .remote = remote,
403
0
    .local = local,
404
0
  };
405
0
  uint32_t hashval = ucentry_hash(&key);
406
407
0
  dns_ucentry_t *found = ucentry_lookup(ht, hashval, &key);
408
0
  if (found != NULL) {
409
0
    ucentry_evict(ht, found);
410
0
  }
411
412
0
  isc_tid_t tid = isc_tid();
413
0
  struct cds_list_head *lru = &uc->lru[tid];
414
0
  ucentry_purge(ht, lru, now);
415
416
0
  rcu_read_unlock();
417
0
}
418
419
void
420
0
dns_unreachcache_flush(dns_unreachcache_t *uc) {
421
0
  REQUIRE(VALID_UNREACHCACHE(uc));
422
423
0
  rcu_read_lock();
424
0
  struct cds_lfht *ht = rcu_dereference(uc->ht);
425
0
  INSIST(ht != NULL);
426
427
  /* Flush the hash table */
428
0
  dns_ucentry_t *unreach;
429
0
  struct cds_lfht_iter iter;
430
0
  cds_lfht_for_each_entry(ht, &iter, unreach, ht_node) {
431
0
    ucentry_evict(ht, unreach);
432
0
  }
433
434
  rcu_read_unlock();
435
0
}