From: Colin Vidal Date: Tue, 20 Jan 2026 13:58:53 +0000 (+0100) Subject: Introduce a delegation database X-Git-Tag: v9.21.21~4^2~31 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1b5f7570844e6b2684710e657b7f4f08a491b27b;p=thirdparty%2Fbind9.git Introduce a delegation database Add `dns_delegdb_t`, a qpmulti-based database enabling to lookup a delegation set (`dns_delegset_t`) from a zonecut name (`dns_name_t`). A delegation set object essentially contains an expiration time and a list of delegation (`dns_deleg_t`). Finally, a delegation can be either: - A list of IP addresses (`isc_netaddrlist_t`), for NS-based delegation providing glues or DELEG-based delegation using `server-ipv4=` or `server-ipv6=`; - Or a list of nameserver names, for NS-based delegation without glues, or DELEG-based delegation using `server-name=`; - Or a list of nameserver names, for DELEG-based delegation using `include-delegparam=`. The delegation database API provides lookup by closest zonecut, delegation and delegation set builders as well as insertion of those newly built delegation set, dumping to a `FILE *`, conversion from an NS rdataset to a delegation set, deletion of a specific zonecut or all the sub-tree of a given zonecut. A memory context is internally used inside the delegation database and can be constraint to a maximum size. Once it gets close to its maximum size and a new delegation set is inserted into the database, a reclamation flow is run internally removing the least recently used entries. The delegation set and delegation objects are, once they been inserted into the database, read-only object. Thus, the caller can use them without concurrency or locking concerns, and must detached them once its done with it. --- diff --git a/lib/dns/deleg.c b/lib/dns/deleg.c new file mode 100644 index 00000000000..dc7f5a42dac --- /dev/null +++ b/lib/dns/deleg.c @@ -0,0 +1,988 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define DELEGDB_NODE_MAGIC ISC_MAGIC('D', 'e', 'G', 'N') +#define VALID_DELEGDB_NODE(node) ISC_MAGIC_VALID(node, DELEGDB_NODE_MAGIC) + +#define DELEGDB_MAGIC ISC_MAGIC('D', 'e', 'G', 'D') +#define VALID_DELEGDB(db) ISC_MAGIC_VALID(db, DELEGDB_MAGIC) + +#define DELEGDB_MINSIZE (1024 * 1024) /* 1MiB */ + +typedef struct delegdb_node delegdb_node_t; + +struct dns_delegdb { + unsigned int magic; + + /* + * The DB uses its own memory context in order to easily enforce + * overmem policies based on allocations made from this memory context. + */ + isc_mem_t *mctx; + isc_refcount_t references; + + size_t nloops; + ISC_SIEVE(delegdb_node_t) * lru; + + dns_qpmulti_t *nodes; + + /* + * Keep track of now many owners are actually using the delegdb. For + * instance: + * + * - During a server reload, the new view will (by default) + * start owning the existing delegdb from the previous instance of the + * same view using `dns_delegdb_reuse()`. This will increase `owners` + * by one. + * + * - Later on, either the old instance of the view (or the new one, + * in case of reload failure) will call `dns_delegdb_shutdown()` on + * the delegdb. This will decrement `owners` by one. + * + * If `owners` is bigger than 1 when `dns_delegdb_shutdown()` is called, + * it means the delegdb must not be shutdown because there are other + * owners using it, so `dns_delegdb_shutdown()` bails off in this case. + * (After decrementing `owners`.) + */ + isc_refcount_t owners; + + size_t lowater; + size_t hiwater; +}; + +static void +delegdb_destroy(dns_delegdb_t *delegdb) { + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(delegdb->nodes == NULL); + + delegdb->magic = 0; + isc_mem_cput(delegdb->mctx, delegdb->lru, delegdb->nloops, + sizeof(delegdb->lru[0])); + + isc_mem_putanddetach(&delegdb->mctx, delegdb, sizeof(*delegdb)); +} + +ISC_REFCOUNT_IMPL(dns_delegdb, delegdb_destroy); + +struct delegdb_node { + unsigned int magic; + dns_delegdb_t *delegdb; + isc_refcount_t references; + + /* LRU */ + isc_loop_t *loop; + ISC_LINK(delegdb_node_t) link; + bool visited; + + /* + * Used to build a list of nodes to be deleted (when running the + * delete tree flow). + */ + ISC_LINK(delegdb_node_t) deadlink; + + /* + * Immutable node data + */ + size_t size; + dns_name_t zonecut; + dns_delegset_t *delegset; +}; + +/* + * All node cleanup is done on the node's owning loop so that the node + * remains fully valid (name, delegset, SIEVE link) until it is actually + * destroyed. This is important because after a node is removed from the + * QP trie, it may still be linked in the owning loop's SIEVE list; if + * another thread's eviction could encounter a half-destroyed node, we + * would get a use-after-free. By deferring everything to the owning + * loop, the node is intact until the SIEVE unlink happens. + */ +static void +delegdb_node_destroy_async(void *arg) { + delegdb_node_t *node = arg; + isc_mem_t *mctx = NULL; + + REQUIRE(VALID_DELEGDB_NODE(node)); + REQUIRE(DNS_DELEGSET_VALID(node->delegset)); + + node->magic = 0; + + isc_mem_attach(node->delegdb->mctx, &mctx); + + if (ISC_SIEVE_LINKED(node, link)) { + ISC_SIEVE_UNLINK(node->delegdb->lru[isc_tid()], node, link); + } + + dns_name_free(&node->zonecut, mctx); + dns_delegset_detach(&node->delegset); + + dns_delegdb_detach(&node->delegdb); + isc_loop_unref(node->loop); + isc_mem_putanddetach(&mctx, node, sizeof(*node)); +} + +static void +delegdb_node_destroy(delegdb_node_t *node) { + REQUIRE(VALID_DELEGDB_NODE(node)); + + if (node->loop == isc_loop()) { + delegdb_node_destroy_async(node); + } else { + isc_async_run(node->loop, delegdb_node_destroy_async, node); + } +} + +#ifdef DNS_DELEGDB_NODETRACE +#define delegdb_node_ref(ptr) \ + delegdb_node__ref(ptr, __func__, __FILE__, __LINE__) +#define delegdb_node_unref(ptr) \ + delegdb_node__unref(ptr, __func__, __FILE__, __LINE__) +ISC_REFCOUNT_STATIC_TRACE_DECL(delegdb_node); +ISC_REFCOUNT_STATIC_TRACE_IMPL(delegdb_node, delegdb_node_destroy); +#else +ISC_REFCOUNT_STATIC_DECL(delegdb_node); +ISC_REFCOUNT_STATIC_IMPL(delegdb_node, delegdb_node_destroy); +#endif + +static void +dbnode_attach(ISC_ATTR_UNUSED void *uctx, void *pval, + ISC_ATTR_UNUSED uint32_t ival) { + delegdb_node_t *node = pval; + + REQUIRE(VALID_DELEGDB_NODE(node)); + delegdb_node_ref(node); +} + +static void +dbnode_detach(ISC_ATTR_UNUSED void *uctx, void *pval, + ISC_ATTR_UNUSED uint32_t ival) { + delegdb_node_t *node = pval; + + REQUIRE(VALID_DELEGDB_NODE(node)); + delegdb_node_unref(node); +} + +static size_t +makekey(dns_qpkey_t key, void *uctx ISC_ATTR_UNUSED, void *pval, + uint32_t ival ISC_ATTR_UNUSED) { + delegdb_node_t *data = pval; + return dns_qpkey_fromname(key, &data->zonecut, DNS_DBNAMESPACE_NORMAL); +} + +static void +triename(ISC_ATTR_UNUSED void *uctx, char *buf, size_t size) { + (void)strncpy(buf, "delegdb", size); +} + +static dns_qpmethods_t qpmethods = { .attach = dbnode_attach, + .detach = dbnode_detach, + .makekey = makekey, + .triename = triename }; + +void +dns_delegdb_create(dns_delegdb_t **delegdbp) { + isc_mem_t *mctx = NULL; + dns_delegdb_t *delegdb = NULL; + + REQUIRE(isc_loop_get(isc_tid()) == isc_loop_main()); + REQUIRE(delegdbp != NULL && *delegdbp == NULL); + + isc_mem_create("dns_delegdb", &mctx); + isc_mem_setdestroycheck(mctx, true); + + delegdb = isc_mem_get(mctx, sizeof(*delegdb)); + *delegdb = (dns_delegdb_t){ .magic = DELEGDB_MAGIC, + .mctx = mctx, + .references = ISC_REFCOUNT_INITIALIZER(1), + .nloops = isc_loopmgr_nloops(), + .owners = ISC_REFCOUNT_INITIALIZER(1) }; + + dns_qpmulti_create(mctx, &qpmethods, &delegdb->nodes, &delegdb->nodes); + + delegdb->lru = isc_mem_cget(mctx, delegdb->nloops, + sizeof(delegdb->lru[0])); + for (size_t i = 0; i < delegdb->nloops; i++) { + ISC_SIEVE_INIT(delegdb->lru[i]); + } + + *delegdbp = delegdb; +} + +void +dns_delegdb_reuse(dns_view_t *oldview, dns_view_t *newview) { + REQUIRE(isc_loop_get(isc_tid()) == isc_loop_main()); + REQUIRE(DNS_VIEW_VALID(oldview)); + REQUIRE(DNS_VIEW_VALID(newview)); + + dns_delegdb_attach(oldview->deleg, &newview->deleg); + isc_refcount_increment(&oldview->deleg->owners); +} + +typedef struct nodes_rcu_head { + isc_mem_t *mctx; + dns_qpmulti_t *nodes; + struct rcu_head rcu_head; +} nodes_rcu_head_t; + +static void +deleg_destroy_qpmulti(struct rcu_head *rcu_head) { + nodes_rcu_head_t *nrh = caa_container_of(rcu_head, nodes_rcu_head_t, + rcu_head); + + dns_qpmulti_destroy(&nrh->nodes); + + isc_mem_putanddetach(&nrh->mctx, nrh, sizeof(*nrh)); +} + +inline static bool +isactive(delegdb_node_t *node, dns_ttl_t now) { + return node->delegset->expires > now; +} + +static void +getparentnode(dns_qpchain_t *chain, delegdb_node_t **node, dns_ttl_t now) { + size_t len = dns_qpchain_length(chain); + + while (len >= 2) { + *node = NULL; + dns_qpchain_node(chain, len - 2, (void **)node, NULL); + + if (isactive(*node, now)) { + break; + } + len--; + } +} + +/* + * NOTE: Caller needs to hold a RCU read critical section. + */ +static isc_result_t +dns__deleg_lookup(dns_delegdb_t *delegdb, dns_qpread_t *qpr, + const dns_name_t *name, isc_stdtime_t optnow, + unsigned int options, dns_name_t *zonecut, + dns_name_t *deepestzonecut, dns_delegset_t **delegsetp) { + isc_result_t result = ISC_R_SUCCESS; + delegdb_node_t *node = NULL; + isc_stdtime_t now = optnow > 0 ? optnow : isc_stdtime_now(); + + dns_qpchain_t chain = {}; + bool noexact = (options & DNS_DBFIND_NOEXACT) != 0; + + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(DNS_NAME_VALID(name)); + REQUIRE(dns_name_hasbuffer(zonecut)); + REQUIRE(deepestzonecut == NULL || dns_name_hasbuffer(deepestzonecut)); + + result = dns_qp_lookup(qpr, name, DNS_DBNAMESPACE_NORMAL, NULL, &chain, + (void **)&node, NULL); + + if (result != ISC_R_SUCCESS && result != DNS_R_PARTIALMATCH) { + return ISC_R_NOTFOUND; + } + INSIST(VALID_DELEGDB_NODE(node)); + + if (deepestzonecut != NULL) { + dns_name_copy(&node->zonecut, deepestzonecut); + } + + if (result == ISC_R_SUCCESS && (noexact || !isactive(node, now))) { + getparentnode(&chain, &node, now); + } else if (result == DNS_R_PARTIALMATCH && !isactive(node, now)) { + getparentnode(&chain, &node, now); + } + + result = isactive(node, now) ? ISC_R_SUCCESS : ISC_R_NOTFOUND; + if (result == ISC_R_SUCCESS) { + dns_name_copy(&node->zonecut, zonecut); + INSIST(node->delegset); + dns_delegset_attach(node->delegset, delegsetp); + ISC_SIEVE_MARK(node, visited); + } else { + /* + * FIXME: if we lookup something that has expired, we need + * either the "deadnodes" (see qpcache) mechanism here - or call + * something like isc_async_run(delete_me, node). + */ + } + + return result; +} + +isc_result_t +dns_delegdb_lookup(dns_delegdb_t *delegdb, const dns_name_t *name, + isc_stdtime_t now, unsigned int options, dns_name_t *zonecut, + dns_name_t *deepestzonecut, dns_delegset_t **delegsetp) { + isc_result_t result = ISC_R_SHUTTINGDOWN; + dns_qpmulti_t *nodes = NULL; + dns_qpread_t qpr = {}; + + rcu_read_lock(); + nodes = rcu_dereference(delegdb->nodes); + if (nodes != NULL) { + dns_qpmulti_query(nodes, &qpr); + + result = dns__deleg_lookup(delegdb, &qpr, name, now, options, + zonecut, deepestzonecut, delegsetp); + dns_qpread_destroy(nodes, &qpr); + } + rcu_read_unlock(); + + return result; +} + +void +dns_delegset_allocset(dns_delegdb_t *delegdb, dns_delegset_t **delegsetp) { + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(delegsetp != NULL && *delegsetp == NULL); + + dns_delegset_t *delegset = isc_mem_get(delegdb->mctx, + sizeof(*delegset)); + *delegset = (dns_delegset_t){ + .magic = DNS_DELEGSET_MAGIC, + .references = ISC_REFCOUNT_INITIALIZER(1), + .delegs = ISC_LIST_INITIALIZER, + }; + isc_mem_attach(delegdb->mctx, &delegset->mctx); + + *delegsetp = delegset; +} + +void +dns_delegset_allocdeleg(dns_delegset_t *delegset, dns_deleg_type_t type, + dns_deleg_t **delegp) { + dns_deleg_t *deleg = NULL; + + REQUIRE(DNS_DELEGSET_VALID(delegset)); + REQUIRE(delegp != NULL && *delegp == NULL); + REQUIRE(type != DNS_DELEGTYPE_UNDEFINED); + + deleg = isc_mem_get(delegset->mctx, sizeof(*deleg)); + *deleg = (dns_deleg_t){ .addresses = ISC_LIST_INITIALIZER, + .names = ISC_LIST_INITIALIZER, + .type = type, + .link = ISC_LINK_INITIALIZER }; + + ISC_LIST_APPEND(delegset->delegs, deleg, link); + *delegp = deleg; +} + +void +dns_delegset_addaddr(dns_delegset_t *delegset, dns_deleg_t *deleg, + const isc_netaddr_t *addr) { + isc_netaddrlink_t *addrlink = NULL; + + REQUIRE(DNS_DELEGSET_VALID(delegset)); + REQUIRE(deleg != NULL); + REQUIRE(addr != NULL); + REQUIRE(deleg->type == DNS_DELEGTYPE_DELEG_ADDRESSES || + deleg->type == DNS_DELEGTYPE_NS_GLUES); + + addrlink = isc_mem_get(delegset->mctx, sizeof(*addrlink)); + *addrlink = (isc_netaddrlink_t){ .addr = *addr, + .link = ISC_LINK_INITIALIZER }; + + ISC_LIST_APPEND(deleg->addresses, addrlink, link); +} + +static void +addname(dns_delegset_t *delegset, dns_namelist_t *list, + const dns_name_t *name) { + dns_name_t *clone = NULL; + + REQUIRE(DNS_DELEGSET_VALID(delegset)); + REQUIRE(DNS_NAME_VALID(name)); + + clone = isc_mem_get(delegset->mctx, sizeof(*clone)); + dns_name_init(clone); + dns_name_dup(name, delegset->mctx, clone); + ISC_LIST_APPEND(*list, clone, link); +} + +void +dns_delegset_adddelegparam(dns_delegset_t *delegset, dns_deleg_t *deleg, + const dns_name_t *name) { + REQUIRE(deleg != NULL); + REQUIRE(deleg->type == DNS_DELEGTYPE_DELEG_PARAMS); + addname(delegset, &deleg->names, name); +} + +void +dns_delegset_addns(dns_delegset_t *delegset, dns_deleg_t *deleg, + const dns_name_t *name) { + REQUIRE(deleg != NULL); + + REQUIRE(deleg->type == DNS_DELEGTYPE_DELEG_NAMES || + deleg->type == DNS_DELEGTYPE_NS_NAMES); + addname(delegset, &deleg->names, name); +} + +static void +delegdb_cleanup(dns_delegdb_t *delegdb, dns_qpmulti_t *nodes) { + dns_qp_t *qp = NULL; + delegdb_node_t *node = NULL; + size_t reclaimed = 0; + size_t requested = 0; + + if (!isc_mem_isovermem(delegdb->mctx)) { + return; + } + requested = delegdb->hiwater - delegdb->lowater; + + dns_qpmulti_write(nodes, &qp); + + while (reclaimed < requested) { + node = ISC_SIEVE_NEXT(delegdb->lru[isc_tid()], visited, link); + + if (node == NULL) { + break; + } + reclaimed += node->size; + + ISC_SIEVE_UNLINK(delegdb->lru[isc_tid()], node, link); + (void)dns_qp_deletename(qp, &node->zonecut, + DNS_DBNAMESPACE_NORMAL, NULL, NULL); + } + + dns_qp_compact(qp, DNS_QPGC_ALL); + dns_qpmulti_commit(nodes, &qp); +} + +static size_t +delegset_size(dns_delegset_t *delegset) { + size_t sz = 0; + + sz += sizeof(*delegset); + ISC_LIST_FOREACH(delegset->delegs, deleg, link) { + sz += sizeof(*deleg); + ISC_LIST_FOREACH(deleg->addresses, address, link) { + sz += sizeof(*address); + } + ISC_LIST_FOREACH(deleg->names, name, link) { + sz += sizeof(*name) + dns_name_size(name); + } + } + + return sz; +} + +static size_t +delegdb_node_size(const dns_name_t *zonecut, dns_delegset_t *delegset) { + size_t sz = 0; + + sz += sizeof(delegdb_node_t); + sz += dns_name_size(zonecut); + sz += delegset_size(delegset); + + return sz; +} + +static void +delegdb_node_prepare(dns_delegdb_t *delegdb, dns_qpmulti_t *nodes, + isc_stdtime_t now, dns_ttl_t ttl, + const dns_name_t *zonecut, dns_delegset_t *delegset, + delegdb_node_t **nodep) { + delegdb_cleanup(delegdb, nodes); + + if (ttl == 0) { + ttl = 1; + } + delegset->expires = ttl + now; + + *nodep = isc_mem_get(delegdb->mctx, sizeof(**nodep)); + **nodep = + (delegdb_node_t){ .magic = DELEGDB_NODE_MAGIC, + .references = ISC_REFCOUNT_INITIALIZER(1), + .zonecut = DNS_NAME_INITEMPTY, + .link = ISC_LINK_INITIALIZER, + .deadlink = ISC_LINK_INITIALIZER, + .size = delegdb_node_size(zonecut, delegset), + .loop = isc_loop_ref(isc_loop()) }; + + dns_delegdb_attach(delegdb, &(*nodep)->delegdb); + dns_delegset_attach(delegset, &(*nodep)->delegset); + dns_name_dup(zonecut, delegdb->mctx, &(*nodep)->zonecut); +} + +isc_result_t +dns_delegset_insert(dns_delegdb_t *delegdb, const dns_name_t *zonecut, + dns_ttl_t ttl, dns_delegset_t *delegset) { + isc_result_t result; + delegdb_node_t *node = NULL; + dns_qp_t *qp = NULL; + dns_qpread_t qpr = {}; + isc_stdtime_t now = isc_stdtime_now(); + dns_qpmulti_t *nodes = NULL; + + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(DNS_NAME_VALID(zonecut)); + REQUIRE(DNS_DELEGSET_VALID(delegset)); + + /* + * Only delegset allocated by the delegdb memory context can be added in + * the delegdb. This exclude transient delegset built from rdataset (see + * dns_delegset_fromrdataset()). + */ + REQUIRE(delegset->mctx == delegdb->mctx); + + rcu_read_lock(); + nodes = rcu_dereference(delegdb->nodes); + if (nodes == NULL) { + rcu_read_unlock(); + return ISC_R_SHUTTINGDOWN; + } + + /* + * First, check (without write txn) if the node already exists and is + * still valid. + */ + dns_qpmulti_query(nodes, &qpr); + result = dns_qp_lookup(&qpr, zonecut, DNS_DBNAMESPACE_NORMAL, NULL, + NULL, (void **)&node, NULL); + if (result == ISC_R_SUCCESS) { + INSIST(VALID_DELEGDB_NODE(node)); + if (node->delegset->expires > now) { + dns_qpread_destroy(nodes, &qpr); + CLEANUP(ISC_R_EXISTS); + } + } + dns_qpread_destroy(nodes, &qpr); + + /* + * We're about to add a new delegation, check for state of overmem, and + * clean up expired/least recently used delegation, then allocate and + * initialize a new node. + */ + delegdb_node_prepare(delegdb, nodes, now, ttl, zonecut, delegset, + &node); + + /* + * Add the node in the DB + */ + dns_qpmulti_write(nodes, &qp); + if (result == ISC_R_SUCCESS) { + /* + * A node at the same zonecut exists, and it is expired. Ignore + * the return value, in case the overriden node would be removed + * in meantime by someone else. + */ + (void)dns_qp_deletename(qp, zonecut, DNS_DBNAMESPACE_NORMAL, + NULL, NULL); + } + + result = dns_qp_insert(qp, node, 0); + if (result != ISC_R_SUCCESS) { + /* + * Someone else added the node before (and there was no node to + * delete). + */ + + delegdb_node_unref(node); + + /* + * Since not using an update (but write) transaction, + * _rollback() won't work here. + */ + dns_qpmulti_commit(nodes, &qp); + CLEANUP(ISC_R_EXISTS); + } + + /* + * The new delegation is added, and can be referenced by SIEVE + */ + ISC_SIEVE_INSERT(delegdb->lru[isc_tid()], node, link); + + delegdb_node_unref(node); + dns_qp_compact(qp, DNS_QPGC_MAYBE); + dns_qpmulti_commit(nodes, &qp); + +cleanup: + rcu_read_unlock(); + + return result; +} + +static void +delegset_destroy(dns_delegset_t *delegset) { + REQUIRE(DNS_DELEGSET_VALID(delegset)); + + delegset->magic = 0; + ISC_LIST_FOREACH(delegset->delegs, deleg, link) { + deleg->type = DNS_DELEGTYPE_UNDEFINED; + + ISC_LIST_UNLINK(delegset->delegs, deleg, link); + + ISC_LIST_FOREACH(deleg->addresses, address, link) { + ISC_LIST_UNLINK(deleg->addresses, address, link); + isc_mem_put(delegset->mctx, address, sizeof(*address)); + } + + ISC_LIST_FOREACH(deleg->names, nameserver, link) { + ISC_LIST_UNLINK(deleg->names, nameserver, link); + dns_name_free(nameserver, delegset->mctx); + isc_mem_put(delegset->mctx, nameserver, + sizeof(*nameserver)); + } + + isc_mem_put(delegset->mctx, deleg, sizeof(*deleg)); + } + + isc_mem_putanddetach(&delegset->mctx, delegset, sizeof(*delegset)); +} +ISC_REFCOUNT_IMPL(dns_delegset, delegset_destroy); + +static void +tostring_namelist(dns_namelist_t *namelist, const char *id, FILE *fp) { + if (!ISC_LIST_EMPTY(*namelist)) { + fprintf(fp, " %s=", id); + ISC_LIST_FOREACH(*namelist, name, link) { + isc_buffer_t nameb; + char bdata[DNS_NAME_MAXWIRE] = { 0 }; + + isc_buffer_init(&nameb, bdata, sizeof(bdata)); + dns_name_totext(name, 0, &nameb); + fprintf(fp, "%s", bdata); + + if (name != ISC_LIST_TAIL(*namelist)) { + fprintf(fp, ","); + } + } + } +} + +static void +deleg_tostring_addresses(dns_deleg_t *deleg, FILE *fp) { + bool hasv4 = false; + bool hasv6 = false; + + ISC_LIST_FOREACH(deleg->addresses, address, link) { + if (address->addr.family == AF_INET) { + hasv4 = true; + } else { + hasv6 = true; + } + } + + if (hasv4) { + bool first = true; + + fprintf(fp, " server-ipv4="); + ISC_LIST_FOREACH(deleg->addresses, address, link) { + char addrstr[] = "255.255.255.255"; + + if (address->addr.family == AF_INET6) { + continue; + } + + if (!first) { + fprintf(fp, ","); + } + first = false; + + inet_ntop(AF_INET, &address->addr.type, addrstr, + sizeof(addrstr)); + fprintf(fp, "%s", addrstr); + } + } + + if (hasv6) { + bool first = true; + + fprintf(fp, " server-ipv6="); + ISC_LIST_FOREACH(deleg->addresses, address, link) { + char addrstr[INET6_ADDRSTRLEN]; + + if (address->addr.family == AF_INET) { + continue; + } + + if (!first) { + fprintf(fp, ","); + } + first = false; + + inet_ntop(AF_INET6, &address->addr.type, addrstr, + sizeof(addrstr)); + fprintf(fp, "%s", addrstr); + } + } +} + +static void +delegset_tostring(const dns_name_t *zonecut, dns_delegset_t *delegset, + isc_stdtime_t now, bool expired, FILE *fp) { + ISC_LIST_FOREACH(delegset->delegs, deleg, link) { + isc_buffer_t zonecutb; + char bdata[DNS_NAME_MAXWIRE]; + dns_ttl_t ttl = 0; + + if (delegset->expires > now) { + ttl = delegset->expires - now; + } else { + INSIST(expired); + } + + isc_buffer_init(&zonecutb, bdata, sizeof(bdata)); + dns_name_totext(zonecut, 0, &zonecutb); + fprintf(fp, "%s %u DELEG", bdata, ttl); + + if (deleg->type == DNS_DELEGTYPE_DELEG_ADDRESSES || + deleg->type == DNS_DELEGTYPE_NS_GLUES) + { + deleg_tostring_addresses(deleg, fp); + } else if (deleg->type == DNS_DELEGTYPE_DELEG_NAMES || + deleg->type == DNS_DELEGTYPE_NS_NAMES) + { + tostring_namelist(&deleg->names, "server-name", fp); + } else if (deleg->type == DNS_DELEGTYPE_DELEG_PARAMS) { + tostring_namelist(&deleg->names, "include-delegparam", + fp); + } else { + UNREACHABLE(); + } + + fprintf(fp, "\n"); + } +} + +void +dns_delegdb_dump(dns_delegdb_t *delegdb, bool expired, FILE *fp) { + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(fp != NULL); + + dns_qpiter_t it; + dns_qpread_t qpr = {}; + delegdb_node_t *node = NULL; + isc_stdtime_t now = isc_stdtime_now(); + dns_qpmulti_t *nodes = NULL; + + rcu_read_lock(); + nodes = rcu_dereference(delegdb->nodes); + if (nodes == NULL) { + rcu_read_unlock(); + return; + } + + dns_qpmulti_query(nodes, &qpr); + + dns_qpiter_init(&qpr, &it); + while (dns_qpiter_next(&it, (void **)&node, NULL) == ISC_R_SUCCESS) { + if (!expired && !isactive(node, now)) { + continue; + } + + delegset_tostring(&node->zonecut, node->delegset, now, expired, + fp); + } + + dns_qpread_destroy(nodes, &qpr); + + rcu_read_unlock(); +} + +void +dns_delegset_fromnsrdataset(dns_rdataset_t *rdataset, + dns_delegset_t **delegsetp) { + dns_delegset_t *delegset = NULL; + dns_deleg_t *deleg = NULL; + + if (rdataset == NULL || !dns_rdataset_isassociated(rdataset) || + delegsetp == NULL || *delegsetp != NULL) + { + return; + } + + REQUIRE(rdataset->type == dns_rdatatype_ns); + + delegset = isc_mem_get(isc_g_mctx, sizeof(*delegset)); + *delegset = (dns_delegset_t){ + .magic = DNS_DELEGSET_MAGIC, + .references = ISC_REFCOUNT_INITIALIZER(1), + .delegs = ISC_LIST_INITIALIZER, + .expires = rdataset->ttl + isc_stdtime_now(), + .staticstub = rdataset->attributes.staticstub + }; + isc_mem_attach(isc_g_mctx, &delegset->mctx); + + deleg = isc_mem_get(isc_g_mctx, sizeof(*deleg)); + *deleg = (dns_deleg_t){ .addresses = ISC_LIST_INITIALIZER, + .names = ISC_LIST_INITIALIZER, + .type = DNS_DELEGTYPE_NS_NAMES, + .link = ISC_LINK_INITIALIZER }; + ISC_LIST_APPEND(delegset->delegs, deleg, link); + + DNS_RDATASET_FOREACH(rdataset) { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_ns_t ns; + + dns_rdataset_current(rdataset, &rdata); + dns_rdata_tostruct(&rdata, &ns, NULL); + dns_delegset_addns(delegset, deleg, &ns.name); + } + + *delegsetp = delegset; +} + +static isc_result_t +deleg_deletetree(dns_qp_t *qp, const dns_name_t *name) { + isc_result_t result; + delegdb_node_t *node = NULL; + dns_qpiter_t it; + ISC_LIST(delegdb_node_t) deadnodes = ISC_LIST_INITIALIZER; + + result = dns_qp_lookup(qp, name, DNS_DBNAMESPACE_NORMAL, &it, NULL, + (void **)&node, NULL); + if (result != ISC_R_SUCCESS) { + goto out; + } + + INSIST(VALID_DELEGDB_NODE(node)); + do { + /* + * Because QP doesn't allow deleting a node while using the + * iterator, the approach is different than `deleg_deletenode()` + * here. Instead of removing the node immediately, we add it + * into a list that we'll go through after, then delete each + * node. + */ + ISC_LIST_APPEND(deadnodes, node, deadlink); + + result = dns_qpiter_next(&it, (void **)&node, NULL); + if (result == ISC_R_NOMORE) { + result = ISC_R_SUCCESS; + break; + } + + INSIST(VALID_DELEGDB_NODE(node)); + if (!dns_name_issubdomain(&node->zonecut, name)) { + break; + } + } while (result == ISC_R_SUCCESS); + +out: + if (ISC_LIST_EMPTY(deadnodes)) { + result = ISC_R_NOTFOUND; + } else { + /* + * Let's actually delete the deadnodes! + */ + ISC_LIST_FOREACH(deadnodes, deadnode, deadlink) { + result = dns_qp_deletename(qp, &deadnode->zonecut, + DNS_DBNAMESPACE_NORMAL, NULL, + NULL); + INSIST(result == ISC_R_SUCCESS); + } + } + + return result; +} + +static isc_result_t +deleg_deletenode(dns_qp_t *qp, const dns_name_t *name) { + return dns_qp_deletename(qp, name, DNS_DBNAMESPACE_NORMAL, NULL, NULL); +} + +isc_result_t +dns_delegdb_delete(dns_delegdb_t *delegdb, const dns_name_t *name, bool tree) { + REQUIRE(VALID_DELEGDB(delegdb)); + REQUIRE(DNS_NAME_VALID(name)); + + dns_qpmulti_t *nodes = NULL; + dns_qp_t *qp = NULL; + isc_result_t result = ISC_R_SHUTTINGDOWN; + + rcu_read_lock(); + nodes = rcu_dereference(delegdb->nodes); + if (nodes != NULL) { + dns_qpmulti_write(nodes, &qp); + if (tree) { + result = deleg_deletetree(qp, name); + } else { + result = deleg_deletenode(qp, name); + } + if (result == ISC_R_SUCCESS) { + dns_qp_compact(qp, DNS_QPGC_MAYBE); + } + dns_qpmulti_commit(nodes, &qp); + } + rcu_read_unlock(); + + return result; +} + +static void +delegdb_shutdown_async(void *arg) { + dns_delegdb_t *delegdb = arg; + + REQUIRE(isc_loop_get(isc_tid()) == isc_loop_main()); + REQUIRE(delegdb != NULL && VALID_DELEGDB(delegdb)); + if (isc_refcount_decrement(&delegdb->owners) == 1) { + dns_qpmulti_t *nodes = rcu_xchg_pointer(&delegdb->nodes, NULL); + + if (nodes != NULL) { + nodes_rcu_head_t *nrh = isc_mem_get(delegdb->mctx, + sizeof(*nrh)); + *nrh = (nodes_rcu_head_t){ + .mctx = isc_mem_ref(delegdb->mctx), + .nodes = nodes, + }; + call_rcu(&nrh->rcu_head, deleg_destroy_qpmulti); + } + } +} + +void +dns_delegdb_shutdown(dns_delegdb_t *delegdb) { + if (isc_loop_get(isc_tid()) == isc_loop_main()) { + delegdb_shutdown_async(delegdb); + } else { + isc_async_run(isc_loop_main(), delegdb_shutdown_async, delegdb); + } +} + +void +dns_delegdb_setsize(dns_delegdb_t *delegdb, size_t size) { + REQUIRE(VALID_DELEGDB(delegdb)); + + if (size != 0 && size < DELEGDB_MINSIZE) { + size = DELEGDB_MINSIZE; + } + + delegdb->hiwater = size - (size >> 3); /* Approximately 7/8ths. */ + delegdb->lowater = size - (size >> 2); /* Approximately 3/4ths. */ + + if (size == 0 || delegdb->hiwater == 0 || delegdb->lowater == 0) { + isc_mem_clearwater(delegdb->mctx); + + /* + * TODO: Is it worth a warning if size > 0? Sounds like + * implicit overmem bypass, so the user should be warned... + */ + } else { + isc_mem_setwater(delegdb->mctx, delegdb->hiwater, + delegdb->lowater); + } +} diff --git a/lib/dns/include/dns/deleg.h b/lib/dns/include/dns/deleg.h new file mode 100644 index 00000000000..4a7bccb0041 --- /dev/null +++ b/lib/dns/include/dns/deleg.h @@ -0,0 +1,227 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#pragma once + +#include +#include + +#include + +/* + * A `dns_deleg_t` object represents either: + * + * - a DELEG-based delegation with `server-ipv4=` and/or `server-ipv6=` + * (DNS_DELEGTYPE_DELEG_ADDRESS) + * + * - a DELEG-based delegation with `server-name=` (DNS_DELEGTYPE_DELEG_NAMES) + * + * - a DELEG-based delegation with `include-delegparam=` + * (DNS_DELEGTYPE_DELEG_PARAMS) + * + * - an NS-based delegation with glues (DNS_DELEGTYPE_NS_GLUES) + * + * - an NS-based delegation with no glues (DNS_DELEGTYPE_NS_NAMES) + * + * This object must be allocated using `dns_deleg_allocdeleg()`. + */ + +typedef enum { + DNS_DELEGTYPE_UNDEFINED, + DNS_DELEGTYPE_DELEG_ADDRESSES, + DNS_DELEGTYPE_DELEG_NAMES, + DNS_DELEGTYPE_DELEG_PARAMS, + DNS_DELEGTYPE_NS_GLUES, + DNS_DELEGTYPE_NS_NAMES +} dns_deleg_type_t; + +struct dns_deleg { + isc_netaddrlist_t addresses; + dns_namelist_t names; + dns_deleg_type_t type; + ISC_LINK(dns_deleg_t) link; +}; + +/* + * A delegation set. Once it's added to the delegation DB, it gets a + * read-only object thus doesn't require any locking nor copying when the + * caller gets it. + * + * The TTL is common to all the DELEG RR for the same zonecut + * https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 + * + * When the delegation is NS-based, the TTL is the lowest TTL of the referral + * (either of the NS, A or AAAA glues). + * + * If a zone contains NS and DELEG delegations, this delegation must only + * store the DELEG ones. (This is resolver responsibility to ensure that.) + */ +struct dns_delegset { + unsigned int magic; + isc_mem_t *mctx; + isc_refcount_t references; + + dns_deleglist_t delegs; + isc_stdtime_t expires; + + /* + * Used only when a delegation is built from a local zone. + */ + bool staticstub; +}; +ISC_REFCOUNT_DECL(dns_delegset); + +#define DNS_DELEGSET_MAGIC ISC_MAGIC('D', 'e', 'G', 's') +#define DNS_DELEGSET_VALID(delegset) \ + ISC_MAGIC_VALID(delegset, DNS_DELEGSET_MAGIC) + +typedef struct dns_delegdb dns_delegdb_t; + +/* + * Allocate and initialize the delegation database. `db` is attached to the + * caller. + */ +void +dns_delegdb_create(dns_delegdb_t **delegdbp); + +/* + * Attach a delegation DB from an existing view to another view. Used when + * reloading the server and the delegation DB is reused. + */ +void +dns_delegdb_reuse(dns_view_t *oldview, dns_view_t *newview); + +/* + * Shutdown the delegation database. Must be called from any view shutting down + * which either created a delegdb or reused a delegdb. + */ +void +dns_delegdb_shutdown(dns_delegdb_t *delegdb); + +/* + * Lookup for delegations of a given name in the DB. If found, the zonecut is + * written and the delegation set is attached to the caller, so it must be + * detached once the caller is done with it. Even though `delegset` is not + * const (for convenience with ISC_LIST_FOREACH macros, _attach, _detach + * functions, etc.) the `delegset` _is_ a read-only object, and must not be + * modified. + * + * If only the zonecut is needed from the caller, `delegset` can be NULL, it + * won't be attached. + * + * The zonecut must be a initialized and attached to a buffer. + * + * If `now` is 0, the actual expiration time is `isc_stdtime_now()`. + */ +isc_result_t +dns_delegdb_lookup(dns_delegdb_t *db, const dns_name_t *name, isc_stdtime_t now, + unsigned int options, dns_name_t *zonecut, + dns_name_t *deepestzonecut, dns_delegset_t **delegset); + +/* + * Allocate and attach to the caller a new empty delegation set, but do not + * attach it in the DB yet, so the following API can be used to set its + * various properties. + * + * Because all those API calls (dns_deleg_alloc* and dns_deleg_add*) use + * the internal delegdb memory context, it _might_ in some circumstances + * allocate above its hiwater mark without reclaiming memory. The flow + * reclaiming memory is then run when adding the delegset into the database + * (dns_deleg_writeanddetach()). + * + * This could be changed to run through those API calls also if needed. + */ +void +dns_delegset_allocset(dns_delegdb_t *db, dns_delegset_t **delegsetp); + +/* + * Allocate a new deleg struct and insert it into the delegation set. Can't + * be used on delegation set already attached in the DB. + */ +void +dns_delegset_allocdeleg(dns_delegset_t *delegset, dns_deleg_type_t type, + dns_deleg_t **delegp); + +/* + * Add a new IP into a delegation. Can't be used on a delegation from a + * delegation set already attached in the DB. + */ +void +dns_delegset_addaddr(dns_delegset_t *delegset, dns_deleg_t *deleg, + const isc_netaddr_t *addr); + +/* + * Add a new DELEGPARAM name into a delegation. Can't be used on a delegation + * from a delegation set already attached in the DB. + */ +void +dns_delegset_adddelegparam(dns_delegset_t *delegset, dns_deleg_t *deleg, + const dns_name_t *name); + +/* + * Add a new nameserver name into a delegation. Can't be used on a delegation + * from a delegation set already attached in the DB. + */ +void +dns_delegset_addns(dns_delegset_t *delegset, dns_deleg_t *deleg, + const dns_name_t *name); + +/* + * Add a delegation set into the DB for the given zonecut and a time to live. If + * a delegation already exists and is not expired, ISC_R_EXISTS is returned and + * the DB is not altered. + * + * This function also cleanup least recently used delegation is the database in + * an overmemory conditions (See dns_deleg_setsize()). + * + * TODO: once DELEG is supported, attempting to add a delegation from NS + * where a delegation from DELEG already exists would be rejected too. + */ +isc_result_t +dns_delegset_insert(dns_delegdb_t *db, const dns_name_t *zonecut, dns_ttl_t ttl, + dns_delegset_t *delegset); + +/* + * Dump the database in a textual format for a given name. If `expired` is + * false, only the non expired entries are shown. All entries are shown + * otherwise. + */ +void +dns_delegdb_dump(dns_delegdb_t *db, bool expired, FILE *fp); + +/* + * Convert an NS rdataset into a delegset containing a single delegation + * (with possibly multiple nameserver). The allocated delegset is using the + * main memory context, thus, is not expected to be added into the deleg DB + * (which accepts only delegset allocated using `dns_deleg_alloc*()` APIs. + */ +void +dns_delegset_fromnsrdataset(dns_rdataset_t *rdataset, + dns_delegset_t **delegsetp); + +/* + * Delete a delegation matching a name. If `tree` is true, this will also + * delete all names below `name`. + */ +isc_result_t +dns_delegdb_delete(dns_delegdb_t *db, const dns_name_t *name, bool tree); + +/* + * Defines the size of the delegation cache. Whenever the effective cache + * size comes close to this size, least recently used cache entries are + * discarded. Value `0` means there is no limitation. + */ +void +dns_delegdb_setsize(dns_delegdb_t *db, size_t size); + +ISC_REFCOUNT_DECL(dns_delegdb); diff --git a/lib/dns/include/dns/types.h b/lib/dns/include/dns/types.h index f8d228c6d07..ea65a16bf95 100644 --- a/lib/dns/include/dns/types.h +++ b/lib/dns/include/dns/types.h @@ -178,6 +178,9 @@ typedef ISC_LIST(dns_zone_t) dns_zonelist_t; typedef struct dns_zonemgr dns_zonemgr_t; typedef struct dns_zt dns_zt_t; typedef struct dns_ipkeylist dns_ipkeylist_t; +typedef struct dns_deleg dns_deleg_t; +typedef ISC_LIST(dns_deleg_t) dns_deleglist_t; +typedef struct dns_delegset dns_delegset_t; typedef struct dst_gssapi_signverifyctx dst_gssapi_signverifyctx_t; diff --git a/lib/dns/meson.build b/lib/dns/meson.build index 92174973ff4..42b9268cae1 100644 --- a/lib/dns/meson.build +++ b/lib/dns/meson.build @@ -89,6 +89,7 @@ dns_srcset.add( 'compress.c', 'db.c', 'dbiterator.c', + 'deleg.c', 'diff.c', 'dispatch.c', 'dlz.c', diff --git a/tests/dns/deleg_test.c b/tests/dns/deleg_test.c new file mode 100644 index 00000000000..904eaeef00c --- /dev/null +++ b/tests/dns/deleg_test.c @@ -0,0 +1,723 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#include +#include /* IWYU pragma: keep */ +#include +#include +#include +#include +#include +#include +#include + +#define UNIT_TESTING +#include + +/* + * Mock isc_stdtime_now() as it makes testing easier (to compare + * generated/expected deleg data). + */ +static uint32_t stdtime_now = 100; + +static uint32_t +isc_stdtime_now(void) { + return stdtime_now; +} + +#include +#include +#include +#include + +#include +#include +#include +#include + +/* + * Because of the mock above. + */ +#include "../dns/deleg.c" + +#include + +static void +shutdownloop(ISC_ATTR_UNUSED void *arg) { + isc_loopmgr_shutdown(); +} + +static void +shutdowntest(dns_delegdb_t **dbp) { + dns_delegdb_shutdown(*dbp); + dns_delegdb_detach(dbp); + shutdownloop(NULL); +} + +static void +rundelegtest(isc_job_cb testcb) { + isc_loopmgr_create(isc_g_mctx, 1); + + isc_loop_setup(isc_loop_main(), testcb, NULL); + isc_loopmgr_run(); + + isc_loopmgr_destroy(); +} + +static void +addnamedeleg(const char *addrstr, dns_delegset_t *delegset, dns_deleg_t *deleg, + void (*fn)(dns_delegset_t *, dns_deleg_t *, const dns_name_t *)) { + dns_fixedname_t fname; + dns_name_t *name = dns_fixedname_initname(&fname); + + dns_name_fromstring(name, addrstr, NULL, 0, NULL); + fn(delegset, deleg, name); +} + +static void +addipdeleg(unsigned int af, const char *addrstr, dns_delegset_t *delegset, + dns_deleg_t *deleg) { + isc_netaddr_t addr = { .family = af }; + + assert_true(af == AF_INET || af == AF_INET6); + assert_int_equal(inet_pton(af, addrstr, &addr.type), 1); + dns_delegset_addaddr(delegset, deleg, &addr); +} + +static void +writedb(dns_delegdb_t *db, const char *zonecutstr, dns_ttl_t expire, + dns_delegset_t **delegsetp, bool expectsuccess) { + dns_fixedname_t fzonecut; + dns_name_t *zonecut = dns_fixedname_initname(&fzonecut); + isc_result_t result; + + dns_name_fromstring(zonecut, zonecutstr, NULL, 0, NULL); + result = dns_delegset_insert(db, zonecut, expire, *delegsetp); + + dns_delegset_detach(delegsetp); + assert_null(*delegsetp); + + if (expectsuccess) { + assert_int_equal(result, ISC_R_SUCCESS); + } else { + assert_int_equal(result, ISC_R_EXISTS); + } +} + +static isc_result_t +lookupdb(dns_delegdb_t *db, const char *namestr, isc_stdtime_t now, + unsigned int options, const char *expectedzcstr, + dns_delegset_t **delegsetp) { + isc_result_t result; + dns_fixedname_t fname, fexpectedzc, fzonecut; + dns_name_t *name = dns_fixedname_initname(&fname), + *expectedzc = dns_fixedname_initname(&fexpectedzc), + *zonecut = dns_fixedname_initname(&fzonecut); + + dns_name_fromstring(expectedzc, expectedzcstr, NULL, 0, NULL); + dns_name_fromstring(name, namestr, NULL, 0, NULL); + result = dns_delegdb_lookup(db, name, now, options, zonecut, NULL, + delegsetp); + + if (result == ISC_R_SUCCESS) { + assert_non_null(*delegsetp); + assert_true(dns_name_equal(zonecut, expectedzc)); + } else { + assert_null(*delegsetp); + } + + return result; +} + +static void +dumpdb(dns_delegdb_t *db, bool expired, const char *expected) { + constexpr char *filename = "delegdb-dump-test.db"; + char buffer[1024 * 4] = { 0 }; + FILE *fp = fopen(filename, "w+"); + + REQUIRE(fp != NULL); + dns_delegdb_dump(db, expired, fp); + + fp = freopen(filename, "r", fp); + REQUIRE(fp != NULL); + REQUIRE(fread(buffer, sizeof(buffer) - 1, 1, fp) == 0); + + assert_string_equal(expected, buffer); + + REQUIRE(fclose(fp) == 0); + REQUIRE(unlink(filename) == 0); +} + +static void +basictests(ISC_ATTR_UNUSED void *arg) { + isc_result_t result; + dns_delegdb_t *db = NULL; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + isc_stdtime_t now = isc_stdtime_now(); + + dns_delegdb_create(&db); + assert_non_null(db); + + /* + * A non expired delegation for foo. zonecut + */ + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_NAMES, &deleg); + addnamedeleg("ns.foo.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_GLUES, &deleg); + addipdeleg(AF_INET, "1.2.3.4", delegset, deleg); + addipdeleg(AF_INET6, "1111:2222:3333::4444", delegset, deleg); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_NAMES, &deleg); + assert_non_null(deleg); + addnamedeleg("ns.example.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + writedb(db, "foo.", 30, &delegset, true); + + result = lookupdb(db, "baz.bar.gee.", 0, 0, "", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + result = lookupdb(db, "baz.bar.foo.", 0, 0, "foo.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + /* + * A non expired delegation for bar.foo. zonecut + */ + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_NAMES, &deleg); + addnamedeleg("ns.bar.foo.", delegset, deleg, dns_delegset_addns); + addnamedeleg("ns2.bar.foo.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_GLUES, &deleg); + addipdeleg(AF_INET, "8.9.10.11", delegset, deleg); + addipdeleg(AF_INET, "9.9.10.12", delegset, deleg); + addipdeleg(AF_INET6, "ACDC::ACDC", delegset, deleg); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "ABBA::ABBA", delegset, deleg); + addipdeleg(AF_INET, "13.14.15.16", delegset, deleg); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_PARAMS, &deleg); + addnamedeleg("delegns.gee.", delegset, deleg, + dns_delegset_adddelegparam); + addnamedeleg("delegns2.gee.", delegset, deleg, + dns_delegset_adddelegparam); + deleg = NULL; + + writedb(db, "bar.foo.", 25, &delegset, true); + + result = lookupdb(db, "baz.bar.gee.", 0, 0, "", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + result = lookupdb(db, "baz.bar.foo.", 0, 0, "bar.foo.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + /* + * A expired delegation for bar.stuff. zonecut + */ + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_NAMES, &deleg); + addnamedeleg("ns.bar.stuff.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_GLUES, &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + deleg = NULL; + + writedb(db, "bar.stuff.", 10, &delegset, true); + deleg = NULL; + + result = lookupdb(db, "baz.bar.stuff.", now + 10, 0, "", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* + * But, if we ask for a date before its expiration, it is visible. And + * it is possible to dump it as well. But of course the dump when + * expired won't get anythig. + */ + result = lookupdb(db, "baz.bar.stuff.", now + 9, 0, "bar.stuff.", + &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + /* + * A non expired delegation for bar.stuff. zonecut replace the expired + * one. Move the time forward 10 to make the entry expired. + */ + stdtime_now += 10; + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_NAMES, &deleg); + addnamedeleg("ns.bar.stuff.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_GLUES, &deleg); + addipdeleg(AF_INET6, "1111::3333", delegset, deleg); + deleg = NULL; + + writedb(db, "bar.stuff.", 2, &delegset, true); + + /* + * Attempt to override bar.stuff. even though the existing delegation is + * not expired. This will be rejected. + */ + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_NAMES, &deleg); + addnamedeleg("wontbeindb.bar.stuff.", delegset, deleg, + dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "acdc::acdc", delegset, deleg); + deleg = NULL; + + writedb(db, "bar.stuff.", 2, &delegset, false); + deleg = NULL; + + result = lookupdb(db, "stuff.", 0, 0, "", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + result = lookupdb(db, "idonotknowthis.at.all.stuff.", 0, 0, "", + &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + result = lookupdb(db, "baz.bar.stuff.", 0, 0, "bar.stuff.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + char expected_dbdump[] = + "foo. 20 DELEG server-name=ns.foo.\n" + "foo. 20 DELEG server-ipv4=1.2.3.4 " + "server-ipv6=1111:2222:3333::4444\n" + "foo. 20 DELEG server-name=ns.example.\n" + "bar.foo. 15 DELEG server-name=ns.bar.foo.,ns2.bar.foo.\n" + "bar.foo. 15 DELEG server-ipv4=8.9.10.11,9.9.10.12 " + "server-ipv6=acdc::acdc\n" + "bar.foo. 15 DELEG server-ipv4=13.14.15.16 " + "server-ipv6=abba::abba\n" + "bar.foo. 15 DELEG " + "include-delegparam=delegns.gee.,delegns2.gee.\n" + "bar.stuff. 2 DELEG server-name=ns.bar.stuff.\n" + "bar.stuff. 2 DELEG server-ipv6=1111::3333\n"; + dumpdb(db, false, expected_dbdump); + + /* + * Dump in the "future", everything is seen as expired + */ + stdtime_now += 300; + dumpdb(db, false, ""); + + /* + * Bump if we ask to dump expired entries, they'll be there (with TTL 0) + */ + char expected_expired_dbdump[] = + "foo. 0 DELEG server-name=ns.foo.\n" + "foo. 0 DELEG server-ipv4=1.2.3.4 " + "server-ipv6=1111:2222:3333::4444\n" + "foo. 0 DELEG server-name=ns.example.\n" + "bar.foo. 0 DELEG server-name=ns.bar.foo.,ns2.bar.foo.\n" + "bar.foo. 0 DELEG server-ipv4=8.9.10.11,9.9.10.12 " + "server-ipv6=acdc::acdc\n" + "bar.foo. 0 DELEG server-ipv4=13.14.15.16 " + "server-ipv6=abba::abba\n" + "bar.foo. 0 DELEG " + "include-delegparam=delegns.gee.,delegns2.gee.\n" + "bar.stuff. 0 DELEG server-name=ns.bar.stuff.\n" + "bar.stuff. 0 DELEG server-ipv6=1111::3333\n"; + dumpdb(db, true, expected_expired_dbdump); + + shutdowntest(&db); +} + +static void +ttl0tests(ISC_ATTR_UNUSED void *arg) { + isc_result_t result; + dns_delegdb_t *db = NULL; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + isc_stdtime_t now = isc_stdtime_now(); + isc_buffer_t b; + char bdata[2048]; + + isc_buffer_init(&b, bdata, sizeof(bdata)); + dns_delegdb_create(&db); + assert_non_null(db); + + dns_delegset_allocset(db, &delegset); + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_NAMES, &deleg); + addnamedeleg("ns.bar.stuff.", delegset, deleg, dns_delegset_addns); + deleg = NULL; + + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + deleg = NULL; + + writedb(db, "bar.stuff.", 0, &delegset, true); + deleg = NULL; + + result = lookupdb(db, "baz.bar.stuff.", now, 0, "bar.stuff.", + &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + result = lookupdb(db, "baz.bar.stuff.", now + 1, 0, "", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + shutdowntest(&db); +} + +static void +noexacttests(ISC_ATTR_UNUSED void *arg) { + isc_result_t result; + dns_delegdb_t *db = NULL; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + isc_stdtime_t now = isc_stdtime_now(); + isc_buffer_t b; + char bdata[2048]; + + isc_buffer_init(&b, bdata, sizeof(bdata)); + dns_delegdb_create(&db); + assert_non_null(db); + + struct { + const char *name; + const char *expected; + const char *noexactexpected; + dns_ttl_t ttl; + } zonecuts[] = { + { "stuff.", "stuff.", "stuff.", 30 }, + { "foo.stuff.", "foo.stuff.", "stuff.", 30 }, + { "expired.foo.stuff.", "foo.stuff.", "foo.stuff.", 1 }, + { "bar.expired.foo.stuff.", "bar.expired.foo.stuff.", + "foo.stuff.", 30 }, + { "baz.bar.expired.foo.stuff.", "baz.bar.expired.foo.stuff.", + "bar.expired.foo.stuff.", 30 } + }; + + for (size_t i = 0; i < ARRAY_SIZE(zonecuts); i++) { + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_NS_GLUES, + &deleg); + addipdeleg(AF_INET6, "1111::1111", delegset, deleg); + writedb(db, zonecuts[i].name, zonecuts[i].ttl, &delegset, true); + deleg = NULL; + } + + for (size_t i = 0; i < ARRAY_SIZE(zonecuts); i++) { + result = lookupdb(db, zonecuts[i].name, now + 1, 0, + zonecuts[i].expected, &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + result = lookupdb(db, zonecuts[i].name, now + 1, + DNS_DBFIND_NOEXACT, + zonecuts[i].noexactexpected, &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + } + + result = lookupdb(db, "gee.expired.foo.stuff.", now + 1, 0, + "foo.stuff.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + shutdowntest(&db); +} + +static void +deletetests(ISC_ATTR_UNUSED void *arg) { + isc_result_t result; + dns_delegdb_t *db = NULL; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + dns_fixedname_t fname; + dns_name_t *name = dns_fixedname_initname(&fname); + + dns_delegdb_create(&db); + assert_non_null(db); + + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "stuff.", 10, &delegset, true); + deleg = NULL; + + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "baz.stuff.", 10, &delegset, true); + deleg = NULL; + + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "bar.baz.stuff.", 10, &delegset, true); + deleg = NULL; + + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "foo.bar.baz.stuff.", 10, &delegset, true); + deleg = NULL; + + dns_name_fromstring(name, "foo.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_NOTFOUND); + result = dns_delegdb_delete(db, name, true); + assert_int_equal(result, ISC_R_NOTFOUND); + + dns_name_fromstring(name, "gee.foo.bar.stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_NOTFOUND); + + dns_name_fromstring(name, "foo.bar.baz.stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_name_fromstring(name, "foo.bar.baz.stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_NOTFOUND); + + dns_name_fromstring(name, "baz.stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_SUCCESS); + + result = lookupdb(db, "bar.baz.stuff.", 5, 0, "bar.baz.stuff.", + &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + dns_name_fromstring(name, "stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, true); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_name_fromstring(name, "stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_NOTFOUND); + + dns_name_fromstring(name, "bar.baz.stuff.", NULL, 0, NULL); + result = dns_delegdb_delete(db, name, false); + assert_int_equal(result, ISC_R_NOTFOUND); + + result = lookupdb(db, "bar.baz.stuff.", 5, 0, "bar.baz.stuff.", + &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* + * Let's add stuff. back and query bar.baz.stuff. again. Because the + * node is NULL, it should go up until it finds stuff. + */ + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "stuff.", 10, &delegset, true); + deleg = NULL; + + result = lookupdb(db, "bar.baz.stuff.", 5, 0, "stuff.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + shutdowntest(&db); +} + +/* + * The cleanup test is split into phases because node destruction is now + * fully deferred to the node's owning loop via isc_async_run(). After + * rcu_barrier() completes, the QP reclamation has fired (calling + * delegdb_node_destroy which schedules the async callback), but the + * actual memory free hasn't happened yet — it's pending on the loop's + * event queue. We must return to the loop between phases so it can + * process the pending node destroys before we check memory usage. + */ +typedef struct { + dns_delegdb_t *db; + isc_stdtime_t now; +} cleanup_ctx_t; + +static void +cleanuptests_phase3(void *arg) { + cleanup_ctx_t *ctx = arg; + dns_delegdb_t *db = ctx->db; + isc_stdtime_t now = ctx->now; + dns_delegset_t *delegset = NULL; + isc_result_t result; + + assert_int_in_range(isc_mem_inuse(db->mctx), 4000000, 4100000); + + /* + * baz. is there, but bar. is gone, as it has been + * removed (even if it wasn't expired.) + */ + result = lookupdb(db, "baz.", now, 0, "baz.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + result = lookupdb(db, "bar.", now, 0, "bar.", &delegset); + assert_int_equal(result, ISC_R_NOTFOUND); + + shutdowntest(&db); +} + +static void +cleanuptests_phase2(void *arg) { + cleanup_ctx_t *ctx = arg; + dns_delegdb_t *db = ctx->db; + isc_stdtime_t now = ctx->now; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + isc_result_t result; + + assert_int_in_range(isc_mem_inuse(db->mctx), 4000000, 4100000); + + /* + * bar. is there + */ + result = lookupdb(db, "bar.", now, 0, "bar.", &delegset); + assert_int_equal(result, ISC_R_SUCCESS); + dns_delegset_detach(&delegset); + + /* + * Add yet another non expired record. But LRU will have to get + * rid of it because we're hitting the hiwater mark again. + */ + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + + for (size_t i = 0; i < 99999; i++) { + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + } + assert_int_in_range(isc_mem_inuse(db->mctx), 8000000, 8100000); + writedb(db, "baz.", 30, &delegset, true); + deleg = NULL; + + rcu_barrier(); + isc_async_run(isc_loop(), cleanuptests_phase3, ctx); +} + +static void +cleanuptests(ISC_ATTR_UNUSED void *arg) { + static cleanup_ctx_t ctx; + dns_delegdb_t *db = NULL; + dns_deleg_t *deleg = NULL; + dns_delegset_t *delegset = NULL; + + dns_delegdb_create(&db); + assert_non_null(db); + + ctx = (cleanup_ctx_t){ .db = db, .now = isc_stdtime_now() }; + + /* + * hiwater is 4375000 = 5000000 - (5000000 >> 3) + * lowater is 3750000 = 5000000 - (5000000 >> 2) + */ + dns_delegdb_setsize(db, 5000000); + + /* + * A valid record + */ + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + writedb(db, "baz.", 300, &delegset, true); + deleg = NULL; + + /* + * An expired record + */ + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + + assert_int_in_range(isc_mem_inuse(db->mctx), 500, 2000); + + for (size_t i = 0; i < 99999; i++) { + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + } + + assert_int_in_range(isc_mem_inuse(db->mctx), 4000000, 4100000); + + writedb(db, "stuff.", 10, &delegset, true); + deleg = NULL; + stdtime_now += 10; + + /* + * A non expired record + */ + dns_delegset_allocset(db, &delegset); + dns_delegset_allocdeleg(delegset, DNS_DELEGTYPE_DELEG_ADDRESSES, + &deleg); + + for (size_t i = 0; i < 99999; i++) { + addipdeleg(AF_INET6, "1111::2222", delegset, deleg); + } + + /* + * The zonecut is not added yet but the delegset being huge (allocated + * with DB mem context) overmem conditions will be detected, and the + * expired node will be removed + */ + assert_int_in_range(isc_mem_inuse(db->mctx), 8000000, 8100000); + writedb(db, "bar.", 30, &delegset, true); + deleg = NULL; + + /* + * stuff. internal node (and delegset) is now removed. rcu_barrier() + * is needed to kick off QP reclamation flow (and run the detaching + * functions from the DB nodes). The actual memory free is deferred + * to the loop via isc_async_run(), so we continue in phase2 to let + * the loop process the pending node destroys. + */ + rcu_barrier(); + isc_async_run(isc_loop(), cleanuptests_phase2, &ctx); +} + +ISC_RUN_TEST_IMPL(dns_deleg_basictests) { rundelegtest(basictests); } +ISC_RUN_TEST_IMPL(dns_deleg_ttl0tests) { rundelegtest(ttl0tests); } +ISC_RUN_TEST_IMPL(dns_deleg_noexacttests) { rundelegtest(noexacttests); } +ISC_RUN_TEST_IMPL(dns_deleg_deletetests) { rundelegtest(deletetests); } +ISC_RUN_TEST_IMPL(dns_deleg_cleanuptests) { rundelegtest(cleanuptests); } + +ISC_TEST_LIST_START +ISC_TEST_ENTRY(dns_deleg_basictests) +ISC_TEST_ENTRY(dns_deleg_ttl0tests) +ISC_TEST_ENTRY(dns_deleg_noexacttests) +ISC_TEST_ENTRY(dns_deleg_deletetests) +ISC_TEST_ENTRY(dns_deleg_cleanuptests) +ISC_TEST_LIST_END + +ISC_TEST_MAIN diff --git a/tests/dns/meson.build b/tests/dns/meson.build index 73c811f10ef..72b67547193 100644 --- a/tests/dns/meson.build +++ b/tests/dns/meson.build @@ -17,6 +17,7 @@ dns_tests = [ 'dbdiff', 'dbiterator', 'dbversion', + 'deleg', 'dispatch', 'dns64', 'dst',