Coverage Report

Created: 2026-06-15 06:55

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