diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 07:24:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 07:24:22 +0000 |
commit | 45d6379135504814ab723b57f0eb8be23393a51d (patch) | |
tree | d4f2ec4acca824a8446387a758b0ce4238a4dffa /lib/ns | |
parent | Initial commit. (diff) | |
download | bind9-upstream.tar.xz bind9-upstream.zip |
Adding upstream version 1:9.16.44.upstream/1%9.16.44upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/ns')
51 files changed, 28732 insertions, 0 deletions
diff --git a/lib/ns/Kyuafile b/lib/ns/Kyuafile new file mode 100644 index 0000000..c796010 --- /dev/null +++ b/lib/ns/Kyuafile @@ -0,0 +1,15 @@ +-- 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. + +syntax(2) +test_suite('bind9') + +include('tests/Kyuafile') diff --git a/lib/ns/Makefile.in b/lib/ns/Makefile.in new file mode 100644 index 0000000..efadf78 --- /dev/null +++ b/lib/ns/Makefile.in @@ -0,0 +1,89 @@ +# 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. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +VERSION=@BIND9_VERSION@ +@BIND9_MAJOR@ + +@BIND9_MAKE_INCLUDES@ + +CINCLUDES = -I. -I${top_srcdir}/lib/ns -Iinclude \ + ${NS_INCLUDES} ${DNS_INCLUDES} ${ISC_INCLUDES} \ + ${OPENSSL_CFLAGS} @DST_GSSAPI_INC@ \ + ${FSTRM_CFLAGS} + +CDEFINES = -DNAMED_PLUGINDIR=\"${plugindir}\" + +CWARNINGS = + +ISCLIBS = ../../lib/isc/libisc.@A@ + +ISCDEPLIBS = ../../lib/isc/libisc.@A@ + +DNSLIBS = ../../lib/dns/libdns.@A@ @NO_LIBTOOL_DNSLIBS@ + +DNSDEPLIBS = ../../lib/dns/libdns.@A@ + +LIBS = @LIBS@ + +# Alphabetically +OBJS = client.@O@ hooks.@O@ interfacemgr.@O@ lib.@O@ \ + listenlist.@O@ log.@O@ notify.@O@ query.@O@ \ + server.@O@ sortlist.@O@ stats.@O@ update.@O@ \ + version.@O@ xfrout.@O@ + +SRCS = client.c hooks.c interfacemgr.c lib.c listenlist.c \ + log.c notify.c query.c server.c sortlist.c stats.c \ + update.c version.c xfrout.c + +SUBDIRS = include +TESTDIRS = @UNITTESTS@ +TARGETS = timestamp + +SO_CFLAGS = @CFLAGS@ @SO_CFLAGS@ +SO_LDFLAGS = @LDFLAGS@ @SO_LDFLAGS@ + +@BIND9_MAKE_RULES@ + +version.@O@: version.c + ${LIBTOOL_MODE_COMPILE} ${CC} ${ALL_CFLAGS} \ + -DVERSION=\"${VERSION}\" \ + -DMAJOR=\"${MAJOR}\" \ + -c ${srcdir}/version.c + +libns.@SA@: ${OBJS} + ${AR} ${ARFLAGS} $@ ${OBJS} + ${RANLIB} $@ + +libns.la: ${OBJS} + ${LIBTOOL_MODE_LINK} \ + ${CC} ${ALL_CFLAGS} ${LDFLAGS} -o libns.la -rpath ${libdir} \ + -release "${VERSION}" \ + ${OBJS} ${ISCLIBS} ${DNSLIBS} @DNS_CRYPTO_LIBS@ ${LIBS} + +timestamp: libns.@A@ + touch timestamp + +installdirs: + $(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${libdir} + +install:: timestamp installdirs + ${LIBTOOL_MODE_INSTALL} ${INSTALL_LIBRARY} libns.@A@ \ + ${DESTDIR}${libdir} + +uninstall:: + ${LIBTOOL_MODE_UNINSTALL} rm -f ${DESTDIR}${libdir}/libns.@A@ + +clean distclean:: + rm -f libns.@A@ timestamp diff --git a/lib/ns/client.c b/lib/ns/client.c new file mode 100644 index 0000000..d4ce000 --- /dev/null +++ b/lib/ns/client.c @@ -0,0 +1,3053 @@ +/* + * 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 <inttypes.h> +#include <limits.h> +#include <stdbool.h> + +#include <isc/aes.h> +#include <isc/atomic.h> +#include <isc/formatcheck.h> +#include <isc/fuzz.h> +#include <isc/hmac.h> +#include <isc/mutex.h> +#include <isc/nonce.h> +#include <isc/once.h> +#include <isc/platform.h> +#include <isc/print.h> +#include <isc/random.h> +#include <isc/safe.h> +#include <isc/serial.h> +#include <isc/siphash.h> +#include <isc/stats.h> +#include <isc/stdio.h> +#include <isc/string.h> +#include <isc/task.h> +#include <isc/timer.h> +#include <isc/util.h> + +#include <dns/adb.h> +#include <dns/badcache.h> +#include <dns/cache.h> +#include <dns/db.h> +#include <dns/dispatch.h> +#include <dns/dnstap.h> +#include <dns/edns.h> +#include <dns/events.h> +#include <dns/message.h> +#include <dns/peer.h> +#include <dns/rcode.h> +#include <dns/rdata.h> +#include <dns/rdataclass.h> +#include <dns/rdatalist.h> +#include <dns/rdataset.h> +#include <dns/resolver.h> +#include <dns/stats.h> +#include <dns/tsig.h> +#include <dns/view.h> +#include <dns/zone.h> + +#include <ns/client.h> +#include <ns/interfacemgr.h> +#include <ns/log.h> +#include <ns/notify.h> +#include <ns/server.h> +#include <ns/stats.h> +#include <ns/update.h> + +#ifndef _POSIX_HOST_NAME_MAX +#define _POSIX_HOST_NAME_MAX 255 +#endif + +/*** + *** Client + ***/ + +/*! \file + * Client Routines + * + * Important note! + * + * All client state changes, other than that from idle to listening, occur + * as a result of events. This guarantees serialization and avoids the + * need for locking. + * + * If a routine is ever created that allows someone other than the client's + * task to change the client, then the client will have to be locked. + */ + +#ifdef NS_CLIENT_TRACE +#define CTRACE(m) \ + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, \ + ISC_LOG_DEBUG(3), "%s", (m)) +#define MTRACE(m) \ + isc_log_write(ns_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, \ + ISC_LOG_DEBUG(3), "clientmgr @%p: %s", manager, (m)) +#else /* ifdef NS_CLIENT_TRACE */ +#define CTRACE(m) ((void)(m)) +#define MTRACE(m) ((void)(m)) +#endif /* ifdef NS_CLIENT_TRACE */ + +#define TCP_CLIENT(c) (((c)->attributes & NS_CLIENTATTR_TCP) != 0) + +#define COOKIE_SIZE 24U /* 8 + 4 + 4 + 8 */ +#define ECS_SIZE 20U /* 2 + 1 + 1 + [0..16] */ + +#define WANTNSID(x) (((x)->attributes & NS_CLIENTATTR_WANTNSID) != 0) +#define WANTEXPIRE(x) (((x)->attributes & NS_CLIENTATTR_WANTEXPIRE) != 0) +#define WANTPAD(x) (((x)->attributes & NS_CLIENTATTR_WANTPAD) != 0) +#define USEKEEPALIVE(x) (((x)->attributes & NS_CLIENTATTR_USEKEEPALIVE) != 0) + +#define MANAGER_MAGIC ISC_MAGIC('N', 'S', 'C', 'm') +#define VALID_MANAGER(m) ISC_MAGIC_VALID(m, MANAGER_MAGIC) + +/* + * Enable ns_client_dropport() by default. + */ +#ifndef NS_CLIENT_DROPPORT +#define NS_CLIENT_DROPPORT 1 +#endif /* ifndef NS_CLIENT_DROPPORT */ + +#define CLIENT_NMCTXS_PERCPU 8 +/*%< + * Number of 'mctx pools' for clients. (Should this be configurable?) + * When enabling threads, we use a pool of memory contexts shared by + * client objects, since concurrent access to a shared context would cause + * heavy contentions. The above constant is expected to be enough for + * completely avoiding contentions among threads for an authoritative-only + * server. + */ + +#define CLIENT_NTASKS_PERCPU 32 +/*%< + * Number of tasks to be used by clients - those are used only when recursing + */ + +#if defined(_WIN32) && !defined(_WIN64) +LIBNS_EXTERNAL_DATA atomic_uint_fast32_t ns_client_requests = 0; +#else /* if defined(_WIN32) && !defined(_WIN64) */ +LIBNS_EXTERNAL_DATA atomic_uint_fast64_t ns_client_requests = 0; +#endif /* if defined(_WIN32) && !defined(_WIN64) */ + +static void +clientmgr_attach(ns_clientmgr_t *source, ns_clientmgr_t **targetp); +static void +clientmgr_detach(ns_clientmgr_t **mp); +static void +clientmgr_destroy(ns_clientmgr_t *manager); +static void +ns_client_endrequest(ns_client_t *client); +static void +ns_client_dumpmessage(ns_client_t *client, const char *reason); +static void +compute_cookie(ns_client_t *client, uint32_t when, uint32_t nonce, + const unsigned char *secret, isc_buffer_t *buf); +static void +get_clientmctx(ns_clientmgr_t *manager, isc_mem_t **mctxp); +static void +get_clienttask(ns_clientmgr_t *manager, isc_task_t **taskp); + +void +ns_client_recursing(ns_client_t *client) { + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(client->state == NS_CLIENTSTATE_WORKING); + + LOCK(&client->manager->reclock); + client->state = NS_CLIENTSTATE_RECURSING; + ISC_LIST_APPEND(client->manager->recursing, client, rlink); + UNLOCK(&client->manager->reclock); +} + +void +ns_client_killoldestquery(ns_client_t *client) { + ns_client_t *oldest; + REQUIRE(NS_CLIENT_VALID(client)); + + LOCK(&client->manager->reclock); + oldest = ISC_LIST_HEAD(client->manager->recursing); + if (oldest != NULL) { + ISC_LIST_UNLINK(client->manager->recursing, oldest, rlink); + ns_query_cancel(oldest); + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_reclimitdropped); + } + UNLOCK(&client->manager->reclock); +} + +void +ns_client_settimeout(ns_client_t *client, unsigned int seconds) { + UNUSED(client); + UNUSED(seconds); + /* XXXWPK TODO use netmgr to set timeout */ +} + +static void +ns_client_endrequest(ns_client_t *client) { + INSIST(client->nupdates == 0); + INSIST(client->state == NS_CLIENTSTATE_WORKING || + client->state == NS_CLIENTSTATE_RECURSING); + + CTRACE("endrequest"); + + if (client->state == NS_CLIENTSTATE_RECURSING) { + LOCK(&client->manager->reclock); + if (ISC_LINK_LINKED(client, rlink)) { + ISC_LIST_UNLINK(client->manager->recursing, client, + rlink); + } + UNLOCK(&client->manager->reclock); + } + + if (client->cleanup != NULL) { + (client->cleanup)(client); + client->cleanup = NULL; + } + + if (client->view != NULL) { +#ifdef ENABLE_AFL + if (client->sctx->fuzztype == isc_fuzz_resolver) { + dns_cache_clean(client->view->cache, INT_MAX); + dns_adb_flush(client->view->adb); + } +#endif /* ifdef ENABLE_AFL */ + dns_view_detach(&client->view); + } + if (client->opt != NULL) { + INSIST(dns_rdataset_isassociated(client->opt)); + dns_rdataset_disassociate(client->opt); + dns_message_puttemprdataset(client->message, &client->opt); + } + + client->signer = NULL; + client->udpsize = 512; + client->extflags = 0; + client->ednsversion = -1; + dns_ecs_init(&client->ecs); + dns_message_reset(client->message, DNS_MESSAGE_INTENTPARSE); + + /* + * Clean up from recursion - normally this would be done in + * fetch_callback(), but if we're shutting down and canceling then + * it might not have happened. + */ + if (client->recursionquota != NULL) { + isc_quota_detach(&client->recursionquota); + ns_stats_decrement(client->sctx->nsstats, + ns_statscounter_recursclients); + } + + /* + * Clear all client attributes that are specific to the request + */ + client->attributes = 0; +#ifdef ENABLE_AFL + if (client->sctx->fuzznotify != NULL && + (client->sctx->fuzztype == isc_fuzz_client || + client->sctx->fuzztype == isc_fuzz_tcpclient || + client->sctx->fuzztype == isc_fuzz_resolver)) + { + client->sctx->fuzznotify(); + } +#endif /* ENABLE_AFL */ +} + +void +ns_client_drop(ns_client_t *client, isc_result_t result) { + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(client->state == NS_CLIENTSTATE_WORKING || + client->state == NS_CLIENTSTATE_RECURSING); + + CTRACE("drop"); + if (result != ISC_R_SUCCESS) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "request failed: %s", isc_result_totext(result)); + } +} + +static void +client_senddone(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) { + ns_client_t *client = cbarg; + + REQUIRE(client->sendhandle == handle); + + CTRACE("senddone"); + + /* + * Set sendhandle to NULL, but don't detach it immediately, in + * case we need to retry the send. If we do resend, then + * sendhandle will be reattached. Whether or not we resend, + * we will then detach the handle from *this* send by detaching + * 'handle' directly below. + */ + client->sendhandle = NULL; + + if (result != ISC_R_SUCCESS) { + if (!TCP_CLIENT(client) && result == ISC_R_MAXSIZE) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "send exceeded maximum size: truncating"); + client->query.attributes &= ~NS_QUERYATTR_ANSWERED; + client->rcode_override = dns_rcode_noerror; + ns_client_error(client, ISC_R_MAXSIZE); + } else { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "send failed: %s", + isc_result_totext(result)); + } + } + + isc_nmhandle_detach(&handle); +} + +static void +client_allocsendbuf(ns_client_t *client, isc_buffer_t *buffer, + unsigned char **datap) { + unsigned char *data; + uint32_t bufsize; + + REQUIRE(datap != NULL); + + if (TCP_CLIENT(client)) { + INSIST(client->tcpbuf == NULL); + client->tcpbuf = isc_mem_get(client->mctx, + NS_CLIENT_TCP_BUFFER_SIZE); + data = client->tcpbuf; + isc_buffer_init(buffer, data, NS_CLIENT_TCP_BUFFER_SIZE); + } else { + data = client->sendbuf; + if ((client->attributes & NS_CLIENTATTR_HAVECOOKIE) == 0) { + if (client->view != NULL) { + bufsize = client->view->nocookieudp; + } else { + bufsize = 512; + } + } else { + bufsize = client->udpsize; + } + if (bufsize > client->udpsize) { + bufsize = client->udpsize; + } + if (bufsize > NS_CLIENT_SEND_BUFFER_SIZE) { + bufsize = NS_CLIENT_SEND_BUFFER_SIZE; + } + isc_buffer_init(buffer, data, bufsize); + } + *datap = data; +} + +static void +client_sendpkg(ns_client_t *client, isc_buffer_t *buffer) { + isc_region_t r; + + REQUIRE(client->sendhandle == NULL); + + isc_buffer_usedregion(buffer, &r); + isc_nmhandle_attach(client->handle, &client->sendhandle); + isc_nm_send(client->handle, &r, client_senddone, client); +} + +void +ns_client_sendraw(ns_client_t *client, dns_message_t *message) { + isc_result_t result; + unsigned char *data; + isc_buffer_t buffer; + isc_region_t r; + isc_region_t *mr; + + REQUIRE(NS_CLIENT_VALID(client)); + + CTRACE("sendraw"); + + mr = dns_message_getrawmessage(message); + if (mr == NULL) { + result = ISC_R_UNEXPECTEDEND; + goto done; + } + + client_allocsendbuf(client, &buffer, &data); + + if (mr->length > isc_buffer_length(&buffer)) { + result = ISC_R_NOSPACE; + goto done; + } + + /* + * Copy message to buffer and fixup id. + */ + isc_buffer_availableregion(&buffer, &r); + result = isc_buffer_copyregion(&buffer, mr); + if (result != ISC_R_SUCCESS) { + goto done; + } + r.base[0] = (client->message->id >> 8) & 0xff; + r.base[1] = client->message->id & 0xff; + +#ifdef HAVE_DNSTAP + if (client->view != NULL) { + bool tcp = TCP_CLIENT(client); + dns_dtmsgtype_t dtmsgtype; + if (client->message->opcode == dns_opcode_update) { + dtmsgtype = DNS_DTTYPE_UR; + } else if ((client->message->flags & DNS_MESSAGEFLAG_RD) != 0) { + dtmsgtype = DNS_DTTYPE_CR; + } else { + dtmsgtype = DNS_DTTYPE_AR; + } + dns_dt_send(client->view, dtmsgtype, &client->peeraddr, + &client->destsockaddr, tcp, NULL, + &client->requesttime, NULL, &buffer); + } +#endif + + client_sendpkg(client, &buffer); + + return; +done: + if (client->tcpbuf != NULL) { + isc_mem_put(client->mctx, client->tcpbuf, + NS_CLIENT_TCP_BUFFER_SIZE); + client->tcpbuf = NULL; + } + + ns_client_drop(client, result); +} + +void +ns_client_send(ns_client_t *client) { + isc_result_t result; + unsigned char *data; + isc_buffer_t buffer = { .magic = 0 }; + isc_region_t r; + dns_compress_t cctx; + bool cleanup_cctx = false; + unsigned int render_opts; + unsigned int preferred_glue; + bool opt_included = false; + size_t respsize; + dns_aclenv_t *env; +#ifdef HAVE_DNSTAP + unsigned char zone[DNS_NAME_MAXWIRE]; + dns_dtmsgtype_t dtmsgtype; + isc_region_t zr; +#endif /* HAVE_DNSTAP */ + + REQUIRE(NS_CLIENT_VALID(client)); + + if ((client->query.attributes & NS_QUERYATTR_ANSWERED) != 0) { + return; + } + + /* + * XXXWPK TODO + * Delay the response according to the -T delay option + */ + + env = ns_interfacemgr_getaclenv(client->manager->interface->mgr); + + CTRACE("send"); + + if (client->message->opcode == dns_opcode_query && + (client->attributes & NS_CLIENTATTR_RA) != 0) + { + client->message->flags |= DNS_MESSAGEFLAG_RA; + } + + if ((client->attributes & NS_CLIENTATTR_WANTDNSSEC) != 0) { + render_opts = 0; + } else { + render_opts = DNS_MESSAGERENDER_OMITDNSSEC; + } + + preferred_glue = 0; + if (client->view != NULL) { + if (client->view->preferred_glue == dns_rdatatype_a) { + preferred_glue = DNS_MESSAGERENDER_PREFER_A; + } else if (client->view->preferred_glue == dns_rdatatype_aaaa) { + preferred_glue = DNS_MESSAGERENDER_PREFER_AAAA; + } + } + if (preferred_glue == 0) { + if (isc_sockaddr_pf(&client->peeraddr) == AF_INET) { + preferred_glue = DNS_MESSAGERENDER_PREFER_A; + } else { + preferred_glue = DNS_MESSAGERENDER_PREFER_AAAA; + } + } + + /* + * Create an OPT for our reply. + */ + if ((client->attributes & NS_CLIENTATTR_WANTOPT) != 0) { + result = ns_client_addopt(client, client->message, + &client->opt); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + } + + client_allocsendbuf(client, &buffer, &data); + + result = dns_compress_init(&cctx, -1, client->mctx); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + if (client->peeraddr_valid && client->view != NULL) { + isc_netaddr_t netaddr; + dns_name_t *name = NULL; + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + if (client->message->tsigkey != NULL) { + name = &client->message->tsigkey->name; + } + + if (client->view->nocasecompress == NULL || + !dns_acl_allowed(&netaddr, name, + client->view->nocasecompress, env)) + { + dns_compress_setsensitive(&cctx, true); + } + + if (!client->view->msgcompression) { + dns_compress_disable(&cctx); + } + } + cleanup_cctx = true; + + result = dns_message_renderbegin(client->message, &cctx, &buffer); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + if (client->opt != NULL) { + result = dns_message_setopt(client->message, client->opt); + opt_included = true; + client->opt = NULL; + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + } + result = dns_message_rendersection(client->message, + DNS_SECTION_QUESTION, 0); + if (result == ISC_R_NOSPACE) { + client->message->flags |= DNS_MESSAGEFLAG_TC; + goto renderend; + } + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + /* + * Stop after the question if TC was set for rate limiting. + */ + if ((client->message->flags & DNS_MESSAGEFLAG_TC) != 0) { + goto renderend; + } + result = dns_message_rendersection(client->message, DNS_SECTION_ANSWER, + DNS_MESSAGERENDER_PARTIAL | + render_opts); + if (result == ISC_R_NOSPACE) { + client->message->flags |= DNS_MESSAGEFLAG_TC; + goto renderend; + } + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + result = dns_message_rendersection( + client->message, DNS_SECTION_AUTHORITY, + DNS_MESSAGERENDER_PARTIAL | render_opts); + if (result == ISC_R_NOSPACE) { + client->message->flags |= DNS_MESSAGEFLAG_TC; + goto renderend; + } + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + result = dns_message_rendersection(client->message, + DNS_SECTION_ADDITIONAL, + preferred_glue | render_opts); + if (result != ISC_R_SUCCESS && result != ISC_R_NOSPACE) { + goto cleanup; + } +renderend: + result = dns_message_renderend(client->message); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + +#ifdef HAVE_DNSTAP + memset(&zr, 0, sizeof(zr)); + if (((client->message->flags & DNS_MESSAGEFLAG_AA) != 0) && + (client->query.authzone != NULL)) + { + isc_result_t eresult; + isc_buffer_t b; + dns_name_t *zo = dns_zone_getorigin(client->query.authzone); + + isc_buffer_init(&b, zone, sizeof(zone)); + dns_compress_setmethods(&cctx, DNS_COMPRESS_NONE); + eresult = dns_name_towire(zo, &cctx, &b); + if (eresult == ISC_R_SUCCESS) { + isc_buffer_usedregion(&b, &zr); + } + } + + if (client->message->opcode == dns_opcode_update) { + dtmsgtype = DNS_DTTYPE_UR; + } else if ((client->message->flags & DNS_MESSAGEFLAG_RD) != 0) { + dtmsgtype = DNS_DTTYPE_CR; + } else { + dtmsgtype = DNS_DTTYPE_AR; + } +#endif /* HAVE_DNSTAP */ + + if (cleanup_cctx) { + dns_compress_invalidate(&cctx); + } + + if (client->sendcb != NULL) { + client->sendcb(&buffer); + } else if (TCP_CLIENT(client)) { + isc_buffer_usedregion(&buffer, &r); +#ifdef HAVE_DNSTAP + if (client->view != NULL) { + dns_dt_send(client->view, dtmsgtype, &client->peeraddr, + &client->destsockaddr, true, &zr, + &client->requesttime, NULL, &buffer); + } +#endif /* HAVE_DNSTAP */ + + respsize = isc_buffer_usedlength(&buffer); + + client_sendpkg(client, &buffer); + + switch (isc_sockaddr_pf(&client->peeraddr)) { + case AF_INET: + isc_stats_increment(client->sctx->tcpoutstats4, + ISC_MIN((int)respsize / 16, 256)); + break; + case AF_INET6: + isc_stats_increment(client->sctx->tcpoutstats6, + ISC_MIN((int)respsize / 16, 256)); + break; + default: + UNREACHABLE(); + } + } else { +#ifdef HAVE_DNSTAP + /* + * Log dnstap data first, because client_sendpkg() may + * leave client->view set to NULL. + */ + if (client->view != NULL) { + dns_dt_send(client->view, dtmsgtype, &client->peeraddr, + &client->destsockaddr, false, &zr, + &client->requesttime, NULL, &buffer); + } +#endif /* HAVE_DNSTAP */ + + respsize = isc_buffer_usedlength(&buffer); + + client_sendpkg(client, &buffer); + + switch (isc_sockaddr_pf(&client->peeraddr)) { + case AF_INET: + isc_stats_increment(client->sctx->udpoutstats4, + ISC_MIN((int)respsize / 16, 256)); + break; + case AF_INET6: + isc_stats_increment(client->sctx->udpoutstats6, + ISC_MIN((int)respsize / 16, 256)); + break; + default: + UNREACHABLE(); + } + } + + /* update statistics (XXXJT: is it okay to access message->xxxkey?) */ + ns_stats_increment(client->sctx->nsstats, ns_statscounter_response); + + dns_rcodestats_increment(client->sctx->rcodestats, + client->message->rcode); + if (opt_included) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_edns0out); + } + if (client->message->tsigkey != NULL) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_tsigout); + } + if (client->message->sig0key != NULL) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_sig0out); + } + if ((client->message->flags & DNS_MESSAGEFLAG_TC) != 0) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_truncatedresp); + } + + client->query.attributes |= NS_QUERYATTR_ANSWERED; + + return; + +cleanup: + if (client->tcpbuf != NULL) { + isc_mem_put(client->mctx, client->tcpbuf, + NS_CLIENT_TCP_BUFFER_SIZE); + client->tcpbuf = NULL; + } + + if (cleanup_cctx) { + dns_compress_invalidate(&cctx); + } +} + +#if NS_CLIENT_DROPPORT +#define DROPPORT_NO 0 +#define DROPPORT_REQUEST 1 +#define DROPPORT_RESPONSE 2 +/*% + * ns_client_dropport determines if certain requests / responses + * should be dropped based on the port number. + * + * Returns: + * \li 0: Don't drop. + * \li 1: Drop request. + * \li 2: Drop (error) response. + */ +static int +ns_client_dropport(in_port_t port) { + switch (port) { + case 7: /* echo */ + case 13: /* daytime */ + case 19: /* chargen */ + case 37: /* time */ + return (DROPPORT_REQUEST); + case 464: /* kpasswd */ + return (DROPPORT_RESPONSE); + } + return (DROPPORT_NO); +} +#endif /* if NS_CLIENT_DROPPORT */ + +void +ns_client_error(ns_client_t *client, isc_result_t result) { + dns_message_t *message = NULL; + dns_rcode_t rcode; + bool trunc = false; + + REQUIRE(NS_CLIENT_VALID(client)); + + CTRACE("error"); + + message = client->message; + + if (client->rcode_override == -1) { + rcode = dns_result_torcode(result); + } else { + rcode = (dns_rcode_t)(client->rcode_override & 0xfff); + } + + if (result == ISC_R_MAXSIZE) { + trunc = true; + } + +#if NS_CLIENT_DROPPORT + /* + * Don't send FORMERR to ports on the drop port list. + */ + if (rcode == dns_rcode_formerr && + ns_client_dropport(isc_sockaddr_getport(&client->peeraddr)) != + DROPPORT_NO) + { + char buf[64]; + isc_buffer_t b; + + isc_buffer_init(&b, buf, sizeof(buf) - 1); + if (dns_rcode_totext(rcode, &b) != ISC_R_SUCCESS) { + isc_buffer_putstr(&b, "UNKNOWN RCODE"); + } + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(10), + "dropped error (%.*s) response: suspicious port", + (int)isc_buffer_usedlength(&b), buf); + ns_client_drop(client, ISC_R_SUCCESS); + return; + } +#endif /* if NS_CLIENT_DROPPORT */ + + /* + * Try to rate limit error responses. + */ + if (client->view != NULL && client->view->rrl != NULL) { + bool wouldlog; + char log_buf[DNS_RRL_LOG_BUF_LEN]; + dns_rrl_result_t rrl_result; + int loglevel; + + if ((client->sctx->options & NS_SERVER_LOGQUERIES) != 0) { + loglevel = DNS_RRL_LOG_DROP; + } else { + loglevel = ISC_LOG_DEBUG(1); + } + wouldlog = isc_log_wouldlog(ns_lctx, loglevel); + rrl_result = dns_rrl(client->view, NULL, &client->peeraddr, + TCP_CLIENT(client), dns_rdataclass_in, + dns_rdatatype_none, NULL, result, + client->now, wouldlog, log_buf, + sizeof(log_buf)); + if (rrl_result != DNS_RRL_RESULT_OK) { + /* + * Log dropped errors in the query category + * so that they are not lost in silence. + * Starts of rate-limited bursts are logged in + * NS_LOGCATEGORY_RRL. + */ + if (wouldlog) { + ns_client_log(client, + NS_LOGCATEGORY_QUERY_ERRORS, + NS_LOGMODULE_CLIENT, loglevel, + "%s", log_buf); + } + /* + * Some error responses cannot be 'slipped', + * so don't try to slip any error responses. + */ + if (!client->view->rrl->log_only) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_ratedropped); + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_dropped); + ns_client_drop(client, DNS_R_DROP); + return; + } + } + } + + /* + * Message may be an in-progress reply that we had trouble + * with, in which case QR will be set. We need to clear QR before + * calling dns_message_reply() to avoid triggering an assertion. + */ + message->flags &= ~DNS_MESSAGEFLAG_QR; + /* + * AA and AD shouldn't be set. + */ + message->flags &= ~(DNS_MESSAGEFLAG_AA | DNS_MESSAGEFLAG_AD); + result = dns_message_reply(message, true); + if (result != ISC_R_SUCCESS) { + /* + * It could be that we've got a query with a good header, + * but a bad question section, so we try again with + * want_question_section set to false. + */ + result = dns_message_reply(message, false); + if (result != ISC_R_SUCCESS) { + ns_client_drop(client, result); + return; + } + } + + message->rcode = rcode; + if (trunc) { + message->flags |= DNS_MESSAGEFLAG_TC; + } + + if (rcode == dns_rcode_formerr) { + /* + * FORMERR loop avoidance: If we sent a FORMERR message + * with the same ID to the same client less than two + * seconds ago, assume that we are in an infinite error + * packet dialog with a server for some protocol whose + * error responses look enough like DNS queries to + * elicit a FORMERR response. Drop a packet to break + * the loop. + */ + if (isc_sockaddr_equal(&client->peeraddr, + &client->formerrcache.addr) && + message->id == client->formerrcache.id && + (isc_time_seconds(&client->requesttime) - + client->formerrcache.time) < 2) + { + /* Drop packet. */ + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "possible error packet loop, " + "FORMERR dropped"); + ns_client_drop(client, result); + return; + } + client->formerrcache.addr = client->peeraddr; + client->formerrcache.time = + isc_time_seconds(&client->requesttime); + client->formerrcache.id = message->id; + } else if (rcode == dns_rcode_servfail && client->query.qname != NULL && + client->view != NULL && client->view->fail_ttl != 0 && + ((client->attributes & NS_CLIENTATTR_NOSETFC) == 0)) + { + /* + * SERVFAIL caching: store qname/qtype of failed queries + */ + isc_time_t expire; + isc_interval_t i; + uint32_t flags = 0; + + if ((message->flags & DNS_MESSAGEFLAG_CD) != 0) { + flags = NS_FAILCACHE_CD; + } + + isc_interval_set(&i, client->view->fail_ttl, 0); + result = isc_time_nowplusinterval(&expire, &i); + if (result == ISC_R_SUCCESS) { + dns_badcache_add( + client->view->failcache, client->query.qname, + client->query.qtype, true, flags, &expire); + } + } + + ns_client_send(client); +} + +isc_result_t +ns_client_addopt(ns_client_t *client, dns_message_t *message, + dns_rdataset_t **opt) { + unsigned char ecs[ECS_SIZE]; + char nsid[_POSIX_HOST_NAME_MAX + 1], *nsidp = NULL; + unsigned char cookie[COOKIE_SIZE]; + isc_result_t result; + dns_view_t *view; + dns_resolver_t *resolver; + uint16_t udpsize; + dns_ednsopt_t ednsopts[DNS_EDNSOPTIONS]; + int count = 0; + unsigned int flags; + unsigned char expire[4]; + unsigned char advtimo[2]; + dns_aclenv_t *env; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(opt != NULL && *opt == NULL); + REQUIRE(message != NULL); + + env = ns_interfacemgr_getaclenv(client->manager->interface->mgr); + view = client->view; + resolver = (view != NULL) ? view->resolver : NULL; + if (resolver != NULL) { + udpsize = dns_resolver_getudpsize(resolver); + } else { + udpsize = client->sctx->udpsize; + } + + flags = client->extflags & DNS_MESSAGEEXTFLAG_REPLYPRESERVE; + + /* Set EDNS options if applicable */ + if (WANTNSID(client)) { + if (client->sctx->server_id != NULL) { + nsidp = client->sctx->server_id; + } else if (client->sctx->gethostname != NULL) { + result = client->sctx->gethostname(nsid, sizeof(nsid)); + if (result != ISC_R_SUCCESS) { + goto no_nsid; + } + nsidp = nsid; + } else { + goto no_nsid; + } + + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_NSID; + ednsopts[count].length = (uint16_t)strlen(nsidp); + ednsopts[count].value = (unsigned char *)nsidp; + count++; + } +no_nsid: + if ((client->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0) { + isc_buffer_t buf; + isc_stdtime_t now; + uint32_t nonce; + + isc_buffer_init(&buf, cookie, sizeof(cookie)); + isc_stdtime_get(&now); + + isc_random_buf(&nonce, sizeof(nonce)); + + compute_cookie(client, now, nonce, client->sctx->secret, &buf); + + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_COOKIE; + ednsopts[count].length = COOKIE_SIZE; + ednsopts[count].value = cookie; + count++; + } + if ((client->attributes & NS_CLIENTATTR_HAVEEXPIRE) != 0) { + isc_buffer_t buf; + + INSIST(count < DNS_EDNSOPTIONS); + + isc_buffer_init(&buf, expire, sizeof(expire)); + isc_buffer_putuint32(&buf, client->expire); + ednsopts[count].code = DNS_OPT_EXPIRE; + ednsopts[count].length = 4; + ednsopts[count].value = expire; + count++; + } + if (((client->attributes & NS_CLIENTATTR_HAVEECS) != 0) && + (client->ecs.addr.family == AF_INET || + client->ecs.addr.family == AF_INET6 || + client->ecs.addr.family == AF_UNSPEC)) + { + isc_buffer_t buf; + uint8_t addr[16]; + uint32_t plen, addrl; + uint16_t family = 0; + + /* Add CLIENT-SUBNET option. */ + + plen = client->ecs.source; + + /* Round up prefix len to a multiple of 8 */ + addrl = (plen + 7) / 8; + + switch (client->ecs.addr.family) { + case AF_UNSPEC: + INSIST(plen == 0); + family = 0; + break; + case AF_INET: + INSIST(plen <= 32); + family = 1; + memmove(addr, &client->ecs.addr.type, addrl); + break; + case AF_INET6: + INSIST(plen <= 128); + family = 2; + memmove(addr, &client->ecs.addr.type, addrl); + break; + default: + UNREACHABLE(); + } + + isc_buffer_init(&buf, ecs, sizeof(ecs)); + /* family */ + isc_buffer_putuint16(&buf, family); + /* source prefix-length */ + isc_buffer_putuint8(&buf, client->ecs.source); + /* scope prefix-length */ + isc_buffer_putuint8(&buf, client->ecs.scope); + + /* address */ + if (addrl > 0) { + /* Mask off last address byte */ + if ((plen % 8) != 0) { + addr[addrl - 1] &= ~0U << (8 - (plen % 8)); + } + isc_buffer_putmem(&buf, addr, (unsigned)addrl); + } + + ednsopts[count].code = DNS_OPT_CLIENT_SUBNET; + ednsopts[count].length = addrl + 4; + ednsopts[count].value = ecs; + count++; + } + if (TCP_CLIENT(client) && USEKEEPALIVE(client)) { + isc_buffer_t buf; + uint32_t adv; + + INSIST(count < DNS_EDNSOPTIONS); + + isc_nm_gettimeouts(isc_nmhandle_netmgr(client->handle), NULL, + NULL, NULL, &adv); + adv /= 100; /* units of 100 milliseconds */ + isc_buffer_init(&buf, advtimo, sizeof(advtimo)); + isc_buffer_putuint16(&buf, (uint16_t)adv); + ednsopts[count].code = DNS_OPT_TCP_KEEPALIVE; + ednsopts[count].length = 2; + ednsopts[count].value = advtimo; + count++; + } + + /* Padding must be added last */ + if ((view != NULL) && (view->padding > 0) && WANTPAD(client) && + (TCP_CLIENT(client) || + ((client->attributes & NS_CLIENTATTR_HAVECOOKIE) != 0))) + { + isc_netaddr_t netaddr; + int match; + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + result = dns_acl_match(&netaddr, NULL, view->pad_acl, env, + &match, NULL); + if (result == ISC_R_SUCCESS && match > 0) { + INSIST(count < DNS_EDNSOPTIONS); + + ednsopts[count].code = DNS_OPT_PAD; + ednsopts[count].length = 0; + ednsopts[count].value = NULL; + count++; + + dns_message_setpadding(message, view->padding); + } + } + + result = dns_message_buildopt(message, opt, 0, udpsize, flags, ednsopts, + count); + return (result); +} + +static void +compute_cookie(ns_client_t *client, uint32_t when, uint32_t nonce, + const unsigned char *secret, isc_buffer_t *buf) { + unsigned char digest[ISC_MAX_MD_SIZE] ISC_NONSTRING = { 0 }; + STATIC_ASSERT(ISC_MAX_MD_SIZE >= ISC_SIPHASH24_TAG_LENGTH, "You need " + "to " + "increase " + "the digest " + "buffer."); + STATIC_ASSERT(ISC_MAX_MD_SIZE >= ISC_AES_BLOCK_LENGTH, "You need to " + "increase the " + "digest " + "buffer."); + + switch (client->sctx->cookiealg) { + case ns_cookiealg_siphash24: { + unsigned char input[16 + 16] ISC_NONSTRING = { 0 }; + size_t inputlen = 0; + isc_netaddr_t netaddr; + unsigned char *cp; + + cp = isc_buffer_used(buf); + isc_buffer_putmem(buf, client->cookie, 8); + isc_buffer_putuint8(buf, NS_COOKIE_VERSION_1); + isc_buffer_putuint24(buf, 0); /* Reserved */ + isc_buffer_putuint32(buf, when); + + memmove(input, cp, 16); + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + switch (netaddr.family) { + case AF_INET: + cp = (unsigned char *)&netaddr.type.in; + memmove(input + 16, cp, 4); + inputlen = 20; + break; + case AF_INET6: + cp = (unsigned char *)&netaddr.type.in6; + memmove(input + 16, cp, 16); + inputlen = 32; + break; + default: + UNREACHABLE(); + } + + isc_siphash24(secret, input, inputlen, digest); + isc_buffer_putmem(buf, digest, 8); + break; + } + case ns_cookiealg_aes: { + unsigned char input[4 + 4 + 16] ISC_NONSTRING = { 0 }; + isc_netaddr_t netaddr; + unsigned char *cp; + unsigned int i; + + cp = isc_buffer_used(buf); + isc_buffer_putmem(buf, client->cookie, 8); + isc_buffer_putuint32(buf, nonce); + isc_buffer_putuint32(buf, when); + memmove(input, cp, 16); + isc_aes128_crypt(secret, input, digest); + for (i = 0; i < 8; i++) { + input[i] = digest[i] ^ digest[i + 8]; + } + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + switch (netaddr.family) { + case AF_INET: + cp = (unsigned char *)&netaddr.type.in; + memmove(input + 8, cp, 4); + memset(input + 12, 0, 4); + isc_aes128_crypt(secret, input, digest); + break; + case AF_INET6: + cp = (unsigned char *)&netaddr.type.in6; + memmove(input + 8, cp, 16); + isc_aes128_crypt(secret, input, digest); + for (i = 0; i < 8; i++) { + input[i + 8] = digest[i] ^ digest[i + 8]; + } + isc_aes128_crypt(client->sctx->secret, input + 8, + digest); + break; + default: + UNREACHABLE(); + } + for (i = 0; i < 8; i++) { + digest[i] ^= digest[i + 8]; + } + isc_buffer_putmem(buf, digest, 8); + break; + } + + default: + UNREACHABLE(); + } +} + +static void +process_cookie(ns_client_t *client, isc_buffer_t *buf, size_t optlen) { + ns_altsecret_t *altsecret; + unsigned char dbuf[COOKIE_SIZE]; + unsigned char *old; + isc_stdtime_t now; + uint32_t when; + uint32_t nonce; + isc_buffer_t db; + + /* + * If we have already seen a cookie option skip this cookie option. + */ + if ((!client->sctx->answercookie) || + (client->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0) + { + isc_buffer_forward(buf, (unsigned int)optlen); + return; + } + + client->attributes |= NS_CLIENTATTR_WANTCOOKIE; + + ns_stats_increment(client->sctx->nsstats, ns_statscounter_cookiein); + + if (optlen != COOKIE_SIZE) { + /* + * Not our token. + */ + INSIST(optlen >= 8U); + memmove(client->cookie, isc_buffer_current(buf), 8); + isc_buffer_forward(buf, (unsigned int)optlen); + + if (optlen == 8U) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookienew); + } else { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookiebadsize); + } + return; + } + + /* + * Process all of the incoming buffer. + */ + old = isc_buffer_current(buf); + memmove(client->cookie, old, 8); + isc_buffer_forward(buf, 8); + nonce = isc_buffer_getuint32(buf); + when = isc_buffer_getuint32(buf); + isc_buffer_forward(buf, 8); + + /* + * Allow for a 5 minute clock skew between servers sharing a secret. + * Only accept COOKIE if we have talked to the client in the last hour. + */ + isc_stdtime_get(&now); + if (isc_serial_gt(when, (now + 300)) || /* In the future. */ + isc_serial_lt(when, (now - 3600))) + { /* In the past. */ + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookiebadtime); + return; + } + + isc_buffer_init(&db, dbuf, sizeof(dbuf)); + compute_cookie(client, when, nonce, client->sctx->secret, &db); + + if (isc_safe_memequal(old, dbuf, COOKIE_SIZE)) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookiematch); + client->attributes |= NS_CLIENTATTR_HAVECOOKIE; + return; + } + + for (altsecret = ISC_LIST_HEAD(client->sctx->altsecrets); + altsecret != NULL; altsecret = ISC_LIST_NEXT(altsecret, link)) + { + isc_buffer_init(&db, dbuf, sizeof(dbuf)); + compute_cookie(client, when, nonce, altsecret->secret, &db); + if (isc_safe_memequal(old, dbuf, COOKIE_SIZE)) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookiematch); + client->attributes |= NS_CLIENTATTR_HAVECOOKIE; + return; + } + } + + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_cookienomatch); +} + +static isc_result_t +process_ecs(ns_client_t *client, isc_buffer_t *buf, size_t optlen) { + uint16_t family; + uint8_t addrlen, addrbytes, scope, *paddr; + isc_netaddr_t caddr; + + /* + * If we have already seen a ECS option skip this ECS option. + */ + if ((client->attributes & NS_CLIENTATTR_HAVEECS) != 0) { + isc_buffer_forward(buf, (unsigned int)optlen); + return (ISC_R_SUCCESS); + } + + /* + * XXXMUKS: Is there any need to repeat these checks here + * (except query's scope length) when they are done in the OPT + * RDATA fromwire code? + */ + + if (optlen < 4U) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option too short"); + return (DNS_R_FORMERR); + } + + family = isc_buffer_getuint16(buf); + addrlen = isc_buffer_getuint8(buf); + scope = isc_buffer_getuint8(buf); + optlen -= 4; + + if (scope != 0U) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: invalid scope"); + return (DNS_R_OPTERR); + } + + memset(&caddr, 0, sizeof(caddr)); + switch (family) { + case 0: + /* + * XXXMUKS: In queries, if FAMILY is set to 0, SOURCE + * PREFIX-LENGTH must be 0 and ADDRESS should not be + * present as the address and prefix lengths don't make + * sense because the family is unknown. + */ + if (addrlen != 0U) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: invalid " + "address length (%u) for FAMILY=0", + addrlen); + return (DNS_R_OPTERR); + } + caddr.family = AF_UNSPEC; + break; + case 1: + if (addrlen > 32U) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: invalid " + "address length (%u) for IPv4", + addrlen); + return (DNS_R_OPTERR); + } + caddr.family = AF_INET; + break; + case 2: + if (addrlen > 128U) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: invalid " + "address length (%u) for IPv6", + addrlen); + return (DNS_R_OPTERR); + } + caddr.family = AF_INET6; + break; + default: + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: invalid family"); + return (DNS_R_OPTERR); + } + + addrbytes = (addrlen + 7) / 8; + if (isc_buffer_remaininglength(buf) < addrbytes) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(2), + "EDNS client-subnet option: address too short"); + return (DNS_R_OPTERR); + } + + paddr = (uint8_t *)&caddr.type; + if (addrbytes != 0U) { + memmove(paddr, isc_buffer_current(buf), addrbytes); + isc_buffer_forward(buf, addrbytes); + optlen -= addrbytes; + + if ((addrlen % 8) != 0) { + uint8_t bits = ~0U << (8 - (addrlen % 8)); + bits &= paddr[addrbytes - 1]; + if (bits != paddr[addrbytes - 1]) { + return (DNS_R_OPTERR); + } + } + } + + memmove(&client->ecs.addr, &caddr, sizeof(caddr)); + client->ecs.source = addrlen; + client->ecs.scope = 0; + client->attributes |= NS_CLIENTATTR_HAVEECS; + + isc_buffer_forward(buf, (unsigned int)optlen); + return (ISC_R_SUCCESS); +} + +static isc_result_t +process_keytag(ns_client_t *client, isc_buffer_t *buf, size_t optlen) { + if (optlen == 0 || (optlen % 2) != 0) { + isc_buffer_forward(buf, (unsigned int)optlen); + return (DNS_R_OPTERR); + } + + /* Silently drop additional keytag options. */ + if (client->keytag != NULL) { + isc_buffer_forward(buf, (unsigned int)optlen); + return (ISC_R_SUCCESS); + } + + client->keytag = isc_mem_get(client->mctx, optlen); + { + client->keytag_len = (uint16_t)optlen; + memmove(client->keytag, isc_buffer_current(buf), optlen); + } + isc_buffer_forward(buf, (unsigned int)optlen); + return (ISC_R_SUCCESS); +} + +static isc_result_t +process_opt(ns_client_t *client, dns_rdataset_t *opt) { + dns_rdata_t rdata; + isc_buffer_t optbuf; + isc_result_t result; + uint16_t optcode; + uint16_t optlen; + + /* + * Set the client's UDP buffer size. + */ + client->udpsize = opt->rdclass; + + /* + * If the requested UDP buffer size is less than 512, + * ignore it and use 512. + */ + if (client->udpsize < 512) { + client->udpsize = 512; + } + + /* + * Get the flags out of the OPT record. + */ + client->extflags = (uint16_t)(opt->ttl & 0xFFFF); + + /* + * Do we understand this version of EDNS? + * + * XXXRTH need library support for this! + */ + client->ednsversion = (opt->ttl & 0x00FF0000) >> 16; + if (client->ednsversion > DNS_EDNS_VERSION) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_badednsver); + result = ns_client_addopt(client, client->message, + &client->opt); + if (result == ISC_R_SUCCESS) { + result = DNS_R_BADVERS; + } + ns_client_error(client, result); + return (result); + } + + /* Check for NSID request */ + result = dns_rdataset_first(opt); + if (result == ISC_R_SUCCESS) { + dns_rdata_init(&rdata); + dns_rdataset_current(opt, &rdata); + isc_buffer_init(&optbuf, rdata.data, rdata.length); + isc_buffer_add(&optbuf, rdata.length); + while (isc_buffer_remaininglength(&optbuf) >= 4) { + optcode = isc_buffer_getuint16(&optbuf); + optlen = isc_buffer_getuint16(&optbuf); + switch (optcode) { + case DNS_OPT_NSID: + if (!WANTNSID(client)) { + ns_stats_increment( + client->sctx->nsstats, + ns_statscounter_nsidopt); + } + client->attributes |= NS_CLIENTATTR_WANTNSID; + isc_buffer_forward(&optbuf, optlen); + break; + case DNS_OPT_COOKIE: + process_cookie(client, &optbuf, optlen); + break; + case DNS_OPT_EXPIRE: + if (!WANTEXPIRE(client)) { + ns_stats_increment( + client->sctx->nsstats, + ns_statscounter_expireopt); + } + client->attributes |= NS_CLIENTATTR_WANTEXPIRE; + isc_buffer_forward(&optbuf, optlen); + break; + case DNS_OPT_CLIENT_SUBNET: + result = process_ecs(client, &optbuf, optlen); + if (result != ISC_R_SUCCESS) { + ns_client_error(client, result); + return (result); + } + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_ecsopt); + break; + case DNS_OPT_TCP_KEEPALIVE: + if (!USEKEEPALIVE(client)) { + ns_stats_increment( + client->sctx->nsstats, + ns_statscounter_keepaliveopt); + } + client->attributes |= + NS_CLIENTATTR_USEKEEPALIVE; + isc_nmhandle_keepalive(client->handle, true); + isc_buffer_forward(&optbuf, optlen); + break; + case DNS_OPT_PAD: + client->attributes |= NS_CLIENTATTR_WANTPAD; + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_padopt); + isc_buffer_forward(&optbuf, optlen); + break; + case DNS_OPT_KEY_TAG: + result = process_keytag(client, &optbuf, + optlen); + if (result != ISC_R_SUCCESS) { + ns_client_error(client, result); + return (result); + } + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_keytagopt); + break; + default: + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_otheropt); + isc_buffer_forward(&optbuf, optlen); + break; + } + } + } + + ns_stats_increment(client->sctx->nsstats, ns_statscounter_edns0in); + client->attributes |= NS_CLIENTATTR_WANTOPT; + + return (result); +} + +void +ns__client_reset_cb(void *client0) { + ns_client_t *client = client0; + + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), "reset client"); + + /* + * We never started processing this client, possible if we're + * shutting down, just exit. + */ + if (client->state == NS_CLIENTSTATE_READY) { + return; + } + + ns_client_endrequest(client); + if (client->tcpbuf != NULL) { + isc_mem_put(client->mctx, client->tcpbuf, + NS_CLIENT_TCP_BUFFER_SIZE); + } + + if (client->keytag != NULL) { + isc_mem_put(client->mctx, client->keytag, client->keytag_len); + client->keytag_len = 0; + } + + client->state = NS_CLIENTSTATE_READY; + INSIST(client->recursionquota == NULL); +} + +void +ns__client_put_cb(void *client0) { + ns_client_t *client = client0; + + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), "freeing client"); + + /* + * Call this first because it requires a valid client. + */ + ns_query_free(client); + + client->magic = 0; + client->shuttingdown = true; + + if (client->manager != NULL) { + clientmgr_detach(&client->manager); + } + + isc_mem_put(client->mctx, client->sendbuf, NS_CLIENT_SEND_BUFFER_SIZE); + if (client->opt != NULL) { + INSIST(dns_rdataset_isassociated(client->opt)); + dns_rdataset_disassociate(client->opt); + dns_message_puttemprdataset(client->message, &client->opt); + } + + dns_message_detach(&client->message); + + /* + * Detaching the task must be done after unlinking from + * the manager's lists because the manager accesses + * client->task. + */ + if (client->task != NULL) { + isc_task_detach(&client->task); + } + + /* + * Destroy the fetchlock mutex that was created in + * ns_query_init(). + */ + isc_mutex_destroy(&client->query.fetchlock); + + if (client->sctx != NULL) { + ns_server_detach(&client->sctx); + } + + if (client->mctx != NULL) { + isc_mem_detach(&client->mctx); + } +} + +/* + * Handle an incoming request event from the socket (UDP case) + * or tcpmsg (TCP case). + */ +void +ns__client_request(isc_nmhandle_t *handle, isc_result_t eresult, + isc_region_t *region, void *arg) { + ns_client_t *client = NULL; + isc_result_t result; + isc_result_t sigresult = ISC_R_SUCCESS; + isc_buffer_t *buffer = NULL; + isc_buffer_t tbuffer; + dns_rdataset_t *opt = NULL; + const dns_name_t *signame = NULL; + bool ra; /* Recursion available. */ + isc_netaddr_t netaddr; + int match; + dns_messageid_t id; + unsigned int flags; + bool notimp; + size_t reqsize; + dns_aclenv_t *env = NULL; +#ifdef HAVE_DNSTAP + dns_dtmsgtype_t dtmsgtype; +#endif /* ifdef HAVE_DNSTAP */ + + if (eresult != ISC_R_SUCCESS) { + return; + } + + client = isc_nmhandle_getdata(handle); + if (client == NULL) { + ns_interface_t *ifp = (ns_interface_t *)arg; + + INSIST(VALID_MANAGER(ifp->clientmgr)); + + client = isc_nmhandle_getextra(handle); + + result = ns__client_setup(client, ifp->clientmgr, true); + if (result != ISC_R_SUCCESS) { + return; + } + + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "allocate new client"); + } else { + result = ns__client_setup(client, NULL, false); + if (result != ISC_R_SUCCESS) { + return; + } + } + + client->state = NS_CLIENTSTATE_READY; + + isc_task_pause(client->task); + if (client->handle == NULL) { + isc_nmhandle_setdata(handle, client, ns__client_reset_cb, + ns__client_put_cb); + client->handle = handle; + } + + if (isc_nmhandle_is_stream(handle)) { + client->attributes |= NS_CLIENTATTR_TCP; + } + + INSIST(client->recursionquota == NULL); + + INSIST(client->state == NS_CLIENTSTATE_READY); + + (void)atomic_fetch_add_relaxed(&ns_client_requests, 1); + + isc_buffer_init(&tbuffer, region->base, region->length); + isc_buffer_add(&tbuffer, region->length); + buffer = &tbuffer; + + client->peeraddr = isc_nmhandle_peeraddr(handle); + client->peeraddr_valid = true; + + reqsize = isc_buffer_usedlength(buffer); + + client->state = NS_CLIENTSTATE_WORKING; + + TIME_NOW(&client->requesttime); + client->tnow = client->requesttime; + client->now = isc_time_seconds(&client->tnow); + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + +#if NS_CLIENT_DROPPORT + if (ns_client_dropport(isc_sockaddr_getport(&client->peeraddr)) == + DROPPORT_REQUEST) + { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(10), + "dropped request: suspicious port"); + isc_task_unpause(client->task); + return; + } +#endif /* if NS_CLIENT_DROPPORT */ + + env = ns_interfacemgr_getaclenv(client->manager->interface->mgr); + if (client->sctx->blackholeacl != NULL && + (dns_acl_match(&netaddr, NULL, client->sctx->blackholeacl, env, + &match, NULL) == ISC_R_SUCCESS) && + match > 0) + { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(10), + "dropped request: blackholed peer"); + isc_task_unpause(client->task); + return; + } + + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), "%s request", + TCP_CLIENT(client) ? "TCP" : "UDP"); + + result = dns_message_peekheader(buffer, &id, &flags); + if (result != ISC_R_SUCCESS) { + /* + * There isn't enough header to determine whether + * this was a request or a response. Drop it. + */ + isc_task_unpause(client->task); + return; + } + + /* + * The client object handles requests, not responses. + * If this is a UDP response, forward it to the dispatcher. + * If it's a TCP response, discard it here. + */ + if ((flags & DNS_MESSAGEFLAG_QR) != 0) { + CTRACE("unexpected response"); + isc_task_unpause(client->task); + return; + } + + /* + * Update some statistics counters. Don't count responses. + */ + if (isc_sockaddr_pf(&client->peeraddr) == PF_INET) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_requestv4); + } else { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_requestv6); + } + if (TCP_CLIENT(client)) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_requesttcp); + switch (isc_sockaddr_pf(&client->peeraddr)) { + case AF_INET: + isc_stats_increment(client->sctx->tcpinstats4, + ISC_MIN((int)reqsize / 16, 18)); + break; + case AF_INET6: + isc_stats_increment(client->sctx->tcpinstats6, + ISC_MIN((int)reqsize / 16, 18)); + break; + default: + UNREACHABLE(); + } + } else { + switch (isc_sockaddr_pf(&client->peeraddr)) { + case AF_INET: + isc_stats_increment(client->sctx->udpinstats4, + ISC_MIN((int)reqsize / 16, 18)); + break; + case AF_INET6: + isc_stats_increment(client->sctx->udpinstats6, + ISC_MIN((int)reqsize / 16, 18)); + break; + default: + UNREACHABLE(); + } + } + + /* + * It's a request. Parse it. + */ + result = dns_message_parse(client->message, buffer, 0); + if (result != ISC_R_SUCCESS) { + /* + * Parsing the request failed. Send a response + * (typically FORMERR or SERVFAIL). + */ + if (result == DNS_R_OPTERR) { + (void)ns_client_addopt(client, client->message, + &client->opt); + } + + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "message parsing failed: %s", + isc_result_totext(result)); + if (result == ISC_R_NOSPACE || result == DNS_R_BADTSIG) { + result = DNS_R_FORMERR; + } + ns_client_error(client, result); + isc_task_unpause(client->task); + return; + } + + /* + * Disable pipelined TCP query processing if necessary. + */ + if (TCP_CLIENT(client) && + (client->message->opcode != dns_opcode_query || + (client->sctx->keepresporder != NULL && + dns_acl_allowed(&netaddr, NULL, client->sctx->keepresporder, + env)))) + { + isc_nm_tcpdns_sequential(handle); + } + + dns_opcodestats_increment(client->sctx->opcodestats, + client->message->opcode); + switch (client->message->opcode) { + case dns_opcode_query: + case dns_opcode_update: + case dns_opcode_notify: + notimp = false; + break; + case dns_opcode_iquery: + default: + notimp = true; + break; + } + + client->message->rcode = dns_rcode_noerror; + + /* + * Deal with EDNS. + */ + if ((client->sctx->options & NS_SERVER_NOEDNS) != 0) { + opt = NULL; + } else { + opt = dns_message_getopt(client->message); + } + + client->ecs.source = 0; + client->ecs.scope = 0; + + if (opt != NULL) { + /* + * Are returning FORMERR to all EDNS queries? + * Simulate a STD13 compliant server. + */ + if ((client->sctx->options & NS_SERVER_EDNSFORMERR) != 0) { + ns_client_error(client, DNS_R_FORMERR); + isc_task_unpause(client->task); + return; + } + + /* + * Are returning NOTIMP to all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_EDNSNOTIMP) != 0) { + ns_client_error(client, DNS_R_NOTIMP); + isc_task_unpause(client->task); + return; + } + + /* + * Are returning REFUSED to all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_EDNSREFUSED) != 0) { + ns_client_error(client, DNS_R_REFUSED); + isc_task_unpause(client->task); + return; + } + + /* + * Are we dropping all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_DROPEDNS) != 0) { + ns_client_drop(client, ISC_R_SUCCESS); + isc_task_unpause(client->task); + return; + } + + result = process_opt(client, opt); + if (result != ISC_R_SUCCESS) { + isc_task_unpause(client->task); + return; + } + } + + if (client->message->rdclass == 0) { + if ((client->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && + client->message->opcode == dns_opcode_query && + client->message->counts[DNS_SECTION_QUESTION] == 0U) + { + result = dns_message_reply(client->message, true); + if (result != ISC_R_SUCCESS) { + ns_client_error(client, result); + isc_task_unpause(client->task); + return; + } + + if (notimp) { + client->message->rcode = dns_rcode_notimp; + } + + ns_client_send(client); + isc_task_unpause(client->task); + return; + } + + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "message class could not be determined"); + ns_client_dumpmessage(client, "message class could not be " + "determined"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + isc_task_unpause(client->task); + return; + } + + if ((client->manager->interface->flags & NS_INTERFACEFLAG_ANYADDR) == 0) + { + client->destsockaddr = client->manager->interface->addr; + } else { + client->destsockaddr = isc_nmhandle_localaddr(handle); + } + isc_netaddr_fromsockaddr(&client->destaddr, &client->destsockaddr); + + result = client->sctx->matchingview(&netaddr, &client->destaddr, + client->message, env, &sigresult, + &client->view); + if (result != ISC_R_SUCCESS) { + char classname[DNS_RDATACLASS_FORMATSIZE]; + + /* + * Do a dummy TSIG verification attempt so that the + * response will have a TSIG if the query did, as + * required by RFC2845. + */ + isc_buffer_t b; + isc_region_t *r; + + dns_message_resetsig(client->message); + + r = dns_message_getrawmessage(client->message); + isc_buffer_init(&b, r->base, r->length); + isc_buffer_add(&b, r->length); + (void)dns_tsig_verify(&b, client->message, NULL, NULL); + + dns_rdataclass_format(client->message->rdclass, classname, + sizeof(classname)); + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "no matching view in class '%s'", classname); + ns_client_dumpmessage(client, "no matching view in class"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_REFUSED); + isc_task_unpause(client->task); + return; + } + + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(5), "using view '%s'", client->view->name); + + /* + * Check for a signature. We log bad signatures regardless of + * whether they ultimately cause the request to be rejected or + * not. We do not log the lack of a signature unless we are + * debugging. + */ + client->signer = NULL; + dns_name_init(&client->signername, NULL); + result = dns_message_signer(client->message, &client->signername); + if (result != ISC_R_NOTFOUND) { + signame = NULL; + if (dns_message_gettsig(client->message, &signame) != NULL) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_tsigin); + } else { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_sig0in); + } + } + if (result == ISC_R_SUCCESS) { + char namebuf[DNS_NAME_FORMATSIZE]; + dns_name_format(&client->signername, namebuf, sizeof(namebuf)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "request has valid signature: %s", namebuf); + client->signer = &client->signername; + } else if (result == ISC_R_NOTFOUND) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "request is not signed"); + } else if (result == DNS_R_NOIDENTITY) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "request is signed by a nonauthoritative key"); + } else { + char tsigrcode[64]; + isc_buffer_t b; + dns_rcode_t status; + isc_result_t tresult; + + /* There is a signature, but it is bad. */ + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_invalidsig); + signame = NULL; + if (dns_message_gettsig(client->message, &signame) != NULL) { + char namebuf[DNS_NAME_FORMATSIZE]; + char cnamebuf[DNS_NAME_FORMATSIZE]; + dns_name_format(signame, namebuf, sizeof(namebuf)); + status = client->message->tsigstatus; + isc_buffer_init(&b, tsigrcode, sizeof(tsigrcode) - 1); + tresult = dns_tsigrcode_totext(status, &b); + INSIST(tresult == ISC_R_SUCCESS); + tsigrcode[isc_buffer_usedlength(&b)] = '\0'; + if (client->message->tsigkey->generated) { + dns_name_format( + client->message->tsigkey->creator, + cnamebuf, sizeof(cnamebuf)); + ns_client_log( + client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_ERROR, + "request has invalid signature: " + "TSIG %s (%s): %s (%s)", + namebuf, cnamebuf, + isc_result_totext(result), tsigrcode); + } else { + ns_client_log( + client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_ERROR, + "request has invalid signature: " + "TSIG %s: %s (%s)", + namebuf, isc_result_totext(result), + tsigrcode); + } + } else { + status = client->message->sig0status; + isc_buffer_init(&b, tsigrcode, sizeof(tsigrcode) - 1); + tresult = dns_tsigrcode_totext(status, &b); + INSIST(tresult == ISC_R_SUCCESS); + tsigrcode[isc_buffer_usedlength(&b)] = '\0'; + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_ERROR, + "request has invalid signature: %s (%s)", + isc_result_totext(result), tsigrcode); + } + + /* + * Accept update messages signed by unknown keys so that + * update forwarding works transparently through slaves + * that don't have all the same keys as the master. + */ + if (!(client->message->tsigstatus == dns_tsigerror_badkey && + client->message->opcode == dns_opcode_update)) + { + ns_client_error(client, sigresult); + isc_task_unpause(client->task); + return; + } + } + + /* + * Decide whether recursive service is available to this client. + * We do this here rather than in the query code so that we can + * set the RA bit correctly on all kinds of responses, not just + * responses to ordinary queries. Note if you can't query the + * cache there is no point in setting RA. + */ + ra = false; + if (client->view->resolver != NULL && client->view->recursion && + ns_client_checkaclsilent(client, NULL, client->view->recursionacl, + true) == ISC_R_SUCCESS && + ns_client_checkaclsilent(client, NULL, client->view->cacheacl, + true) == ISC_R_SUCCESS && + ns_client_checkaclsilent(client, &client->destaddr, + client->view->recursiononacl, + true) == ISC_R_SUCCESS && + ns_client_checkaclsilent(client, &client->destaddr, + client->view->cacheonacl, + true) == ISC_R_SUCCESS) + { + ra = true; + } + + if (ra) { + client->attributes |= NS_CLIENTATTR_RA; + } + + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), + ra ? "recursion available" : "recursion not available"); + + /* + * Adjust maximum UDP response size for this client. + */ + if (client->udpsize > 512) { + dns_peer_t *peer = NULL; + uint16_t udpsize = client->view->maxudp; + (void)dns_peerlist_peerbyaddr(client->view->peers, &netaddr, + &peer); + if (peer != NULL) { + dns_peer_getmaxudp(peer, &udpsize); + } + if (client->udpsize > udpsize) { + client->udpsize = udpsize; + } + } + + /* + * Dispatch the request. + */ + switch (client->message->opcode) { + case dns_opcode_query: + CTRACE("query"); +#ifdef HAVE_DNSTAP + if (ra && (client->message->flags & DNS_MESSAGEFLAG_RD) != 0) { + dtmsgtype = DNS_DTTYPE_CQ; + } else { + dtmsgtype = DNS_DTTYPE_AQ; + } + + dns_dt_send(client->view, dtmsgtype, &client->peeraddr, + &client->destsockaddr, TCP_CLIENT(client), NULL, + &client->requesttime, NULL, buffer); +#endif /* HAVE_DNSTAP */ + + ns_query_start(client, handle); + break; + case dns_opcode_update: + CTRACE("update"); +#ifdef HAVE_DNSTAP + dns_dt_send(client->view, DNS_DTTYPE_UQ, &client->peeraddr, + &client->destsockaddr, TCP_CLIENT(client), NULL, + &client->requesttime, NULL, buffer); +#endif /* HAVE_DNSTAP */ + ns_client_settimeout(client, 60); + ns_update_start(client, handle, sigresult); + break; + case dns_opcode_notify: + CTRACE("notify"); + ns_client_settimeout(client, 60); + ns_notify_start(client, handle); + break; + case dns_opcode_iquery: + CTRACE("iquery"); + ns_client_error(client, DNS_R_NOTIMP); + break; + default: + CTRACE("unknown opcode"); + ns_client_error(client, DNS_R_NOTIMP); + } + + isc_task_unpause(client->task); +} + +isc_result_t +ns__client_tcpconn(isc_nmhandle_t *handle, isc_result_t result, void *arg) { + ns_interface_t *ifp = (ns_interface_t *)arg; + dns_aclenv_t *env = ns_interfacemgr_getaclenv(ifp->mgr); + ns_server_t *sctx = ns_interfacemgr_getserver(ifp->mgr); + unsigned int tcpquota; + isc_sockaddr_t peeraddr; + isc_netaddr_t netaddr; + int match; + + if (result != ISC_R_SUCCESS) { + return (result); + } + + if (handle != NULL) { + peeraddr = isc_nmhandle_peeraddr(handle); + isc_netaddr_fromsockaddr(&netaddr, &peeraddr); + + if (sctx->blackholeacl != NULL && + (dns_acl_match(&netaddr, NULL, sctx->blackholeacl, env, + &match, NULL) == ISC_R_SUCCESS) && + match > 0) + { + return (ISC_R_CONNREFUSED); + } + } + + tcpquota = isc_quota_getused(&sctx->tcpquota); + ns_stats_update_if_greater(sctx->nsstats, ns_statscounter_tcphighwater, + tcpquota); + + return (ISC_R_SUCCESS); +} + +static void +get_clientmctx(ns_clientmgr_t *manager, isc_mem_t **mctxp) { + isc_mem_t *clientmctx; + MTRACE("clientmctx"); + + int tid = isc_nm_tid(); + if (tid < 0) { + tid = isc_random_uniform(manager->ncpus); + } + int rand = isc_random_uniform(CLIENT_NMCTXS_PERCPU); + int nextmctx = (rand * manager->ncpus) + tid; + clientmctx = manager->mctxpool[nextmctx]; + + isc_mem_attach(clientmctx, mctxp); +} + +static void +get_clienttask(ns_clientmgr_t *manager, isc_task_t **taskp) { + MTRACE("clienttask"); + + int tid = isc_nm_tid(); + if (tid < 0) { + tid = isc_random_uniform(manager->ncpus); + } + + int rand = isc_random_uniform(CLIENT_NTASKS_PERCPU); + int nexttask = (rand * manager->ncpus) + tid; + isc_task_attach(manager->taskpool[nexttask], taskp); +} + +isc_result_t +ns__client_setup(ns_client_t *client, ns_clientmgr_t *mgr, bool new) { + isc_result_t result; + + /* + * Caller must be holding the manager lock. + * + * Note: creating a client does not add the client to the + * manager's client list or set the client's manager pointer. + * The caller is responsible for that. + */ + + REQUIRE(NS_CLIENT_VALID(client) || (new &&client != NULL)); + REQUIRE(VALID_MANAGER(mgr) || !new); + + if (new) { + *client = (ns_client_t){ .magic = 0 }; + + get_clientmctx(mgr, &client->mctx); + clientmgr_attach(mgr, &client->manager); + ns_server_attach(mgr->sctx, &client->sctx); + get_clienttask(mgr, &client->task); + + dns_message_create(client->mctx, DNS_MESSAGE_INTENTPARSE, + &client->message); + + client->sendbuf = isc_mem_get(client->mctx, + NS_CLIENT_SEND_BUFFER_SIZE); + /* + * Set magic earlier than usual because ns_query_init() + * and the functions it calls will require it. + */ + client->magic = NS_CLIENT_MAGIC; + result = ns_query_init(client); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + } else { + ns_clientmgr_t *oldmgr = client->manager; + ns_server_t *sctx = client->sctx; + isc_task_t *task = client->task; + unsigned char *sendbuf = client->sendbuf; + dns_message_t *message = client->message; + isc_mem_t *oldmctx = client->mctx; + ns_query_t query = client->query; + + /* + * Retain these values from the existing client, but + * zero every thing else. + */ + *client = (ns_client_t){ .magic = 0, + .mctx = oldmctx, + .manager = oldmgr, + .sctx = sctx, + .task = task, + .sendbuf = sendbuf, + .message = message, + .query = query }; + } + + client->query.attributes &= ~NS_QUERYATTR_ANSWERED; + client->state = NS_CLIENTSTATE_INACTIVE; + client->udpsize = 512; + client->ednsversion = -1; + dns_name_init(&client->signername, NULL); + dns_ecs_init(&client->ecs); + isc_sockaddr_any(&client->formerrcache.addr); + client->formerrcache.time = 0; + client->formerrcache.id = 0; + ISC_LINK_INIT(client, rlink); + client->rcode_override = -1; /* not set */ + + client->magic = NS_CLIENT_MAGIC; + + CTRACE("client_setup"); + + return (ISC_R_SUCCESS); + +cleanup: + if (client->sendbuf != NULL) { + isc_mem_put(client->mctx, client->sendbuf, + NS_CLIENT_SEND_BUFFER_SIZE); + } + + if (client->message != NULL) { + dns_message_detach(&client->message); + } + + if (client->task != NULL) { + isc_task_detach(&client->task); + } + + if (client->manager != NULL) { + clientmgr_detach(&client->manager); + } + if (client->mctx != NULL) { + isc_mem_detach(&client->mctx); + } + if (client->sctx != NULL) { + ns_server_detach(&client->sctx); + } + + return (result); +} + +bool +ns_client_shuttingdown(ns_client_t *client) { + return (client->shuttingdown); +} + +/*** + *** Client Manager + ***/ + +static void +clientmgr_attach(ns_clientmgr_t *source, ns_clientmgr_t **targetp) { + int32_t oldrefs; + + REQUIRE(VALID_MANAGER(source)); + REQUIRE(targetp != NULL && *targetp == NULL); + + oldrefs = isc_refcount_increment0(&source->references); + isc_log_write(ns_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), "clientmgr @%p attach: %d", source, + oldrefs + 1); + + *targetp = source; +} + +static void +clientmgr_detach(ns_clientmgr_t **mp) { + int32_t oldrefs; + ns_clientmgr_t *mgr = *mp; + *mp = NULL; + + oldrefs = isc_refcount_decrement(&mgr->references); + isc_log_write(ns_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(3), "clientmgr @%p detach: %d", mgr, + oldrefs - 1); + if (oldrefs == 1) { + clientmgr_destroy(mgr); + } +} + +static void +clientmgr_destroy(ns_clientmgr_t *manager) { + int i; + + MTRACE("clientmgr_destroy"); + + isc_refcount_destroy(&manager->references); + manager->magic = 0; + + for (i = 0; i < manager->ncpus * CLIENT_NMCTXS_PERCPU; i++) { + isc_mem_detach(&manager->mctxpool[i]); + } + isc_mem_put(manager->mctx, manager->mctxpool, + manager->ncpus * CLIENT_NMCTXS_PERCPU * + sizeof(isc_mem_t *)); + + if (manager->interface != NULL) { + ns_interface_detach(&manager->interface); + } + + isc_mutex_destroy(&manager->lock); + isc_mutex_destroy(&manager->reclock); + + if (manager->excl != NULL) { + isc_task_detach(&manager->excl); + } + + for (i = 0; i < manager->ncpus * CLIENT_NTASKS_PERCPU; i++) { + if (manager->taskpool[i] != NULL) { + isc_task_detach(&manager->taskpool[i]); + } + } + isc_mem_put(manager->mctx, manager->taskpool, + manager->ncpus * CLIENT_NTASKS_PERCPU * + sizeof(isc_task_t *)); + ns_server_detach(&manager->sctx); + + isc_mem_put(manager->mctx, manager, sizeof(*manager)); +} + +isc_result_t +ns_clientmgr_create(isc_mem_t *mctx, ns_server_t *sctx, isc_taskmgr_t *taskmgr, + isc_timermgr_t *timermgr, ns_interface_t *interface, + int ncpus, ns_clientmgr_t **managerp) { + ns_clientmgr_t *manager; + isc_result_t result; + int i; + int npools; + + manager = isc_mem_get(mctx, sizeof(*manager)); + *manager = (ns_clientmgr_t){ .magic = 0 }; + + isc_mutex_init(&manager->lock); + isc_mutex_init(&manager->reclock); + + manager->excl = NULL; + result = isc_taskmgr_excltask(taskmgr, &manager->excl); + if (result != ISC_R_SUCCESS) { + goto cleanup_reclock; + } + + manager->mctx = mctx; + manager->taskmgr = taskmgr; + manager->timermgr = timermgr; + manager->ncpus = ncpus; + + ns_interface_attach(interface, &manager->interface); + + manager->exiting = false; + int ntasks = CLIENT_NTASKS_PERCPU * manager->ncpus; + manager->taskpool = isc_mem_get(mctx, ntasks * sizeof(isc_task_t *)); + for (i = 0; i < ntasks; i++) { + manager->taskpool[i] = NULL; + result = isc_task_create_bound(manager->taskmgr, 20, + &manager->taskpool[i], + i % CLIENT_NTASKS_PERCPU); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + } + isc_refcount_init(&manager->references, 1); + manager->sctx = NULL; + ns_server_attach(sctx, &manager->sctx); + + ISC_LIST_INIT(manager->recursing); + + npools = CLIENT_NMCTXS_PERCPU * manager->ncpus; + manager->mctxpool = isc_mem_get(manager->mctx, + npools * sizeof(isc_mem_t *)); + for (i = 0; i < npools; i++) { + manager->mctxpool[i] = NULL; + isc_mem_create(&manager->mctxpool[i]); + isc_mem_setname(manager->mctxpool[i], "client", NULL); + } + + manager->magic = MANAGER_MAGIC; + + MTRACE("create"); + + *managerp = manager; + + return (ISC_R_SUCCESS); + +cleanup_reclock: + isc_mutex_destroy(&manager->reclock); + isc_mutex_destroy(&manager->lock); + + isc_mem_put(mctx, manager, sizeof(*manager)); + + return (result); +} + +void +ns_clientmgr_shutdown(ns_clientmgr_t *manager) { + REQUIRE(VALID_MANAGER(manager)); + + LOCK(&manager->reclock); + for (ns_client_t *client = ISC_LIST_HEAD(manager->recursing); + client != NULL; client = ISC_LIST_NEXT(client, rlink)) + { + ns_query_cancel(client); + } + UNLOCK(&manager->reclock); +} + +void +ns_clientmgr_destroy(ns_clientmgr_t **managerp) { + isc_result_t result; + ns_clientmgr_t *manager; + bool unlock = false; + + REQUIRE(managerp != NULL); + manager = *managerp; + *managerp = NULL; + REQUIRE(VALID_MANAGER(manager)); + + MTRACE("destroy"); + + /* + * Check for success because we may already be task-exclusive + * at this point. Only if we succeed at obtaining an exclusive + * lock now will we need to relinquish it later. + */ + result = isc_task_beginexclusive(manager->excl); + if (result == ISC_R_SUCCESS) { + unlock = true; + } + + manager->exiting = true; + + if (unlock) { + isc_task_endexclusive(manager->excl); + } + + if (isc_refcount_decrement(&manager->references) == 1) { + clientmgr_destroy(manager); + } +} + +isc_sockaddr_t * +ns_client_getsockaddr(ns_client_t *client) { + return (&client->peeraddr); +} + +isc_sockaddr_t * +ns_client_getdestaddr(ns_client_t *client) { + return (&client->destsockaddr); +} + +isc_result_t +ns_client_checkaclsilent(ns_client_t *client, isc_netaddr_t *netaddr, + dns_acl_t *acl, bool default_allow) { + isc_result_t result; + dns_aclenv_t *env = + ns_interfacemgr_getaclenv(client->manager->interface->mgr); + isc_netaddr_t tmpnetaddr; + int match; + + if (acl == NULL) { + if (default_allow) { + goto allow; + } else { + goto deny; + } + } + + if (netaddr == NULL) { + isc_netaddr_fromsockaddr(&tmpnetaddr, &client->peeraddr); + netaddr = &tmpnetaddr; + } + + result = dns_acl_match(netaddr, client->signer, acl, env, &match, NULL); + if (result != ISC_R_SUCCESS) { + goto deny; /* Internal error, already logged. */ + } + + if (match > 0) { + goto allow; + } + goto deny; /* Negative match or no match. */ + +allow: + return (ISC_R_SUCCESS); + +deny: + return (DNS_R_REFUSED); +} + +isc_result_t +ns_client_checkacl(ns_client_t *client, isc_sockaddr_t *sockaddr, + const char *opname, dns_acl_t *acl, bool default_allow, + int log_level) { + isc_result_t result; + isc_netaddr_t netaddr; + + if (sockaddr != NULL) { + isc_netaddr_fromsockaddr(&netaddr, sockaddr); + } + + result = ns_client_checkaclsilent(client, sockaddr ? &netaddr : NULL, + acl, default_allow); + + if (result == ISC_R_SUCCESS) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(3), + "%s approved", opname); + } else { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, log_level, "%s denied", + opname); + } + return (result); +} + +static void +ns_client_name(ns_client_t *client, char *peerbuf, size_t len) { + if (client->peeraddr_valid) { + isc_sockaddr_format(&client->peeraddr, peerbuf, + (unsigned int)len); + } else { + snprintf(peerbuf, len, "@%p", client); + } +} + +void +ns_client_logv(ns_client_t *client, isc_logcategory_t *category, + isc_logmodule_t *module, int level, const char *fmt, + va_list ap) { + char msgbuf[4096]; + char signerbuf[DNS_NAME_FORMATSIZE], qnamebuf[DNS_NAME_FORMATSIZE]; + char peerbuf[ISC_SOCKADDR_FORMATSIZE]; + const char *viewname = ""; + const char *sep1 = "", *sep2 = "", *sep3 = "", *sep4 = ""; + const char *signer = "", *qname = ""; + dns_name_t *q = NULL; + + REQUIRE(client != NULL); + + vsnprintf(msgbuf, sizeof(msgbuf), fmt, ap); + + if (client->signer != NULL) { + dns_name_format(client->signer, signerbuf, sizeof(signerbuf)); + sep1 = "/key "; + signer = signerbuf; + } + + q = client->query.origqname != NULL ? client->query.origqname + : client->query.qname; + if (q != NULL) { + dns_name_format(q, qnamebuf, sizeof(qnamebuf)); + sep2 = " ("; + sep3 = ")"; + qname = qnamebuf; + } + + if (client->view != NULL && strcmp(client->view->name, "_bind") != 0 && + strcmp(client->view->name, "_default") != 0) + { + sep4 = ": view "; + viewname = client->view->name; + } + + if (client->peeraddr_valid) { + isc_sockaddr_format(&client->peeraddr, peerbuf, + sizeof(peerbuf)); + } else { + snprintf(peerbuf, sizeof(peerbuf), "(no-peer)"); + } + + isc_log_write(ns_lctx, category, module, level, + "client @%p %s%s%s%s%s%s%s%s: %s", client, peerbuf, sep1, + signer, sep2, qname, sep3, sep4, viewname, msgbuf); +} + +void +ns_client_log(ns_client_t *client, isc_logcategory_t *category, + isc_logmodule_t *module, int level, const char *fmt, ...) { + va_list ap; + + if (!isc_log_wouldlog(ns_lctx, level)) { + return; + } + + va_start(ap, fmt); + ns_client_logv(client, category, module, level, fmt, ap); + va_end(ap); +} + +void +ns_client_aclmsg(const char *msg, const dns_name_t *name, dns_rdatatype_t type, + dns_rdataclass_t rdclass, char *buf, size_t len) { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(type, typebuf, sizeof(typebuf)); + dns_rdataclass_format(rdclass, classbuf, sizeof(classbuf)); + (void)snprintf(buf, len, "%s '%s/%s/%s'", msg, namebuf, typebuf, + classbuf); +} + +static void +ns_client_dumpmessage(ns_client_t *client, const char *reason) { + isc_buffer_t buffer; + char *buf = NULL; + int len = 1024; + isc_result_t result; + + if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1))) { + return; + } + + /* + * Note that these are multiline debug messages. We want a newline + * to appear in the log after each message. + */ + + do { + buf = isc_mem_get(client->mctx, len); + isc_buffer_init(&buffer, buf, len); + result = dns_message_totext( + client->message, &dns_master_style_debug, 0, &buffer); + if (result == ISC_R_NOSPACE) { + isc_mem_put(client->mctx, buf, len); + len += 1024; + } else if (result == ISC_R_SUCCESS) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "%s\n%.*s", reason, + (int)isc_buffer_usedlength(&buffer), buf); + } + } while (result == ISC_R_NOSPACE); + + if (buf != NULL) { + isc_mem_put(client->mctx, buf, len); + } +} + +void +ns_client_dumprecursing(FILE *f, ns_clientmgr_t *manager) { + ns_client_t *client; + char namebuf[DNS_NAME_FORMATSIZE]; + char original[DNS_NAME_FORMATSIZE]; + char peerbuf[ISC_SOCKADDR_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + const char *name; + const char *sep; + const char *origfor; + dns_rdataset_t *rdataset; + + REQUIRE(VALID_MANAGER(manager)); + + LOCK(&manager->reclock); + client = ISC_LIST_HEAD(manager->recursing); + while (client != NULL) { + INSIST(client->state == NS_CLIENTSTATE_RECURSING); + + ns_client_name(client, peerbuf, sizeof(peerbuf)); + if (client->view != NULL && + strcmp(client->view->name, "_bind") != 0 && + strcmp(client->view->name, "_default") != 0) + { + name = client->view->name; + sep = ": view "; + } else { + name = ""; + sep = ""; + } + + LOCK(&client->query.fetchlock); + INSIST(client->query.qname != NULL); + dns_name_format(client->query.qname, namebuf, sizeof(namebuf)); + if (client->query.qname != client->query.origqname && + client->query.origqname != NULL) + { + origfor = " for "; + dns_name_format(client->query.origqname, original, + sizeof(original)); + } else { + origfor = ""; + original[0] = '\0'; + } + rdataset = ISC_LIST_HEAD(client->query.qname->list); + if (rdataset == NULL && client->query.origqname != NULL) { + rdataset = ISC_LIST_HEAD(client->query.origqname->list); + } + if (rdataset != NULL) { + dns_rdatatype_format(rdataset->type, typebuf, + sizeof(typebuf)); + dns_rdataclass_format(rdataset->rdclass, classbuf, + sizeof(classbuf)); + } else { + strlcpy(typebuf, "-", sizeof(typebuf)); + strlcpy(classbuf, "-", sizeof(classbuf)); + } + UNLOCK(&client->query.fetchlock); + fprintf(f, + "; client %s%s%s: id %u '%s/%s/%s'%s%s " + "requesttime %u\n", + peerbuf, sep, name, client->message->id, namebuf, + typebuf, classbuf, origfor, original, + isc_time_seconds(&client->requesttime)); + client = ISC_LIST_NEXT(client, rlink); + } + UNLOCK(&manager->reclock); +} + +void +ns_client_qnamereplace(ns_client_t *client, dns_name_t *name) { + LOCK(&client->query.fetchlock); + if (client->query.restarts > 0) { + /* + * client->query.qname was dynamically allocated. + */ + dns_message_puttempname(client->message, &client->query.qname); + } + client->query.qname = name; + client->query.attributes &= ~NS_QUERYATTR_REDIRECT; + UNLOCK(&client->query.fetchlock); +} + +isc_result_t +ns_client_sourceip(dns_clientinfo_t *ci, isc_sockaddr_t **addrp) { + ns_client_t *client = (ns_client_t *)ci->data; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(addrp != NULL); + + *addrp = &client->peeraddr; + return (ISC_R_SUCCESS); +} + +dns_rdataset_t * +ns_client_newrdataset(ns_client_t *client) { + dns_rdataset_t *rdataset; + isc_result_t result; + + REQUIRE(NS_CLIENT_VALID(client)); + + rdataset = NULL; + result = dns_message_gettemprdataset(client->message, &rdataset); + if (result != ISC_R_SUCCESS) { + return (NULL); + } + + return (rdataset); +} + +void +ns_client_putrdataset(ns_client_t *client, dns_rdataset_t **rdatasetp) { + dns_rdataset_t *rdataset; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(rdatasetp != NULL); + + rdataset = *rdatasetp; + + if (rdataset != NULL) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + dns_message_puttemprdataset(client->message, rdatasetp); + } +} + +isc_result_t +ns_client_newnamebuf(ns_client_t *client) { + isc_buffer_t *dbuf = NULL; + + CTRACE("ns_client_newnamebuf"); + + isc_buffer_allocate(client->mctx, &dbuf, 1024); + ISC_LIST_APPEND(client->query.namebufs, dbuf, link); + + CTRACE("ns_client_newnamebuf: done"); + return (ISC_R_SUCCESS); +} + +dns_name_t * +ns_client_newname(ns_client_t *client, isc_buffer_t *dbuf, isc_buffer_t *nbuf) { + dns_name_t *name = NULL; + isc_region_t r; + isc_result_t result; + + REQUIRE((client->query.attributes & NS_QUERYATTR_NAMEBUFUSED) == 0); + + CTRACE("ns_client_newname"); + + result = dns_message_gettempname(client->message, &name); + if (result != ISC_R_SUCCESS) { + CTRACE("ns_client_newname: " + "dns_message_gettempname failed: done"); + return (NULL); + } + isc_buffer_availableregion(dbuf, &r); + isc_buffer_init(nbuf, r.base, r.length); + dns_name_setbuffer(name, NULL); + dns_name_setbuffer(name, nbuf); + client->query.attributes |= NS_QUERYATTR_NAMEBUFUSED; + + CTRACE("ns_client_newname: done"); + return (name); +} + +isc_buffer_t * +ns_client_getnamebuf(ns_client_t *client) { + isc_buffer_t *dbuf; + isc_region_t r; + + CTRACE("ns_client_getnamebuf"); + + /*% + * Return a name buffer with space for a maximal name, allocating + * a new one if necessary. + */ + if (ISC_LIST_EMPTY(client->query.namebufs)) { + ns_client_newnamebuf(client); + } + + dbuf = ISC_LIST_TAIL(client->query.namebufs); + INSIST(dbuf != NULL); + isc_buffer_availableregion(dbuf, &r); + if (r.length < DNS_NAME_MAXWIRE) { + ns_client_newnamebuf(client); + dbuf = ISC_LIST_TAIL(client->query.namebufs); + isc_buffer_availableregion(dbuf, &r); + INSIST(r.length >= 255); + } + CTRACE("ns_client_getnamebuf: done"); + return (dbuf); +} + +void +ns_client_keepname(ns_client_t *client, dns_name_t *name, isc_buffer_t *dbuf) { + isc_region_t r; + + CTRACE("ns_client_keepname"); + + /*% + * 'name' is using space in 'dbuf', but 'dbuf' has not yet been + * adjusted to take account of that. We do the adjustment. + */ + REQUIRE((client->query.attributes & NS_QUERYATTR_NAMEBUFUSED) != 0); + + dns_name_toregion(name, &r); + isc_buffer_add(dbuf, r.length); + dns_name_setbuffer(name, NULL); + client->query.attributes &= ~NS_QUERYATTR_NAMEBUFUSED; +} + +void +ns_client_releasename(ns_client_t *client, dns_name_t **namep) { + /*% + * 'name' is no longer needed. Return it to our pool of temporary + * names. If it is using a name buffer, relinquish its exclusive + * rights on the buffer. + */ + + CTRACE("ns_client_releasename"); + client->query.attributes &= ~NS_QUERYATTR_NAMEBUFUSED; + dns_message_puttempname(client->message, namep); + CTRACE("ns_client_releasename: done"); +} + +isc_result_t +ns_client_newdbversion(ns_client_t *client, unsigned int n) { + unsigned int i; + ns_dbversion_t *dbversion = NULL; + + for (i = 0; i < n; i++) { + dbversion = isc_mem_get(client->mctx, sizeof(*dbversion)); + *dbversion = (ns_dbversion_t){ 0 }; + ISC_LIST_INITANDAPPEND(client->query.freeversions, dbversion, + link); + } + + return (ISC_R_SUCCESS); +} + +static ns_dbversion_t * +client_getdbversion(ns_client_t *client) { + ns_dbversion_t *dbversion = NULL; + + if (ISC_LIST_EMPTY(client->query.freeversions)) { + ns_client_newdbversion(client, 1); + } + dbversion = ISC_LIST_HEAD(client->query.freeversions); + INSIST(dbversion != NULL); + ISC_LIST_UNLINK(client->query.freeversions, dbversion, link); + + return (dbversion); +} + +ns_dbversion_t * +ns_client_findversion(ns_client_t *client, dns_db_t *db) { + ns_dbversion_t *dbversion; + + for (dbversion = ISC_LIST_HEAD(client->query.activeversions); + dbversion != NULL; dbversion = ISC_LIST_NEXT(dbversion, link)) + { + if (dbversion->db == db) { + break; + } + } + + if (dbversion == NULL) { + /* + * This is a new zone for this query. Add it to + * the active list. + */ + dbversion = client_getdbversion(client); + if (dbversion == NULL) { + return (NULL); + } + dns_db_attach(db, &dbversion->db); + dns_db_currentversion(db, &dbversion->version); + dbversion->acl_checked = false; + dbversion->queryok = false; + ISC_LIST_APPEND(client->query.activeversions, dbversion, link); + } + + return (dbversion); +} diff --git a/lib/ns/hooks.c b/lib/ns/hooks.c new file mode 100644 index 0000000..1b8b8d0 --- /dev/null +++ b/lib/ns/hooks.c @@ -0,0 +1,544 @@ +/* + * 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. + */ + +/*! \file */ + +#include <errno.h> +#include <stdio.h> +#include <string.h> + +#if HAVE_DLFCN_H +#include <dlfcn.h> +#elif _WIN32 +#include <windows.h> +#endif /* if HAVE_DLFCN_H */ + +#include <isc/errno.h> +#include <isc/list.h> +#include <isc/log.h> +#include <isc/mem.h> +#include <isc/mutex.h> +#include <isc/platform.h> +#include <isc/print.h> +#include <isc/result.h> +#include <isc/types.h> +#include <isc/util.h> + +#include <dns/view.h> + +#include <ns/hooks.h> +#include <ns/log.h> +#include <ns/query.h> + +#define CHECK(op) \ + do { \ + result = (op); \ + if (result != ISC_R_SUCCESS) { \ + goto cleanup; \ + } \ + } while (0) + +struct ns_plugin { + isc_mem_t *mctx; + void *handle; + void *inst; + char *modpath; + ns_plugin_check_t *check_func; + ns_plugin_register_t *register_func; + ns_plugin_destroy_t *destroy_func; + LINK(ns_plugin_t) link; +}; + +static ns_hooklist_t default_hooktable[NS_HOOKPOINTS_COUNT]; +LIBNS_EXTERNAL_DATA ns_hooktable_t *ns__hook_table = &default_hooktable; + +isc_result_t +ns_plugin_expandpath(const char *src, char *dst, size_t dstsize) { + int result; + +#ifndef WIN32 + /* + * On Unix systems, differentiate between paths and filenames. + */ + if (strchr(src, '/') != NULL) { + /* + * 'src' is an absolute or relative path. Copy it verbatim. + */ + result = snprintf(dst, dstsize, "%s", src); + } else { + /* + * 'src' is a filename. Prepend default plugin directory path. + */ + result = snprintf(dst, dstsize, "%s/%s", NAMED_PLUGINDIR, src); + } +#else /* ifndef WIN32 */ + /* + * On Windows, always copy 'src' do 'dst'. + */ + result = snprintf(dst, dstsize, "%s", src); +#endif /* ifndef WIN32 */ + + if (result < 0) { + return (isc_errno_toresult(errno)); + } else if ((size_t)result >= dstsize) { + return (ISC_R_NOSPACE); + } else { + return (ISC_R_SUCCESS); + } +} + +#if HAVE_DLFCN_H && HAVE_DLOPEN +static isc_result_t +load_symbol(void *handle, const char *modpath, const char *symbol_name, + void **symbolp) { + void *symbol = NULL; + + REQUIRE(handle != NULL); + REQUIRE(symbolp != NULL && *symbolp == NULL); + + /* + * Clear any pre-existing error conditions before running dlsym(). + * (In this case, we expect dlsym() to return non-NULL values + * and will always return an error if it returns NULL, but + * this ensures that we'll report the correct error condition + * if there is one.) + */ + dlerror(); + symbol = dlsym(handle, symbol_name); + if (symbol == NULL) { + const char *errmsg = dlerror(); + if (errmsg == NULL) { + errmsg = "returned function pointer is NULL"; + } + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "failed to look up symbol %s in " + "plugin '%s': %s", + symbol_name, modpath, errmsg); + return (ISC_R_FAILURE); + } + + *symbolp = symbol; + + return (ISC_R_SUCCESS); +} + +static isc_result_t +load_plugin(isc_mem_t *mctx, const char *modpath, ns_plugin_t **pluginp) { + isc_result_t result; + void *handle = NULL; + ns_plugin_t *plugin = NULL; + ns_plugin_check_t *check_func = NULL; + ns_plugin_register_t *register_func = NULL; + ns_plugin_destroy_t *destroy_func = NULL; + ns_plugin_version_t *version_func = NULL; + int version, flags; + + REQUIRE(pluginp != NULL && *pluginp == NULL); + + flags = RTLD_LAZY | RTLD_LOCAL; +#if defined(RTLD_DEEPBIND) && !__SANITIZE_ADDRESS__ && !__SANITIZE_THREAD__ + flags |= RTLD_DEEPBIND; +#endif /* if defined(RTLD_DEEPBIND) && !__SANITIZE_ADDRESS__ && \ + !__SANITIZE_THREAD__ */ + + handle = dlopen(modpath, flags); + if (handle == NULL) { + const char *errmsg = dlerror(); + if (errmsg == NULL) { + errmsg = "unknown error"; + } + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "failed to dlopen() plugin '%s': %s", modpath, + errmsg); + return (ISC_R_FAILURE); + } + + CHECK(load_symbol(handle, modpath, "plugin_version", + (void **)&version_func)); + + version = version_func(); + if (version < (NS_PLUGIN_VERSION - NS_PLUGIN_AGE) || + version > NS_PLUGIN_VERSION) + { + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "plugin API version mismatch: %d/%d", version, + NS_PLUGIN_VERSION); + CHECK(ISC_R_FAILURE); + } + + CHECK(load_symbol(handle, modpath, "plugin_check", + (void **)&check_func)); + CHECK(load_symbol(handle, modpath, "plugin_register", + (void **)®ister_func)); + CHECK(load_symbol(handle, modpath, "plugin_destroy", + (void **)&destroy_func)); + + plugin = isc_mem_get(mctx, sizeof(*plugin)); + memset(plugin, 0, sizeof(*plugin)); + isc_mem_attach(mctx, &plugin->mctx); + plugin->handle = handle; + plugin->modpath = isc_mem_strdup(plugin->mctx, modpath); + plugin->check_func = check_func; + plugin->register_func = register_func; + plugin->destroy_func = destroy_func; + + ISC_LINK_INIT(plugin, link); + + *pluginp = plugin; + plugin = NULL; + +cleanup: + if (result != ISC_R_SUCCESS) { + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "failed to dynamically load " + "plugin '%s': %s", + modpath, isc_result_totext(result)); + + if (plugin != NULL) { + isc_mem_putanddetach(&plugin->mctx, plugin, + sizeof(*plugin)); + } + + (void)dlclose(handle); + } + + return (result); +} + +static void +unload_plugin(ns_plugin_t **pluginp) { + ns_plugin_t *plugin = NULL; + + REQUIRE(pluginp != NULL && *pluginp != NULL); + + plugin = *pluginp; + *pluginp = NULL; + + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_DEBUG(1), "unloading plugin '%s'", + plugin->modpath); + + if (plugin->inst != NULL) { + plugin->destroy_func(&plugin->inst); + } + if (plugin->handle != NULL) { + (void)dlclose(plugin->handle); + } + if (plugin->modpath != NULL) { + isc_mem_free(plugin->mctx, plugin->modpath); + } + + isc_mem_putanddetach(&plugin->mctx, plugin, sizeof(*plugin)); +} +#elif _WIN32 +static isc_result_t +load_symbol(HMODULE handle, const char *modpath, const char *symbol_name, + void **symbolp) { + void *symbol = NULL; + + REQUIRE(handle != NULL); + REQUIRE(symbolp != NULL && *symbolp == NULL); + + symbol = GetProcAddress(handle, symbol_name); + if (symbol == NULL) { + int errstatus = GetLastError(); + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "failed to look up symbol %s in " + "plugin '%s': %d", + symbol_name, modpath, errstatus); + return (ISC_R_FAILURE); + } + + *symbolp = symbol; + + return (ISC_R_SUCCESS); +} + +static isc_result_t +load_plugin(isc_mem_t *mctx, const char *modpath, ns_plugin_t **pluginp) { + isc_result_t result; + HMODULE handle; + ns_plugin_t *plugin = NULL; + ns_plugin_register_t *register_func = NULL; + ns_plugin_destroy_t *destroy_func = NULL; + ns_plugin_version_t *version_func = NULL; + int version; + + REQUIRE(pluginp != NULL && *pluginp == NULL); + + handle = LoadLibraryA(modpath); + if (handle == NULL) { + CHECK(ISC_R_FAILURE); + } + + CHECK(load_symbol(handle, modpath, "plugin_version", + (void **)&version_func)); + + version = version_func(); + if (version < (NS_PLUGIN_VERSION - NS_PLUGIN_AGE) || + version > NS_PLUGIN_VERSION) + { + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "plugin API version mismatch: %d/%d", version, + NS_PLUGIN_VERSION); + CHECK(ISC_R_FAILURE); + } + + CHECK(load_symbol(handle, modpath, "plugin_register", + (void **)®ister_func)); + CHECK(load_symbol(handle, modpath, "plugin_destroy", + (void **)&destroy_func)); + + plugin = isc_mem_get(mctx, sizeof(*plugin)); + memset(plugin, 0, sizeof(*plugin)); + isc_mem_attach(mctx, &plugin->mctx); + plugin->handle = handle; + plugin->modpath = isc_mem_strdup(plugin->mctx, modpath); + plugin->register_func = register_func; + plugin->destroy_func = destroy_func; + + ISC_LINK_INIT(plugin, link); + + *pluginp = plugin; + plugin = NULL; + +cleanup: + if (result != ISC_R_SUCCESS) { + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_HOOKS, ISC_LOG_ERROR, + "failed to dynamically load " + "plugin '%s': %d (%s)", + modpath, GetLastError(), + isc_result_totext(result)); + + if (plugin != NULL) { + isc_mem_putanddetach(&plugin->mctx, plugin, + sizeof(*plugin)); + } + + if (handle != NULL) { + FreeLibrary(handle); + } + } + + return (result); +} + +static void +unload_plugin(ns_plugin_t **pluginp) { + ns_plugin_t *plugin = NULL; + + REQUIRE(pluginp != NULL && *pluginp != NULL); + + plugin = *pluginp; + *pluginp = NULL; + + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_DEBUG(1), "unloading plugin '%s'", + plugin->modpath); + + if (plugin->inst != NULL) { + plugin->destroy_func(&plugin->inst); + } + if (plugin->handle != NULL) { + FreeLibrary(plugin->handle); + } + + if (plugin->modpath != NULL) { + isc_mem_free(plugin->mctx, plugin->modpath); + } + + isc_mem_putanddetach(&plugin->mctx, plugin, sizeof(*plugin)); +} +#else /* HAVE_DLFCN_H || _WIN32 */ +static isc_result_t +load_plugin(isc_mem_t *mctx, const char *modpath, ns_plugin_t **pluginp) { + UNUSED(mctx); + UNUSED(modpath); + UNUSED(pluginp); + + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_ERROR, "plugin support is not implemented"); + + return (ISC_R_NOTIMPLEMENTED); +} + +static void +unload_plugin(ns_plugin_t **pluginp) { + UNUSED(pluginp); +} +#endif /* HAVE_DLFCN_H */ + +isc_result_t +ns_plugin_register(const char *modpath, const char *parameters, const void *cfg, + const char *cfg_file, unsigned long cfg_line, + isc_mem_t *mctx, isc_log_t *lctx, void *actx, + dns_view_t *view) { + isc_result_t result; + ns_plugin_t *plugin = NULL; + + REQUIRE(mctx != NULL); + REQUIRE(lctx != NULL); + REQUIRE(view != NULL); + + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_INFO, "loading plugin '%s'", modpath); + + CHECK(load_plugin(mctx, modpath, &plugin)); + + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_INFO, "registering plugin '%s'", modpath); + + CHECK(plugin->register_func(parameters, cfg, cfg_file, cfg_line, mctx, + lctx, actx, view->hooktable, + &plugin->inst)); + + ISC_LIST_APPEND(*(ns_plugins_t *)view->plugins, plugin, link); + +cleanup: + if (result != ISC_R_SUCCESS && plugin != NULL) { + unload_plugin(&plugin); + } + + return (result); +} + +isc_result_t +ns_plugin_check(const char *modpath, const char *parameters, const void *cfg, + const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx, + isc_log_t *lctx, void *actx) { + isc_result_t result; + ns_plugin_t *plugin = NULL; + + CHECK(load_plugin(mctx, modpath, &plugin)); + + result = plugin->check_func(parameters, cfg, cfg_file, cfg_line, mctx, + lctx, actx); + +cleanup: + if (plugin != NULL) { + unload_plugin(&plugin); + } + + return (result); +} + +void +ns_hooktable_init(ns_hooktable_t *hooktable) { + int i; + + for (i = 0; i < NS_HOOKPOINTS_COUNT; i++) { + ISC_LIST_INIT((*hooktable)[i]); + } +} + +isc_result_t +ns_hooktable_create(isc_mem_t *mctx, ns_hooktable_t **tablep) { + ns_hooktable_t *hooktable = NULL; + + REQUIRE(tablep != NULL && *tablep == NULL); + + hooktable = isc_mem_get(mctx, sizeof(*hooktable)); + + ns_hooktable_init(hooktable); + + *tablep = hooktable; + + return (ISC_R_SUCCESS); +} + +void +ns_hooktable_free(isc_mem_t *mctx, void **tablep) { + ns_hooktable_t *table = NULL; + ns_hook_t *hook = NULL, *next = NULL; + int i = 0; + + REQUIRE(tablep != NULL && *tablep != NULL); + + table = *tablep; + *tablep = NULL; + + for (i = 0; i < NS_HOOKPOINTS_COUNT; i++) { + for (hook = ISC_LIST_HEAD((*table)[i]); hook != NULL; + hook = next) + { + next = ISC_LIST_NEXT(hook, link); + ISC_LIST_UNLINK((*table)[i], hook, link); + if (hook->mctx != NULL) { + isc_mem_putanddetach(&hook->mctx, hook, + sizeof(*hook)); + } + } + } + + isc_mem_put(mctx, table, sizeof(*table)); +} + +void +ns_hook_add(ns_hooktable_t *hooktable, isc_mem_t *mctx, + ns_hookpoint_t hookpoint, const ns_hook_t *hook) { + ns_hook_t *copy = NULL; + + REQUIRE(hooktable != NULL); + REQUIRE(mctx != NULL); + REQUIRE(hookpoint < NS_HOOKPOINTS_COUNT); + REQUIRE(hook != NULL); + + copy = isc_mem_get(mctx, sizeof(*copy)); + memset(copy, 0, sizeof(*copy)); + + copy->action = hook->action; + copy->action_data = hook->action_data; + isc_mem_attach(mctx, ©->mctx); + + ISC_LINK_INIT(copy, link); + ISC_LIST_APPEND((*hooktable)[hookpoint], copy, link); +} + +void +ns_plugins_create(isc_mem_t *mctx, ns_plugins_t **listp) { + ns_plugins_t *plugins = NULL; + + REQUIRE(listp != NULL && *listp == NULL); + + plugins = isc_mem_get(mctx, sizeof(*plugins)); + memset(plugins, 0, sizeof(*plugins)); + ISC_LIST_INIT(*plugins); + + *listp = plugins; +} + +void +ns_plugins_free(isc_mem_t *mctx, void **listp) { + ns_plugins_t *list = NULL; + ns_plugin_t *plugin = NULL, *next = NULL; + + REQUIRE(listp != NULL && *listp != NULL); + + list = *listp; + *listp = NULL; + + for (plugin = ISC_LIST_HEAD(*list); plugin != NULL; plugin = next) { + next = ISC_LIST_NEXT(plugin, link); + ISC_LIST_UNLINK(*list, plugin, link); + unload_plugin(&plugin); + } + + isc_mem_put(mctx, list, sizeof(*list)); +} diff --git a/lib/ns/include/.clang-format b/lib/ns/include/.clang-format new file mode 120000 index 0000000..0e62f72 --- /dev/null +++ b/lib/ns/include/.clang-format @@ -0,0 +1 @@ +../../../.clang-format.headers
\ No newline at end of file diff --git a/lib/ns/include/Makefile.in b/lib/ns/include/Makefile.in new file mode 100644 index 0000000..5863acb --- /dev/null +++ b/lib/ns/include/Makefile.in @@ -0,0 +1,19 @@ +# 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. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +SUBDIRS = ns +TARGETS = + +@BIND9_MAKE_RULES@ diff --git a/lib/ns/include/ns/Makefile.in b/lib/ns/include/ns/Makefile.in new file mode 100644 index 0000000..6976e1e --- /dev/null +++ b/lib/ns/include/ns/Makefile.in @@ -0,0 +1,37 @@ +# 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. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +VERSION=@BIND9_VERSION@ + +HEADERS = client.h hooks.h interfacemgr.h lib.h listenlist.h log.h \ + notify.h query.h server.h sortlist.h stats.h \ + types.h update.h version.h xfrout.h +SUBDIRS = +TARGETS = + +@BIND9_MAKE_RULES@ + +installdirs: + $(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${includedir}/ns + +install:: installdirs + for i in ${HEADERS}; do \ + ${INSTALL_DATA} ${srcdir}/$$i ${DESTDIR}${includedir}/ns || exit 1 ; \ + done + +uninstall:: + for i in ${HEADERS}; do \ + rm -f ${DESTDIR}${includedir}/ns/$$i || exit 1 ; \ + done diff --git a/lib/ns/include/ns/client.h b/lib/ns/include/ns/client.h new file mode 100644 index 0000000..d1e2fde --- /dev/null +++ b/lib/ns/include/ns/client.h @@ -0,0 +1,585 @@ +/* + * 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. + */ + +#ifndef NS_CLIENT_H +#define NS_CLIENT_H 1 + +/***** +***** Module Info +*****/ + +/*! \file + * \brief + * This module defines two objects, ns_client_t and ns_clientmgr_t. + * + * An ns_client_t object handles incoming DNS requests from clients + * on a given network interface. + * + * Each ns_client_t object can handle only one TCP connection or UDP + * request at a time. Therefore, several ns_client_t objects are + * typically created to serve each network interface, e.g., one + * for handling TCP requests and a few (one per CPU) for handling + * UDP requests. + * + * Incoming requests are classified as queries, zone transfer + * requests, update requests, notify requests, etc, and handed off + * to the appropriate request handler. When the request has been + * fully handled (which can be much later), the ns_client_t must be + * notified of this by calling one of the following functions + * exactly once in the context of its task: + * \code + * ns_client_send() (sending a non-error response) + * ns_client_sendraw() (sending a raw response) + * ns_client_error() (sending an error response) + * ns_client_drop() (sending no response, logging the reason) + *\endcode + * This will release any resources used by the request and + * and allow the ns_client_t to listen for the next request. + * + * A ns_clientmgr_t manages a number of ns_client_t objects. + * New ns_client_t objects are created by calling + * ns_clientmgr_createclients(). They are destroyed by + * destroying their manager. + */ + +/*** + *** Imports + ***/ + +#include <inttypes.h> +#include <stdbool.h> + +#include <isc/atomic.h> +#include <isc/buffer.h> +#include <isc/magic.h> +#include <isc/netmgr.h> +#include <isc/platform.h> +#include <isc/quota.h> +#include <isc/stdtime.h> + +#include <dns/db.h> +#include <dns/ecs.h> +#include <dns/fixedname.h> +#include <dns/name.h> +#include <dns/rdataclass.h> +#include <dns/rdatatype.h> +#include <dns/tcpmsg.h> +#include <dns/types.h> + +#include <ns/query.h> +#include <ns/types.h> + +/*** + *** Types + ***/ + +#define NS_CLIENT_TCP_BUFFER_SIZE 65535 +#define NS_CLIENT_SEND_BUFFER_SIZE 4096 + +/*! + * Client object states. Ordering is significant: higher-numbered + * states are generally "more active", meaning that the client can + * have more dynamically allocated data, outstanding events, etc. + * In the list below, any such properties listed for state N + * also apply to any state > N. + */ + +typedef enum { + NS_CLIENTSTATE_FREED = 0, + /*%< + * The client object no longer exists. + */ + + NS_CLIENTSTATE_INACTIVE = 1, + /*%< + * The client object exists and has a task and timer. + * Its "query" struct and sendbuf are initialized. + * It has a message and OPT, both in the reset state. + */ + + NS_CLIENTSTATE_READY = 2, + /*%< + * The client object is either a TCP or a UDP one, and + * it is associated with a network interface. It is on the + * client manager's list of active clients. + * + * If it is a TCP client object, it has a TCP listener socket + * and an outstanding TCP listen request. + * + * If it is a UDP client object, it has a UDP listener socket + * and an outstanding UDP receive request. + */ + + NS_CLIENTSTATE_WORKING = 3, + /*%< + * The client object has received a request and is working + * on it. It has a view, and it may have any of a non-reset OPT, + * recursion quota, and an outstanding write request. + */ + + NS_CLIENTSTATE_RECURSING = 4, + /*%< + * The client object is recursing. It will be on the + * 'recursing' list. + */ + + NS_CLIENTSTATE_MAX = 5 + /*%< + * Sentinel value used to indicate "no state". + */ +} ns_clientstate_t; + +typedef ISC_LIST(ns_client_t) client_list_t; + +/*% nameserver client manager structure */ +struct ns_clientmgr { + /* Unlocked. */ + unsigned int magic; + + isc_mem_t *mctx; + ns_server_t *sctx; + isc_taskmgr_t *taskmgr; + isc_timermgr_t *timermgr; + isc_task_t *excl; + isc_refcount_t references; + int ncpus; + + /* Attached by clients, needed for e.g. recursion */ + isc_task_t **taskpool; + + ns_interface_t *interface; + + /* Lock covers manager state. */ + isc_mutex_t lock; + bool exiting; + + /* Lock covers the recursing list */ + isc_mutex_t reclock; + client_list_t recursing; /*%< Recursing clients */ + + /*%< mctx pool for clients. */ + isc_mem_t **mctxpool; +}; + +/*% nameserver client structure */ +struct ns_client { + unsigned int magic; + isc_mem_t *mctx; + bool allocated; /* Do we need to free it? */ + ns_server_t *sctx; + ns_clientmgr_t *manager; + ns_clientstate_t state; + int nupdates; + bool nodetach; + bool shuttingdown; + unsigned int attributes; + isc_task_t *task; + dns_view_t *view; + dns_dispatch_t *dispatch; + isc_nmhandle_t *handle; /* Permanent pointer to handle */ + isc_nmhandle_t *sendhandle; /* Waiting for send callback */ + isc_nmhandle_t *reqhandle; /* Waiting for request callback + (query, update, notify) */ + isc_nmhandle_t *fetchhandle; /* Waiting for recursive fetch */ + isc_nmhandle_t *prefetchhandle; /* Waiting for prefetch / rpzfetch */ + isc_nmhandle_t *updatehandle; /* Waiting for update callback */ + unsigned char *tcpbuf; + dns_message_t *message; + unsigned char *sendbuf; + dns_rdataset_t *opt; + uint16_t udpsize; + uint16_t extflags; + int16_t ednsversion; /* -1 noedns */ + void (*cleanup)(ns_client_t *); + ns_query_t query; + isc_time_t requesttime; + isc_stdtime_t now; + isc_time_t tnow; + dns_name_t signername; /*%< [T]SIG key name */ + dns_name_t *signer; /*%< NULL if not valid sig */ + bool mortal; /*%< Die after handling request */ + isc_quota_t *recursionquota; + + isc_sockaddr_t peeraddr; + bool peeraddr_valid; + isc_netaddr_t destaddr; + isc_sockaddr_t destsockaddr; + + dns_ecs_t ecs; /*%< EDNS client subnet sent by client */ + + struct in6_pktinfo pktinfo; + isc_dscp_t dscp; + /*% + * Information about recent FORMERR response(s), for + * FORMERR loop avoidance. This is separate for each + * client object rather than global only to avoid + * the need for locking. + */ + struct { + isc_sockaddr_t addr; + isc_stdtime_t time; + dns_messageid_t id; + } formerrcache; + + /*% Callback function to send a response when unit testing */ + void (*sendcb)(isc_buffer_t *buf); + + ISC_LINK(ns_client_t) rlink; + unsigned char cookie[8]; + uint32_t expire; + unsigned char *keytag; + uint16_t keytag_len; + + /*% + * Used to override the DNS response code in ns_client_error(). + * If set to -1, the rcode is determined from the result code, + * but if set to any other value, the least significant 12 + * bits will be used as the rcode in the response message. + */ + int32_t rcode_override; +}; + +#define NS_CLIENT_MAGIC ISC_MAGIC('N', 'S', 'C', 'c') +#define NS_CLIENT_VALID(c) ISC_MAGIC_VALID(c, NS_CLIENT_MAGIC) + +#define NS_CLIENTATTR_TCP 0x00001 +#define NS_CLIENTATTR_RA 0x00002 /*%< Client gets recursive service */ +#define NS_CLIENTATTR_PKTINFO 0x00004 /*%< pktinfo is valid */ +#define NS_CLIENTATTR_MULTICAST 0x00008 /*%< recv'd from multicast */ +#define NS_CLIENTATTR_WANTDNSSEC 0x00010 /*%< include dnssec records */ +#define NS_CLIENTATTR_WANTNSID 0x00020 /*%< include nameserver ID */ +/* Obsolete: NS_CLIENTATTR_FILTER_AAAA 0x00040 */ +/* Obsolete: NS_CLIENTATTR_FILTER_AAAA_RC 0x00080 */ +#define NS_CLIENTATTR_WANTAD 0x00100 /*%< want AD in response if possible */ +#define NS_CLIENTATTR_WANTCOOKIE 0x00200 /*%< return a COOKIE */ +#define NS_CLIENTATTR_HAVECOOKIE 0x00400 /*%< has a valid COOKIE */ +#define NS_CLIENTATTR_WANTEXPIRE 0x00800 /*%< return seconds to expire */ +#define NS_CLIENTATTR_HAVEEXPIRE 0x01000 /*%< return seconds to expire */ +#define NS_CLIENTATTR_WANTOPT 0x02000 /*%< add opt to reply */ +#define NS_CLIENTATTR_HAVEECS 0x04000 /*%< received an ECS option */ +#define NS_CLIENTATTR_WANTPAD 0x08000 /*%< pad reply */ +#define NS_CLIENTATTR_USEKEEPALIVE 0x10000 /*%< use TCP keepalive */ + +#define NS_CLIENTATTR_NOSETFC 0x20000 /*%< don't set servfail cache */ + +/* + * Flag to use with the SERVFAIL cache to indicate + * that a query had the CD bit set. + */ +#define NS_FAILCACHE_CD 0x01 + +#if defined(_WIN32) && !defined(_WIN64) +LIBNS_EXTERNAL_DATA extern atomic_uint_fast32_t ns_client_requests; +#else /* if defined(_WIN32) && !defined(_WIN64) */ +LIBNS_EXTERNAL_DATA extern atomic_uint_fast64_t ns_client_requests; +#endif /* if defined(_WIN32) && !defined(_WIN64) */ + +/*** + *** Functions + ***/ + +/* + * Note! These ns_client_ routines MUST be called ONLY from the client's + * task in order to ensure synchronization. + */ + +void +ns_client_send(ns_client_t *client); +/*%< + * Finish processing the current client request and + * send client->message as a response. + * \brief + * Note! These ns_client_ routines MUST be called ONLY from the client's + * task in order to ensure synchronization. + */ + +void +ns_client_sendraw(ns_client_t *client, dns_message_t *msg); +/*%< + * Finish processing the current client request and + * send msg as a response using client->message->id for the id. + */ + +void +ns_client_error(ns_client_t *client, isc_result_t result); +/*%< + * Finish processing the current client request and return + * an error response to the client. The error response + * will have an RCODE determined by 'result'. + */ + +void +ns_client_drop(ns_client_t *client, isc_result_t result); +/*%< + * Log the reason the current client request has failed; no response + * will be sent. + */ + +bool +ns_client_shuttingdown(ns_client_t *client); +/*%< + * Return true iff the client is currently shutting down. + */ + +isc_result_t +ns_client_replace(ns_client_t *client); +/*%< + * Try to replace the current client with a new one, so that the + * current one can go off and do some lengthy work without + * leaving the dispatch/socket without service. + */ + +void +ns_client_settimeout(ns_client_t *client, unsigned int seconds); +/*%< + * Set a timer in the client to go off in the specified amount of time. + */ + +isc_result_t +ns_clientmgr_create(isc_mem_t *mctx, ns_server_t *sctx, isc_taskmgr_t *taskmgr, + isc_timermgr_t *timermgr, ns_interface_t *ifp, int ncpus, + ns_clientmgr_t **managerp); +/*%< + * Create a client manager. + */ + +void +ns_clientmgr_shutdown(ns_clientmgr_t *manager); +/*%< + * Shutdown a client manager and all ns_client_t objects + * managed by it. + */ + +void +ns_clientmgr_destroy(ns_clientmgr_t **managerp); +/*%< + * Destroy a client manager. + */ + +isc_sockaddr_t * +ns_client_getsockaddr(ns_client_t *client); +/*%< + * Get the socket address of the client whose request is + * currently being processed. + */ + +isc_sockaddr_t * +ns_client_getdestaddr(ns_client_t *client); +/*%< + * Get the destination address (server) for the request that is + * currently being processed. + */ + +isc_result_t +ns_client_checkaclsilent(ns_client_t *client, isc_netaddr_t *netaddr, + dns_acl_t *acl, bool default_allow); + +/*%< + * Convenience function for client request ACL checking. + * + * Check the current client request against 'acl'. If 'acl' + * is NULL, allow the request iff 'default_allow' is true. + * If netaddr is NULL, check the ACL against client->peeraddr; + * otherwise check it against netaddr. + * + * Notes: + *\li This is appropriate for checking allow-update, + * allow-query, allow-transfer, etc. It is not appropriate + * for checking the blackhole list because we treat positive + * matches as "allow" and negative matches as "deny"; in + * the case of the blackhole list this would be backwards. + * + * Requires: + *\li 'client' points to a valid client. + *\li 'netaddr' points to a valid address, or is NULL. + *\li 'acl' points to a valid ACL, or is NULL. + * + * Returns: + *\li ISC_R_SUCCESS if the request should be allowed + * \li DNS_R_REFUSED if the request should be denied + *\li No other return values are possible. + */ + +isc_result_t +ns_client_checkacl(ns_client_t *client, isc_sockaddr_t *sockaddr, + const char *opname, dns_acl_t *acl, bool default_allow, + int log_level); +/*%< + * Like ns_client_checkaclsilent, except the outcome of the check is + * logged at log level 'log_level' if denied, and at debug 3 if approved. + * Log messages will refer to the request as an 'opname' request. + * + * Requires: + *\li 'client' points to a valid client. + *\li 'sockaddr' points to a valid address, or is NULL. + *\li 'acl' points to a valid ACL, or is NULL. + *\li 'opname' points to a null-terminated string. + */ + +void +ns_client_log(ns_client_t *client, isc_logcategory_t *category, + isc_logmodule_t *module, int level, const char *fmt, ...) + ISC_FORMAT_PRINTF(5, 6); + +void +ns_client_logv(ns_client_t *client, isc_logcategory_t *category, + isc_logmodule_t *module, int level, const char *fmt, va_list ap) + ISC_FORMAT_PRINTF(5, 0); + +void +ns_client_aclmsg(const char *msg, const dns_name_t *name, dns_rdatatype_t type, + dns_rdataclass_t rdclass, char *buf, size_t len); + +#define NS_CLIENT_ACLMSGSIZE(x) \ + (DNS_NAME_FORMATSIZE + DNS_RDATATYPE_FORMATSIZE + \ + DNS_RDATACLASS_FORMATSIZE + sizeof(x) + sizeof("'/'")) + +void +ns_client_recursing(ns_client_t *client); +/*%< + * Add client to end of th recursing list. + */ + +void +ns_client_killoldestquery(ns_client_t *client); +/*%< + * Kill the oldest recursive query (recursing list head). + */ + +void +ns_client_dumprecursing(FILE *f, ns_clientmgr_t *manager); +/*%< + * Dump the outstanding recursive queries to 'f'. + */ + +void +ns_client_qnamereplace(ns_client_t *client, dns_name_t *name); +/*%< + * Replace the qname. + */ + +isc_result_t +ns_client_sourceip(dns_clientinfo_t *ci, isc_sockaddr_t **addrp); + +isc_result_t +ns_client_addopt(ns_client_t *client, dns_message_t *message, + dns_rdataset_t **opt); + +/*%< + * Get a client object from the inactive queue, or create one, as needed. + * (Not intended for use outside this module and associated tests.) + */ + +void +ns__client_request(isc_nmhandle_t *handle, isc_result_t eresult, + isc_region_t *region, void *arg); + +/*%< + * Handle client requests. + * (Not intended for use outside this module and associated tests.) + */ + +isc_result_t +ns__client_tcpconn(isc_nmhandle_t *handle, isc_result_t result, void *arg); + +/*%< + * Called every time a TCP connection is establish. This is used for + * updating TCP statistics. + */ + +dns_rdataset_t * +ns_client_newrdataset(ns_client_t *client); + +void +ns_client_putrdataset(ns_client_t *client, dns_rdataset_t **rdatasetp); +/*%< + * Get and release temporary rdatasets in the client message; + * used in query.c and in plugins. + */ + +isc_result_t +ns_client_newnamebuf(ns_client_t *client); +/*%< + * Allocate a name buffer for the client message. + */ + +dns_name_t * +ns_client_newname(ns_client_t *client, isc_buffer_t *dbuf, isc_buffer_t *nbuf); +/*%< + * Get a temporary name for the client message. + */ + +isc_buffer_t * +ns_client_getnamebuf(ns_client_t *client); +/*%< + * Get a name buffer from the pool, or allocate a new one if needed. + */ + +void +ns_client_keepname(ns_client_t *client, dns_name_t *name, isc_buffer_t *dbuf); +/*%< + * Adjust buffer 'dbuf' to reflect that 'name' is using space in it, + * and set client attributes appropriately. + */ + +void +ns_client_releasename(ns_client_t *client, dns_name_t **namep); +/*%< + * Release 'name' back to the pool of temporary names for the client + * message. If it is using a name buffer, relinquish its exclusive + * rights on the buffer. + */ + +isc_result_t +ns_client_newdbversion(ns_client_t *client, unsigned int n); +/*%< + * Allocate 'n' new database versions for use by client queries. + */ + +ns_dbversion_t * +ns_client_getdbversion(ns_client_t *client); +/*%< + * Get a free database version for use by a client query, allocating + * a new one if necessary. + */ + +ns_dbversion_t * +ns_client_findversion(ns_client_t *client, dns_db_t *db); +/*%< + * Find the correct database version to use with a client query. + * If we have already done a query related to the database 'db', + * make sure subsequent queries are from the same version; + * otherwise, take a database version from the list of dbversions + * allocated by ns_client_newdbversion(). + */ + +isc_result_t +ns__client_setup(ns_client_t *client, ns_clientmgr_t *manager, bool new); +/*%< + * Perform initial setup of an allocated client. + */ + +void +ns__client_reset_cb(void *client0); +/*%< + * Reset the client object so that it can be reused. + */ + +void +ns__client_put_cb(void *client0); +/*%< + * Free all resources allocated to this client object, so that + * it can be freed. + */ + +#endif /* NS_CLIENT_H */ diff --git a/lib/ns/include/ns/hooks.h b/lib/ns/include/ns/hooks.h new file mode 100644 index 0000000..db3846d --- /dev/null +++ b/lib/ns/include/ns/hooks.h @@ -0,0 +1,420 @@ +/* + * 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. + */ + +#ifndef NS_HOOKS_H +#define NS_HOOKS_H 1 + +/*! \file */ + +#include <stdbool.h> + +#include <isc/list.h> +#include <isc/magic.h> +#include <isc/result.h> + +#include <dns/rdatatype.h> + +#include <ns/client.h> +#include <ns/query.h> +/* + * "Hooks" are a mechanism to call a defined function or set of functions once + * a certain place in code is reached. Hook actions can inspect and alter the + * state of an ongoing process, allowing processing to continue afterward or + * triggering an early return. + * + * Currently hooks are used in two ways: in plugins, which use them to + * add functionality to query processing, and in the unit tests for libns, + * where they are used to inspect state before and after certain functions have + * run. + * + * Both of these uses are limited to libns, so hooks are currently defined in + * the ns/hooks.h header file, and hook-related macro and function names are + * prefixed with `NS_` and `ns_`. However, the design is fairly generic and + * could be repurposed for general use, e.g. as part of libisc, after some + * further customization. + * + * Hooks are created by defining a hook point identifier in the ns_hookpoint_t + * enum below, and placing a special call at a corresponding location in the + * code which invokes the action(s) for that hook; there are two such special + * calls currently implemented, namely the CALL_HOOK() and CALL_HOOK_NORETURN() + * macros in query.c. The former macro contains a "goto cleanup" statement + * which is inlined into the function into which the hook has been inserted; + * this enables the hook action to cause the calling function to return from + * the hook insertion point. For functions returning isc_result_t, if a hook + * action intends to cause a return at hook insertion point, it also has to set + * the value to be returned by the calling function. + * + * A hook table is an array (indexed by the value of the hook point identifier) + * in which each cell contains a linked list of structures, each of which + * contains a function pointer to a hook action and a pointer to data which is + * to be passed to the action function when it is called. + * + * Each view has its own separate hook table, populated by loading plugin + * modules specified in the "plugin" statements in named.conf. There is also a + * special, global hook table (ns__hook_table) that is only used by libns unit + * tests and whose existence can be safely ignored by plugin modules. + * + * Hook actions are functions which: + * + * - return an ns_hookresult_t value: + * - if NS_HOOK_RETURN is returned by the hook action, the function + * into which the hook is inserted will return and no further hook + * actions at the same hook point will be invoked, + * - if NS_HOOK_CONTINUE is returned by the hook action and there are + * further hook actions set up at the same hook point, they will be + * processed; if NS_HOOK_CONTINUE is returned and there are no + * further hook actions set up at the same hook point, execution of + * the function into which the hook has been inserted will be + * resumed. + * + * - accept three pointers as arguments: + * - a pointer specified by the special call at the hook insertion point, + * - a pointer specified upon inserting the action into the hook table, + * - a pointer to an isc_result_t value which will be returned by the + * function into which the hook is inserted if the action returns + * NS_HOOK_RETURN. + * + * In order for a hook action to be called for a given hook, a pointer to that + * action function (along with an optional pointer to action-specific data) has + * to be inserted into the relevant hook table entry for that hook using an + * ns_hook_add() call. If multiple actions are set up at a single hook point + * (e.g. by multiple plugin modules), they are processed in FIFO order, that is + * they are performed in the same order in which their relevant ns_hook_add() + * calls were issued. Since the configuration is loaded from a single thread, + * this means that multiple actions at a single hook point are determined by + * the order in which the relevant plugin modules were declared in the + * configuration file(s). The hook API currently does not support changing + * this order. + * + * As an example, consider the following hypothetical function in query.c: + * + * ---------------------------------------------------------------------------- + * static isc_result_t + * query_foo(query_ctx_t *qctx) { + * isc_result_t result; + * + * CALL_HOOK(NS_QUERY_FOO_BEGIN, qctx); + * + * ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, + * ISC_LOG_DEBUG(99), "Lorem ipsum dolor sit amet..."); + * + * result = ISC_R_COMPLETE; + * + * cleanup: + * return (result); + * } + * ---------------------------------------------------------------------------- + * + * and the following hook action: + * + * ---------------------------------------------------------------------------- + * static ns_hookresult_t + * cause_failure(void *hook_data, void *action_data, isc_result_t *resultp) { + * UNUSED(hook_data); + * UNUSED(action_data); + * + * *resultp = ISC_R_FAILURE; + * + * return (NS_HOOK_RETURN); + * } + * ---------------------------------------------------------------------------- + * + * If this hook action was installed in the hook table using: + * + * ---------------------------------------------------------------------------- + * const ns_hook_t foo_fail = { + * .action = cause_failure, + * }; + * + * ns_hook_add(..., NS_QUERY_FOO_BEGIN, &foo_fail); + * ---------------------------------------------------------------------------- + * + * then query_foo() would return ISC_R_FAILURE every time it is called due + * to the cause_failure() hook action returning NS_HOOK_RETURN and setting + * '*resultp' to ISC_R_FAILURE. query_foo() would also never log the + * "Lorem ipsum dolor sit amet..." message. + * + * Consider a different hook action: + * + * ---------------------------------------------------------------------------- + * static ns_hookresult_t + * log_qtype(void *hook_data, void *action_data, isc_result_t *resultp) { + * query_ctx_t *qctx = (query_ctx_t *)hook_data; + * FILE *stream = (FILE *)action_data; + * + * UNUSED(resultp); + * + * fprintf(stream, "QTYPE=%u\n", qctx->qtype); + * + * return (NS_HOOK_CONTINUE); + * } + * ---------------------------------------------------------------------------- + * + * If this hook action was installed in the hook table instead of + * cause_failure(), using: + * + * ---------------------------------------------------------------------------- + * const ns_hook_t foo_log_qtype = { + * .action = log_qtype, + * .action_data = stderr, + * }; + * + * ns_hook_add(..., NS_QUERY_FOO_BEGIN, &foo_log_qtype); + * ---------------------------------------------------------------------------- + * + * then the QTYPE stored in the query context passed to query_foo() would be + * logged to stderr upon each call to that function; 'qctx' would be passed to + * the hook action in 'hook_data' since it is specified in the CALL_HOOK() call + * inside query_foo() while stderr would be passed to the hook action in + * 'action_data' since it is specified in the ns_hook_t structure passed to + * ns_hook_add(). As the hook action returns NS_HOOK_CONTINUE, + * query_foo() would also be logging the "Lorem ipsum dolor sit amet..." + * message before returning ISC_R_COMPLETE. + */ + +/*! + * Currently-defined hook points. So long as these are unique, + * the order in which they are declared is unimportant, but + * currently matches the order in which they are referenced in + * query.c. + */ +typedef enum { + /* hookpoints from query.c */ + NS_QUERY_QCTX_INITIALIZED, + NS_QUERY_QCTX_DESTROYED, + NS_QUERY_SETUP, + NS_QUERY_START_BEGIN, + NS_QUERY_LOOKUP_BEGIN, + NS_QUERY_RESUME_BEGIN, + NS_QUERY_RESUME_RESTORED, + NS_QUERY_GOT_ANSWER_BEGIN, + NS_QUERY_RESPOND_ANY_BEGIN, + NS_QUERY_RESPOND_ANY_FOUND, + NS_QUERY_ADDANSWER_BEGIN, + NS_QUERY_RESPOND_BEGIN, + NS_QUERY_NOTFOUND_BEGIN, + NS_QUERY_NOTFOUND_RECURSE, + NS_QUERY_PREP_DELEGATION_BEGIN, + NS_QUERY_ZONE_DELEGATION_BEGIN, + NS_QUERY_DELEGATION_BEGIN, + NS_QUERY_DELEGATION_RECURSE_BEGIN, + NS_QUERY_NODATA_BEGIN, + NS_QUERY_NXDOMAIN_BEGIN, + NS_QUERY_NCACHE_BEGIN, + NS_QUERY_ZEROTTL_RECURSE, + NS_QUERY_CNAME_BEGIN, + NS_QUERY_DNAME_BEGIN, + NS_QUERY_PREP_RESPONSE_BEGIN, + NS_QUERY_DONE_BEGIN, + NS_QUERY_DONE_SEND, + + /* XXX other files could be added later */ + + NS_HOOKPOINTS_COUNT /* MUST BE LAST */ +} ns_hookpoint_t; + +/* + * Returned by a hook action to indicate how to proceed after it has + * been called: continue processing, or return immediately. + */ +typedef enum { + NS_HOOK_CONTINUE, + NS_HOOK_RETURN, +} ns_hookresult_t; + +typedef ns_hookresult_t (*ns_hook_action_t)(void *arg, void *data, + isc_result_t *resultp); + +typedef struct ns_hook { + isc_mem_t *mctx; + ns_hook_action_t action; + void *action_data; + ISC_LINK(struct ns_hook) link; +} ns_hook_t; + +typedef ISC_LIST(ns_hook_t) ns_hooklist_t; +typedef ns_hooklist_t ns_hooktable_t[NS_HOOKPOINTS_COUNT]; + +/*% + * ns__hook_table is a global hook table, which is used if view->hooktable + * is NULL. It's intended only for use by unit tests. + */ +LIBNS_EXTERNAL_DATA extern ns_hooktable_t *ns__hook_table; + +/* + * Plugin API version + * + * When the API changes, increment NS_PLUGIN_VERSION. If the + * change is backward-compatible (e.g., adding a new function call + * but not changing or removing an old one), increment NS_PLUGIN_AGE + * as well; if not, set NS_PLUGIN_AGE to 0. + */ +#ifndef NS_PLUGIN_VERSION +#define NS_PLUGIN_VERSION 1 +#define NS_PLUGIN_AGE 0 +#endif /* ifndef NS_PLUGIN_VERSION */ + +typedef isc_result_t +ns_plugin_register_t(const char *parameters, const void *cfg, const char *file, + unsigned long line, isc_mem_t *mctx, isc_log_t *lctx, + void *actx, ns_hooktable_t *hooktable, void **instp); +/*%< + * Called when registering a new plugin. + * + * 'parameters' contains the plugin configuration text. + * + * '*instp' will be set to the module instance handle if the function + * is successful. + * + * Returns: + *\li #ISC_R_SUCCESS + *\li #ISC_R_NOMEMORY + *\li Other errors are possible + */ + +typedef void +ns_plugin_destroy_t(void **instp); +/*%< + * Destroy a plugin instance. + * + * '*instp' must be set to NULL by the function before it returns. + */ + +typedef isc_result_t +ns_plugin_check_t(const char *parameters, const void *cfg, const char *file, + unsigned long line, isc_mem_t *mctx, isc_log_t *lctx, + void *actx); +/*%< + * Check the validity of 'parameters'. + */ + +typedef int +ns_plugin_version_t(void); +/*%< + * Return the API version number a plugin was compiled with. + * + * If the returned version number is no greater than + * NS_PLUGIN_VERSION, and no less than NS_PLUGIN_VERSION - NS_PLUGIN_AGE, + * then the module is API-compatible with named. + */ + +/*% + * Prototypes for API functions to be defined in each module. + */ +ns_plugin_check_t plugin_check; +ns_plugin_destroy_t plugin_destroy; +ns_plugin_register_t plugin_register; +ns_plugin_version_t plugin_version; + +isc_result_t +ns_plugin_expandpath(const char *src, char *dst, size_t dstsize); +/*%< + * Prepare the plugin location to be passed to dlopen() based on the plugin + * path or filename found in the configuration file ('src'). Store the result + * in 'dst', which is 'dstsize' bytes large. + * + * On Unix systems, two classes of 'src' are recognized: + * + * - If 'src' is an absolute or relative path, it will be copied to 'dst' + * verbatim. + * + * - If 'src' is a filename (i.e. does not contain a path separator), the + * path to the directory into which named plugins are installed will be + * prepended to it and the result will be stored in 'dst'. + * + * On Windows, 'src' is always copied to 'dst' verbatim. + * + * Returns: + *\li #ISC_R_SUCCESS Success + *\li #ISC_R_NOSPACE 'dst' is not large enough to hold the output string + *\li Other result snprintf() returned a negative value + */ + +isc_result_t +ns_plugin_register(const char *modpath, const char *parameters, const void *cfg, + const char *cfg_file, unsigned long cfg_line, + isc_mem_t *mctx, isc_log_t *lctx, void *actx, + dns_view_t *view); +/*%< + * Load the plugin module specified from the file 'modpath', and + * register an instance using 'parameters'. + * + * 'cfg_file' and 'cfg_line' specify the location of the plugin + * declaration in the configuration file. + * + * 'cfg' and 'actx' are the configuration context and ACL configuration + * context, respectively; they are passed as void * here in order to + * prevent this library from having a dependency on libisccfg). + * + * 'instp' will be left pointing to the instance of the plugin + * created by the module's plugin_register function. + */ + +isc_result_t +ns_plugin_check(const char *modpath, const char *parameters, const void *cfg, + const char *cfg_file, unsigned long cfg_line, isc_mem_t *mctx, + isc_log_t *lctx, void *actx); +/*%< + * Open the plugin module at 'modpath' and check the validity of + * 'parameters', logging any errors or warnings found, then + * close it without configuring it. + */ + +void +ns_plugins_create(isc_mem_t *mctx, ns_plugins_t **listp); +/*%< + * Create and initialize a plugin list. + */ + +void +ns_plugins_free(isc_mem_t *mctx, void **listp); +/*%< + * Close each plugin module in a plugin list, then free the list object. + */ + +void +ns_hooktable_free(isc_mem_t *mctx, void **tablep); +/*%< + * Free a hook table. + */ + +void +ns_hook_add(ns_hooktable_t *hooktable, isc_mem_t *mctx, + ns_hookpoint_t hookpoint, const ns_hook_t *hook); +/*%< + * Allocate (using memory context 'mctx') a copy of the 'hook' structure + * describing a hook action and append it to the list of hooks at 'hookpoint' + * in 'hooktable'. + * + * Requires: + *\li 'hooktable' is not NULL + * + *\li 'mctx' is not NULL + * + *\li 'hookpoint' is less than NS_QUERY_HOOKS_COUNT + * + *\li 'hook' is not NULL + */ + +void +ns_hooktable_init(ns_hooktable_t *hooktable); +/*%< + * Initialize a hook table. + */ + +isc_result_t +ns_hooktable_create(isc_mem_t *mctx, ns_hooktable_t **tablep); +/*%< + * Allocate and initialize a hook table. + */ +#endif /* NS_HOOKS_H */ diff --git a/lib/ns/include/ns/interfacemgr.h b/lib/ns/include/ns/interfacemgr.h new file mode 100644 index 0000000..9f696e2 --- /dev/null +++ b/lib/ns/include/ns/interfacemgr.h @@ -0,0 +1,202 @@ +/* + * 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. + */ + +#ifndef NS_INTERFACEMGR_H +#define NS_INTERFACEMGR_H 1 + +/***** +***** Module Info +*****/ + +/*! \file + * \brief + * The interface manager monitors the operating system's list + * of network interfaces, creating and destroying listeners + * as needed. + * + * Reliability: + *\li No impact expected. + * + * Resources: + * + * Security: + * \li The server will only be able to bind to the DNS port on + * newly discovered interfaces if it is running as root. + * + * Standards: + *\li The API for scanning varies greatly among operating systems. + * This module attempts to hide the differences. + */ + +/*** + *** Imports + ***/ + +#include <stdbool.h> + +#include <isc/magic.h> +#include <isc/mem.h> +#include <isc/netmgr.h> +#include <isc/refcount.h> +#include <isc/socket.h> + +#include <dns/geoip.h> +#include <dns/result.h> + +#include <ns/listenlist.h> +#include <ns/types.h> + +/*** + *** Types + ***/ + +#define IFACE_MAGIC ISC_MAGIC('I', ':', '-', ')') +#define NS_INTERFACE_VALID(t) ISC_MAGIC_VALID(t, IFACE_MAGIC) + +#define NS_INTERFACEFLAG_ANYADDR 0x01U /*%< bound to "any" address */ +#define MAX_UDP_DISPATCH \ + 128 /*%< Maximum number of UDP dispatchers \ + * to start per interface */ +/*% The nameserver interface structure */ +struct ns_interface { + unsigned int magic; /*%< Magic number. */ + ns_interfacemgr_t *mgr; /*%< Interface manager. */ + isc_mutex_t lock; + isc_refcount_t references; + unsigned int generation; /*%< Generation number. */ + isc_sockaddr_t addr; /*%< Address and port. */ + unsigned int flags; /*%< Interface flags */ + char name[32]; /*%< Null terminated. */ + dns_dispatch_t *udpdispatch[MAX_UDP_DISPATCH]; + /*%< UDP dispatchers. */ + isc_socket_t *tcpsocket; /*%< TCP socket. */ + isc_nmsocket_t *udplistensocket; + isc_nmsocket_t *tcplistensocket; + isc_dscp_t dscp; /*%< "listen-on" DSCP value */ + isc_refcount_t ntcpaccepting; /*%< Number of clients + * ready to accept new + * TCP connections on this + * interface */ + isc_refcount_t ntcpactive; /*%< Number of clients + * servicing TCP queries + * (whether accepting or + * connected) */ + int nudpdispatch; /*%< Number of UDP dispatches */ + ns_clientmgr_t *clientmgr; /*%< Client manager. */ + ISC_LINK(ns_interface_t) link; +}; + +/*** + *** Functions + ***/ + +isc_result_t +ns_interfacemgr_create(isc_mem_t *mctx, ns_server_t *sctx, + isc_taskmgr_t *taskmgr, isc_timermgr_t *timermgr, + isc_socketmgr_t *socketmgr, isc_nm_t *nm, + dns_dispatchmgr_t *dispatchmgr, isc_task_t *task, + unsigned int udpdisp, dns_geoip_databases_t *geoip, + int ncpus, ns_interfacemgr_t **mgrp); +/*%< + * Create a new interface manager. + * + * Initially, the new manager will not listen on any interfaces. + * Call ns_interfacemgr_setlistenon() and/or ns_interfacemgr_setlistenon6() + * to set nonempty listen-on lists. + */ + +void +ns_interfacemgr_attach(ns_interfacemgr_t *source, ns_interfacemgr_t **target); + +void +ns_interfacemgr_detach(ns_interfacemgr_t **targetp); + +void +ns_interfacemgr_shutdown(ns_interfacemgr_t *mgr); + +void +ns_interfacemgr_setbacklog(ns_interfacemgr_t *mgr, int backlog); +/*%< + * Set the size of the listen() backlog queue. + */ + +bool +ns_interfacemgr_islistening(ns_interfacemgr_t *mgr); +/*%< + * Return if the manager is listening on any interface. It can be called + * after a scan or adjust. + */ + +isc_result_t +ns_interfacemgr_scan(ns_interfacemgr_t *mgr, bool verbose); +/*%< + * Scan the operatings system's list of network interfaces + * and create listeners when new interfaces are discovered. + * Shut down the sockets for interfaces that go away. + * + * This should be called once on server startup and then + * periodically according to the 'interface-interval' option + * in named.conf. + */ + +void +ns_interfacemgr_setlistenon4(ns_interfacemgr_t *mgr, ns_listenlist_t *value); +/*%< + * Set the IPv4 "listen-on" list of 'mgr' to 'value'. + * The previous IPv4 listen-on list is freed. + */ + +void +ns_interfacemgr_setlistenon6(ns_interfacemgr_t *mgr, ns_listenlist_t *value); +/*%< + * Set the IPv6 "listen-on" list of 'mgr' to 'value'. + * The previous IPv6 listen-on list is freed. + */ + +dns_aclenv_t * +ns_interfacemgr_getaclenv(ns_interfacemgr_t *mgr); + +void +ns_interface_attach(ns_interface_t *source, ns_interface_t **target); + +void +ns_interface_detach(ns_interface_t **targetp); + +void +ns_interface_shutdown(ns_interface_t *ifp); +/*%< + * Stop listening for queries on interface 'ifp'. + * May safely be called multiple times. + */ + +void +ns_interfacemgr_dumprecursing(FILE *f, ns_interfacemgr_t *mgr); + +bool +ns_interfacemgr_listeningon(ns_interfacemgr_t *mgr, const isc_sockaddr_t *addr); + +ns_server_t * +ns_interfacemgr_getserver(ns_interfacemgr_t *mgr); +/*%< + * Returns the ns_server object associated with the interface manager. + */ + +ns_interface_t * +ns__interfacemgr_getif(ns_interfacemgr_t *mgr); +ns_interface_t * +ns__interfacemgr_nextif(ns_interface_t *ifp); +/* + * Functions to allow external callers to walk the interfaces list. + * (Not intended for use outside this module and associated tests.) + */ +#endif /* NS_INTERFACEMGR_H */ diff --git a/lib/ns/include/ns/lib.h b/lib/ns/include/ns/lib.h new file mode 100644 index 0000000..8c1d113 --- /dev/null +++ b/lib/ns/include/ns/lib.h @@ -0,0 +1,37 @@ +/* + * 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 + +/*! \file include/ns/lib.h */ + +#include <isc/lang.h> +#include <isc/types.h> + +ISC_LANG_BEGINDECLS + +LIBNS_EXTERNAL_DATA extern unsigned int ns_pps; + +isc_result_t +ns_lib_init(void); +/*%< + * A set of initialization procedures used in the NS library. + */ + +void +ns_lib_shutdown(void); +/*%< + * Free temporary resources allocated in ns_lib_init(). + */ + +ISC_LANG_ENDDECLS diff --git a/lib/ns/include/ns/listenlist.h b/lib/ns/include/ns/listenlist.h new file mode 100644 index 0000000..74ff87b --- /dev/null +++ b/lib/ns/include/ns/listenlist.h @@ -0,0 +1,101 @@ +/* + * 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. + */ + +#ifndef NS_LISTENLIST_H +#define NS_LISTENLIST_H 1 + +/***** +***** Module Info +*****/ + +/*! \file + * \brief + * "Listen lists", as in the "listen-on" configuration statement. + */ + +/*** + *** Imports + ***/ + +#include <stdbool.h> + +#include <isc/net.h> + +#include <dns/types.h> + +/*** + *** Types + ***/ + +typedef struct ns_listenelt ns_listenelt_t; +typedef struct ns_listenlist ns_listenlist_t; + +struct ns_listenelt { + isc_mem_t *mctx; + in_port_t port; + isc_dscp_t dscp; /* -1 = not set, 0..63 */ + dns_acl_t *acl; + ISC_LINK(ns_listenelt_t) link; +}; + +struct ns_listenlist { + isc_mem_t *mctx; + int refcount; + ISC_LIST(ns_listenelt_t) elts; +}; + +/*** + *** Functions + ***/ + +isc_result_t +ns_listenelt_create(isc_mem_t *mctx, in_port_t port, isc_dscp_t dscp, + dns_acl_t *acl, ns_listenelt_t **target); +/*%< + * Create a listen-on list element. + */ + +void +ns_listenelt_destroy(ns_listenelt_t *elt); +/*%< + * Destroy a listen-on list element. + */ + +isc_result_t +ns_listenlist_create(isc_mem_t *mctx, ns_listenlist_t **target); +/*%< + * Create a new, empty listen-on list. + */ + +void +ns_listenlist_attach(ns_listenlist_t *source, ns_listenlist_t **target); +/*%< + * Attach '*target' to '*source'. + */ + +void +ns_listenlist_detach(ns_listenlist_t **listp); +/*%< + * Detach 'listp'. + */ + +isc_result_t +ns_listenlist_default(isc_mem_t *mctx, in_port_t port, isc_dscp_t dscp, + bool enabled, ns_listenlist_t **target); +/*%< + * Create a listen-on list with default contents, matching + * all addresses with port 'port' (if 'enabled' is true), + * or no addresses (if 'enabled' is false). + */ + +#endif /* NS_LISTENLIST_H */ diff --git a/lib/ns/include/ns/log.h b/lib/ns/include/ns/log.h new file mode 100644 index 0000000..eb12ff6 --- /dev/null +++ b/lib/ns/include/ns/log.h @@ -0,0 +1,74 @@ +/* + * 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. + */ + +#ifndef NS_LOG_H +#define NS_LOG_H 1 + +/*! \file */ + +#include <isc/log.h> +#include <isc/types.h> + +LIBNS_EXTERNAL_DATA extern isc_log_t *ns_lctx; +LIBNS_EXTERNAL_DATA extern isc_logcategory_t ns_categories[]; +LIBNS_EXTERNAL_DATA extern isc_logmodule_t ns_modules[]; + +#define NS_LOGCATEGORY_CLIENT (&ns_categories[0]) +#define NS_LOGCATEGORY_NETWORK (&ns_categories[1]) +#define NS_LOGCATEGORY_UPDATE (&ns_categories[2]) +#define NS_LOGCATEGORY_QUERIES (&ns_categories[3]) +#define NS_LOGCATEGORY_UPDATE_SECURITY (&ns_categories[4]) +#define NS_LOGCATEGORY_QUERY_ERRORS (&ns_categories[5]) +#define NS_LOGCATEGORY_TAT (&ns_categories[6]) +#define NS_LOGCATEGORY_SERVE_STALE (&ns_categories[7]) + +/* + * Backwards compatibility. + */ +#define NS_LOGCATEGORY_GENERAL ISC_LOGCATEGORY_GENERAL + +#define NS_LOGMODULE_CLIENT (&ns_modules[0]) +#define NS_LOGMODULE_QUERY (&ns_modules[1]) +#define NS_LOGMODULE_INTERFACEMGR (&ns_modules[2]) +#define NS_LOGMODULE_UPDATE (&ns_modules[3]) +#define NS_LOGMODULE_XFER_IN (&ns_modules[4]) +#define NS_LOGMODULE_XFER_OUT (&ns_modules[5]) +#define NS_LOGMODULE_NOTIFY (&ns_modules[6]) +#define NS_LOGMODULE_HOOKS (&ns_modules[7]) + +void +ns_log_init(isc_log_t *lctx); +/*%< + * Make the libns categories and modules available for use with the + * ISC logging library. + * + * Requires: + *\li lctx is a valid logging context. + * + *\li ns_log_init() is called only once. + * + * Ensures: + *\li The categories and modules defined above are available for + * use by isc_log_usechannnel() and isc_log_write(). + */ + +void +ns_log_setcontext(isc_log_t *lctx); +/*%< + * Make the libns library use the provided context for logging internal + * messages. + * + * Requires: + *\li lctx is a valid logging context. + */ +#endif /* NS_LOG_H */ diff --git a/lib/ns/include/ns/notify.h b/lib/ns/include/ns/notify.h new file mode 100644 index 0000000..2c5fcf3 --- /dev/null +++ b/lib/ns/include/ns/notify.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef NS_NOTIFY_H +#define NS_NOTIFY_H 1 + +#include <ns/client.h> + +/*** + *** Module Info + ***/ + +/*! \file + * \brief + * RFC1996 + * A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY) + */ + +/*** + *** Functions. + ***/ + +void +ns_notify_start(ns_client_t *client, isc_nmhandle_t *handle); + +/*%< + * Examines the incoming message to determine appropriate zone. + * Returns FORMERR if there is not exactly one question. + * Returns REFUSED if we do not serve the listed zone. + * Pass the message to the zone module for processing + * and returns the return status. + * + * Requires + *\li client to be valid. + */ + +#endif /* NS_NOTIFY_H */ diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h new file mode 100644 index 0000000..be0dadd --- /dev/null +++ b/lib/ns/include/ns/query.h @@ -0,0 +1,235 @@ +/* + * 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. + */ + +#ifndef NS_QUERY_H +#define NS_QUERY_H 1 + +/*! \file */ + +#include <stdbool.h> + +#include <isc/buffer.h> +#include <isc/netaddr.h> +#include <isc/types.h> + +#include <dns/rdataset.h> +#include <dns/resolver.h> +#include <dns/rpz.h> +#include <dns/types.h> + +#include <ns/types.h> + +/*% nameserver database version structure */ +typedef struct ns_dbversion { + dns_db_t *db; + dns_dbversion_t *version; + bool acl_checked; + bool queryok; + ISC_LINK(struct ns_dbversion) link; +} ns_dbversion_t; + +/*% + * nameserver recursion parameters, to uniquely identify a recursion + * query; this is used to detect a recursion loop + */ +typedef struct ns_query_recparam { + dns_rdatatype_t qtype; + dns_name_t *qname; + dns_fixedname_t fqname; + dns_name_t *qdomain; + dns_fixedname_t fqdomain; +} ns_query_recparam_t; + +/*% nameserver query structure */ +struct ns_query { + unsigned int attributes; + unsigned int restarts; + bool timerset; + dns_name_t *qname; + dns_name_t *origqname; + dns_rdatatype_t qtype; + unsigned int dboptions; + unsigned int fetchoptions; + dns_db_t *gluedb; + dns_db_t *authdb; + dns_zone_t *authzone; + bool authdbset; + bool isreferral; + isc_mutex_t fetchlock; + dns_fetch_t *fetch; + dns_fetch_t *prefetch; + dns_rpz_st_t *rpz_st; + isc_bufferlist_t namebufs; + ISC_LIST(ns_dbversion_t) activeversions; + ISC_LIST(ns_dbversion_t) freeversions; + dns_rdataset_t *dns64_aaaa; + dns_rdataset_t *dns64_sigaaaa; + bool *dns64_aaaaok; + unsigned int dns64_aaaaoklen; + unsigned int dns64_options; + unsigned int dns64_ttl; + + struct { + dns_db_t *db; + dns_zone_t *zone; + dns_dbnode_t *node; + dns_rdatatype_t qtype; + dns_name_t *fname; + dns_fixedname_t fixed; + isc_result_t result; + dns_rdataset_t *rdataset; + dns_rdataset_t *sigrdataset; + bool authoritative; + bool is_zone; + } redirect; + + ns_query_recparam_t recparam; + + dns_keytag_t root_key_sentinel_keyid; + bool root_key_sentinel_is_ta; + bool root_key_sentinel_not_ta; +}; + +#define NS_QUERYATTR_RECURSIONOK 0x000001 +#define NS_QUERYATTR_CACHEOK 0x000002 +#define NS_QUERYATTR_PARTIALANSWER 0x000004 +#define NS_QUERYATTR_NAMEBUFUSED 0x000008 +#define NS_QUERYATTR_RECURSING 0x000010 +#define NS_QUERYATTR_QUERYOKVALID 0x000040 +#define NS_QUERYATTR_QUERYOK 0x000080 +#define NS_QUERYATTR_WANTRECURSION 0x000100 +#define NS_QUERYATTR_SECURE 0x000200 +#define NS_QUERYATTR_NOAUTHORITY 0x000400 +#define NS_QUERYATTR_NOADDITIONAL 0x000800 +#define NS_QUERYATTR_CACHEACLOKVALID 0x001000 +#define NS_QUERYATTR_CACHEACLOK 0x002000 +#define NS_QUERYATTR_DNS64 0x004000 +#define NS_QUERYATTR_DNS64EXCLUDE 0x008000 +#define NS_QUERYATTR_RRL_CHECKED 0x010000 +#define NS_QUERYATTR_REDIRECT 0x020000 +#define NS_QUERYATTR_ANSWERED 0x040000 +#define NS_QUERYATTR_STALEOK 0x080000 +#define NS_QUERYATTR_STALEPENDING 0x100000 + +typedef struct query_ctx query_ctx_t; + +/* query context structure */ +struct query_ctx { + isc_buffer_t *dbuf; /* name buffer */ + dns_name_t *fname; /* found name from DB lookup */ + dns_name_t *tname; /* temporary name, used + * when processing ANY + * queries */ + dns_rdataset_t *rdataset; /* found rdataset */ + dns_rdataset_t *sigrdataset; /* found sigrdataset */ + dns_rdataset_t *noqname; /* rdataset needing + * NOQNAME proof */ + dns_rdatatype_t qtype; + dns_rdatatype_t type; + + unsigned int options; /* DB lookup options */ + + bool redirected; /* nxdomain redirected? */ + bool is_zone; /* is DB a zone DB? */ + bool is_staticstub_zone; + bool resuming; /* resumed from recursion? */ + bool dns64, dns64_exclude, rpz; + bool authoritative; /* authoritative query? */ + bool want_restart; /* CNAME chain or other + * restart needed */ + bool refresh_rrset; /* stale RRset refresh needed */ + bool need_wildcardproof; /* wildcard proof needed */ + bool nxrewrite; /* negative answer from RPZ */ + bool findcoveringnsec; /* lookup covering NSEC */ + bool answer_has_ns; /* NS is in answer */ + dns_fixedname_t wildcardname; /* name needing wcard proof */ + dns_fixedname_t dsname; /* name needing DS */ + + ns_client_t *client; /* client object */ + bool detach_client; /* client needs detaching */ + + dns_fetchevent_t *event; /* recursion event */ + + dns_db_t *db; /* zone or cache database */ + dns_dbversion_t *version; /* DB version */ + dns_dbnode_t *node; /* DB node */ + + dns_db_t *zdb; /* zone DB values, saved */ + dns_dbnode_t *znode; /* while searching cache */ + dns_name_t *zfname; /* for a better answer */ + dns_dbversion_t *zversion; + dns_rdataset_t *zrdataset; + dns_rdataset_t *zsigrdataset; + + dns_rpz_st_t *rpz_st; /* RPZ state */ + dns_zone_t *zone; /* zone to search */ + + dns_view_t *view; /* client view */ + + isc_result_t result; /* query result */ + int line; /* line to report error */ +}; + +isc_result_t +ns_query_done(query_ctx_t *qctx); +/*%< + * Finalize this phase of the query process: + * + * - Clean up. + * - If we have an answer ready (positive or negative), send it. + * - If we need to restart for a chaining query, call ns__query_start() again. + * - If we've started recursion, then just clean up; things will be + * restarted via fetch_callback()/query_resume(). + */ + +isc_result_t +ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, + dns_name_t *qdomain, dns_rdataset_t *nameservers, + bool resuming); +/*%< + * Prepare client for recursion, then create a resolver fetch, with + * the event callback set to fetch_callback(). Afterward we terminate + * this phase of the query, and resume with a new query context when + * recursion completes. + */ + +isc_result_t +ns_query_init(ns_client_t *client); + +void +ns_query_free(ns_client_t *client); + +void +ns_query_start(ns_client_t *client, isc_nmhandle_t *handle); + +void +ns_query_cancel(ns_client_t *client); + +/* + * The following functions are expected to be used only within query.c + * and query modules. + */ + +isc_result_t +ns__query_sfcache(query_ctx_t *qctx); +/*%< + * (Must not be used outside this module and its associated unit tests.) + */ + +isc_result_t +ns__query_start(query_ctx_t *qctx); +/*%< + * (Must not be used outside this module and its associated unit tests.) + */ + +#endif /* NS_QUERY_H */ diff --git a/lib/ns/include/ns/server.h b/lib/ns/include/ns/server.h new file mode 100644 index 0000000..6ace9f3 --- /dev/null +++ b/lib/ns/include/ns/server.h @@ -0,0 +1,189 @@ +/* + * 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. + */ + +#ifndef NS_SERVER_H +#define NS_SERVER_H 1 + +/*! \file */ + +#include <inttypes.h> +#include <stdbool.h> + +#include <isc/fuzz.h> +#include <isc/log.h> +#include <isc/magic.h> +#include <isc/quota.h> +#include <isc/random.h> +#include <isc/sockaddr.h> +#include <isc/types.h> + +#include <dns/acl.h> +#include <dns/types.h> + +#include <ns/types.h> + +#define NS_EVENT_CLIENTCONTROL (ISC_EVENTCLASS_NS + 0) + +#define NS_SERVER_LOGQUERIES 0x00000001U /*%< log queries */ +#define NS_SERVER_NOAA 0x00000002U /*%< -T noaa */ +#define NS_SERVER_NOSOA 0x00000004U /*%< -T nosoa */ +#define NS_SERVER_NONEAREST 0x00000008U /*%< -T nonearest */ +#define NS_SERVER_NOEDNS 0x00000020U /*%< -T noedns */ +#define NS_SERVER_DROPEDNS 0x00000040U /*%< -T dropedns */ +#define NS_SERVER_NOTCP 0x00000080U /*%< -T notcp */ +#define NS_SERVER_DISABLE4 0x00000100U /*%< -6 */ +#define NS_SERVER_DISABLE6 0x00000200U /*%< -4 */ +#define NS_SERVER_FIXEDLOCAL 0x00000400U /*%< -T fixedlocal */ +#define NS_SERVER_SIGVALINSECS 0x00000800U /*%< -T sigvalinsecs */ +#define NS_SERVER_EDNSFORMERR 0x00001000U /*%< -T ednsformerr (STD13) */ +#define NS_SERVER_EDNSNOTIMP 0x00002000U /*%< -T ednsnotimp */ +#define NS_SERVER_EDNSREFUSED 0x00004000U /*%< -T ednsrefused */ + +/*% + * Type for callback function to get hostname. + */ +typedef isc_result_t (*ns_hostnamecb_t)(char *buf, size_t len); + +/*% + * Type for callback function to signal the fuzzer thread + * when built with AFL. + */ +typedef void (*ns_fuzzcb_t)(void); + +/*% + * Type for callback function to get the view that can answer a query. + */ +typedef isc_result_t (*ns_matchview_t)( + isc_netaddr_t *srcaddr, isc_netaddr_t *destaddr, dns_message_t *message, + dns_aclenv_t *env, isc_result_t *sigresultp, dns_view_t **viewp); + +/*% + * Server context. + */ +struct ns_server { + unsigned int magic; + isc_mem_t *mctx; + + isc_refcount_t references; + + /*% Server cookie secret and algorithm */ + unsigned char secret[32]; + ns_cookiealg_t cookiealg; + ns_altsecretlist_t altsecrets; + bool answercookie; + + /*% Quotas */ + isc_quota_t recursionquota; + isc_quota_t tcpquota; + isc_quota_t xfroutquota; + isc_quota_t updquota; + + /*% Test options and other configurables */ + uint32_t options; + + dns_acl_t *blackholeacl; + dns_acl_t *keepresporder; + uint16_t udpsize; + uint16_t transfer_tcp_message_size; + bool interface_auto; + dns_tkeyctx_t *tkeyctx; + + /*% Server id for NSID */ + char *server_id; + ns_hostnamecb_t gethostname; + + /*% Fuzzer callback */ + isc_fuzztype_t fuzztype; + ns_fuzzcb_t fuzznotify; + + /*% Callback to find a matching view for a query */ + ns_matchview_t matchingview; + + /*% Stats counters */ + ns_stats_t *nsstats; + dns_stats_t *rcvquerystats; + dns_stats_t *opcodestats; + dns_stats_t *rcodestats; + + isc_stats_t *udpinstats4; + isc_stats_t *udpoutstats4; + isc_stats_t *udpinstats6; + isc_stats_t *udpoutstats6; + + isc_stats_t *tcpinstats4; + isc_stats_t *tcpoutstats4; + isc_stats_t *tcpinstats6; + isc_stats_t *tcpoutstats6; +}; + +struct ns_altsecret { + ISC_LINK(ns_altsecret_t) link; + unsigned char secret[32]; +}; + +isc_result_t +ns_server_create(isc_mem_t *mctx, ns_matchview_t matchingview, + ns_server_t **sctxp); +/*%< + * Create a server context object with default settings. + */ + +void +ns_server_attach(ns_server_t *src, ns_server_t **dest); +/*%< + * Attach a server context. + * + * Requires: + *\li 'src' is valid. + */ + +void +ns_server_detach(ns_server_t **sctxp); +/*%< + * Detach from a server context. If its reference count drops to zero, destroy + * it, freeing its memory. + * + * Requires: + *\li '*sctxp' is valid. + * Ensures: + *\li '*sctxp' is NULL on return. + */ + +isc_result_t +ns_server_setserverid(ns_server_t *sctx, const char *serverid); +/*%< + * Set sctx->server_id to 'serverid'. If it was set previously, free the memory. + * + * Requires: + *\li 'sctx' is valid. + */ + +void +ns_server_setoption(ns_server_t *sctx, unsigned int option, bool value); +/*%< + * Set the given options on (if 'value' == #true) + * or off (if 'value' == #false). + * + * Requires: + *\li 'sctx' is valid + */ + +bool +ns_server_getoption(ns_server_t *sctx, unsigned int option); +/*%< + * Returns the current value of the specified server option. + * + * Requires: + *\li 'sctx' is valid. + */ +#endif /* NS_SERVER_H */ diff --git a/lib/ns/include/ns/sortlist.h b/lib/ns/include/ns/sortlist.h new file mode 100644 index 0000000..a2eedd9 --- /dev/null +++ b/lib/ns/include/ns/sortlist.h @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#ifndef NS_SORTLIST_H +#define NS_SORTLIST_H 1 + +/*! \file */ + +#include <isc/types.h> + +#include <dns/acl.h> +#include <dns/types.h> + +/*% + * Type for callback functions that rank addresses. + */ +typedef int (*dns_addressorderfunc_t)(const isc_netaddr_t *address, + const void *arg); + +/*% + * Return value type for setup_sortlist. + */ +typedef enum { + NS_SORTLISTTYPE_NONE, + NS_SORTLISTTYPE_1ELEMENT, + NS_SORTLISTTYPE_2ELEMENT +} ns_sortlisttype_t; + +ns_sortlisttype_t +ns_sortlist_setup(dns_acl_t *acl, dns_aclenv_t *env, isc_netaddr_t *clientaddr, + const void **argp); +/*%< + * Find the sortlist statement in 'acl' (for ACL environment 'env') + * that applies to 'clientaddr', if any. + * + * If a 1-element sortlist item applies, return NS_SORTLISTTYPE_1ELEMENT and + * make '*argp' point to the matching subelement. + * + * If a 2-element sortlist item applies, return NS_SORTLISTTYPE_2ELEMENT and + * make '*argp' point to ACL that forms the second element. + * + * If no sortlist item applies, return NS_SORTLISTTYPE_NONE and set '*argp' + * to NULL. + */ + +int +ns_sortlist_addrorder1(const isc_netaddr_t *addr, const void *arg); +/*%< + * Find the sort order of 'addr' in 'arg', the matching element + * of a 1-element top-level sortlist statement. + */ + +int +ns_sortlist_addrorder2(const isc_netaddr_t *addr, const void *arg); +/*%< + * Find the sort order of 'addr' in 'arg', a topology-like + * ACL forming the second element in a 2-element top-level + * sortlist statement. + */ + +void +ns_sortlist_byaddrsetup(dns_acl_t *sortlist_acl, dns_aclenv_t *env, + isc_netaddr_t *client_addr, + dns_addressorderfunc_t *orderp, const void **argp); +/*%< + * Find the sortlist statement in 'acl' that applies to 'clientaddr', if any. + * If a sortlist statement applies, return in '*orderp' a pointer to a function + * for ranking network addresses based on that sortlist statement, and in + * '*argp' an argument to pass to said function. If no sortlist statement + * applies, set '*orderp' and '*argp' to NULL. + */ + +#endif /* NS_SORTLIST_H */ diff --git a/lib/ns/include/ns/stats.h b/lib/ns/include/ns/stats.h new file mode 100644 index 0000000..b9564aa --- /dev/null +++ b/lib/ns/include/ns/stats.h @@ -0,0 +1,141 @@ +/* + * 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. + */ + +#ifndef NS_STATS_H +#define NS_STATS_H 1 + +/*! \file include/ns/stats.h */ + +#include <ns/types.h> + +/*% + * Server statistics counters. Used as isc_statscounter_t values. + */ +enum { + ns_statscounter_requestv4 = 0, + ns_statscounter_requestv6 = 1, + ns_statscounter_edns0in = 2, + ns_statscounter_badednsver = 3, + ns_statscounter_tsigin = 4, + ns_statscounter_sig0in = 5, + ns_statscounter_invalidsig = 6, + ns_statscounter_requesttcp = 7, + + ns_statscounter_authrej = 8, + ns_statscounter_recurserej = 9, + ns_statscounter_xfrrej = 10, + ns_statscounter_updaterej = 11, + + ns_statscounter_response = 12, + ns_statscounter_truncatedresp = 13, + ns_statscounter_edns0out = 14, + ns_statscounter_tsigout = 15, + ns_statscounter_sig0out = 16, + + ns_statscounter_success = 17, + ns_statscounter_authans = 18, + ns_statscounter_nonauthans = 19, + ns_statscounter_referral = 20, + ns_statscounter_nxrrset = 21, + ns_statscounter_servfail = 22, + ns_statscounter_formerr = 23, + ns_statscounter_nxdomain = 24, + ns_statscounter_recursion = 25, + ns_statscounter_duplicate = 26, + ns_statscounter_dropped = 27, + ns_statscounter_failure = 28, + + ns_statscounter_xfrdone = 29, + + ns_statscounter_updatereqfwd = 30, + ns_statscounter_updaterespfwd = 31, + ns_statscounter_updatefwdfail = 32, + ns_statscounter_updatedone = 33, + ns_statscounter_updatefail = 34, + ns_statscounter_updatebadprereq = 35, + + ns_statscounter_recursclients = 36, + + ns_statscounter_dns64 = 37, + + ns_statscounter_ratedropped = 38, + ns_statscounter_rateslipped = 39, + + ns_statscounter_rpz_rewrites = 40, + + ns_statscounter_udp = 41, + ns_statscounter_tcp = 42, + + ns_statscounter_nsidopt = 43, + ns_statscounter_expireopt = 44, + ns_statscounter_otheropt = 45, + ns_statscounter_ecsopt = 46, + ns_statscounter_padopt = 47, + ns_statscounter_keepaliveopt = 48, + + ns_statscounter_nxdomainredirect = 49, + ns_statscounter_nxdomainredirect_rlookup = 50, + + ns_statscounter_cookiein = 51, + ns_statscounter_cookiebadsize = 52, + ns_statscounter_cookiebadtime = 53, + ns_statscounter_cookienomatch = 54, + ns_statscounter_cookiematch = 55, + ns_statscounter_cookienew = 56, + ns_statscounter_badcookie = 57, + + ns_statscounter_nxdomainsynth = 58, + ns_statscounter_nodatasynth = 59, + ns_statscounter_wildcardsynth = 60, + + ns_statscounter_trystale = 61, + ns_statscounter_usedstale = 62, + + ns_statscounter_prefetch = 63, + ns_statscounter_keytagopt = 64, + + ns_statscounter_tcphighwater = 65, + + ns_statscounter_reclimitdropped = 66, + + ns_statscounter_updatequota = 67, + + ns_statscounter_max = 68, +}; + +void +ns_stats_attach(ns_stats_t *stats, ns_stats_t **statsp); + +void +ns_stats_detach(ns_stats_t **statsp); + +isc_result_t +ns_stats_create(isc_mem_t *mctx, int ncounters, ns_stats_t **statsp); + +void +ns_stats_increment(ns_stats_t *stats, isc_statscounter_t counter); + +void +ns_stats_decrement(ns_stats_t *stats, isc_statscounter_t counter); + +isc_stats_t * +ns_stats_get(ns_stats_t *stats); + +void +ns_stats_update_if_greater(ns_stats_t *stats, isc_statscounter_t counter, + isc_statscounter_t value); + +isc_statscounter_t +ns_stats_get_counter(ns_stats_t *stats, isc_statscounter_t counter); + +#endif /* NS_STATS_H */ diff --git a/lib/ns/include/ns/types.h b/lib/ns/include/ns/types.h new file mode 100644 index 0000000..f16f9a0 --- /dev/null +++ b/lib/ns/include/ns/types.h @@ -0,0 +1,35 @@ +/* + * 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. + */ + +#ifndef NS_TYPES_H +#define NS_TYPES_H 1 + +/*! \file */ + +typedef struct ns_altsecret ns_altsecret_t; +typedef ISC_LIST(ns_altsecret_t) ns_altsecretlist_t; +typedef struct ns_client ns_client_t; +typedef struct ns_clientmgr ns_clientmgr_t; +typedef struct ns_plugin ns_plugin_t; +typedef ISC_LIST(ns_plugin_t) ns_plugins_t; +typedef struct ns_interface ns_interface_t; +typedef struct ns_interfacemgr ns_interfacemgr_t; +typedef struct ns_query ns_query_t; +typedef struct ns_server ns_server_t; +typedef struct ns_stats ns_stats_t; + +typedef enum { ns_cookiealg_aes, ns_cookiealg_siphash24 } ns_cookiealg_t; + +#define NS_COOKIE_VERSION_1 1 + +#endif /* NS_TYPES_H */ diff --git a/lib/ns/include/ns/update.h b/lib/ns/include/ns/update.h new file mode 100644 index 0000000..72ac604 --- /dev/null +++ b/lib/ns/include/ns/update.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#ifndef NS_UPDATE_H +#define NS_UPDATE_H 1 + +/***** +***** Module Info +*****/ + +/*! \file + * \brief + * RFC2136 Dynamic Update + */ + +/*** + *** Imports + ***/ + +#include <dns/result.h> +#include <dns/types.h> + +/*** + *** Types. + ***/ + +/*** + *** Functions + ***/ + +void +ns_update_start(ns_client_t *client, isc_nmhandle_t *handle, + isc_result_t sigresult); + +#endif /* NS_UPDATE_H */ diff --git a/lib/ns/include/ns/version.h b/lib/ns/include/ns/version.h new file mode 100644 index 0000000..e342717 --- /dev/null +++ b/lib/ns/include/ns/version.h @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/*! \file include/ns/version.h */ + +#include <isc/platform.h> + +LIBNS_EXTERNAL_DATA extern const char ns_version[]; diff --git a/lib/ns/include/ns/xfrout.h b/lib/ns/include/ns/xfrout.h new file mode 100644 index 0000000..8f0809e --- /dev/null +++ b/lib/ns/include/ns/xfrout.h @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#ifndef NS_XFROUT_H +#define NS_XFROUT_H 1 + +/***** +***** Module Info +*****/ + +/*! \file + * \brief + * Outgoing zone transfers (AXFR + IXFR). + */ + +/*** + *** Functions + ***/ + +void +ns_xfr_start(ns_client_t *client, dns_rdatatype_t xfrtype); + +#endif /* NS_XFROUT_H */ diff --git a/lib/ns/interfacemgr.c b/lib/ns/interfacemgr.c new file mode 100644 index 0000000..216e274 --- /dev/null +++ b/lib/ns/interfacemgr.c @@ -0,0 +1,1263 @@ +/* + * 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. + */ + +/*! \file */ + +#include <stdbool.h> + +#include <isc/interfaceiter.h> +#include <isc/netmgr.h> +#include <isc/os.h> +#include <isc/random.h> +#include <isc/string.h> +#include <isc/task.h> +#include <isc/util.h> + +#include <dns/acl.h> +#include <dns/dispatch.h> + +#include <ns/client.h> +#include <ns/interfacemgr.h> +#include <ns/log.h> +#include <ns/server.h> +#include <ns/stats.h> + +#ifdef HAVE_NET_ROUTE_H +#include <net/route.h> +#if defined(RTM_VERSION) && defined(RTM_NEWADDR) && defined(RTM_DELADDR) +#define USE_ROUTE_SOCKET 1 +#define ROUTE_SOCKET_PROTOCOL PF_ROUTE +#define MSGHDR rt_msghdr +#define MSGTYPE rtm_type +#endif /* if defined(RTM_VERSION) && defined(RTM_NEWADDR) && \ + * defined(RTM_DELADDR) */ +#endif /* ifdef HAVE_NET_ROUTE_H */ + +#if defined(HAVE_LINUX_NETLINK_H) && defined(HAVE_LINUX_RTNETLINK_H) +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#if defined(RTM_NEWADDR) && defined(RTM_DELADDR) +#define USE_ROUTE_SOCKET 1 +#define ROUTE_SOCKET_PROTOCOL PF_NETLINK +#define MSGHDR nlmsghdr +#define MSGTYPE nlmsg_type +#endif /* if defined(RTM_NEWADDR) && defined(RTM_DELADDR) */ +#endif /* if defined(HAVE_LINUX_NETLINK_H) && defined(HAVE_LINUX_RTNETLINK_H) \ + */ + +#ifdef TUNE_LARGE +#define UDPBUFFERS 32768 +#else /* ifdef TUNE_LARGE */ +#define UDPBUFFERS 1000 +#endif /* TUNE_LARGE */ + +#define IFMGR_MAGIC ISC_MAGIC('I', 'F', 'M', 'G') +#define NS_INTERFACEMGR_VALID(t) ISC_MAGIC_VALID(t, IFMGR_MAGIC) + +#define IFMGR_COMMON_LOGARGS \ + ns_lctx, NS_LOGCATEGORY_NETWORK, NS_LOGMODULE_INTERFACEMGR + +/*% nameserver interface manager structure */ +struct ns_interfacemgr { + unsigned int magic; /*%< Magic number. */ + isc_refcount_t references; + isc_mutex_t lock; + isc_mem_t *mctx; /*%< Memory context. */ + ns_server_t *sctx; /*%< Server context. */ + isc_taskmgr_t *taskmgr; /*%< Task manager. */ + isc_task_t *excl; /*%< Exclusive task. */ + isc_timermgr_t *timermgr; /*%< Timer manager. */ + isc_socketmgr_t *socketmgr; /*%< Socket manager. */ + isc_nm_t *nm; /*%< Net manager. */ + int ncpus; /*%< Number of workers . */ + dns_dispatchmgr_t *dispatchmgr; + unsigned int generation; /*%< Current generation no. */ + ns_listenlist_t *listenon4; + ns_listenlist_t *listenon6; + dns_aclenv_t aclenv; /*%< Localhost/localnets ACLs */ + ISC_LIST(ns_interface_t) interfaces; /*%< List of interfaces. */ + ISC_LIST(isc_sockaddr_t) listenon; + int backlog; /*%< Listen queue size */ + unsigned int udpdisp; /*%< UDP dispatch count */ + atomic_bool shuttingdown; /*%< Interfacemgr is shutting + * down */ +#ifdef USE_ROUTE_SOCKET + isc_task_t *task; + isc_socket_t *route; + unsigned char buf[2048]; +#endif /* ifdef USE_ROUTE_SOCKET */ +}; + +static void +purge_old_interfaces(ns_interfacemgr_t *mgr); + +static void +clearlistenon(ns_interfacemgr_t *mgr); + +#ifdef USE_ROUTE_SOCKET +static void +route_event(isc_task_t *task, isc_event_t *event) { + isc_socketevent_t *sevent = NULL; + ns_interfacemgr_t *mgr = NULL; + isc_region_t r; + isc_result_t result; + struct MSGHDR *rtm; + bool done = true; + + UNUSED(task); + + REQUIRE(event->ev_type == ISC_SOCKEVENT_RECVDONE); + mgr = event->ev_arg; + sevent = (isc_socketevent_t *)event; + + if (sevent->result != ISC_R_SUCCESS) { + if (sevent->result != ISC_R_CANCELED) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "automatic interface scanning " + "terminated: %s", + isc_result_totext(sevent->result)); + } + ns_interfacemgr_detach(&mgr); + isc_event_free(&event); + return; + } + + rtm = (struct MSGHDR *)mgr->buf; +#ifdef RTM_VERSION + if (rtm->rtm_version != RTM_VERSION) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "automatic interface rescanning disabled: " + "rtm->rtm_version mismatch (%u != %u) " + "recompile required", + rtm->rtm_version, RTM_VERSION); + ns_interfacemgr_detach(&mgr); + isc_event_free(&event); + return; + } +#endif /* ifdef RTM_VERSION */ + + switch (rtm->MSGTYPE) { + case RTM_NEWADDR: + case RTM_DELADDR: + if (mgr->route != NULL && mgr->sctx->interface_auto) { + ns_interfacemgr_scan(mgr, false); + } + break; + default: + break; + } + + LOCK(&mgr->lock); + if (mgr->route != NULL) { + /* + * Look for next route event. + */ + r.base = mgr->buf; + r.length = sizeof(mgr->buf); + result = isc_socket_recv(mgr->route, &r, 1, mgr->task, + route_event, mgr); + if (result == ISC_R_SUCCESS) { + done = false; + } + } + UNLOCK(&mgr->lock); + + if (done) { + ns_interfacemgr_detach(&mgr); + } + isc_event_free(&event); + return; +} +#endif /* ifdef USE_ROUTE_SOCKET */ + +isc_result_t +ns_interfacemgr_create(isc_mem_t *mctx, ns_server_t *sctx, + isc_taskmgr_t *taskmgr, isc_timermgr_t *timermgr, + isc_socketmgr_t *socketmgr, isc_nm_t *nm, + dns_dispatchmgr_t *dispatchmgr, isc_task_t *task, + unsigned int udpdisp, dns_geoip_databases_t *geoip, + int ncpus, ns_interfacemgr_t **mgrp) { + isc_result_t result; + ns_interfacemgr_t *mgr; + +#ifndef USE_ROUTE_SOCKET + UNUSED(task); +#endif /* ifndef USE_ROUTE_SOCKET */ + + REQUIRE(mctx != NULL); + REQUIRE(mgrp != NULL); + REQUIRE(*mgrp == NULL); + + mgr = isc_mem_get(mctx, sizeof(*mgr)); + + mgr->mctx = NULL; + isc_mem_attach(mctx, &mgr->mctx); + + mgr->sctx = NULL; + ns_server_attach(sctx, &mgr->sctx); + + isc_mutex_init(&mgr->lock); + + mgr->excl = NULL; + result = isc_taskmgr_excltask(taskmgr, &mgr->excl); + if (result != ISC_R_SUCCESS) { + goto cleanup_lock; + } + + mgr->taskmgr = taskmgr; + mgr->timermgr = timermgr; + mgr->socketmgr = socketmgr; + mgr->nm = nm; + mgr->dispatchmgr = dispatchmgr; + mgr->generation = 1; + mgr->listenon4 = NULL; + mgr->listenon6 = NULL; + mgr->udpdisp = udpdisp; + mgr->ncpus = ncpus; + atomic_init(&mgr->shuttingdown, false); + + ISC_LIST_INIT(mgr->interfaces); + ISC_LIST_INIT(mgr->listenon); + + /* + * The listen-on lists are initially empty. + */ + result = ns_listenlist_create(mctx, &mgr->listenon4); + if (result != ISC_R_SUCCESS) { + goto cleanup_ctx; + } + ns_listenlist_attach(mgr->listenon4, &mgr->listenon6); + + result = dns_aclenv_init(mctx, &mgr->aclenv); + if (result != ISC_R_SUCCESS) { + goto cleanup_listenon; + } +#if defined(HAVE_GEOIP2) + mgr->aclenv.geoip = geoip; +#else /* if defined(HAVE_GEOIP2) */ + UNUSED(geoip); +#endif /* if defined(HAVE_GEOIP2) */ + +#ifdef USE_ROUTE_SOCKET + mgr->route = NULL; + result = isc_socket_create(mgr->socketmgr, ROUTE_SOCKET_PROTOCOL, + isc_sockettype_raw, &mgr->route); + switch (result) { + case ISC_R_NOPERM: + case ISC_R_SUCCESS: + case ISC_R_NOTIMPLEMENTED: + case ISC_R_FAMILYNOSUPPORT: + break; + default: + goto cleanup_aclenv; + } + + mgr->task = NULL; + if (mgr->route != NULL) { + isc_task_attach(task, &mgr->task); + } + isc_refcount_init(&mgr->references, (mgr->route != NULL) ? 2 : 1); +#else /* ifdef USE_ROUTE_SOCKET */ + isc_refcount_init(&mgr->references, 1); +#endif /* ifdef USE_ROUTE_SOCKET */ + mgr->magic = IFMGR_MAGIC; + *mgrp = mgr; + +#ifdef USE_ROUTE_SOCKET + if (mgr->route != NULL) { + isc_region_t r = { mgr->buf, sizeof(mgr->buf) }; + + result = isc_socket_recv(mgr->route, &r, 1, mgr->task, + route_event, mgr); + if (result != ISC_R_SUCCESS) { + isc_task_detach(&mgr->task); + isc_socket_detach(&mgr->route); + ns_interfacemgr_detach(&mgr); + } + } +#endif /* ifdef USE_ROUTE_SOCKET */ + return (ISC_R_SUCCESS); + +#ifdef USE_ROUTE_SOCKET +cleanup_aclenv: + dns_aclenv_destroy(&mgr->aclenv); +#endif /* ifdef USE_ROUTE_SOCKET */ +cleanup_listenon: + ns_listenlist_detach(&mgr->listenon4); + ns_listenlist_detach(&mgr->listenon6); +cleanup_lock: + isc_mutex_destroy(&mgr->lock); +cleanup_ctx: + ns_server_detach(&mgr->sctx); + isc_mem_putanddetach(&mgr->mctx, mgr, sizeof(*mgr)); + return (result); +} + +static void +ns_interfacemgr_destroy(ns_interfacemgr_t *mgr) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + isc_refcount_destroy(&mgr->references); + +#ifdef USE_ROUTE_SOCKET + if (mgr->route != NULL) { + isc_socket_detach(&mgr->route); + } + if (mgr->task != NULL) { + isc_task_detach(&mgr->task); + } +#endif /* ifdef USE_ROUTE_SOCKET */ + dns_aclenv_destroy(&mgr->aclenv); + ns_listenlist_detach(&mgr->listenon4); + ns_listenlist_detach(&mgr->listenon6); + clearlistenon(mgr); + isc_mutex_destroy(&mgr->lock); + if (mgr->sctx != NULL) { + ns_server_detach(&mgr->sctx); + } + if (mgr->excl != NULL) { + isc_task_detach(&mgr->excl); + } + mgr->magic = 0; + isc_mem_putanddetach(&mgr->mctx, mgr, sizeof(*mgr)); +} + +void +ns_interfacemgr_setbacklog(ns_interfacemgr_t *mgr, int backlog) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + LOCK(&mgr->lock); + mgr->backlog = backlog; + UNLOCK(&mgr->lock); +} + +dns_aclenv_t * +ns_interfacemgr_getaclenv(ns_interfacemgr_t *mgr) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + return (&mgr->aclenv); +} + +void +ns_interfacemgr_attach(ns_interfacemgr_t *source, ns_interfacemgr_t **target) { + REQUIRE(NS_INTERFACEMGR_VALID(source)); + isc_refcount_increment(&source->references); + *target = source; +} + +void +ns_interfacemgr_detach(ns_interfacemgr_t **targetp) { + ns_interfacemgr_t *target = *targetp; + *targetp = NULL; + REQUIRE(target != NULL); + REQUIRE(NS_INTERFACEMGR_VALID(target)); + if (isc_refcount_decrement(&target->references) == 1) { + ns_interfacemgr_destroy(target); + } +} + +void +ns_interfacemgr_shutdown(ns_interfacemgr_t *mgr) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + /*% + * Shut down and detach all interfaces. + * By incrementing the generation count, we make purge_old_interfaces() + * consider all interfaces "old". + */ + mgr->generation++; + atomic_store(&mgr->shuttingdown, true); +#ifdef USE_ROUTE_SOCKET + LOCK(&mgr->lock); + if (mgr->route != NULL) { + isc_socket_cancel(mgr->route, mgr->task, ISC_SOCKCANCEL_RECV); + isc_socket_detach(&mgr->route); + isc_task_detach(&mgr->task); + } + UNLOCK(&mgr->lock); +#endif /* ifdef USE_ROUTE_SOCKET */ + purge_old_interfaces(mgr); +} + +static isc_result_t +ns_interface_create(ns_interfacemgr_t *mgr, isc_sockaddr_t *addr, + const char *name, ns_interface_t **ifpret) { + ns_interface_t *ifp = NULL; + isc_result_t result; + int disp; + + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + ifp = isc_mem_get(mgr->mctx, sizeof(*ifp)); + *ifp = (ns_interface_t){ .generation = mgr->generation, + .addr = *addr, + .dscp = -1 }; + + strlcpy(ifp->name, name, sizeof(ifp->name)); + + isc_mutex_init(&ifp->lock); + + for (disp = 0; disp < MAX_UDP_DISPATCH; disp++) { + ifp->udpdispatch[disp] = NULL; + } + + /* + * Create a single TCP client object. It will replace itself + * with a new one as soon as it gets a connection, so the actual + * connections will be handled in parallel even though there is + * only one client initially. + */ + isc_refcount_init(&ifp->ntcpaccepting, 0); + isc_refcount_init(&ifp->ntcpactive, 0); + + ISC_LINK_INIT(ifp, link); + + ns_interfacemgr_attach(mgr, &ifp->mgr); + isc_refcount_init(&ifp->references, 1); + ifp->magic = IFACE_MAGIC; + + LOCK(&mgr->lock); + ISC_LIST_APPEND(mgr->interfaces, ifp, link); + UNLOCK(&mgr->lock); + + result = ns_clientmgr_create(mgr->mctx, mgr->sctx, mgr->taskmgr, + mgr->timermgr, ifp, mgr->ncpus, + &ifp->clientmgr); + if (result != ISC_R_SUCCESS) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "ns_clientmgr_create() failed: %s", + isc_result_totext(result)); + goto failure; + } + + *ifpret = ifp; + + return (ISC_R_SUCCESS); + +failure: + LOCK(&ifp->mgr->lock); + ISC_LIST_UNLINK(ifp->mgr->interfaces, ifp, link); + UNLOCK(&ifp->mgr->lock); + + ifp->magic = 0; + ns_interfacemgr_detach(&ifp->mgr); + isc_refcount_decrement(&ifp->references); + isc_refcount_destroy(&ifp->references); + isc_mutex_destroy(&ifp->lock); + + isc_mem_put(mgr->mctx, ifp, sizeof(*ifp)); + return (ISC_R_UNEXPECTED); +} + +static isc_result_t +ns_interface_listenudp(ns_interface_t *ifp) { + isc_result_t result; + + /* Reserve space for an ns_client_t with the netmgr handle */ + result = isc_nm_listenudp(ifp->mgr->nm, &ifp->addr, ns__client_request, + ifp, sizeof(ns_client_t), + &ifp->udplistensocket); + return (result); +} + +static isc_result_t +ns_interface_listentcp(ns_interface_t *ifp) { + isc_result_t result; + + result = isc_nm_listentcpdns( + ifp->mgr->nm, &ifp->addr, ns__client_request, ifp, + ns__client_tcpconn, ifp, sizeof(ns_client_t), ifp->mgr->backlog, + &ifp->mgr->sctx->tcpquota, &ifp->tcplistensocket); + if (result != ISC_R_SUCCESS) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "creating TCP socket: %s", + isc_result_totext(result)); + } + + /* + * We call this now to update the tcp-highwater statistic: + * this is necessary because we are adding to the TCP quota just + * by listening. + */ + result = ns__client_tcpconn(NULL, ISC_R_SUCCESS, ifp); + if (result != ISC_R_SUCCESS) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "connecting TCP socket: %s", + isc_result_totext(result)); + } + +#if 0 +#ifndef ISC_ALLOW_MAPPED + isc_socket_ipv6only(ifp->tcpsocket,true); +#endif /* ifndef ISC_ALLOW_MAPPED */ + + if (ifp->dscp != -1) { + isc_socket_dscp(ifp->tcpsocket,ifp->dscp); + } + + (void)isc_socket_filter(ifp->tcpsocket,"dataready"); +#endif /* if 0 */ + return (result); +} + +static isc_result_t +ns_interface_setup(ns_interfacemgr_t *mgr, isc_sockaddr_t *addr, + const char *name, ns_interface_t **ifpret, isc_dscp_t dscp, + bool *addr_in_use) { + isc_result_t result; + ns_interface_t *ifp = NULL; + REQUIRE(ifpret != NULL && *ifpret == NULL); + REQUIRE(addr_in_use == NULL || !*addr_in_use); + + result = ns_interface_create(mgr, addr, name, &ifp); + if (result != ISC_R_SUCCESS) { + return (result); + } + + ifp->dscp = dscp; + + result = ns_interface_listenudp(ifp); + if (result != ISC_R_SUCCESS) { + if ((result == ISC_R_ADDRINUSE) && (addr_in_use != NULL)) { + *addr_in_use = true; + } + goto cleanup_interface; + } + + if (((mgr->sctx->options & NS_SERVER_NOTCP) == 0)) { + result = ns_interface_listentcp(ifp); + if (result != ISC_R_SUCCESS) { + if ((result == ISC_R_ADDRINUSE) && + (addr_in_use != NULL)) + { + *addr_in_use = true; + } + + /* + * XXXRTH We don't currently have a way to easily stop + * dispatch service, so we currently return + * ISC_R_SUCCESS (the UDP stuff will work even if TCP + * creation failed). This will be fixed later. + */ + result = ISC_R_SUCCESS; + } + } + *ifpret = ifp; + return (result); + +cleanup_interface: + LOCK(&ifp->mgr->lock); + ISC_LIST_UNLINK(ifp->mgr->interfaces, ifp, link); + UNLOCK(&ifp->mgr->lock); + ns_interface_shutdown(ifp); + ns_interface_detach(&ifp); + return (result); +} + +void +ns_interface_shutdown(ns_interface_t *ifp) { + if (ifp->udplistensocket != NULL) { + isc_nm_stoplistening(ifp->udplistensocket); + isc_nmsocket_close(&ifp->udplistensocket); + } + if (ifp->tcplistensocket != NULL) { + isc_nm_stoplistening(ifp->tcplistensocket); + isc_nmsocket_close(&ifp->tcplistensocket); + } + if (ifp->clientmgr != NULL) { + ns_clientmgr_shutdown(ifp->clientmgr); + ns_clientmgr_destroy(&ifp->clientmgr); + } +} + +static void +ns_interface_destroy(ns_interface_t *ifp) { + REQUIRE(NS_INTERFACE_VALID(ifp)); + + isc_mem_t *mctx = ifp->mgr->mctx; + + ns_interface_shutdown(ifp); + + for (int disp = 0; disp < ifp->nudpdispatch; disp++) { + if (ifp->udpdispatch[disp] != NULL) { + dns_dispatch_changeattributes( + ifp->udpdispatch[disp], 0, + DNS_DISPATCHATTR_NOLISTEN); + dns_dispatch_detach(&(ifp->udpdispatch[disp])); + } + } + + if (ifp->tcpsocket != NULL) { + isc_socket_detach(&ifp->tcpsocket); + } + + isc_mutex_destroy(&ifp->lock); + + ns_interfacemgr_detach(&ifp->mgr); + + isc_refcount_destroy(&ifp->ntcpactive); + isc_refcount_destroy(&ifp->ntcpaccepting); + + ifp->magic = 0; + + isc_mem_put(mctx, ifp, sizeof(*ifp)); +} + +void +ns_interface_attach(ns_interface_t *source, ns_interface_t **target) { + REQUIRE(NS_INTERFACE_VALID(source)); + isc_refcount_increment(&source->references); + *target = source; +} + +void +ns_interface_detach(ns_interface_t **targetp) { + ns_interface_t *target = *targetp; + *targetp = NULL; + REQUIRE(target != NULL); + REQUIRE(NS_INTERFACE_VALID(target)); + if (isc_refcount_decrement(&target->references) == 1) { + ns_interface_destroy(target); + } +} + +/*% + * Search the interface list for an interface whose address and port + * both match those of 'addr'. Return a pointer to it, or NULL if not found. + */ +static ns_interface_t * +find_matching_interface(ns_interfacemgr_t *mgr, isc_sockaddr_t *addr) { + ns_interface_t *ifp; + LOCK(&mgr->lock); + for (ifp = ISC_LIST_HEAD(mgr->interfaces); ifp != NULL; + ifp = ISC_LIST_NEXT(ifp, link)) + { + if (isc_sockaddr_equal(&ifp->addr, addr)) { + break; + } + } + UNLOCK(&mgr->lock); + return (ifp); +} + +/*% + * Remove any interfaces whose generation number is not the current one. + */ +static void +purge_old_interfaces(ns_interfacemgr_t *mgr) { + ns_interface_t *ifp, *next; + LOCK(&mgr->lock); + for (ifp = ISC_LIST_HEAD(mgr->interfaces); ifp != NULL; ifp = next) { + INSIST(NS_INTERFACE_VALID(ifp)); + next = ISC_LIST_NEXT(ifp, link); + if (ifp->generation != mgr->generation) { + char sabuf[256]; + ISC_LIST_UNLINK(ifp->mgr->interfaces, ifp, link); + isc_sockaddr_format(&ifp->addr, sabuf, sizeof(sabuf)); + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_INFO, + "no longer listening on %s", sabuf); + ns_interface_shutdown(ifp); + ns_interface_detach(&ifp); + } + } + UNLOCK(&mgr->lock); +} + +static isc_result_t +clearacl(isc_mem_t *mctx, dns_acl_t **aclp) { + dns_acl_t *newacl = NULL; + isc_result_t result; + result = dns_acl_create(mctx, 0, &newacl); + if (result != ISC_R_SUCCESS) { + return (result); + } + dns_acl_detach(aclp); + dns_acl_attach(newacl, aclp); + dns_acl_detach(&newacl); + return (ISC_R_SUCCESS); +} + +static bool +listenon_is_ip6_any(ns_listenelt_t *elt) { + REQUIRE(elt && elt->acl); + return (dns_acl_isany(elt->acl)); +} + +static isc_result_t +setup_locals(ns_interfacemgr_t *mgr, isc_interface_t *interface) { + isc_result_t result; + unsigned int prefixlen; + isc_netaddr_t *netaddr; + + netaddr = &interface->address; + + /* First add localhost address */ + prefixlen = (netaddr->family == AF_INET) ? 32 : 128; + result = dns_iptable_addprefix(mgr->aclenv.localhost->iptable, netaddr, + prefixlen, true); + if (result != ISC_R_SUCCESS) { + return (result); + } + + /* Then add localnets prefix */ + result = isc_netaddr_masktoprefixlen(&interface->netmask, &prefixlen); + + /* Non contiguous netmasks not allowed by IPv6 arch. */ + if (result != ISC_R_SUCCESS && netaddr->family == AF_INET6) { + return (result); + } + + if (result != ISC_R_SUCCESS) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_WARNING, + "omitting IPv4 interface %s from " + "localnets ACL: %s", + interface->name, isc_result_totext(result)); + return (ISC_R_SUCCESS); + } + + if (prefixlen == 0U) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_WARNING, + "omitting %s interface %s from localnets ACL: " + "zero prefix length detected", + (netaddr->family == AF_INET) ? "IPv4" : "IPv6", + interface->name); + return (ISC_R_SUCCESS); + } + + result = dns_iptable_addprefix(mgr->aclenv.localnets->iptable, netaddr, + prefixlen, true); + if (result != ISC_R_SUCCESS) { + return (result); + } + + return (ISC_R_SUCCESS); +} + +static void +setup_listenon(ns_interfacemgr_t *mgr, isc_interface_t *interface, + in_port_t port) { + isc_sockaddr_t *addr; + isc_sockaddr_t *old; + + addr = isc_mem_get(mgr->mctx, sizeof(*addr)); + + isc_sockaddr_fromnetaddr(addr, &interface->address, port); + + LOCK(&mgr->lock); + for (old = ISC_LIST_HEAD(mgr->listenon); old != NULL; + old = ISC_LIST_NEXT(old, link)) + { + if (isc_sockaddr_equal(addr, old)) { + break; + } + } + + if (old != NULL) { + isc_mem_put(mgr->mctx, addr, sizeof(*addr)); + } else { + ISC_LIST_APPEND(mgr->listenon, addr, link); + } + UNLOCK(&mgr->lock); +} + +static void +clearlistenon(ns_interfacemgr_t *mgr) { + isc_sockaddr_t *old; + + LOCK(&mgr->lock); + old = ISC_LIST_HEAD(mgr->listenon); + while (old != NULL) { + ISC_LIST_UNLINK(mgr->listenon, old, link); + isc_mem_put(mgr->mctx, old, sizeof(*old)); + old = ISC_LIST_HEAD(mgr->listenon); + } + UNLOCK(&mgr->lock); +} + +static isc_result_t +do_scan(ns_interfacemgr_t *mgr, bool verbose) { + isc_interfaceiter_t *iter = NULL; + bool scan_ipv4 = false; + bool scan_ipv6 = false; + bool ipv6only = true; + bool ipv6pktinfo = true; + isc_result_t result; + isc_netaddr_t zero_address, zero_address6; + ns_listenelt_t *le; + isc_sockaddr_t listen_addr; + ns_interface_t *ifp; + bool log_explicit = false; + bool dolistenon; + char sabuf[ISC_SOCKADDR_FORMATSIZE]; + bool tried_listening; + bool all_addresses_in_use; + + if (isc_net_probeipv6() == ISC_R_SUCCESS) { + scan_ipv6 = true; + } else if ((mgr->sctx->options & NS_SERVER_DISABLE6) == 0) { + isc_log_write(IFMGR_COMMON_LOGARGS, + verbose ? ISC_LOG_INFO : ISC_LOG_DEBUG(1), + "no IPv6 interfaces found"); + } + + if (isc_net_probeipv4() == ISC_R_SUCCESS) { + scan_ipv4 = true; + } else if ((mgr->sctx->options & NS_SERVER_DISABLE4) == 0) { + isc_log_write(IFMGR_COMMON_LOGARGS, + verbose ? ISC_LOG_INFO : ISC_LOG_DEBUG(1), + "no IPv4 interfaces found"); + } + + /* + * A special, but typical case; listen-on-v6 { any; }. + * When we can make the socket IPv6-only, open a single wildcard + * socket for IPv6 communication. Otherwise, make separate + * socket for each IPv6 address in order to avoid accepting IPv4 + * packets as the form of mapped addresses unintentionally + * unless explicitly allowed. + */ +#ifndef ISC_ALLOW_MAPPED + if (scan_ipv6 && isc_net_probe_ipv6only() != ISC_R_SUCCESS) { + ipv6only = false; + log_explicit = true; + } +#endif /* ifndef ISC_ALLOW_MAPPED */ + if (scan_ipv6 && isc_net_probe_ipv6pktinfo() != ISC_R_SUCCESS) { + ipv6pktinfo = false; + log_explicit = true; + } + if (scan_ipv6 && ipv6only && ipv6pktinfo) { + for (le = ISC_LIST_HEAD(mgr->listenon6->elts); le != NULL; + le = ISC_LIST_NEXT(le, link)) + { + struct in6_addr in6a; + + if (!listenon_is_ip6_any(le)) { + continue; + } + + in6a = in6addr_any; + isc_sockaddr_fromin6(&listen_addr, &in6a, le->port); + + ifp = find_matching_interface(mgr, &listen_addr); + if (ifp != NULL) { + ifp->generation = mgr->generation; + if (le->dscp != -1 && ifp->dscp == -1) { + ifp->dscp = le->dscp; + } else if (le->dscp != ifp->dscp) { + isc_sockaddr_format(&listen_addr, sabuf, + sizeof(sabuf)); + isc_log_write(IFMGR_COMMON_LOGARGS, + ISC_LOG_WARNING, + "%s: conflicting DSCP " + "values, using %d", + sabuf, ifp->dscp); + } + } else { + isc_log_write(IFMGR_COMMON_LOGARGS, + ISC_LOG_INFO, + "listening on IPv6 " + "interfaces, port %u", + le->port); + result = ns_interface_setup(mgr, &listen_addr, + "<any>", &ifp, + le->dscp, NULL); + if (result == ISC_R_SUCCESS) { + ifp->flags |= NS_INTERFACEFLAG_ANYADDR; + } else { + isc_log_write(IFMGR_COMMON_LOGARGS, + ISC_LOG_ERROR, + "listening on all IPv6 " + "interfaces failed"); + } + /* Continue. */ + } + } + } + + isc_netaddr_any(&zero_address); + isc_netaddr_any6(&zero_address6); + + result = isc_interfaceiter_create(mgr->mctx, &iter); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = clearacl(mgr->mctx, &mgr->aclenv.localhost); + if (result != ISC_R_SUCCESS) { + goto cleanup_iter; + } + result = clearacl(mgr->mctx, &mgr->aclenv.localnets); + if (result != ISC_R_SUCCESS) { + goto cleanup_iter; + } + clearlistenon(mgr); + + tried_listening = false; + all_addresses_in_use = true; + for (result = isc_interfaceiter_first(iter); result == ISC_R_SUCCESS; + result = isc_interfaceiter_next(iter)) + { + isc_interface_t interface; + ns_listenlist_t *ll; + unsigned int family; + + result = isc_interfaceiter_current(iter, &interface); + if (result != ISC_R_SUCCESS) { + break; + } + + family = interface.address.family; + if (family != AF_INET && family != AF_INET6) { + continue; + } + if (!scan_ipv4 && family == AF_INET) { + continue; + } + if (!scan_ipv6 && family == AF_INET6) { + continue; + } + + /* + * Test for the address being nonzero rather than testing + * INTERFACE_F_UP, because on some systems the latter + * follows the media state and we could end up ignoring + * the interface for an entire rescan interval due to + * a temporary media glitch at rescan time. + */ + if (family == AF_INET && + isc_netaddr_equal(&interface.address, &zero_address)) + { + continue; + } + if (family == AF_INET6 && + isc_netaddr_equal(&interface.address, &zero_address6)) + { + continue; + } + + /* + * If running with -T fixedlocal, then we only + * want 127.0.0.1 and ::1 in the localhost ACL. + */ + if (((mgr->sctx->options & NS_SERVER_FIXEDLOCAL) != 0) && + !isc_netaddr_isloopback(&interface.address)) + { + goto listenon; + } + + result = setup_locals(mgr, &interface); + if (result != ISC_R_SUCCESS) { + goto ignore_interface; + } + + listenon: + ll = (family == AF_INET) ? mgr->listenon4 : mgr->listenon6; + dolistenon = true; + for (le = ISC_LIST_HEAD(ll->elts); le != NULL; + le = ISC_LIST_NEXT(le, link)) + { + int match; + bool ipv6_wildcard = false; + isc_netaddr_t listen_netaddr; + isc_sockaddr_t listen_sockaddr; + + /* + * Construct a socket address for this IP/port + * combination. + */ + if (family == AF_INET) { + isc_netaddr_fromin(&listen_netaddr, + &interface.address.type.in); + } else { + isc_netaddr_fromin6( + &listen_netaddr, + &interface.address.type.in6); + isc_netaddr_setzone(&listen_netaddr, + interface.address.zone); + } + isc_sockaddr_fromnetaddr(&listen_sockaddr, + &listen_netaddr, le->port); + + /* + * See if the address matches the listen-on statement; + * if not, ignore the interface. + */ + (void)dns_acl_match(&listen_netaddr, NULL, le->acl, + &mgr->aclenv, &match, NULL); + if (match <= 0) { + continue; + } + + if (dolistenon) { + setup_listenon(mgr, &interface, le->port); + dolistenon = false; + } + + /* + * The case of "any" IPv6 address will require + * special considerations later, so remember it. + */ + if (family == AF_INET6 && ipv6only && ipv6pktinfo && + listenon_is_ip6_any(le)) + { + ipv6_wildcard = true; + } + + ifp = find_matching_interface(mgr, &listen_sockaddr); + if (ifp != NULL) { + ifp->generation = mgr->generation; + if (le->dscp != -1 && ifp->dscp == -1) { + ifp->dscp = le->dscp; + } else if (le->dscp != ifp->dscp) { + isc_sockaddr_format(&listen_sockaddr, + sabuf, + sizeof(sabuf)); + isc_log_write(IFMGR_COMMON_LOGARGS, + ISC_LOG_WARNING, + "%s: conflicting DSCP " + "values, using %d", + sabuf, ifp->dscp); + } + } else { + bool addr_in_use = false; + + if (ipv6_wildcard) { + continue; + } + + if (log_explicit && family == AF_INET6 && + listenon_is_ip6_any(le)) + { + isc_log_write( + IFMGR_COMMON_LOGARGS, + verbose ? ISC_LOG_INFO + : ISC_LOG_DEBUG(1), + "IPv6 socket API is " + "incomplete; explicitly " + "binding to each IPv6 " + "address separately"); + log_explicit = false; + } + isc_sockaddr_format(&listen_sockaddr, sabuf, + sizeof(sabuf)); + isc_log_write( + IFMGR_COMMON_LOGARGS, ISC_LOG_INFO, + "listening on %s interface " + "%s, %s", + (family == AF_INET) ? "IPv4" : "IPv6", + interface.name, sabuf); + + result = ns_interface_setup( + mgr, &listen_sockaddr, interface.name, + &ifp, le->dscp, &addr_in_use); + + tried_listening = true; + if (!addr_in_use) { + all_addresses_in_use = false; + } + + if (result != ISC_R_SUCCESS) { + isc_log_write(IFMGR_COMMON_LOGARGS, + ISC_LOG_ERROR, + "creating %s interface " + "%s failed; interface " + "ignored", + (family == AF_INET) ? "IP" + "v4" + : "IP" + "v" + "6", + interface.name); + } + /* Continue. */ + } + } + continue; + + ignore_interface: + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_ERROR, + "ignoring %s interface %s: %s", + (family == AF_INET) ? "IPv4" : "IPv6", + interface.name, isc_result_totext(result)); + continue; + } + if (result != ISC_R_NOMORE) { + UNEXPECTED_ERROR(__FILE__, __LINE__, + "interface iteration failed: %s", + isc_result_totext(result)); + } else { + result = ((tried_listening && all_addresses_in_use) + ? ISC_R_ADDRINUSE + : ISC_R_SUCCESS); + } +cleanup_iter: + isc_interfaceiter_destroy(&iter); + return (result); +} + +static isc_result_t +ns_interfacemgr_scan0(ns_interfacemgr_t *mgr, bool verbose) { + isc_result_t result; + bool purge = true; + + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + mgr->generation++; /* Increment the generation count. */ + + result = do_scan(mgr, verbose); + if ((result != ISC_R_SUCCESS) && (result != ISC_R_ADDRINUSE)) { + purge = false; + } + + /* + * Now go through the interface list and delete anything that + * does not have the current generation number. This is + * how we catch interfaces that go away or change their + * addresses. + */ + if (purge) { + purge_old_interfaces(mgr); + } + + /* + * Warn if we are not listening on any interface. + */ + if (ISC_LIST_EMPTY(mgr->interfaces)) { + isc_log_write(IFMGR_COMMON_LOGARGS, ISC_LOG_WARNING, + "not listening on any interfaces"); + } + + return (result); +} + +bool +ns_interfacemgr_islistening(ns_interfacemgr_t *mgr) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + return (ISC_LIST_EMPTY(mgr->interfaces) ? false : true); +} + +isc_result_t +ns_interfacemgr_scan(ns_interfacemgr_t *mgr, bool verbose) { + isc_result_t result; + bool unlock = false; + + /* + * Check for success because we may already be task-exclusive + * at this point. Only if we succeed at obtaining an exclusive + * lock now will we need to relinquish it later. + */ + result = isc_task_beginexclusive(mgr->excl); + if (result == ISC_R_SUCCESS) { + unlock = true; + } + + result = ns_interfacemgr_scan0(mgr, verbose); + + if (unlock) { + isc_task_endexclusive(mgr->excl); + } + + return (result); +} + +void +ns_interfacemgr_setlistenon4(ns_interfacemgr_t *mgr, ns_listenlist_t *value) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + LOCK(&mgr->lock); + ns_listenlist_detach(&mgr->listenon4); + ns_listenlist_attach(value, &mgr->listenon4); + UNLOCK(&mgr->lock); +} + +void +ns_interfacemgr_setlistenon6(ns_interfacemgr_t *mgr, ns_listenlist_t *value) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + LOCK(&mgr->lock); + ns_listenlist_detach(&mgr->listenon6); + ns_listenlist_attach(value, &mgr->listenon6); + UNLOCK(&mgr->lock); +} + +void +ns_interfacemgr_dumprecursing(FILE *f, ns_interfacemgr_t *mgr) { + ns_interface_t *interface; + + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + LOCK(&mgr->lock); + interface = ISC_LIST_HEAD(mgr->interfaces); + while (interface != NULL) { + if (interface->clientmgr != NULL) { + ns_client_dumprecursing(f, interface->clientmgr); + } + interface = ISC_LIST_NEXT(interface, link); + } + UNLOCK(&mgr->lock); +} + +bool +ns_interfacemgr_listeningon(ns_interfacemgr_t *mgr, + const isc_sockaddr_t *addr) { + isc_sockaddr_t *old; + bool result = false; + + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + /* + * If the manager is shutting down it's safer to + * return true. + */ + if (atomic_load(&mgr->shuttingdown)) { + return (true); + } + LOCK(&mgr->lock); + for (old = ISC_LIST_HEAD(mgr->listenon); old != NULL; + old = ISC_LIST_NEXT(old, link)) + { + if (isc_sockaddr_equal(old, addr)) { + result = true; + break; + } + } + UNLOCK(&mgr->lock); + + return (result); +} + +ns_server_t * +ns_interfacemgr_getserver(ns_interfacemgr_t *mgr) { + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + + return (mgr->sctx); +} + +ns_interface_t * +ns__interfacemgr_getif(ns_interfacemgr_t *mgr) { + ns_interface_t *head; + REQUIRE(NS_INTERFACEMGR_VALID(mgr)); + LOCK(&mgr->lock); + head = ISC_LIST_HEAD(mgr->interfaces); + UNLOCK(&mgr->lock); + return (head); +} + +ns_interface_t * +ns__interfacemgr_nextif(ns_interface_t *ifp) { + ns_interface_t *next; + LOCK(&ifp->lock); + next = ISC_LIST_NEXT(ifp, link); + UNLOCK(&ifp->lock); + return (next); +} diff --git a/lib/ns/lib.c b/lib/ns/lib.c new file mode 100644 index 0000000..0c4a18f --- /dev/null +++ b/lib/ns/lib.c @@ -0,0 +1,86 @@ +/* + * 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. + */ + +/*! \file */ + +#include <stdbool.h> +#include <stddef.h> + +#include <isc/mem.h> +#include <isc/mutex.h> +#include <isc/once.h> +#include <isc/refcount.h> +#include <isc/util.h> + +#include <dns/name.h> + +#include <ns/lib.h> + +/*** + *** Globals + ***/ + +LIBNS_EXTERNAL_DATA unsigned int ns_pps = 0U; + +/*** + *** Private + ***/ + +static isc_once_t init_once = ISC_ONCE_INIT; +static isc_mem_t *ns_g_mctx = NULL; +static bool initialize_done = false; +static isc_refcount_t references; + +static void +initialize(void) { + REQUIRE(!initialize_done); + + isc_mem_create(&ns_g_mctx); + + isc_refcount_init(&references, 0); + initialize_done = true; + return; +} + +isc_result_t +ns_lib_init(void) { + isc_result_t result; + + /* + * Since this routine is expected to be used by a normal application, + * it should be better to return an error, instead of an emergency + * abort, on any failure. + */ + result = isc_once_do(&init_once, initialize); + if (result != ISC_R_SUCCESS) { + return (result); + } + + if (!initialize_done) { + return (ISC_R_FAILURE); + } + + isc_refcount_increment0(&references); + + return (ISC_R_SUCCESS); +} + +void +ns_lib_shutdown(void) { + if (isc_refcount_decrement(&references) == 1) { + isc_refcount_destroy(&references); + if (ns_g_mctx != NULL) { + isc_mem_detach(&ns_g_mctx); + } + } +} diff --git a/lib/ns/listenlist.c b/lib/ns/listenlist.c new file mode 100644 index 0000000..9fb3674 --- /dev/null +++ b/lib/ns/listenlist.c @@ -0,0 +1,130 @@ +/* + * 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. + */ + +/*! \file */ + +#include <stdbool.h> + +#include <isc/mem.h> +#include <isc/util.h> + +#include <dns/acl.h> + +#include <ns/listenlist.h> + +static void +destroy(ns_listenlist_t *list); + +isc_result_t +ns_listenelt_create(isc_mem_t *mctx, in_port_t port, isc_dscp_t dscp, + dns_acl_t *acl, ns_listenelt_t **target) { + ns_listenelt_t *elt = NULL; + REQUIRE(target != NULL && *target == NULL); + elt = isc_mem_get(mctx, sizeof(*elt)); + elt->mctx = mctx; + ISC_LINK_INIT(elt, link); + elt->port = port; + elt->dscp = dscp; + elt->acl = acl; + *target = elt; + return (ISC_R_SUCCESS); +} + +void +ns_listenelt_destroy(ns_listenelt_t *elt) { + if (elt->acl != NULL) { + dns_acl_detach(&elt->acl); + } + isc_mem_put(elt->mctx, elt, sizeof(*elt)); +} + +isc_result_t +ns_listenlist_create(isc_mem_t *mctx, ns_listenlist_t **target) { + ns_listenlist_t *list = NULL; + REQUIRE(target != NULL && *target == NULL); + list = isc_mem_get(mctx, sizeof(*list)); + list->mctx = mctx; + list->refcount = 1; + ISC_LIST_INIT(list->elts); + *target = list; + return (ISC_R_SUCCESS); +} + +static void +destroy(ns_listenlist_t *list) { + ns_listenelt_t *elt, *next; + for (elt = ISC_LIST_HEAD(list->elts); elt != NULL; elt = next) { + next = ISC_LIST_NEXT(elt, link); + ns_listenelt_destroy(elt); + } + isc_mem_put(list->mctx, list, sizeof(*list)); +} + +void +ns_listenlist_attach(ns_listenlist_t *source, ns_listenlist_t **target) { + INSIST(source->refcount > 0); + source->refcount++; + *target = source; +} + +void +ns_listenlist_detach(ns_listenlist_t **listp) { + ns_listenlist_t *list = *listp; + *listp = NULL; + INSIST(list->refcount > 0); + list->refcount--; + if (list->refcount == 0) { + destroy(list); + } +} + +isc_result_t +ns_listenlist_default(isc_mem_t *mctx, in_port_t port, isc_dscp_t dscp, + bool enabled, ns_listenlist_t **target) { + isc_result_t result; + dns_acl_t *acl = NULL; + ns_listenelt_t *elt = NULL; + ns_listenlist_t *list = NULL; + + REQUIRE(target != NULL && *target == NULL); + if (enabled) { + result = dns_acl_any(mctx, &acl); + } else { + result = dns_acl_none(mctx, &acl); + } + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + result = ns_listenelt_create(mctx, port, dscp, acl, &elt); + if (result != ISC_R_SUCCESS) { + goto cleanup_acl; + } + + result = ns_listenlist_create(mctx, &list); + if (result != ISC_R_SUCCESS) { + goto cleanup_listenelt; + } + + ISC_LIST_APPEND(list->elts, elt, link); + + *target = list; + return (ISC_R_SUCCESS); + +cleanup_listenelt: + ns_listenelt_destroy(elt); +cleanup_acl: + dns_acl_detach(&acl); +cleanup: + return (result); +} diff --git a/lib/ns/log.c b/lib/ns/log.c new file mode 100644 index 0000000..01ea0b2 --- /dev/null +++ b/lib/ns/log.c @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/*! \file */ + +#include <isc/result.h> +#include <isc/util.h> + +#include <ns/log.h> + +#ifndef ISC_FACILITY +#define ISC_FACILITY LOG_DAEMON +#endif /* ifndef ISC_FACILITY */ + +/*% + * When adding a new category, be sure to add the appropriate + * \#define to <ns/log.h> + */ +LIBNS_EXTERNAL_DATA isc_logcategory_t ns_categories[] = { + { "client", 0 }, + { "network", 0 }, + { "update", 0 }, + { "queries", 0 }, + { "update-security", 0 }, + { "query-errors", 0 }, + { "trust-anchor-telemetry", 0 }, + { "serve-stale", 0 }, + { NULL, 0 } +}; + +/*% + * When adding a new module, be sure to add the appropriate + * \#define to <ns/log.h>. + */ +LIBNS_EXTERNAL_DATA isc_logmodule_t ns_modules[] = { + { "ns/client", 0 }, { "ns/query", 0 }, { "ns/interfacemgr", 0 }, + { "ns/update", 0 }, { "ns/xfer-in", 0 }, { "ns/xfer-out", 0 }, + { "ns/notify", 0 }, { "ns/hooks", 0 }, { NULL, 0 } +}; + +LIBNS_EXTERNAL_DATA isc_log_t *ns_lctx = NULL; + +void +ns_log_init(isc_log_t *lctx) { + REQUIRE(lctx != NULL); + + isc_log_registercategories(lctx, ns_categories); + isc_log_registermodules(lctx, ns_modules); +} + +void +ns_log_setcontext(isc_log_t *lctx) { + ns_lctx = lctx; +} diff --git a/lib/ns/notify.c b/lib/ns/notify.c new file mode 100644 index 0000000..86f8647 --- /dev/null +++ b/lib/ns/notify.c @@ -0,0 +1,179 @@ +/* + * 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 <isc/log.h> +#include <isc/print.h> + +#include <dns/message.h> +#include <dns/rdataset.h> +#include <dns/result.h> +#include <dns/tsig.h> +#include <dns/view.h> +#include <dns/zone.h> +#include <dns/zt.h> + +#include <ns/log.h> +#include <ns/notify.h> +#include <ns/types.h> + +/*! \file + * \brief + * This module implements notify as in RFC1996. + */ + +static void +notify_log(ns_client_t *client, int level, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + ns_client_logv(client, DNS_LOGCATEGORY_NOTIFY, NS_LOGMODULE_NOTIFY, + level, fmt, ap); + va_end(ap); +} + +static void +respond(ns_client_t *client, isc_result_t result) { + dns_rcode_t rcode; + dns_message_t *message; + isc_result_t msg_result; + + message = client->message; + rcode = dns_result_torcode(result); + + msg_result = dns_message_reply(message, true); + if (msg_result != ISC_R_SUCCESS) { + msg_result = dns_message_reply(message, false); + } + if (msg_result != ISC_R_SUCCESS) { + ns_client_drop(client, msg_result); + isc_nmhandle_detach(&client->reqhandle); + return; + } + message->rcode = rcode; + if (rcode == dns_rcode_noerror) { + message->flags |= DNS_MESSAGEFLAG_AA; + } else { + message->flags &= ~DNS_MESSAGEFLAG_AA; + } + + ns_client_send(client); + isc_nmhandle_detach(&client->reqhandle); +} + +void +ns_notify_start(ns_client_t *client, isc_nmhandle_t *handle) { + dns_message_t *request = client->message; + isc_result_t result; + dns_name_t *zonename; + dns_rdataset_t *zone_rdataset; + dns_zone_t *zone = NULL; + char namebuf[DNS_NAME_FORMATSIZE]; + char tsigbuf[DNS_NAME_FORMATSIZE * 2 + sizeof(": TSIG '' ()")]; + dns_tsigkey_t *tsigkey; + + /* + * Attach to the request handle + */ + isc_nmhandle_attach(handle, &client->reqhandle); + + /* + * Interpret the question section. + */ + result = dns_message_firstname(request, DNS_SECTION_QUESTION); + if (result != ISC_R_SUCCESS) { + notify_log(client, ISC_LOG_NOTICE, + "notify question section empty"); + result = DNS_R_FORMERR; + goto done; + } + + /* + * The question section must contain exactly one question. + */ + zonename = NULL; + dns_message_currentname(request, DNS_SECTION_QUESTION, &zonename); + zone_rdataset = ISC_LIST_HEAD(zonename->list); + if (ISC_LIST_NEXT(zone_rdataset, link) != NULL) { + notify_log(client, ISC_LOG_NOTICE, + "notify question section contains multiple RRs"); + result = DNS_R_FORMERR; + goto done; + } + + /* The zone section must have exactly one name. */ + result = dns_message_nextname(request, DNS_SECTION_ZONE); + if (result != ISC_R_NOMORE) { + notify_log(client, ISC_LOG_NOTICE, + "notify question section contains multiple RRs"); + result = DNS_R_FORMERR; + goto done; + } + + /* The one rdataset must be an SOA. */ + if (zone_rdataset->type != dns_rdatatype_soa) { + notify_log(client, ISC_LOG_NOTICE, + "notify question section contains no SOA"); + result = DNS_R_FORMERR; + goto done; + } + + tsigkey = dns_message_gettsigkey(request); + if (tsigkey != NULL) { + dns_name_format(&tsigkey->name, namebuf, sizeof(namebuf)); + + if (tsigkey->generated) { + char cnamebuf[DNS_NAME_FORMATSIZE]; + dns_name_format(tsigkey->creator, cnamebuf, + sizeof(cnamebuf)); + snprintf(tsigbuf, sizeof(tsigbuf), ": TSIG '%s' (%s)", + namebuf, cnamebuf); + } else { + snprintf(tsigbuf, sizeof(tsigbuf), ": TSIG '%s'", + namebuf); + } + } else { + tsigbuf[0] = '\0'; + } + + dns_name_format(zonename, namebuf, sizeof(namebuf)); + result = dns_zt_find(client->view->zonetable, zonename, 0, NULL, &zone); + if (result == ISC_R_SUCCESS) { + dns_zonetype_t zonetype = dns_zone_gettype(zone); + + if ((zonetype == dns_zone_primary) || + (zonetype == dns_zone_secondary) || + (zonetype == dns_zone_mirror) || + (zonetype == dns_zone_stub)) + { + isc_sockaddr_t *from = ns_client_getsockaddr(client); + isc_sockaddr_t *to = ns_client_getdestaddr(client); + notify_log(client, ISC_LOG_INFO, + "received notify for zone '%s'%s", namebuf, + tsigbuf); + result = dns_zone_notifyreceive(zone, from, to, + request); + goto done; + } + } + + notify_log(client, ISC_LOG_NOTICE, + "received notify for zone '%s'%s: not authoritative", + namebuf, tsigbuf); + result = DNS_R_NOTAUTH; + +done: + if (zone != NULL) { + dns_zone_detach(&zone); + } + respond(client, result); +} diff --git a/lib/ns/query.c b/lib/ns/query.c new file mode 100644 index 0000000..4503b8d --- /dev/null +++ b/lib/ns/query.c @@ -0,0 +1,12013 @@ +/* + * 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. + */ + +/*! \file */ + +#include <ctype.h> +#include <inttypes.h> +#include <stdbool.h> +#include <string.h> + +#include <isc/hex.h> +#include <isc/mem.h> +#include <isc/once.h> +#include <isc/print.h> +#include <isc/random.h> +#include <isc/rwlock.h> +#include <isc/serial.h> +#include <isc/stats.h> +#include <isc/string.h> +#include <isc/thread.h> +#include <isc/util.h> + +#include <dns/adb.h> +#include <dns/badcache.h> +#include <dns/byaddr.h> +#include <dns/cache.h> +#include <dns/db.h> +#include <dns/dlz.h> +#include <dns/dns64.h> +#include <dns/dnsrps.h> +#include <dns/dnssec.h> +#include <dns/events.h> +#include <dns/keytable.h> +#include <dns/message.h> +#include <dns/ncache.h> +#include <dns/nsec.h> +#include <dns/nsec3.h> +#include <dns/order.h> +#include <dns/rdata.h> +#include <dns/rdataclass.h> +#include <dns/rdatalist.h> +#include <dns/rdataset.h> +#include <dns/rdatasetiter.h> +#include <dns/rdatastruct.h> +#include <dns/rdatatype.h> +#include <dns/resolver.h> +#include <dns/result.h> +#include <dns/stats.h> +#include <dns/tkey.h> +#include <dns/types.h> +#include <dns/view.h> +#include <dns/zone.h> +#include <dns/zt.h> + +#include <ns/client.h> +#include <ns/hooks.h> +#include <ns/interfacemgr.h> +#include <ns/log.h> +#include <ns/server.h> +#include <ns/sortlist.h> +#include <ns/stats.h> +#include <ns/xfrout.h> + +#if 0 +/* + * It has been recommended that DNS64 be changed to return excluded + * AAAA addresses if DNS64 synthesis does not occur. This minimises + * the impact on the lookup results. While most DNS AAAA lookups are + * done to send IP packets to a host, not all of them are and filtering + * excluded addresses has a negative impact on those uses. + */ +#define dns64_bis_return_excluded_addresses 1 +#endif /* if 0 */ + +/*% + * Maximum number of chained queries before we give up + * to prevent CNAME loops. + */ +#define MAX_RESTARTS 16 + +#define QUERY_ERROR(qctx, r) \ + do { \ + (qctx)->result = r; \ + (qctx)->want_restart = false; \ + (qctx)->line = __LINE__; \ + } while (0) + +/*% Partial answer? */ +#define PARTIALANSWER(c) \ + (((c)->query.attributes & NS_QUERYATTR_PARTIALANSWER) != 0) +/*% Use Cache? */ +#define USECACHE(c) (((c)->query.attributes & NS_QUERYATTR_CACHEOK) != 0) +/*% Recursion OK? */ +#define RECURSIONOK(c) (((c)->query.attributes & NS_QUERYATTR_RECURSIONOK) != 0) +/*% Recursing? */ +#define RECURSING(c) (((c)->query.attributes & NS_QUERYATTR_RECURSING) != 0) +/*% Want Recursion? */ +#define WANTRECURSION(c) \ + (((c)->query.attributes & NS_QUERYATTR_WANTRECURSION) != 0) +/*% Is TCP? */ +#define TCP(c) (((c)->attributes & NS_CLIENTATTR_TCP) != 0) + +/*% Want DNSSEC? */ +#define WANTDNSSEC(c) (((c)->attributes & NS_CLIENTATTR_WANTDNSSEC) != 0) +/*% Want WANTAD? */ +#define WANTAD(c) (((c)->attributes & NS_CLIENTATTR_WANTAD) != 0) +/*% Client presented a valid COOKIE. */ +#define HAVECOOKIE(c) (((c)->attributes & NS_CLIENTATTR_HAVECOOKIE) != 0) +/*% Client presented a COOKIE. */ +#define WANTCOOKIE(c) (((c)->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0) +/*% Client presented a CLIENT-SUBNET option. */ +#define HAVEECS(c) (((c)->attributes & NS_CLIENTATTR_HAVEECS) != 0) +/*% No authority? */ +#define NOAUTHORITY(c) (((c)->query.attributes & NS_QUERYATTR_NOAUTHORITY) != 0) +/*% No additional? */ +#define NOADDITIONAL(c) \ + (((c)->query.attributes & NS_QUERYATTR_NOADDITIONAL) != 0) +/*% Secure? */ +#define SECURE(c) (((c)->query.attributes & NS_QUERYATTR_SECURE) != 0) +/*% DNS64 A lookup? */ +#define DNS64(c) (((c)->query.attributes & NS_QUERYATTR_DNS64) != 0) + +#define DNS64EXCLUDE(c) \ + (((c)->query.attributes & NS_QUERYATTR_DNS64EXCLUDE) != 0) + +#define REDIRECT(c) (((c)->query.attributes & NS_QUERYATTR_REDIRECT) != 0) + +/*% Was the client already sent a response? */ +#define QUERY_ANSWERED(q) (((q)->attributes & NS_QUERYATTR_ANSWERED) != 0) + +/*% Have we already processed an answer via stale-answer-client-timeout? */ +#define QUERY_STALEPENDING(q) \ + (((q)->attributes & NS_QUERYATTR_STALEPENDING) != 0) + +/*% Does the query allow stale data in the response? */ +#define QUERY_STALEOK(q) (((q)->attributes & NS_QUERYATTR_STALEOK) != 0) + +/*% Does the query wants to check for stale RRset due to a timeout? */ +#define QUERY_STALETIMEOUT(q) (((q)->dboptions & DNS_DBFIND_STALETIMEOUT) != 0) + +/*% Does the rdataset 'r' have an attached 'No QNAME Proof'? */ +#define NOQNAME(r) (((r)->attributes & DNS_RDATASETATTR_NOQNAME) != 0) + +/*% Does the rdataset 'r' contain a stale answer? */ +#define STALE(r) (((r)->attributes & DNS_RDATASETATTR_STALE) != 0) + +/*% Does the rdataset 'r' is stale and within stale-refresh-time? */ +#define STALE_WINDOW(r) (((r)->attributes & DNS_RDATASETATTR_STALE_WINDOW) != 0) + +#ifdef WANT_QUERYTRACE +static void +client_trace(ns_client_t *client, int level, const char *message) { + if (client != NULL && client->query.qname != NULL) { + if (isc_log_wouldlog(ns_lctx, level)) { + char qbuf[DNS_NAME_FORMATSIZE]; + char tbuf[DNS_RDATATYPE_FORMATSIZE]; + dns_name_format(client->query.qname, qbuf, + sizeof(qbuf)); + dns_rdatatype_format(client->query.qtype, tbuf, + sizeof(tbuf)); + isc_log_write(ns_lctx, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, level, + "query client=%p thread=0x%" PRIxPTR + "(%s/%s): %s", + client, isc_thread_self(), qbuf, tbuf, + message); + } + } else { + isc_log_write(ns_lctx, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, level, + "query client=%p thread=0x%" PRIxPTR + "(<unknown-query>): %s", + client, isc_thread_self(), message); + } +} +#define CTRACE(l, m) client_trace(client, l, m) +#define CCTRACE(l, m) client_trace(qctx->client, l, m) +#else /* ifdef WANT_QUERYTRACE */ +#define CTRACE(l, m) ((void)m) +#define CCTRACE(l, m) ((void)m) +#endif /* WANT_QUERYTRACE */ + +#define DNS_GETDB_NOEXACT 0x01U +#define DNS_GETDB_NOLOG 0x02U +#define DNS_GETDB_PARTIAL 0x04U +#define DNS_GETDB_IGNOREACL 0x08U +#define DNS_GETDB_STALEFIRST 0X0CU + +#define PENDINGOK(x) (((x)&DNS_DBFIND_PENDINGOK) != 0) + +#define SFCACHE_CDFLAG 0x1 + +/* + * These have the same semantics as: + * + * foo_attach(b, a); + * foo_detach(&a); + * + * without the locking and magic testing. + * + * We use SAVE and RESTORE as that shows the operation being performed. + */ +#define SAVE(a, b) \ + do { \ + INSIST(a == NULL); \ + a = b; \ + b = NULL; \ + } while (0) +#define RESTORE(a, b) SAVE(a, b) + +static bool +validate(ns_client_t *client, dns_db_t *db, dns_name_t *name, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset); + +static void +query_findclosestnsec3(dns_name_t *qname, dns_db_t *db, + dns_dbversion_t *version, ns_client_t *client, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset, + dns_name_t *fname, bool exact, dns_name_t *found); + +static void +log_queryerror(ns_client_t *client, isc_result_t result, int line, int level); + +static void +rpz_st_clear(ns_client_t *client); + +static bool +rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset); + +static void +log_noexistnodata(void *val, int level, const char *fmt, ...) + ISC_FORMAT_PRINTF(3, 4); + +/* + * Return the hooktable in use with 'qctx', or if there isn't one + * set, return the default hooktable. + */ +static ns_hooktable_t * +get_hooktab(query_ctx_t *qctx) { + if (qctx == NULL || qctx->view == NULL || qctx->view->hooktable == NULL) + { + return (ns__hook_table); + } + + return (qctx->view->hooktable); +} + +/* + * Call the specified hook function in every configured module that implements + * that function. If any hook function returns NS_HOOK_RETURN, we + * set 'result' and terminate processing by jumping to the 'cleanup' tag. + * + * (Note that a hook function may set the 'result' to ISC_R_SUCCESS but + * still terminate processing within the calling function. That's why this + * is a macro instead of a static function; it needs to be able to use + * 'goto cleanup' regardless of the return value.) + */ +#define CALL_HOOK(_id, _qctx) \ + do { \ + isc_result_t _res; \ + ns_hooktable_t *_tab = get_hooktab(_qctx); \ + ns_hook_t *_hook; \ + _hook = ISC_LIST_HEAD((*_tab)[_id]); \ + while (_hook != NULL) { \ + ns_hook_action_t _func = _hook->action; \ + void *_data = _hook->action_data; \ + INSIST(_func != NULL); \ + switch (_func(_qctx, _data, &_res)) { \ + case NS_HOOK_CONTINUE: \ + _hook = ISC_LIST_NEXT(_hook, link); \ + break; \ + case NS_HOOK_RETURN: \ + result = _res; \ + goto cleanup; \ + default: \ + UNREACHABLE(); \ + } \ + } \ + } while (false) + +/* + * Call the specified hook function in every configured module that + * implements that function. All modules are called; hook function return + * codes are ignored. This is intended for use with initialization and + * destruction calls which *must* run in every configured module. + * + * (This could be implemented as a static void function, but is left as a + * macro for symmetry with CALL_HOOK above.) + */ +#define CALL_HOOK_NORETURN(_id, _qctx) \ + do { \ + isc_result_t _res; \ + ns_hooktable_t *_tab = get_hooktab(_qctx); \ + ns_hook_t *_hook; \ + _hook = ISC_LIST_HEAD((*_tab)[_id]); \ + while (_hook != NULL) { \ + ns_hook_action_t _func = _hook->action; \ + void *_data = _hook->action_data; \ + INSIST(_func != NULL); \ + _func(_qctx, _data, &_res); \ + _hook = ISC_LIST_NEXT(_hook, link); \ + } \ + } while (false) + +/* + * The functions defined below implement the query logic that previously lived + * in the single very complex function query_find(). The query_ctx_t structure + * defined in <ns/query.h> maintains state from function to function. The call + * flow for the general query processing algorithm is described below: + * + * 1. Set up query context and other resources for a client + * query (query_setup()) + * + * 2. Start the search (ns__query_start()) + * + * 3. Identify authoritative data sources which may have an answer; + * search them (query_lookup()). If an answer is found, go to 7. + * + * 4. If recursion or cache access are allowed, search the cache + * (query_lookup() again, using the cache database) to find a better + * answer. If an answer is found, go to 7. + * + * 5. If recursion is allowed, begin recursion (ns_query_recurse()). + * Go to 15 to clean up this phase of the query. When recursion + * is complete, processing will resume at 6. + * + * 6. Resume from recursion; set up query context for resumed processing. + * + * 7. Determine what sort of answer we've found (query_gotanswer()) + * and call other functions accordingly: + * - not found (auth or cache), go to 8 + * - delegation, go to 9 + * - no such domain (auth), go to 10 + * - empty answer (auth), go to 11 + * - negative response (cache), go to 12 + * - answer found, go to 13 + * + * 8. The answer was not found in the database (query_notfound(). + * Set up a referral and go to 9. + * + * 9. Handle a delegation response (query_delegation()). If we need + * to and are allowed to recurse (query_delegation_recurse()), go to 5, + * otherwise go to 15 to clean up and return the delegation to the client. + * + * 10. No such domain (query_nxdomain()). Attempt redirection; if + * unsuccessful, add authority section records (query_addsoa(), + * query_addauth()), then go to 15 to return NXDOMAIN to client. + * + * 11. Empty answer (query_nodata()). Add authority section records + * (query_addsoa(), query_addauth()) and signatures if authoritative + * (query_sign_nodata()) then go to 15 and return + * NOERROR/ANCOUNT=0 to client. + * + * 12. No such domain or empty answer returned from cache (query_ncache()). + * Set response code appropriately, go to 11. + * + * 13. Prepare a response (query_prepresponse()) and then fill it + * appropriately (query_respond(), or for type ANY, + * query_respond_any()). + * + * 14. If a restart is needed due to CNAME/DNAME chaining, go to 2. + * + * 15. Clean up resources. If recursing, stop and wait for the event + * handler to be called back (step 6). If an answer is ready, + * return it to the client. + * + * (XXX: This description omits several special cases including + * DNS64, RPZ, RRL, and the SERVFAIL cache. It also doesn't discuss + * plugins.) + */ + +static void +query_trace(query_ctx_t *qctx); + +static void +qctx_init(ns_client_t *client, dns_fetchevent_t **eventp, dns_rdatatype_t qtype, + query_ctx_t *qctx); + +static isc_result_t +query_setup(ns_client_t *client, dns_rdatatype_t qtype); + +static isc_result_t +query_lookup(query_ctx_t *qctx); + +static void +fetch_callback(isc_task_t *task, isc_event_t *event); + +static void +recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype, + const dns_name_t *qname, const dns_name_t *qdomain); + +static isc_result_t +query_resume(query_ctx_t *qctx); + +static isc_result_t +query_checkrrl(query_ctx_t *qctx, isc_result_t result); + +static isc_result_t +query_checkrpz(query_ctx_t *qctx, isc_result_t result); + +static isc_result_t +query_rpzcname(query_ctx_t *qctx, dns_name_t *cname); + +static isc_result_t +query_gotanswer(query_ctx_t *qctx, isc_result_t result); + +static void +query_addnoqnameproof(query_ctx_t *qctx); + +static isc_result_t +query_respond_any(query_ctx_t *qctx); + +static isc_result_t +query_respond(query_ctx_t *qctx); + +static isc_result_t +query_dns64(query_ctx_t *qctx); + +static void +query_filter64(query_ctx_t *qctx); + +static isc_result_t +query_notfound(query_ctx_t *qctx); + +static isc_result_t +query_zone_delegation(query_ctx_t *qctx); + +static isc_result_t +query_delegation(query_ctx_t *qctx); + +static isc_result_t +query_delegation_recurse(query_ctx_t *qctx); + +static void +query_addds(query_ctx_t *qctx); + +static isc_result_t +query_nodata(query_ctx_t *qctx, isc_result_t result); + +static isc_result_t +query_sign_nodata(query_ctx_t *qctx); + +static void +query_addnxrrsetnsec(query_ctx_t *qctx); + +static isc_result_t +query_nxdomain(query_ctx_t *qctx, bool empty_wild); + +static isc_result_t +query_redirect(query_ctx_t *qctx); + +static isc_result_t +query_ncache(query_ctx_t *qctx, isc_result_t result); + +static isc_result_t +query_coveringnsec(query_ctx_t *qctx); + +static isc_result_t +query_zerottl_refetch(query_ctx_t *qctx); + +static isc_result_t +query_cname(query_ctx_t *qctx); + +static isc_result_t +query_dname(query_ctx_t *qctx); + +static isc_result_t +query_addcname(query_ctx_t *qctx, dns_trust_t trust, dns_ttl_t ttl); + +static isc_result_t +query_prepresponse(query_ctx_t *qctx); + +static isc_result_t +query_addsoa(query_ctx_t *qctx, unsigned int override_ttl, + dns_section_t section); + +static isc_result_t +query_addns(query_ctx_t *qctx); + +static void +query_addbestns(query_ctx_t *qctx); + +static void +query_addwildcardproof(query_ctx_t *qctx, bool ispositive, bool nodata); + +static void +query_addauth(query_ctx_t *qctx); + +static void +query_clear_stale(ns_client_t *client); + +/* + * Increment query statistics counters. + */ +static void +inc_stats(ns_client_t *client, isc_statscounter_t counter) { + dns_zone_t *zone = client->query.authzone; + dns_rdatatype_t qtype; + dns_rdataset_t *rdataset; + isc_stats_t *zonestats; + dns_stats_t *querystats = NULL; + + ns_stats_increment(client->sctx->nsstats, counter); + + if (zone == NULL) { + return; + } + + /* Do regular response type stats */ + zonestats = dns_zone_getrequeststats(zone); + + if (zonestats != NULL) { + isc_stats_increment(zonestats, counter); + } + + /* Do query type statistics + * + * We only increment per-type if we're using the authoritative + * answer counter, preventing double-counting. + */ + if (counter == ns_statscounter_authans) { + querystats = dns_zone_getrcvquerystats(zone); + if (querystats != NULL) { + rdataset = ISC_LIST_HEAD(client->query.qname->list); + if (rdataset != NULL) { + qtype = rdataset->type; + dns_rdatatypestats_increment(querystats, qtype); + } + } + } +} + +static void +query_send(ns_client_t *client) { + isc_statscounter_t counter; + + if ((client->message->flags & DNS_MESSAGEFLAG_AA) == 0) { + inc_stats(client, ns_statscounter_nonauthans); + } else { + inc_stats(client, ns_statscounter_authans); + } + + if (client->message->rcode == dns_rcode_noerror) { + dns_section_t answer = DNS_SECTION_ANSWER; + if (ISC_LIST_EMPTY(client->message->sections[answer])) { + if (client->query.isreferral) { + counter = ns_statscounter_referral; + } else { + counter = ns_statscounter_nxrrset; + } + } else { + counter = ns_statscounter_success; + } + } else if (client->message->rcode == dns_rcode_nxdomain) { + counter = ns_statscounter_nxdomain; + } else if (client->message->rcode == dns_rcode_badcookie) { + counter = ns_statscounter_badcookie; + } else { /* We end up here in case of YXDOMAIN, and maybe others */ + counter = ns_statscounter_failure; + } + + inc_stats(client, counter); + ns_client_send(client); + + if (!client->nodetach) { + isc_nmhandle_detach(&client->reqhandle); + } +} + +static void +query_error(ns_client_t *client, isc_result_t result, int line) { + int loglevel = ISC_LOG_DEBUG(3); + + switch (dns_result_torcode(result)) { + case dns_rcode_servfail: + loglevel = ISC_LOG_DEBUG(1); + inc_stats(client, ns_statscounter_servfail); + break; + case dns_rcode_formerr: + inc_stats(client, ns_statscounter_formerr); + break; + default: + inc_stats(client, ns_statscounter_failure); + break; + } + + if ((client->sctx->options & NS_SERVER_LOGQUERIES) != 0) { + loglevel = ISC_LOG_INFO; + } + + log_queryerror(client, result, line, loglevel); + + ns_client_error(client, result); + + if (!client->nodetach) { + isc_nmhandle_detach(&client->reqhandle); + } +} + +static void +query_next(ns_client_t *client, isc_result_t result) { + if (result == DNS_R_DUPLICATE) { + inc_stats(client, ns_statscounter_duplicate); + } else if (result == DNS_R_DROP) { + inc_stats(client, ns_statscounter_dropped); + } else { + inc_stats(client, ns_statscounter_failure); + } + ns_client_drop(client, result); + + if (!client->nodetach) { + isc_nmhandle_detach(&client->reqhandle); + } +} + +static void +query_freefreeversions(ns_client_t *client, bool everything) { + ns_dbversion_t *dbversion, *dbversion_next; + unsigned int i; + + for (dbversion = ISC_LIST_HEAD(client->query.freeversions), i = 0; + dbversion != NULL; dbversion = dbversion_next, i++) + { + dbversion_next = ISC_LIST_NEXT(dbversion, link); + /* + * If we're not freeing everything, we keep the first three + * dbversions structures around. + */ + if (i > 3 || everything) { + ISC_LIST_UNLINK(client->query.freeversions, dbversion, + link); + isc_mem_put(client->mctx, dbversion, + sizeof(*dbversion)); + } + } +} + +void +ns_query_cancel(ns_client_t *client) { + REQUIRE(NS_CLIENT_VALID(client)); + + LOCK(&client->query.fetchlock); + if (client->query.fetch != NULL) { + dns_resolver_cancelfetch(client->query.fetch); + + client->query.fetch = NULL; + } + UNLOCK(&client->query.fetchlock); +} + +static void +query_reset(ns_client_t *client, bool everything) { + isc_buffer_t *dbuf, *dbuf_next; + ns_dbversion_t *dbversion, *dbversion_next; + + CTRACE(ISC_LOG_DEBUG(3), "query_reset"); + + /*% + * Reset the query state of a client to its default state. + */ + + /* + * Cancel the fetch if it's running. + */ + ns_query_cancel(client); + + /* + * Cleanup any active versions. + */ + for (dbversion = ISC_LIST_HEAD(client->query.activeversions); + dbversion != NULL; dbversion = dbversion_next) + { + dbversion_next = ISC_LIST_NEXT(dbversion, link); + dns_db_closeversion(dbversion->db, &dbversion->version, false); + dns_db_detach(&dbversion->db); + ISC_LIST_INITANDAPPEND(client->query.freeversions, dbversion, + link); + } + ISC_LIST_INIT(client->query.activeversions); + + if (client->query.authdb != NULL) { + dns_db_detach(&client->query.authdb); + } + if (client->query.authzone != NULL) { + dns_zone_detach(&client->query.authzone); + } + + if (client->query.dns64_aaaa != NULL) { + ns_client_putrdataset(client, &client->query.dns64_aaaa); + } + if (client->query.dns64_sigaaaa != NULL) { + ns_client_putrdataset(client, &client->query.dns64_sigaaaa); + } + if (client->query.dns64_aaaaok != NULL) { + isc_mem_put(client->mctx, client->query.dns64_aaaaok, + client->query.dns64_aaaaoklen * sizeof(bool)); + client->query.dns64_aaaaok = NULL; + client->query.dns64_aaaaoklen = 0; + } + + ns_client_putrdataset(client, &client->query.redirect.rdataset); + ns_client_putrdataset(client, &client->query.redirect.sigrdataset); + if (client->query.redirect.db != NULL) { + if (client->query.redirect.node != NULL) { + dns_db_detachnode(client->query.redirect.db, + &client->query.redirect.node); + } + dns_db_detach(&client->query.redirect.db); + } + if (client->query.redirect.zone != NULL) { + dns_zone_detach(&client->query.redirect.zone); + } + + query_freefreeversions(client, everything); + + for (dbuf = ISC_LIST_HEAD(client->query.namebufs); dbuf != NULL; + dbuf = dbuf_next) + { + dbuf_next = ISC_LIST_NEXT(dbuf, link); + if (dbuf_next != NULL || everything) { + ISC_LIST_UNLINK(client->query.namebufs, dbuf, link); + isc_buffer_free(&dbuf); + } + } + + if (client->query.restarts > 0) { + /* + * client->query.qname was dynamically allocated. + */ + dns_message_puttempname(client->message, &client->query.qname); + } + client->query.qname = NULL; + client->query.attributes = (NS_QUERYATTR_RECURSIONOK | + NS_QUERYATTR_CACHEOK | NS_QUERYATTR_SECURE); + client->query.restarts = 0; + client->query.timerset = false; + if (client->query.rpz_st != NULL) { + rpz_st_clear(client); + if (everything) { + INSIST(client->query.rpz_st->rpsdb == NULL); + isc_mem_put(client->mctx, client->query.rpz_st, + sizeof(*client->query.rpz_st)); + client->query.rpz_st = NULL; + } + } + client->query.origqname = NULL; + client->query.dboptions = 0; + client->query.fetchoptions = 0; + client->query.gluedb = NULL; + client->query.authdbset = false; + client->query.isreferral = false; + client->query.dns64_options = 0; + client->query.dns64_ttl = UINT32_MAX; + recparam_update(&client->query.recparam, 0, NULL, NULL); + client->query.root_key_sentinel_keyid = 0; + client->query.root_key_sentinel_is_ta = false; + client->query.root_key_sentinel_not_ta = false; +} + +static void +query_cleanup(ns_client_t *client) { + query_reset(client, false); +} + +void +ns_query_free(ns_client_t *client) { + REQUIRE(NS_CLIENT_VALID(client)); + + query_reset(client, true); +} + +isc_result_t +ns_query_init(ns_client_t *client) { + isc_result_t result = ISC_R_SUCCESS; + + REQUIRE(NS_CLIENT_VALID(client)); + + ISC_LIST_INIT(client->query.namebufs); + ISC_LIST_INIT(client->query.activeversions); + ISC_LIST_INIT(client->query.freeversions); + client->query.restarts = 0; + client->query.timerset = false; + client->query.rpz_st = NULL; + client->query.qname = NULL; + /* + * This mutex is destroyed when the client is destroyed in + * exit_check(). + */ + isc_mutex_init(&client->query.fetchlock); + + client->query.fetch = NULL; + client->query.prefetch = NULL; + client->query.authdb = NULL; + client->query.authzone = NULL; + client->query.authdbset = false; + client->query.isreferral = false; + client->query.dns64_aaaa = NULL; + client->query.dns64_sigaaaa = NULL; + client->query.dns64_aaaaok = NULL; + client->query.dns64_aaaaoklen = 0; + client->query.redirect.db = NULL; + client->query.redirect.node = NULL; + client->query.redirect.zone = NULL; + client->query.redirect.qtype = dns_rdatatype_none; + client->query.redirect.result = ISC_R_SUCCESS; + client->query.redirect.rdataset = NULL; + client->query.redirect.sigrdataset = NULL; + client->query.redirect.authoritative = false; + client->query.redirect.is_zone = false; + client->query.redirect.fname = + dns_fixedname_initname(&client->query.redirect.fixed); + query_reset(client, false); + ns_client_newdbversion(client, 3); + ns_client_newnamebuf(client); + + return (result); +} + +/*% + * Check if 'client' is allowed to query the cache of its associated view. + * Unless 'options' has DNS_GETDB_NOLOG set, log the result of cache ACL + * evaluation using the appropriate level, along with 'name' and 'qtype'. + * + * The cache ACL is only evaluated once for each client and then the result is + * cached: if NS_QUERYATTR_CACHEACLOKVALID is set in client->query.attributes, + * cache ACL evaluation has already been performed. The evaluation result is + * also stored in client->query.attributes: if NS_QUERYATTR_CACHEACLOK is set, + * the client is allowed cache access. + * + * Returns: + * + *\li #ISC_R_SUCCESS 'client' is allowed to access cache + *\li #DNS_R_REFUSED 'client' is not allowed to access cache + */ +static isc_result_t +query_checkcacheaccess(ns_client_t *client, const dns_name_t *name, + dns_rdatatype_t qtype, unsigned int options) { + isc_result_t result; + + if ((client->query.attributes & NS_QUERYATTR_CACHEACLOKVALID) == 0) { + /* + * The view's cache ACLs have not yet been evaluated. + * Do it now. Both allow-query-cache and + * allow-query-cache-on must be satsified. + */ + bool log = ((options & DNS_GETDB_NOLOG) == 0); + char msg[NS_CLIENT_ACLMSGSIZE("query (cache)")]; + + result = ns_client_checkaclsilent(client, NULL, + client->view->cacheacl, true); + if (result == ISC_R_SUCCESS) { + result = ns_client_checkaclsilent( + client, &client->destaddr, + client->view->cacheonacl, true); + } + if (result == ISC_R_SUCCESS) { + /* + * We were allowed by the "allow-query-cache" ACL. + */ + client->query.attributes |= NS_QUERYATTR_CACHEACLOK; + if (log && isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(3))) + { + ns_client_aclmsg("query (cache)", name, qtype, + client->view->rdclass, msg, + sizeof(msg)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, + ISC_LOG_DEBUG(3), "%s approved", + msg); + } + } else if (log) { + /* + * We were denied by the "allow-query-cache" ACL. + * There is no need to clear NS_QUERYATTR_CACHEACLOK + * since it is cleared by query_reset(), before query + * processing starts. + */ + ns_client_aclmsg("query (cache)", name, qtype, + client->view->rdclass, msg, + sizeof(msg)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s denied", msg); + } + + /* + * Evaluation has been finished; make sure we will just consult + * NS_QUERYATTR_CACHEACLOK for this client from now on. + */ + client->query.attributes |= NS_QUERYATTR_CACHEACLOKVALID; + } + + return ((client->query.attributes & NS_QUERYATTR_CACHEACLOK) != 0 + ? ISC_R_SUCCESS + : DNS_R_REFUSED); +} + +static isc_result_t +query_validatezonedb(ns_client_t *client, const dns_name_t *name, + dns_rdatatype_t qtype, unsigned int options, + dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t **versionp) { + isc_result_t result; + dns_acl_t *queryacl, *queryonacl; + ns_dbversion_t *dbversion; + + REQUIRE(zone != NULL); + REQUIRE(db != NULL); + + /* + * Mirror zone data is treated as cache data. + */ + if (dns_zone_gettype(zone) == dns_zone_mirror) { + return (query_checkcacheaccess(client, name, qtype, options)); + } + + /* + * This limits our searching to the zone where the first name + * (the query target) was looked for. This prevents following + * CNAMES or DNAMES into other zones and prevents returning + * additional data from other zones. This does not apply if we're + * answering a query where recursion is requested and allowed. + */ + if (client->query.rpz_st == NULL && + !(WANTRECURSION(client) && RECURSIONOK(client)) && + client->query.authdbset && db != client->query.authdb) + { + return (DNS_R_REFUSED); + } + + /* + * Non recursive query to a static-stub zone is prohibited; its + * zone content is not public data, but a part of local configuration + * and should not be disclosed. + */ + if (dns_zone_gettype(zone) == dns_zone_staticstub && + !RECURSIONOK(client)) + { + return (DNS_R_REFUSED); + } + + /* + * If the zone has an ACL, we'll check it, otherwise + * we use the view's "allow-query" ACL. Each ACL is only checked + * once per query. + * + * Also, get the database version to use. + */ + + /* + * Get the current version of this database. + */ + dbversion = ns_client_findversion(client, db); + if (dbversion == NULL) { + CTRACE(ISC_LOG_ERROR, "unable to get db version"); + return (DNS_R_SERVFAIL); + } + + if ((options & DNS_GETDB_IGNOREACL) != 0) { + goto approved; + } + if (dbversion->acl_checked) { + if (!dbversion->queryok) { + return (DNS_R_REFUSED); + } + goto approved; + } + + queryacl = dns_zone_getqueryacl(zone); + if (queryacl == NULL) { + queryacl = client->view->queryacl; + if ((client->query.attributes & NS_QUERYATTR_QUERYOKVALID) != 0) + { + /* + * We've evaluated the view's queryacl already. If + * NS_QUERYATTR_QUERYOK is set, then the client is + * allowed to make queries, otherwise the query should + * be refused. + */ + dbversion->acl_checked = true; + if ((client->query.attributes & NS_QUERYATTR_QUERYOK) == + 0) + { + dbversion->queryok = false; + return (DNS_R_REFUSED); + } + dbversion->queryok = true; + goto approved; + } + } + + result = ns_client_checkaclsilent(client, NULL, queryacl, true); + if ((options & DNS_GETDB_NOLOG) == 0) { + char msg[NS_CLIENT_ACLMSGSIZE("query")]; + if (result == ISC_R_SUCCESS) { + if (isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(3))) { + ns_client_aclmsg("query", name, qtype, + client->view->rdclass, msg, + sizeof(msg)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, + ISC_LOG_DEBUG(3), "%s approved", + msg); + } + } else { + ns_client_aclmsg("query", name, qtype, + client->view->rdclass, msg, + sizeof(msg)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s denied", msg); + } + } + + if (queryacl == client->view->queryacl) { + if (result == ISC_R_SUCCESS) { + /* + * We were allowed by the default + * "allow-query" ACL. Remember this so we + * don't have to check again. + */ + client->query.attributes |= NS_QUERYATTR_QUERYOK; + } + /* + * We've now evaluated the view's query ACL, and + * the NS_QUERYATTR_QUERYOK attribute is now valid. + */ + client->query.attributes |= NS_QUERYATTR_QUERYOKVALID; + } + + /* If and only if we've gotten this far, check allow-query-on too */ + if (result == ISC_R_SUCCESS) { + queryonacl = dns_zone_getqueryonacl(zone); + if (queryonacl == NULL) { + queryonacl = client->view->queryonacl; + } + + result = ns_client_checkaclsilent(client, &client->destaddr, + queryonacl, true); + if ((options & DNS_GETDB_NOLOG) == 0 && result != ISC_R_SUCCESS) + { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "query-on denied"); + } + } + + dbversion->acl_checked = true; + if (result != ISC_R_SUCCESS) { + dbversion->queryok = false; + return (DNS_R_REFUSED); + } + dbversion->queryok = true; + +approved: + /* Transfer ownership, if necessary. */ + if (versionp != NULL) { + *versionp = dbversion->version; + } + return (ISC_R_SUCCESS); +} + +static isc_result_t +query_getzonedb(ns_client_t *client, const dns_name_t *name, + dns_rdatatype_t qtype, unsigned int options, dns_zone_t **zonep, + dns_db_t **dbp, dns_dbversion_t **versionp) { + isc_result_t result; + unsigned int ztoptions; + dns_zone_t *zone = NULL; + dns_db_t *db = NULL; + bool partial = false; + + REQUIRE(zonep != NULL && *zonep == NULL); + REQUIRE(dbp != NULL && *dbp == NULL); + + /*% + * Find a zone database to answer the query. + */ + ztoptions = DNS_ZTFIND_MIRROR; + if ((options & DNS_GETDB_NOEXACT) != 0) { + ztoptions |= DNS_ZTFIND_NOEXACT; + } + + result = dns_zt_find(client->view->zonetable, name, ztoptions, NULL, + &zone); + + if (result == DNS_R_PARTIALMATCH) { + partial = true; + } + if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) { + result = dns_zone_getdb(zone, &db); + } + + if (result != ISC_R_SUCCESS) { + goto fail; + } + + result = query_validatezonedb(client, name, qtype, options, zone, db, + versionp); + + if (result != ISC_R_SUCCESS) { + goto fail; + } + + /* Transfer ownership. */ + *zonep = zone; + *dbp = db; + + if (partial && (options & DNS_GETDB_PARTIAL) != 0) { + return (DNS_R_PARTIALMATCH); + } + return (ISC_R_SUCCESS); + +fail: + if (zone != NULL) { + dns_zone_detach(&zone); + } + if (db != NULL) { + dns_db_detach(&db); + } + + return (result); +} + +static void +rpz_log_rewrite(ns_client_t *client, bool disabled, dns_rpz_policy_t policy, + dns_rpz_type_t type, dns_zone_t *p_zone, dns_name_t *p_name, + dns_name_t *cname, dns_rpz_num_t rpz_num) { + char cname_buf[DNS_NAME_FORMATSIZE] = { 0 }; + char p_name_buf[DNS_NAME_FORMATSIZE]; + char qname_buf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + const char *s1 = cname_buf, *s2 = cname_buf; + dns_rdataset_t *rdataset; + dns_rpz_st_t *st; + isc_stats_t *zonestats; + + /* + * Count enabled rewrites in the global counter. + * Count both enabled and disabled rewrites for each zone. + */ + if (!disabled && policy != DNS_RPZ_POLICY_PASSTHRU) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_rpz_rewrites); + } + if (p_zone != NULL) { + zonestats = dns_zone_getrequeststats(p_zone); + if (zonestats != NULL) { + isc_stats_increment(zonestats, + ns_statscounter_rpz_rewrites); + } + } + + if (!isc_log_wouldlog(ns_lctx, DNS_RPZ_INFO_LEVEL)) { + return; + } + + st = client->query.rpz_st; + if ((st->popt.no_log & DNS_RPZ_ZBIT(rpz_num)) != 0) { + return; + } + + dns_name_format(client->query.qname, qname_buf, sizeof(qname_buf)); + dns_name_format(p_name, p_name_buf, sizeof(p_name_buf)); + if (cname != NULL) { + s1 = " (CNAME to: "; + dns_name_format(cname, cname_buf, sizeof(cname_buf)); + s2 = ")"; + } + + /* + * Log Qclass and Qtype in addition to existing + * fields. + */ + rdataset = ISC_LIST_HEAD(client->query.origqname->list); + INSIST(rdataset != NULL); + dns_rdataclass_format(rdataset->rdclass, classbuf, sizeof(classbuf)); + dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf)); + + ns_client_log(client, DNS_LOGCATEGORY_RPZ, NS_LOGMODULE_QUERY, + DNS_RPZ_INFO_LEVEL, + "%srpz %s %s rewrite %s/%s/%s via %s%s%s%s", + disabled ? "disabled " : "", dns_rpz_type2str(type), + dns_rpz_policy2str(policy), qname_buf, typebuf, classbuf, + p_name_buf, s1, cname_buf, s2); +} + +static void +rpz_log_fail_helper(ns_client_t *client, int level, dns_name_t *p_name, + dns_rpz_type_t rpz_type1, dns_rpz_type_t rpz_type2, + const char *str, isc_result_t result) { + char qnamebuf[DNS_NAME_FORMATSIZE]; + char p_namebuf[DNS_NAME_FORMATSIZE]; + const char *failed, *via, *slash, *str_blank; + const char *rpztypestr1; + const char *rpztypestr2; + + if (!isc_log_wouldlog(ns_lctx, level)) { + return; + } + + /* + * bin/tests/system/rpz/tests.sh looks for "rpz.*failed" for problems. + */ + if (level <= DNS_RPZ_DEBUG_LEVEL1) { + failed = " failed: "; + } else { + failed = ": "; + } + + rpztypestr1 = dns_rpz_type2str(rpz_type1); + if (rpz_type2 != DNS_RPZ_TYPE_BAD) { + slash = "/"; + rpztypestr2 = dns_rpz_type2str(rpz_type2); + } else { + slash = ""; + rpztypestr2 = ""; + } + + str_blank = (*str != ' ' && *str != '\0') ? " " : ""; + + dns_name_format(client->query.qname, qnamebuf, sizeof(qnamebuf)); + + if (p_name != NULL) { + via = " via "; + dns_name_format(p_name, p_namebuf, sizeof(p_namebuf)); + } else { + via = ""; + p_namebuf[0] = '\0'; + } + + ns_client_log(client, NS_LOGCATEGORY_QUERY_ERRORS, NS_LOGMODULE_QUERY, + level, "rpz %s%s%s rewrite %s%s%s%s%s%s%s", rpztypestr1, + slash, rpztypestr2, qnamebuf, via, p_namebuf, str_blank, + str, failed, isc_result_totext(result)); +} + +static void +rpz_log_fail(ns_client_t *client, int level, dns_name_t *p_name, + dns_rpz_type_t rpz_type, const char *str, isc_result_t result) { + rpz_log_fail_helper(client, level, p_name, rpz_type, DNS_RPZ_TYPE_BAD, + str, result); +} + +/* + * Get a policy rewrite zone database. + */ +static isc_result_t +rpz_getdb(ns_client_t *client, dns_name_t *p_name, dns_rpz_type_t rpz_type, + dns_zone_t **zonep, dns_db_t **dbp, dns_dbversion_t **versionp) { + char qnamebuf[DNS_NAME_FORMATSIZE]; + char p_namebuf[DNS_NAME_FORMATSIZE]; + dns_dbversion_t *rpz_version = NULL; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_getdb"); + + result = query_getzonedb(client, p_name, dns_rdatatype_any, + DNS_GETDB_IGNOREACL, zonep, dbp, &rpz_version); + if (result == ISC_R_SUCCESS) { + dns_rpz_st_t *st = client->query.rpz_st; + + /* + * It isn't meaningful to log this message when + * logging is disabled for some policy zones. + */ + if (st->popt.no_log == 0 && + isc_log_wouldlog(ns_lctx, DNS_RPZ_DEBUG_LEVEL2)) + { + dns_name_format(client->query.qname, qnamebuf, + sizeof(qnamebuf)); + dns_name_format(p_name, p_namebuf, sizeof(p_namebuf)); + ns_client_log(client, DNS_LOGCATEGORY_RPZ, + NS_LOGMODULE_QUERY, DNS_RPZ_DEBUG_LEVEL2, + "try rpz %s rewrite %s via %s", + dns_rpz_type2str(rpz_type), qnamebuf, + p_namebuf); + } + *versionp = rpz_version; + return (ISC_R_SUCCESS); + } + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name, rpz_type, + "query_getzonedb()", result); + return (result); +} + +/*% + * Find a cache database to answer the query. This may fail with DNS_R_REFUSED + * if the client is not allowed to use the cache. + */ +static isc_result_t +query_getcachedb(ns_client_t *client, const dns_name_t *name, + dns_rdatatype_t qtype, dns_db_t **dbp, unsigned int options) { + isc_result_t result; + dns_db_t *db = NULL; + + REQUIRE(dbp != NULL && *dbp == NULL); + + if (!USECACHE(client)) { + return (DNS_R_REFUSED); + } + + dns_db_attach(client->view->cachedb, &db); + + result = query_checkcacheaccess(client, name, qtype, options); + if (result != ISC_R_SUCCESS) { + dns_db_detach(&db); + } + + /* + * If query_checkcacheaccess() succeeded, transfer ownership of 'db'. + * Otherwise, 'db' will be NULL due to the dns_db_detach() call above. + */ + *dbp = db; + + return (result); +} + +static isc_result_t +query_getdb(ns_client_t *client, dns_name_t *name, dns_rdatatype_t qtype, + unsigned int options, dns_zone_t **zonep, dns_db_t **dbp, + dns_dbversion_t **versionp, bool *is_zonep) { + isc_result_t result; + isc_result_t tresult; + unsigned int namelabels; + unsigned int zonelabels; + dns_zone_t *zone = NULL; + + REQUIRE(zonep != NULL && *zonep == NULL); + + /* Calculate how many labels are in name. */ + namelabels = dns_name_countlabels(name); + zonelabels = 0; + + /* Try to find name in bind's standard database. */ + result = query_getzonedb(client, name, qtype, options, &zone, dbp, + versionp); + + /* See how many labels are in the zone's name. */ + if (result == ISC_R_SUCCESS && zone != NULL) { + zonelabels = dns_name_countlabels(dns_zone_getorigin(zone)); + } + + /* + * If # zone labels < # name labels, try to find an even better match + * Only try if DLZ drivers are loaded for this view + */ + if (ISC_UNLIKELY(zonelabels < namelabels && + !ISC_LIST_EMPTY(client->view->dlz_searched))) + { + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_db_t *tdbp; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, &client->ecs, NULL); + + tdbp = NULL; + tresult = dns_view_searchdlz(client->view, name, zonelabels, + &cm, &ci, &tdbp); + /* If we successful, we found a better match. */ + if (tresult == ISC_R_SUCCESS) { + ns_dbversion_t *dbversion; + + /* + * If the previous search returned a zone, detach it. + */ + if (zone != NULL) { + dns_zone_detach(&zone); + } + + /* + * If the previous search returned a database, + * detach it. + */ + if (*dbp != NULL) { + dns_db_detach(dbp); + } + + /* + * If the previous search returned a version, clear it. + */ + *versionp = NULL; + + dbversion = ns_client_findversion(client, tdbp); + if (dbversion == NULL) { + tresult = ISC_R_NOMEMORY; + } else { + /* + * Be sure to return our database. + */ + *dbp = tdbp; + *versionp = dbversion->version; + } + + /* + * We return a null zone, No stats for DLZ zones. + */ + zone = NULL; + result = tresult; + } + } + + /* If successful, Transfer ownership of zone. */ + if (result == ISC_R_SUCCESS) { + *zonep = zone; + /* + * If neither attempt above succeeded, return the cache instead + */ + *is_zonep = true; + } else { + if (result == ISC_R_NOTFOUND) { + result = query_getcachedb(client, name, qtype, dbp, + options); + } + *is_zonep = false; + } + return (result); +} + +static bool +query_isduplicate(ns_client_t *client, dns_name_t *name, dns_rdatatype_t type, + dns_name_t **mnamep) { + dns_section_t section; + dns_name_t *mname = NULL; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate"); + + for (section = DNS_SECTION_ANSWER; section <= DNS_SECTION_ADDITIONAL; + section++) + { + result = dns_message_findname(client->message, section, name, + type, 0, &mname, NULL); + if (result == ISC_R_SUCCESS) { + /* + * We've already got this RRset in the response. + */ + CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate: true: " + "done"); + return (true); + } else if (result == DNS_R_NXRRSET) { + /* + * The name exists, but the rdataset does not. + */ + if (section == DNS_SECTION_ADDITIONAL) { + break; + } + } else { + RUNTIME_CHECK(result == DNS_R_NXDOMAIN); + } + mname = NULL; + } + + if (mnamep != NULL) { + *mnamep = mname; + } + + CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate: false: done"); + return (false); +} + +/* + * Look up data for given 'name' and 'type' in given 'version' of 'db' for + * 'client'. Called from query_additionalauth(). + * + * If the lookup is successful: + * + * - store the node containing the result at 'nodep', + * + * - store the owner name of the returned node in 'fname', + * + * - if 'type' is not ANY, dns_db_findext() will put the exact rdataset being + * looked for in 'rdataset' and its signatures (if any) in 'sigrdataset', + * + * - if 'type' is ANY, dns_db_findext() will leave 'rdataset' and + * 'sigrdataset' disassociated and the returned node will be iterated in + * query_additional_cb(). + * + * If the lookup is not successful: + * + * - 'nodep' will not be written to, + * - 'fname' may still be modified as it is passed to dns_db_findext(), + * - 'rdataset' and 'sigrdataset' will remain disassociated. + */ +static isc_result_t +query_additionalauthfind(dns_db_t *db, dns_dbversion_t *version, + const dns_name_t *name, dns_rdatatype_t type, + ns_client_t *client, dns_dbnode_t **nodep, + dns_name_t *fname, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + dns_clientinfomethods_t cm; + dns_dbnode_t *node = NULL; + dns_clientinfo_t ci; + isc_result_t result; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Since we are looking for authoritative data, we do not set + * the GLUEOK flag. Glue will be looked for later, but not + * necessarily in the same database. + */ + result = dns_db_findext(db, name, version, type, + client->query.dboptions, client->now, &node, + fname, &cm, &ci, rdataset, sigrdataset); + if (result != ISC_R_SUCCESS) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + + if (node != NULL) { + dns_db_detachnode(db, &node); + } + + return (result); + } + + /* + * Do not return signatures if the zone is not fully signed. + */ + if (sigrdataset != NULL && !dns_db_issecure(db) && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + + *nodep = node; + + return (ISC_R_SUCCESS); +} + +/* + * For query context 'qctx', try finding authoritative additional data for + * given 'name' and 'type'. Called from query_additional_cb(). + * + * If successful: + * + * - store pointers to the database and node which contain the result in + * 'dbp' and 'nodep', respectively, + * + * - store the owner name of the returned node in 'fname', + * + * - potentially bind 'rdataset' and 'sigrdataset', as explained in the + * comment for query_additionalauthfind(). + * + * If unsuccessful: + * + * - 'dbp' and 'nodep' will not be written to, + * - 'fname' may still be modified as it is passed to dns_db_findext(), + * - 'rdataset' and 'sigrdataset' will remain disassociated. + */ +static isc_result_t +query_additionalauth(query_ctx_t *qctx, const dns_name_t *name, + dns_rdatatype_t type, dns_db_t **dbp, dns_dbnode_t **nodep, + dns_name_t *fname, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + ns_client_t *client = qctx->client; + ns_dbversion_t *dbversion = NULL; + dns_dbversion_t *version = NULL; + dns_dbnode_t *node = NULL; + dns_zone_t *zone = NULL; + dns_db_t *db = NULL; + isc_result_t result; + + /* + * First, look within the same zone database for authoritative + * additional data. + */ + if (!client->query.authdbset || client->query.authdb == NULL) { + return (ISC_R_NOTFOUND); + } + + dbversion = ns_client_findversion(client, client->query.authdb); + if (dbversion == NULL) { + return (ISC_R_NOTFOUND); + } + + dns_db_attach(client->query.authdb, &db); + version = dbversion->version; + + CTRACE(ISC_LOG_DEBUG(3), "query_additionalauth: same zone"); + + result = query_additionalauthfind(db, version, name, type, client, + &node, fname, rdataset, sigrdataset); + if (result != ISC_R_SUCCESS && + qctx->view->minimalresponses == dns_minimal_no && + RECURSIONOK(client)) + { + /* + * If we aren't doing response minimization and recursion is + * allowed, we can try and see if any other zone matches. + */ + version = NULL; + dns_db_detach(&db); + result = query_getzonedb(client, name, type, DNS_GETDB_NOLOG, + &zone, &db, &version); + if (result != ISC_R_SUCCESS) { + return (result); + } + dns_zone_detach(&zone); + + CTRACE(ISC_LOG_DEBUG(3), "query_additionalauth: other zone"); + + result = query_additionalauthfind(db, version, name, type, + client, &node, fname, + rdataset, sigrdataset); + } + + if (result != ISC_R_SUCCESS) { + dns_db_detach(&db); + } else { + *nodep = node; + node = NULL; + + *dbp = db; + db = NULL; + } + + return (result); +} + +static isc_result_t +query_additional_cb(void *arg, const dns_name_t *name, dns_rdatatype_t qtype) { + query_ctx_t *qctx = arg; + ns_client_t *client = qctx->client; + isc_result_t result, eresult = ISC_R_SUCCESS; + dns_dbnode_t *node = NULL; + dns_db_t *db = NULL; + dns_name_t *fname = NULL, *mname = NULL; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + dns_rdataset_t *trdataset = NULL; + isc_buffer_t *dbuf = NULL; + isc_buffer_t b; + ns_dbversion_t *dbversion = NULL; + dns_dbversion_t *version = NULL; + bool added_something = false, need_addname = false; + dns_rdatatype_t type; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_rdatasetadditional_t additionaltype = + dns_rdatasetadditional_fromauth; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(qtype != dns_rdatatype_any); + + if (!WANTDNSSEC(client) && dns_rdatatype_isdnssec(qtype)) { + return (ISC_R_SUCCESS); + } + + CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb"); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * We treat type A additional section processing as if it + * were "any address type" additional section processing. + * To avoid multiple lookups, we do an 'any' database + * lookup and iterate over the node. + */ + if (qtype == dns_rdatatype_a) { + type = dns_rdatatype_any; + } else { + type = qtype; + } + + /* + * Get some resources. + */ + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + rdataset = ns_client_newrdataset(client); + if (fname == NULL || rdataset == NULL) { + goto cleanup; + } + if (WANTDNSSEC(client)) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + goto cleanup; + } + } + + /* + * If we want only minimal responses and are here, then it must + * be for glue. + */ + if (qctx->view->minimalresponses == dns_minimal_yes && + client->query.qtype != dns_rdatatype_ns) + { + goto try_glue; + } + + /* + * First, look for authoritative additional data. + */ + result = query_additionalauth(qctx, name, type, &db, &node, fname, + rdataset, sigrdataset); + if (result == ISC_R_SUCCESS) { + goto found; + } + + /* + * No authoritative data was found. The cache is our next best bet. + */ + if (!qctx->view->recursion) { + goto try_glue; + } + + additionaltype = dns_rdatasetadditional_fromcache; + result = query_getcachedb(client, name, qtype, &db, DNS_GETDB_NOLOG); + if (result != ISC_R_SUCCESS) { + /* + * Most likely the client isn't allowed to query the cache. + */ + goto try_glue; + } + /* + * Attempt to validate glue. + */ + if (sigrdataset == NULL) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + goto cleanup; + } + } + + version = NULL; + result = dns_db_findext(db, name, version, type, + client->query.dboptions | DNS_DBFIND_GLUEOK | + DNS_DBFIND_ADDITIONALOK, + client->now, &node, fname, &cm, &ci, rdataset, + sigrdataset); + + dns_cache_updatestats(qctx->view->cache, result); + if (!WANTDNSSEC(client)) { + ns_client_putrdataset(client, &sigrdataset); + } + if (result == ISC_R_SUCCESS) { + goto found; + } + + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (sigrdataset != NULL && dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + +try_glue: + /* + * No cached data was found. Glue is our last chance. + * RFC1035 sayeth: + * + * NS records cause both the usual additional section + * processing to locate a type A record, and, when used + * in a referral, a special search of the zone in which + * they reside for glue information. + * + * This is the "special search". Note that we must search + * the zone where the NS record resides, not the zone it + * points to, and that we only do the search in the delegation + * case (identified by client->query.gluedb being set). + */ + + if (client->query.gluedb == NULL) { + goto cleanup; + } + + /* + * Don't poison caches using the bailiwick protection model. + */ + if (!dns_name_issubdomain(name, dns_db_origin(client->query.gluedb))) { + goto cleanup; + } + + dbversion = ns_client_findversion(client, client->query.gluedb); + if (dbversion == NULL) { + goto cleanup; + } + + dns_db_attach(client->query.gluedb, &db); + version = dbversion->version; + additionaltype = dns_rdatasetadditional_fromglue; + result = dns_db_findext(db, name, version, type, + client->query.dboptions | DNS_DBFIND_GLUEOK, + client->now, &node, fname, &cm, &ci, rdataset, + sigrdataset); + if (result != ISC_R_SUCCESS && result != DNS_R_ZONECUT && + result != DNS_R_GLUE) + { + goto cleanup; + } + +found: + /* + * We have found a potential additional data rdataset, or + * at least a node to iterate over. + */ + ns_client_keepname(client, fname, dbuf); + + /* + * If we have an rdataset, add it to the additional data + * section. + */ + mname = NULL; + if (dns_rdataset_isassociated(rdataset) && + !query_isduplicate(client, fname, type, &mname)) + { + if (mname != NULL) { + INSIST(mname != fname); + ns_client_releasename(client, &fname); + fname = mname; + } else { + need_addname = true; + } + ISC_LIST_APPEND(fname->list, rdataset, link); + trdataset = rdataset; + rdataset = NULL; + added_something = true; + /* + * Note: we only add SIGs if we've added the type they cover, + * so we do not need to check if the SIG rdataset is already + * in the response. + */ + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + ISC_LIST_APPEND(fname->list, sigrdataset, link); + sigrdataset = NULL; + } + } + + if (qtype == dns_rdatatype_a) { + /* + * We now go looking for A and AAAA records, along with + * their signatures. + * + * XXXRTH This code could be more efficient. + */ + if (rdataset != NULL) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + } else { + rdataset = ns_client_newrdataset(client); + if (rdataset == NULL) { + goto addname; + } + } + if (sigrdataset != NULL) { + if (dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + } else if (WANTDNSSEC(client)) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + goto addname; + } + } + if (query_isduplicate(client, fname, dns_rdatatype_a, NULL)) { + goto aaaa_lookup; + } + result = dns_db_findrdataset(db, node, version, dns_rdatatype_a, + 0, client->now, rdataset, + sigrdataset); + if (result == DNS_R_NCACHENXDOMAIN) { + goto addname; + } else if (result == DNS_R_NCACHENXRRSET) { + dns_rdataset_disassociate(rdataset); + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + } else if (result == ISC_R_SUCCESS) { + bool invalid = false; + mname = NULL; + if (additionaltype == + dns_rdatasetadditional_fromcache && + (DNS_TRUST_PENDING(rdataset->trust) || + DNS_TRUST_GLUE(rdataset->trust))) + { + /* validate() may change rdataset->trust */ + invalid = !validate(client, db, fname, rdataset, + sigrdataset); + } + if (invalid && DNS_TRUST_PENDING(rdataset->trust)) { + dns_rdataset_disassociate(rdataset); + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + } else if (!query_isduplicate(client, fname, + dns_rdatatype_a, &mname)) + { + if (mname != fname) { + if (mname != NULL) { + ns_client_releasename(client, + &fname); + fname = mname; + } else { + need_addname = true; + } + } + ISC_LIST_APPEND(fname->list, rdataset, link); + added_something = true; + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + ISC_LIST_APPEND(fname->list, + sigrdataset, link); + sigrdataset = + ns_client_newrdataset(client); + } + rdataset = ns_client_newrdataset(client); + if (rdataset == NULL) { + goto addname; + } + if (WANTDNSSEC(client) && sigrdataset == NULL) { + goto addname; + } + } else { + dns_rdataset_disassociate(rdataset); + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + } + } + aaaa_lookup: + if (query_isduplicate(client, fname, dns_rdatatype_aaaa, NULL)) + { + goto addname; + } + result = dns_db_findrdataset(db, node, version, + dns_rdatatype_aaaa, 0, client->now, + rdataset, sigrdataset); + if (result == DNS_R_NCACHENXDOMAIN) { + goto addname; + } else if (result == DNS_R_NCACHENXRRSET) { + dns_rdataset_disassociate(rdataset); + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + } else if (result == ISC_R_SUCCESS) { + bool invalid = false; + mname = NULL; + + if (additionaltype == + dns_rdatasetadditional_fromcache && + (DNS_TRUST_PENDING(rdataset->trust) || + DNS_TRUST_GLUE(rdataset->trust))) + { + /* validate() may change rdataset->trust */ + invalid = !validate(client, db, fname, rdataset, + sigrdataset); + } + + if (invalid && DNS_TRUST_PENDING(rdataset->trust)) { + dns_rdataset_disassociate(rdataset); + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + dns_rdataset_disassociate(sigrdataset); + } + } else if (!query_isduplicate(client, fname, + dns_rdatatype_aaaa, + &mname)) + { + if (mname != fname) { + if (mname != NULL) { + ns_client_releasename(client, + &fname); + fname = mname; + } else { + need_addname = true; + } + } + ISC_LIST_APPEND(fname->list, rdataset, link); + added_something = true; + if (sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + ISC_LIST_APPEND(fname->list, + sigrdataset, link); + sigrdataset = NULL; + } + rdataset = NULL; + } + } + } + +addname: + CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: addname"); + /* + * If we haven't added anything, then we're done. + */ + if (!added_something) { + goto cleanup; + } + + /* + * We may have added our rdatasets to an existing name, if so, then + * need_addname will be false. Whether we used an existing name + * or a new one, we must set fname to NULL to prevent cleanup. + */ + if (need_addname) { + dns_message_addname(client->message, fname, + DNS_SECTION_ADDITIONAL); + } + fname = NULL; + + /* + * In some cases, a record that has been added as additional + * data may *also* trigger the addition of additional data. + * This cannot go more than MAX_RESTARTS levels deep. + */ + if (trdataset != NULL && dns_rdatatype_followadditional(type)) { + eresult = dns_rdataset_additionaldata( + trdataset, query_additional_cb, qctx); + } + +cleanup: + CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: cleanup"); + ns_client_putrdataset(client, &rdataset); + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + if (db != NULL) { + dns_db_detach(&db); + } + + CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: done"); + return (eresult); +} + +/* + * Add 'rdataset' to 'name'. + */ +static void +query_addtoname(dns_name_t *name, dns_rdataset_t *rdataset) { + ISC_LIST_APPEND(name->list, rdataset, link); +} + +/* + * Set the ordering for 'rdataset'. + */ +static void +query_setorder(query_ctx_t *qctx, dns_name_t *name, dns_rdataset_t *rdataset) { + ns_client_t *client = qctx->client; + dns_order_t *order = client->view->order; + + CTRACE(ISC_LOG_DEBUG(3), "query_setorder"); + + UNUSED(client); + + if (order != NULL) { + rdataset->attributes |= dns_order_find( + order, name, rdataset->type, rdataset->rdclass); + } + rdataset->attributes |= DNS_RDATASETATTR_LOADORDER; +} + +/* + * Handle glue and fetch any other needed additional data for 'rdataset'. + */ +static void +query_additional(query_ctx_t *qctx, dns_rdataset_t *rdataset) { + ns_client_t *client = qctx->client; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "query_additional"); + + if (NOADDITIONAL(client)) { + return; + } + + /* + * Try to process glue directly. + */ + if (qctx->view->use_glue_cache && + (rdataset->type == dns_rdatatype_ns) && + (client->query.gluedb != NULL) && + dns_db_iszone(client->query.gluedb)) + { + ns_dbversion_t *dbversion; + + dbversion = ns_client_findversion(client, client->query.gluedb); + if (dbversion == NULL) { + goto regular; + } + + result = dns_rdataset_addglue(rdataset, dbversion->version, + client->message); + if (result == ISC_R_SUCCESS) { + return; + } + } + +regular: + /* + * Add other additional data if needed. + * We don't care if dns_rdataset_additionaldata() fails. + */ + (void)dns_rdataset_additionaldata(rdataset, query_additional_cb, qctx); + CTRACE(ISC_LOG_DEBUG(3), "query_additional: done"); +} + +static void +query_addrrset(query_ctx_t *qctx, dns_name_t **namep, + dns_rdataset_t **rdatasetp, dns_rdataset_t **sigrdatasetp, + isc_buffer_t *dbuf, dns_section_t section) { + isc_result_t result; + ns_client_t *client = qctx->client; + dns_name_t *name = *namep, *mname = NULL; + dns_rdataset_t *rdataset = *rdatasetp, *mrdataset = NULL; + dns_rdataset_t *sigrdataset = NULL; + + CTRACE(ISC_LOG_DEBUG(3), "query_addrrset"); + + REQUIRE(name != NULL); + + if (sigrdatasetp != NULL) { + sigrdataset = *sigrdatasetp; + } + + /*% + * To the current response for 'client', add the answer RRset + * '*rdatasetp' and an optional signature set '*sigrdatasetp', with + * owner name '*namep', to section 'section', unless they are + * already there. Also add any pertinent additional data. + * + * If 'dbuf' is not NULL, then '*namep' is the name whose data is + * stored in 'dbuf'. In this case, query_addrrset() guarantees that + * when it returns the name will either have been kept or released. + */ + result = dns_message_findname(client->message, section, name, + rdataset->type, rdataset->covers, &mname, + &mrdataset); + if (result == ISC_R_SUCCESS) { + /* + * We've already got an RRset of the given name and type. + */ + CTRACE(ISC_LOG_DEBUG(3), "query_addrrset: dns_message_findname " + "succeeded: done"); + if (dbuf != NULL) { + ns_client_releasename(client, namep); + } + if ((rdataset->attributes & DNS_RDATASETATTR_REQUIRED) != 0) { + mrdataset->attributes |= DNS_RDATASETATTR_REQUIRED; + } + if ((rdataset->attributes & DNS_RDATASETATTR_STALE_ADDED) != 0) + { + mrdataset->attributes |= DNS_RDATASETATTR_STALE_ADDED; + } + return; + } else if (result == DNS_R_NXDOMAIN) { + /* + * The name doesn't exist. + */ + if (dbuf != NULL) { + ns_client_keepname(client, name, dbuf); + } + dns_message_addname(client->message, name, section); + *namep = NULL; + mname = name; + } else { + RUNTIME_CHECK(result == DNS_R_NXRRSET); + if (dbuf != NULL) { + ns_client_releasename(client, namep); + } + } + + if (rdataset->trust != dns_trust_secure && + (section == DNS_SECTION_ANSWER || section == DNS_SECTION_AUTHORITY)) + { + client->query.attributes &= ~NS_QUERYATTR_SECURE; + } + + /* + * Update message name, set rdataset order, and do additional + * section processing if needed. + */ + query_addtoname(mname, rdataset); + query_setorder(qctx, mname, rdataset); + query_additional(qctx, rdataset); + + /* + * Note: we only add SIGs if we've added the type they cover, so + * we do not need to check if the SIG rdataset is already in the + * response. + */ + *rdatasetp = NULL; + if (sigrdataset != NULL && dns_rdataset_isassociated(sigrdataset)) { + /* + * We have a signature. Add it to the response. + */ + ISC_LIST_APPEND(mname->list, sigrdataset, link); + *sigrdatasetp = NULL; + } + + CTRACE(ISC_LOG_DEBUG(3), "query_addrrset: done"); +} + +/* + * Mark the RRsets as secure. Update the cache (db) to reflect the + * change in trust level. + */ +static void +mark_secure(ns_client_t *client, dns_db_t *db, dns_name_t *name, + dns_rdata_rrsig_t *rrsig, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + isc_result_t result; + dns_dbnode_t *node = NULL; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + isc_stdtime_t now; + + rdataset->trust = dns_trust_secure; + sigrdataset->trust = dns_trust_secure; + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Save the updated secure state. Ignore failures. + */ + result = dns_db_findnodeext(db, name, true, &cm, &ci, &node); + if (result != ISC_R_SUCCESS) { + return; + } + + isc_stdtime_get(&now); + dns_rdataset_trimttl(rdataset, sigrdataset, rrsig, now, + client->view->acceptexpired); + + (void)dns_db_addrdataset(db, node, NULL, client->now, rdataset, 0, + NULL); + (void)dns_db_addrdataset(db, node, NULL, client->now, sigrdataset, 0, + NULL); + dns_db_detachnode(db, &node); +} + +/* + * Find the secure key that corresponds to rrsig. + * Note: 'keyrdataset' maintains state between successive calls, + * there may be multiple keys with the same keyid. + * Return false if we have exhausted all the possible keys. + */ +static bool +get_key(ns_client_t *client, dns_db_t *db, dns_rdata_rrsig_t *rrsig, + dns_rdataset_t *keyrdataset, dst_key_t **keyp) { + isc_result_t result; + dns_dbnode_t *node = NULL; + bool secure = false; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + if (!dns_rdataset_isassociated(keyrdataset)) { + result = dns_db_findnodeext(db, &rrsig->signer, false, &cm, &ci, + &node); + if (result != ISC_R_SUCCESS) { + return (false); + } + + result = dns_db_findrdataset(db, node, NULL, + dns_rdatatype_dnskey, 0, + client->now, keyrdataset, NULL); + dns_db_detachnode(db, &node); + if (result != ISC_R_SUCCESS) { + return (false); + } + + if (keyrdataset->trust != dns_trust_secure) { + return (false); + } + + result = dns_rdataset_first(keyrdataset); + } else { + result = dns_rdataset_next(keyrdataset); + } + + for (; result == ISC_R_SUCCESS; result = dns_rdataset_next(keyrdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + isc_buffer_t b; + + dns_rdataset_current(keyrdataset, &rdata); + isc_buffer_init(&b, rdata.data, rdata.length); + isc_buffer_add(&b, rdata.length); + result = dst_key_fromdns(&rrsig->signer, rdata.rdclass, &b, + client->mctx, keyp); + if (result != ISC_R_SUCCESS) { + continue; + } + if (rrsig->algorithm == (dns_secalg_t)dst_key_alg(*keyp) && + rrsig->keyid == (dns_keytag_t)dst_key_id(*keyp) && + dst_key_iszonekey(*keyp)) + { + secure = true; + break; + } + dst_key_free(keyp); + } + return (secure); +} + +static bool +verify(dst_key_t *key, dns_name_t *name, dns_rdataset_t *rdataset, + dns_rdata_t *rdata, ns_client_t *client) { + isc_result_t result; + dns_fixedname_t fixed; + bool ignore = false; + + dns_fixedname_init(&fixed); + +again: + result = dns_dnssec_verify(name, rdataset, key, ignore, + client->view->maxbits, client->mctx, rdata, + NULL); + if (result == DNS_R_SIGEXPIRED && client->view->acceptexpired) { + ignore = true; + goto again; + } + if (result == ISC_R_SUCCESS || result == DNS_R_FROMWILDCARD) { + return (true); + } + return (false); +} + +/* + * Validate the rdataset if possible with available records. + */ +static bool +validate(ns_client_t *client, dns_db_t *db, dns_name_t *name, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset) { + isc_result_t result; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_rrsig_t rrsig; + dst_key_t *key = NULL; + dns_rdataset_t keyrdataset; + + if (sigrdataset == NULL || !dns_rdataset_isassociated(sigrdataset)) { + return (false); + } + + for (result = dns_rdataset_first(sigrdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(sigrdataset)) + { + dns_rdata_reset(&rdata); + dns_rdataset_current(sigrdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &rrsig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (!dns_resolver_algorithm_supported(client->view->resolver, + name, rrsig.algorithm)) + { + continue; + } + if (!dns_name_issubdomain(name, &rrsig.signer)) { + continue; + } + dns_rdataset_init(&keyrdataset); + do { + if (!get_key(client, db, &rrsig, &keyrdataset, &key)) { + break; + } + if (verify(key, name, rdataset, &rdata, client)) { + dst_key_free(&key); + dns_rdataset_disassociate(&keyrdataset); + mark_secure(client, db, name, &rrsig, rdataset, + sigrdataset); + return (true); + } + dst_key_free(&key); + } while (1); + if (dns_rdataset_isassociated(&keyrdataset)) { + dns_rdataset_disassociate(&keyrdataset); + } + } + return (false); +} + +static void +fixrdataset(ns_client_t *client, dns_rdataset_t **rdataset) { + if (*rdataset == NULL) { + *rdataset = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(*rdataset)) { + dns_rdataset_disassociate(*rdataset); + } +} + +static void +fixfname(ns_client_t *client, dns_name_t **fname, isc_buffer_t **dbuf, + isc_buffer_t *nbuf) { + if (*fname == NULL) { + *dbuf = ns_client_getnamebuf(client); + if (*dbuf == NULL) { + return; + } + *fname = ns_client_newname(client, *dbuf, nbuf); + } +} + +static void +free_devent(ns_client_t *client, isc_event_t **eventp, + dns_fetchevent_t **deventp) { + dns_fetchevent_t *devent = *deventp; + + REQUIRE((void *)(*eventp) == (void *)(*deventp)); + + CTRACE(ISC_LOG_DEBUG(3), "free_devent"); + + if (devent->fetch != NULL) { + dns_resolver_destroyfetch(&devent->fetch); + } + if (devent->node != NULL) { + dns_db_detachnode(devent->db, &devent->node); + } + if (devent->db != NULL) { + dns_db_detach(&devent->db); + } + if (devent->rdataset != NULL) { + ns_client_putrdataset(client, &devent->rdataset); + } + if (devent->sigrdataset != NULL) { + ns_client_putrdataset(client, &devent->sigrdataset); + } + + /* + * If the two pointers are the same then leave the setting of + * (*deventp) to NULL to isc_event_free. + */ + if ((void *)eventp != (void *)deventp) { + (*deventp) = NULL; + } + isc_event_free(eventp); +} + +static void +prefetch_done(isc_task_t *task, isc_event_t *event) { + dns_fetchevent_t *devent = (dns_fetchevent_t *)event; + ns_client_t *client; + + UNUSED(task); + + REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE); + client = devent->ev_arg; + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(task == client->task); + + CTRACE(ISC_LOG_DEBUG(3), "prefetch_done"); + + LOCK(&client->query.fetchlock); + if (client->query.prefetch != NULL) { + INSIST(devent->fetch == client->query.prefetch); + client->query.prefetch = NULL; + } + UNLOCK(&client->query.fetchlock); + + /* + * We're done prefetching, detach from quota. + */ + if (client->recursionquota != NULL) { + isc_quota_detach(&client->recursionquota); + ns_stats_decrement(client->sctx->nsstats, + ns_statscounter_recursclients); + } + + free_devent(client, &event, &devent); + isc_nmhandle_detach(&client->prefetchhandle); +} + +static void +query_prefetch(ns_client_t *client, dns_name_t *qname, + dns_rdataset_t *rdataset) { + isc_result_t result; + isc_sockaddr_t *peeraddr; + dns_rdataset_t *tmprdataset; + unsigned int options; + + CTRACE(ISC_LOG_DEBUG(3), "query_prefetch"); + + if (client->query.prefetch != NULL || + client->view->prefetch_trigger == 0U || + rdataset->ttl > client->view->prefetch_trigger || + (rdataset->attributes & DNS_RDATASETATTR_PREFETCH) == 0) + { + return; + } + + if (client->recursionquota == NULL) { + result = isc_quota_attach(&client->sctx->recursionquota, + &client->recursionquota); + switch (result) { + case ISC_R_SUCCESS: + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_recursclients); + break; + case ISC_R_SOFTQUOTA: + isc_quota_detach(&client->recursionquota); + FALLTHROUGH; + default: + return; + } + } + + tmprdataset = ns_client_newrdataset(client); + if (tmprdataset == NULL) { + return; + } + + if (!TCP(client)) { + peeraddr = &client->peeraddr; + } else { + peeraddr = NULL; + } + + isc_nmhandle_attach(client->handle, &client->prefetchhandle); + options = client->query.fetchoptions | DNS_FETCHOPT_PREFETCH; + result = dns_resolver_createfetch( + client->view->resolver, qname, rdataset->type, NULL, NULL, NULL, + peeraddr, client->message->id, options, 0, NULL, client->task, + prefetch_done, client, tmprdataset, NULL, + &client->query.prefetch); + if (result != ISC_R_SUCCESS) { + ns_client_putrdataset(client, &tmprdataset); + isc_nmhandle_detach(&client->prefetchhandle); + } + + dns_rdataset_clearprefetch(rdataset); + ns_stats_increment(client->sctx->nsstats, ns_statscounter_prefetch); +} + +static void +rpz_clean(dns_zone_t **zonep, dns_db_t **dbp, dns_dbnode_t **nodep, + dns_rdataset_t **rdatasetp) { + if (nodep != NULL && *nodep != NULL) { + REQUIRE(dbp != NULL && *dbp != NULL); + dns_db_detachnode(*dbp, nodep); + } + if (dbp != NULL && *dbp != NULL) { + dns_db_detach(dbp); + } + if (zonep != NULL && *zonep != NULL) { + dns_zone_detach(zonep); + } + if (rdatasetp != NULL && *rdatasetp != NULL && + dns_rdataset_isassociated(*rdatasetp)) + { + dns_rdataset_disassociate(*rdatasetp); + } +} + +static void +rpz_match_clear(dns_rpz_st_t *st) { + rpz_clean(&st->m.zone, &st->m.db, &st->m.node, &st->m.rdataset); + st->m.version = NULL; +} + +static isc_result_t +rpz_ready(ns_client_t *client, dns_rdataset_t **rdatasetp) { + REQUIRE(rdatasetp != NULL); + + CTRACE(ISC_LOG_DEBUG(3), "rpz_ready"); + + if (*rdatasetp == NULL) { + *rdatasetp = ns_client_newrdataset(client); + if (*rdatasetp == NULL) { + CTRACE(ISC_LOG_ERROR, "rpz_ready: " + "ns_client_newrdataset failed"); + return (DNS_R_SERVFAIL); + } + } else if (dns_rdataset_isassociated(*rdatasetp)) { + dns_rdataset_disassociate(*rdatasetp); + } + return (ISC_R_SUCCESS); +} + +static void +rpz_st_clear(ns_client_t *client) { + dns_rpz_st_t *st = client->query.rpz_st; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_st_clear"); + + if (st->m.rdataset != NULL) { + ns_client_putrdataset(client, &st->m.rdataset); + } + rpz_match_clear(st); + + rpz_clean(NULL, &st->r.db, NULL, NULL); + if (st->r.ns_rdataset != NULL) { + ns_client_putrdataset(client, &st->r.ns_rdataset); + } + if (st->r.r_rdataset != NULL) { + ns_client_putrdataset(client, &st->r.r_rdataset); + } + + rpz_clean(&st->q.zone, &st->q.db, &st->q.node, NULL); + if (st->q.rdataset != NULL) { + ns_client_putrdataset(client, &st->q.rdataset); + } + if (st->q.sigrdataset != NULL) { + ns_client_putrdataset(client, &st->q.sigrdataset); + } + st->state = 0; + st->m.type = DNS_RPZ_TYPE_BAD; + st->m.policy = DNS_RPZ_POLICY_MISS; + if (st->rpsdb != NULL) { + dns_db_detach(&st->rpsdb); + } +} + +static dns_rpz_zbits_t +rpz_get_zbits(ns_client_t *client, dns_rdatatype_t ip_type, + dns_rpz_type_t rpz_type) { + dns_rpz_st_t *st; + dns_rpz_zbits_t zbits = 0; + + REQUIRE(client != NULL); + REQUIRE(client->query.rpz_st != NULL); + + st = client->query.rpz_st; + +#ifdef USE_DNSRPS + if (st->popt.dnsrps_enabled) { + if (st->rpsdb == NULL || + librpz->have_trig(dns_dnsrps_type2trig(rpz_type), + ip_type == dns_rdatatype_aaaa, + ((rpsdb_t *)st->rpsdb)->rsp)) + { + return (DNS_RPZ_ALL_ZBITS); + } + return (0); + } +#endif /* ifdef USE_DNSRPS */ + + switch (rpz_type) { + case DNS_RPZ_TYPE_CLIENT_IP: + zbits = st->have.client_ip; + break; + case DNS_RPZ_TYPE_QNAME: + zbits = st->have.qname; + break; + case DNS_RPZ_TYPE_IP: + if (ip_type == dns_rdatatype_a) { + zbits = st->have.ipv4; + } else if (ip_type == dns_rdatatype_aaaa) { + zbits = st->have.ipv6; + } else { + zbits = st->have.ip; + } + break; + case DNS_RPZ_TYPE_NSDNAME: + zbits = st->have.nsdname; + break; + case DNS_RPZ_TYPE_NSIP: + if (ip_type == dns_rdatatype_a) { + zbits = st->have.nsipv4; + } else if (ip_type == dns_rdatatype_aaaa) { + zbits = st->have.nsipv6; + } else { + zbits = st->have.nsip; + } + break; + default: + UNREACHABLE(); + } + + /* + * Choose + * the earliest configured policy zone (rpz->num) + * QNAME over IP over NSDNAME over NSIP (rpz_type) + * the smallest name, + * the longest IP address prefix, + * the lexically smallest address. + */ + if (st->m.policy != DNS_RPZ_POLICY_MISS) { + if (st->m.type >= rpz_type) { + zbits &= DNS_RPZ_ZMASK(st->m.rpz->num); + } else { + zbits &= DNS_RPZ_ZMASK(st->m.rpz->num) >> 1; + } + } + + /* + * If the client wants recursion, allow only compatible policies. + */ + if (!RECURSIONOK(client)) { + zbits &= st->popt.no_rd_ok; + } + + return (zbits); +} + +static void +query_rpzfetch(ns_client_t *client, dns_name_t *qname, dns_rdatatype_t type) { + isc_result_t result; + isc_sockaddr_t *peeraddr; + dns_rdataset_t *tmprdataset; + unsigned int options; + + CTRACE(ISC_LOG_DEBUG(3), "query_rpzfetch"); + + if (client->query.prefetch != NULL) { + return; + } + + if (client->recursionquota == NULL) { + result = isc_quota_attach(&client->sctx->recursionquota, + &client->recursionquota); + switch (result) { + case ISC_R_SUCCESS: + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_recursclients); + break; + case ISC_R_SOFTQUOTA: + isc_quota_detach(&client->recursionquota); + FALLTHROUGH; + default: + return; + } + } + + tmprdataset = ns_client_newrdataset(client); + if (tmprdataset == NULL) { + return; + } + + if (!TCP(client)) { + peeraddr = &client->peeraddr; + } else { + peeraddr = NULL; + } + + options = client->query.fetchoptions; + isc_nmhandle_attach(client->handle, &client->prefetchhandle); + result = dns_resolver_createfetch( + client->view->resolver, qname, type, NULL, NULL, NULL, peeraddr, + client->message->id, options, 0, NULL, client->task, + prefetch_done, client, tmprdataset, NULL, + &client->query.prefetch); + if (result != ISC_R_SUCCESS) { + ns_client_putrdataset(client, &tmprdataset); + isc_nmhandle_detach(&client->prefetchhandle); + } +} + +/* + * Get an NS, A, or AAAA rrset related to the response for the client + * to check the contents of that rrset for hits by eligible policy zones. + */ +static isc_result_t +rpz_rrset_find(ns_client_t *client, dns_name_t *name, dns_rdatatype_t type, + unsigned int options, dns_rpz_type_t rpz_type, dns_db_t **dbp, + dns_dbversion_t *version, dns_rdataset_t **rdatasetp, + bool resuming) { + dns_rpz_st_t *st; + bool is_zone; + dns_dbnode_t *node; + dns_fixedname_t fixed; + dns_name_t *found; + isc_result_t result; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rrset_find"); + + st = client->query.rpz_st; + if ((st->state & DNS_RPZ_RECURSING) != 0) { + INSIST(st->r.r_type == type); + INSIST(dns_name_equal(name, st->r_name)); + INSIST(*rdatasetp == NULL || + !dns_rdataset_isassociated(*rdatasetp)); + st->state &= ~DNS_RPZ_RECURSING; + RESTORE(*dbp, st->r.db); + if (*rdatasetp != NULL) { + ns_client_putrdataset(client, rdatasetp); + } + RESTORE(*rdatasetp, st->r.r_rdataset); + result = st->r.r_result; + if (result == DNS_R_DELEGATION) { + CTRACE(ISC_LOG_ERROR, "RPZ recursing"); + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name, + rpz_type, "rpz_rrset_find(1)", result); + st->m.policy = DNS_RPZ_POLICY_ERROR; + result = DNS_R_SERVFAIL; + } + return (result); + } + + result = rpz_ready(client, rdatasetp); + if (result != ISC_R_SUCCESS) { + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (result); + } + if (*dbp != NULL) { + is_zone = false; + } else { + dns_zone_t *zone; + + version = NULL; + zone = NULL; + result = query_getdb(client, name, type, 0, &zone, dbp, + &version, &is_zone); + if (result != ISC_R_SUCCESS) { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name, + rpz_type, "rpz_rrset_find(2)", result); + st->m.policy = DNS_RPZ_POLICY_ERROR; + if (zone != NULL) { + dns_zone_detach(&zone); + } + return (result); + } + if (zone != NULL) { + dns_zone_detach(&zone); + } + } + + node = NULL; + found = dns_fixedname_initname(&fixed); + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + result = dns_db_findext(*dbp, name, version, type, options, client->now, + &node, found, &cm, &ci, *rdatasetp, NULL); + if (result == DNS_R_DELEGATION && is_zone && USECACHE(client)) { + /* + * Try the cache if we're authoritative for an + * ancestor but not the domain itself. + */ + rpz_clean(NULL, dbp, &node, rdatasetp); + version = NULL; + dns_db_attach(client->view->cachedb, dbp); + result = dns_db_findext(*dbp, name, version, type, 0, + client->now, &node, found, &cm, &ci, + *rdatasetp, NULL); + } + rpz_clean(NULL, dbp, &node, NULL); + if (result == DNS_R_DELEGATION) { + rpz_clean(NULL, NULL, NULL, rdatasetp); + /* + * Recurse for NS rrset or A or AAAA rrset for an NS. + * Do not recurse for addresses for the query name. + */ + if (rpz_type == DNS_RPZ_TYPE_IP) { + result = DNS_R_NXRRSET; + } else if (!client->view->rpzs->p.nsip_wait_recurse) { + query_rpzfetch(client, name, type); + result = DNS_R_NXRRSET; + } else { + dns_name_copynf(name, st->r_name); + result = ns_query_recurse(client, type, st->r_name, + NULL, NULL, resuming); + if (result == ISC_R_SUCCESS) { + st->state |= DNS_RPZ_RECURSING; + result = DNS_R_DELEGATION; + } + } + } + return (result); +} + +/* + * Compute a policy owner name, p_name, in a policy zone given the needed + * policy type and the trigger name. + */ +static isc_result_t +rpz_get_p_name(ns_client_t *client, dns_name_t *p_name, dns_rpz_zone_t *rpz, + dns_rpz_type_t rpz_type, dns_name_t *trig_name) { + dns_offsets_t prefix_offsets; + dns_name_t prefix, *suffix; + unsigned int first, labels; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_get_p_name"); + + /* + * The policy owner name consists of a suffix depending on the type + * and policy zone and a prefix that is the longest possible string + * from the trigger name that keesp the resulting policy owner name + * from being too long. + */ + switch (rpz_type) { + case DNS_RPZ_TYPE_CLIENT_IP: + suffix = &rpz->client_ip; + break; + case DNS_RPZ_TYPE_QNAME: + suffix = &rpz->origin; + break; + case DNS_RPZ_TYPE_IP: + suffix = &rpz->ip; + break; + case DNS_RPZ_TYPE_NSDNAME: + suffix = &rpz->nsdname; + break; + case DNS_RPZ_TYPE_NSIP: + suffix = &rpz->nsip; + break; + default: + UNREACHABLE(); + } + + /* + * Start with relative version of the full trigger name, + * and trim enough allow the addition of the suffix. + */ + dns_name_init(&prefix, prefix_offsets); + labels = dns_name_countlabels(trig_name); + first = 0; + for (;;) { + dns_name_getlabelsequence(trig_name, first, labels - first - 1, + &prefix); + result = dns_name_concatenate(&prefix, suffix, p_name, NULL); + if (result == ISC_R_SUCCESS) { + break; + } + INSIST(result == DNS_R_NAMETOOLONG); + /* + * Trim the trigger name until the combination is not too long. + */ + if (labels - first < 2) { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, suffix, + rpz_type, "concatenate()", result); + return (ISC_R_FAILURE); + } + /* + * Complain once about trimming the trigger name. + */ + if (first == 0) { + rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, suffix, + rpz_type, "concatenate()", result); + } + ++first; + } + return (ISC_R_SUCCESS); +} + +/* + * Look in policy zone rpz for a policy of rpz_type by p_name. + * The self-name (usually the client qname or an NS name) is compared with + * the target of a CNAME policy for the old style passthru encoding. + * If found, the policy is recorded in *zonep, *dbp, *versionp, *nodep, + * *rdatasetp, and *policyp. + * The target DNS type, qtype, chooses the best rdataset for *rdatasetp. + * The caller must decide if the found policy is most suitable, including + * better than a previously found policy. + * If it is best, the caller records it in client->query.rpz_st->m. + */ +static isc_result_t +rpz_find_p(ns_client_t *client, dns_name_t *self_name, dns_rdatatype_t qtype, + dns_name_t *p_name, dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type, + dns_zone_t **zonep, dns_db_t **dbp, dns_dbversion_t **versionp, + dns_dbnode_t **nodep, dns_rdataset_t **rdatasetp, + dns_rpz_policy_t *policyp) { + dns_fixedname_t foundf; + dns_name_t *found; + isc_result_t result; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + bool found_a = false; + + REQUIRE(nodep != NULL); + + CTRACE(ISC_LOG_DEBUG(3), "rpz_find_p"); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Try to find either a CNAME or the type of record demanded by the + * request from the policy zone. + */ + rpz_clean(zonep, dbp, nodep, rdatasetp); + result = rpz_ready(client, rdatasetp); + if (result != ISC_R_SUCCESS) { + CTRACE(ISC_LOG_ERROR, "rpz_ready() failed"); + return (DNS_R_SERVFAIL); + } + *versionp = NULL; + result = rpz_getdb(client, p_name, rpz_type, zonep, dbp, versionp); + if (result != ISC_R_SUCCESS) { + return (DNS_R_NXDOMAIN); + } + found = dns_fixedname_initname(&foundf); + + result = dns_db_findext(*dbp, p_name, *versionp, dns_rdatatype_any, 0, + client->now, nodep, found, &cm, &ci, *rdatasetp, + NULL); + /* + * Choose the best rdataset if we found something. + */ + if (result == ISC_R_SUCCESS) { + dns_rdatasetiter_t *rdsiter; + + rdsiter = NULL; + result = dns_db_allrdatasets(*dbp, *nodep, *versionp, 0, 0, + &rdsiter); + if (result != ISC_R_SUCCESS) { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name, + rpz_type, "allrdatasets()", result); + CTRACE(ISC_LOG_ERROR, + "rpz_find_p: allrdatasets failed"); + return (DNS_R_SERVFAIL); + } + if (qtype == dns_rdatatype_aaaa && + !ISC_LIST_EMPTY(client->view->dns64)) + { + for (result = dns_rdatasetiter_first(rdsiter); + result == ISC_R_SUCCESS; + result = dns_rdatasetiter_next(rdsiter)) + { + dns_rdatasetiter_current(rdsiter, *rdatasetp); + if ((*rdatasetp)->type == dns_rdatatype_a) { + found_a = true; + } + dns_rdataset_disassociate(*rdatasetp); + } + } + for (result = dns_rdatasetiter_first(rdsiter); + result == ISC_R_SUCCESS; + result = dns_rdatasetiter_next(rdsiter)) + { + dns_rdatasetiter_current(rdsiter, *rdatasetp); + if ((*rdatasetp)->type == dns_rdatatype_cname || + (*rdatasetp)->type == qtype) + { + break; + } + dns_rdataset_disassociate(*rdatasetp); + } + dns_rdatasetiter_destroy(&rdsiter); + if (result != ISC_R_SUCCESS) { + if (result != ISC_R_NOMORE) { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, + p_name, rpz_type, "rdatasetiter", + result); + CTRACE(ISC_LOG_ERROR, "rpz_find_p: " + "rdatasetiter failed"); + return (DNS_R_SERVFAIL); + } + /* + * Ask again to get the right DNS_R_DNAME/NXRRSET/... + * result if there is neither a CNAME nor target type. + */ + if (dns_rdataset_isassociated(*rdatasetp)) { + dns_rdataset_disassociate(*rdatasetp); + } + dns_db_detachnode(*dbp, nodep); + + if (qtype == dns_rdatatype_rrsig || + qtype == dns_rdatatype_sig) + { + result = DNS_R_NXRRSET; + } else { + result = dns_db_findext(*dbp, p_name, *versionp, + qtype, 0, client->now, + nodep, found, &cm, &ci, + *rdatasetp, NULL); + } + } + } + switch (result) { + case ISC_R_SUCCESS: + if ((*rdatasetp)->type != dns_rdatatype_cname) { + *policyp = DNS_RPZ_POLICY_RECORD; + } else { + *policyp = dns_rpz_decode_cname(rpz, *rdatasetp, + self_name); + if ((*policyp == DNS_RPZ_POLICY_RECORD || + *policyp == DNS_RPZ_POLICY_WILDCNAME) && + qtype != dns_rdatatype_cname && + qtype != dns_rdatatype_any) + { + return (DNS_R_CNAME); + } + } + return (ISC_R_SUCCESS); + case DNS_R_NXRRSET: + if (found_a) { + *policyp = DNS_RPZ_POLICY_DNS64; + } else { + *policyp = DNS_RPZ_POLICY_NODATA; + } + return (result); + case DNS_R_DNAME: + /* + * DNAME policy RRs have very few if any uses that are not + * better served with simple wildcards. Making them work would + * require complications to get the number of labels matched + * in the name or the found name to the main DNS_R_DNAME case + * in query_dname(). The domain also does not appear in the + * summary database at the right level, so this happens only + * with a single policy zone when we have no summary database. + * Treat it as a miss. + */ + case DNS_R_NXDOMAIN: + case DNS_R_EMPTYNAME: + return (DNS_R_NXDOMAIN); + default: + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name, rpz_type, "", + result); + CTRACE(ISC_LOG_ERROR, "rpz_find_p: unexpected result"); + return (DNS_R_SERVFAIL); + } +} + +static void +rpz_save_p(dns_rpz_st_t *st, dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type, + dns_rpz_policy_t policy, dns_name_t *p_name, dns_rpz_prefix_t prefix, + isc_result_t result, dns_zone_t **zonep, dns_db_t **dbp, + dns_dbnode_t **nodep, dns_rdataset_t **rdatasetp, + dns_dbversion_t *version) { + dns_rdataset_t *trdataset = NULL; + + rpz_match_clear(st); + st->m.rpz = rpz; + st->m.type = rpz_type; + st->m.policy = policy; + dns_name_copynf(p_name, st->p_name); + st->m.prefix = prefix; + st->m.result = result; + SAVE(st->m.zone, *zonep); + SAVE(st->m.db, *dbp); + SAVE(st->m.node, *nodep); + if (*rdatasetp != NULL && dns_rdataset_isassociated(*rdatasetp)) { + /* + * Save the replacement rdataset from the policy + * and make the previous replacement rdataset scratch. + */ + SAVE(trdataset, st->m.rdataset); + SAVE(st->m.rdataset, *rdatasetp); + SAVE(*rdatasetp, trdataset); + st->m.ttl = ISC_MIN(st->m.rdataset->ttl, rpz->max_policy_ttl); + } else { + st->m.ttl = ISC_MIN(DNS_RPZ_TTL_DEFAULT, rpz->max_policy_ttl); + } + SAVE(st->m.version, version); +} + +#ifdef USE_DNSRPS +/* + * Check the results of a RPZ service interface lookup. + * Stop after an error (<0) or not a hit on a disabled zone (0). + * Continue after a hit on a disabled zone (>0). + */ +static int +dnsrps_ck(librpz_emsg_t *emsg, ns_client_t *client, rpsdb_t *rpsdb, + bool recursed) { + isc_region_t region; + librpz_domain_buf_t pname_buf; + + if (!librpz->rsp_result(emsg, &rpsdb->result, recursed, rpsdb->rsp)) { + return (-1); + } + + /* + * Forget the state from before the IP address or domain check + * if the lookup hit nothing. + */ + if (rpsdb->result.policy == LIBRPZ_POLICY_UNDEFINED || + rpsdb->result.hit_id != rpsdb->hit_id || + rpsdb->result.policy != LIBRPZ_POLICY_DISABLED) + { + if (!librpz->rsp_pop_discard(emsg, rpsdb->rsp)) { + return (-1); + } + return (0); + } + + /* + * Log a hit on a disabled zone. + * Forget the zone to not try it again, and restore the pre-hit state. + */ + if (!librpz->rsp_domain(emsg, &pname_buf, rpsdb->rsp)) { + return (-1); + } + region.base = pname_buf.d; + region.length = pname_buf.size; + dns_name_fromregion(client->query.rpz_st->p_name, ®ion); + rpz_log_rewrite(client, true, dns_dnsrps_2policy(rpsdb->result.zpolicy), + dns_dnsrps_trig2type(rpsdb->result.trig), NULL, + client->query.rpz_st->p_name, NULL, + rpsdb->result.cznum); + + if (!librpz->rsp_forget_zone(emsg, rpsdb->result.cznum, rpsdb->rsp) || + !librpz->rsp_pop(emsg, &rpsdb->result, rpsdb->rsp)) + { + return (-1); + } + return (1); +} + +/* + * Ready the shim database and rdataset for a DNSRPS hit. + */ +static bool +dnsrps_set_p(librpz_emsg_t *emsg, ns_client_t *client, dns_rpz_st_t *st, + dns_rdatatype_t qtype, dns_rdataset_t **p_rdatasetp, + bool recursed) { + rpsdb_t *rpsdb; + librpz_domain_buf_t pname_buf; + isc_region_t region; + dns_zone_t *p_zone; + dns_db_t *p_db; + dns_dbnode_t *p_node; + dns_rpz_policy_t policy; + dns_fixedname_t foundf; + dns_name_t *found; + dns_rdatatype_t foundtype, searchtype; + isc_result_t result; + + rpsdb = (rpsdb_t *)st->rpsdb; + + if (!librpz->rsp_result(emsg, &rpsdb->result, recursed, rpsdb->rsp)) { + return (false); + } + + if (rpsdb->result.policy == LIBRPZ_POLICY_UNDEFINED) { + return (true); + } + + /* + * Give the fake or shim DNSRPS database its new origin. + */ + if (!librpz->rsp_soa(emsg, NULL, NULL, &rpsdb->origin_buf, + &rpsdb->result, rpsdb->rsp)) + { + return (false); + } + region.base = rpsdb->origin_buf.d; + region.length = rpsdb->origin_buf.size; + dns_name_fromregion(&rpsdb->common.origin, ®ion); + + if (!librpz->rsp_domain(emsg, &pname_buf, rpsdb->rsp)) { + return (false); + } + region.base = pname_buf.d; + region.length = pname_buf.size; + dns_name_fromregion(st->p_name, ®ion); + + p_zone = NULL; + p_db = NULL; + p_node = NULL; + rpz_ready(client, p_rdatasetp); + dns_db_attach(st->rpsdb, &p_db); + policy = dns_dnsrps_2policy(rpsdb->result.policy); + if (policy != DNS_RPZ_POLICY_RECORD) { + result = ISC_R_SUCCESS; + } else if (qtype == dns_rdatatype_rrsig) { + /* + * dns_find_db() refuses to look for and fail to + * find dns_rdatatype_rrsig. + */ + result = DNS_R_NXRRSET; + policy = DNS_RPZ_POLICY_NODATA; + } else { + /* + * Get the next (and so first) RR from the policy node. + * If it is a CNAME, then look for it regardless of the + * query type. + */ + if (!librpz->rsp_rr(emsg, &foundtype, NULL, NULL, NULL, + &rpsdb->result, rpsdb->qname->ndata, + rpsdb->qname->length, rpsdb->rsp)) + { + return (false); + } + if (foundtype == dns_rdatatype_cname) { + searchtype = dns_rdatatype_cname; + } else { + searchtype = qtype; + } + /* + * Get the DNSPRS imitation rdataset. + */ + found = dns_fixedname_initname(&foundf); + result = dns_db_find(p_db, st->p_name, NULL, searchtype, 0, 0, + &p_node, found, *p_rdatasetp, NULL); + + if (result == ISC_R_SUCCESS) { + if (searchtype == dns_rdatatype_cname && + qtype != dns_rdatatype_cname) + { + result = DNS_R_CNAME; + } + } else if (result == DNS_R_NXRRSET) { + policy = DNS_RPZ_POLICY_NODATA; + } else { + snprintf(emsg->c, sizeof(emsg->c), "dns_db_find(): %s", + isc_result_totext(result)); + return (false); + } + } + + rpz_save_p(st, client->view->rpzs->zones[rpsdb->result.cznum], + dns_dnsrps_trig2type(rpsdb->result.trig), policy, st->p_name, + 0, result, &p_zone, &p_db, &p_node, p_rdatasetp, NULL); + + rpz_clean(NULL, NULL, NULL, p_rdatasetp); + + return (true); +} + +static isc_result_t +dnsrps_rewrite_ip(ns_client_t *client, const isc_netaddr_t *netaddr, + dns_rpz_type_t rpz_type, dns_rdataset_t **p_rdatasetp) { + dns_rpz_st_t *st; + rpsdb_t *rpsdb; + librpz_trig_t trig = LIBRPZ_TRIG_CLIENT_IP; + bool recursed = false; + int res; + librpz_emsg_t emsg; + isc_result_t result; + + st = client->query.rpz_st; + rpsdb = (rpsdb_t *)st->rpsdb; + + result = rpz_ready(client, p_rdatasetp); + if (result != ISC_R_SUCCESS) { + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (result); + } + + switch (rpz_type) { + case DNS_RPZ_TYPE_CLIENT_IP: + trig = LIBRPZ_TRIG_CLIENT_IP; + recursed = false; + break; + case DNS_RPZ_TYPE_IP: + trig = LIBRPZ_TRIG_IP; + recursed = true; + break; + case DNS_RPZ_TYPE_NSIP: + trig = LIBRPZ_TRIG_NSIP; + recursed = true; + break; + default: + UNREACHABLE(); + } + + do { + if (!librpz->rsp_push(&emsg, rpsdb->rsp) || + !librpz->ck_ip(&emsg, + netaddr->family == AF_INET + ? (const void *)&netaddr->type.in + : (const void *)&netaddr->type.in6, + netaddr->family, trig, ++rpsdb->hit_id, + recursed, rpsdb->rsp) || + (res = dnsrps_ck(&emsg, client, rpsdb, recursed)) < 0) + { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, NULL, + rpz_type, emsg.c, DNS_R_SERVFAIL); + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (DNS_R_SERVFAIL); + } + } while (res != 0); + return (ISC_R_SUCCESS); +} + +static isc_result_t +dnsrps_rewrite_name(ns_client_t *client, dns_name_t *trig_name, bool recursed, + dns_rpz_type_t rpz_type, dns_rdataset_t **p_rdatasetp) { + dns_rpz_st_t *st; + rpsdb_t *rpsdb; + librpz_trig_t trig = LIBRPZ_TRIG_CLIENT_IP; + isc_region_t r; + int res; + librpz_emsg_t emsg; + isc_result_t result; + + st = client->query.rpz_st; + rpsdb = (rpsdb_t *)st->rpsdb; + + result = rpz_ready(client, p_rdatasetp); + if (result != ISC_R_SUCCESS) { + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (result); + } + + switch (rpz_type) { + case DNS_RPZ_TYPE_QNAME: + trig = LIBRPZ_TRIG_QNAME; + break; + case DNS_RPZ_TYPE_NSDNAME: + trig = LIBRPZ_TRIG_NSDNAME; + break; + default: + UNREACHABLE(); + } + + dns_name_toregion(trig_name, &r); + do { + if (!librpz->rsp_push(&emsg, rpsdb->rsp) || + !librpz->ck_domain(&emsg, r.base, r.length, trig, + ++rpsdb->hit_id, recursed, rpsdb->rsp) || + (res = dnsrps_ck(&emsg, client, rpsdb, recursed)) < 0) + { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, NULL, + rpz_type, emsg.c, DNS_R_SERVFAIL); + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (DNS_R_SERVFAIL); + } + } while (res != 0); + return (ISC_R_SUCCESS); +} +#endif /* USE_DNSRPS */ + +/* + * Check this address in every eligible policy zone. + */ +static isc_result_t +rpz_rewrite_ip(ns_client_t *client, const isc_netaddr_t *netaddr, + dns_rdatatype_t qtype, dns_rpz_type_t rpz_type, + dns_rpz_zbits_t zbits, dns_rdataset_t **p_rdatasetp) { + dns_rpz_zones_t *rpzs; + dns_rpz_st_t *st; + dns_rpz_zone_t *rpz; + dns_rpz_prefix_t prefix; + dns_rpz_num_t rpz_num; + dns_fixedname_t ip_namef, p_namef; + dns_name_t *ip_name, *p_name; + dns_zone_t *p_zone; + dns_db_t *p_db; + dns_dbversion_t *p_version; + dns_dbnode_t *p_node; + dns_rpz_policy_t policy; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip"); + + rpzs = client->view->rpzs; + st = client->query.rpz_st; +#ifdef USE_DNSRPS + if (st->popt.dnsrps_enabled) { + return (dnsrps_rewrite_ip(client, netaddr, rpz_type, + p_rdatasetp)); + } +#endif /* ifdef USE_DNSRPS */ + + ip_name = dns_fixedname_initname(&ip_namef); + + p_zone = NULL; + p_db = NULL; + p_node = NULL; + + while (zbits != 0) { + rpz_num = dns_rpz_find_ip(rpzs, rpz_type, zbits, netaddr, + ip_name, &prefix); + if (rpz_num == DNS_RPZ_INVALID_NUM) { + break; + } + zbits &= (DNS_RPZ_ZMASK(rpz_num) >> 1); + + /* + * Do not try applying policy zones that cannot replace a + * previously found policy zone. + * Stop looking if the next best choice cannot + * replace what we already have. + */ + rpz = rpzs->zones[rpz_num]; + if (st->m.policy != DNS_RPZ_POLICY_MISS) { + if (st->m.rpz->num < rpz->num) { + break; + } + if (st->m.rpz->num == rpz->num && + (st->m.type < rpz_type || st->m.prefix > prefix)) + { + break; + } + } + + /* + * Get the policy for a prefix at least as long + * as the prefix of the entry we had before. + */ + p_name = dns_fixedname_initname(&p_namef); + result = rpz_get_p_name(client, p_name, rpz, rpz_type, ip_name); + if (result != ISC_R_SUCCESS) { + continue; + } + result = rpz_find_p(client, ip_name, qtype, p_name, rpz, + rpz_type, &p_zone, &p_db, &p_version, + &p_node, p_rdatasetp, &policy); + switch (result) { + case DNS_R_NXDOMAIN: + /* + * Continue after a policy record that is missing + * contrary to the summary data. The summary + * data can out of date during races with and among + * policy zone updates. + */ + CTRACE(ISC_LOG_ERROR, "rpz_rewrite_ip: mismatched " + "summary data; " + "continuing"); + continue; + case DNS_R_SERVFAIL: + rpz_clean(&p_zone, &p_db, &p_node, p_rdatasetp); + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (DNS_R_SERVFAIL); + default: + /* + * Forget this policy if it is not preferable + * to the previously found policy. + * If this policy is not good, then stop looking + * because none of the later policy zones would work. + * + * With more than one applicable policy, prefer + * the earliest configured policy, + * client-IP over QNAME over IP over NSDNAME over NSIP, + * the longest prefix + * the lexically smallest address. + * dns_rpz_find_ip() ensures st->m.rpz->num >= rpz->num. + * We can compare new and current p_name because + * both are of the same type and in the same zone. + * The tests above eliminate other reasons to + * reject this policy. If this policy can't work, + * then neither can later zones. + */ + if (st->m.policy != DNS_RPZ_POLICY_MISS && + rpz->num == st->m.rpz->num && + (st->m.type == rpz_type && st->m.prefix == prefix && + 0 > dns_name_rdatacompare(st->p_name, p_name))) + { + break; + } + + /* + * Stop checking after saving an enabled hit in this + * policy zone. The radix tree in the policy zone + * ensures that we found the longest match. + */ + if (rpz->policy != DNS_RPZ_POLICY_DISABLED) { + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip: " + "rpz_save_p"); + rpz_save_p(st, rpz, rpz_type, policy, p_name, + prefix, result, &p_zone, &p_db, + &p_node, p_rdatasetp, p_version); + break; + } + + /* + * Log DNS_RPZ_POLICY_DISABLED zones + * and try the next eligible policy zone. + */ + rpz_log_rewrite(client, true, policy, rpz_type, p_zone, + p_name, NULL, rpz_num); + } + } + + rpz_clean(&p_zone, &p_db, &p_node, p_rdatasetp); + return (ISC_R_SUCCESS); +} + +/* + * Check the IP addresses in the A or AAAA rrsets for name against + * all eligible rpz_type (IP or NSIP) response policy rewrite rules. + */ +static isc_result_t +rpz_rewrite_ip_rrset(ns_client_t *client, dns_name_t *name, + dns_rdatatype_t qtype, dns_rpz_type_t rpz_type, + dns_rdatatype_t ip_type, dns_db_t **ip_dbp, + dns_dbversion_t *ip_version, dns_rdataset_t **ip_rdatasetp, + dns_rdataset_t **p_rdatasetp, bool resuming) { + dns_rpz_zbits_t zbits; + isc_netaddr_t netaddr; + struct in_addr ina; + struct in6_addr in6a; + isc_result_t result; + unsigned int options = client->query.dboptions | DNS_DBFIND_GLUEOK; + bool done = false; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip_rrset"); + + do { + zbits = rpz_get_zbits(client, ip_type, rpz_type); + if (zbits == 0) { + return (ISC_R_SUCCESS); + } + + /* + * Get the A or AAAA rdataset. + */ + result = rpz_rrset_find(client, name, ip_type, options, + rpz_type, ip_dbp, ip_version, + ip_rdatasetp, resuming); + switch (result) { + case ISC_R_SUCCESS: + case DNS_R_GLUE: + case DNS_R_ZONECUT: + break; + case DNS_R_EMPTYNAME: + case DNS_R_EMPTYWILD: + case DNS_R_NXDOMAIN: + case DNS_R_NCACHENXDOMAIN: + case DNS_R_NXRRSET: + case DNS_R_NCACHENXRRSET: + case ISC_R_NOTFOUND: + return (ISC_R_SUCCESS); + case DNS_R_DELEGATION: + case DNS_R_DUPLICATE: + case DNS_R_DROP: + return (result); + case DNS_R_CNAME: + case DNS_R_DNAME: + rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, name, + rpz_type, "NS address rewrite rrset", + result); + return (ISC_R_SUCCESS); + default: + if (client->query.rpz_st->m.policy != + DNS_RPZ_POLICY_ERROR) + { + client->query.rpz_st->m.policy = + DNS_RPZ_POLICY_ERROR; + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name, + rpz_type, + "NS address rewrite rrset", + result); + } + CTRACE(ISC_LOG_ERROR, + "rpz_rewrite_ip_rrset: unexpected " + "result"); + return (DNS_R_SERVFAIL); + } + + /* + * If we are processing glue setup for the next loop + * otherwise we are done. + */ + if (result == DNS_R_GLUE) { + options = client->query.dboptions; + } else { + options = client->query.dboptions | DNS_DBFIND_GLUEOK; + done = true; + } + + /* + * Check all of the IP addresses in the rdataset. + */ + for (result = dns_rdataset_first(*ip_rdatasetp); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(*ip_rdatasetp)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(*ip_rdatasetp, &rdata); + switch (rdata.type) { + case dns_rdatatype_a: + INSIST(rdata.length == 4); + memmove(&ina.s_addr, rdata.data, 4); + isc_netaddr_fromin(&netaddr, &ina); + break; + case dns_rdatatype_aaaa: + INSIST(rdata.length == 16); + memmove(in6a.s6_addr, rdata.data, 16); + isc_netaddr_fromin6(&netaddr, &in6a); + break; + default: + continue; + } + + result = rpz_rewrite_ip(client, &netaddr, qtype, + rpz_type, zbits, p_rdatasetp); + if (result != ISC_R_SUCCESS) { + return (result); + } + } + } while (!done && + client->query.rpz_st->m.policy == DNS_RPZ_POLICY_MISS); + + return (ISC_R_SUCCESS); +} + +/* + * Look for IP addresses in A and AAAA rdatasets + * that trigger all eligible IP or NSIP policy rules. + */ +static isc_result_t +rpz_rewrite_ip_rrsets(ns_client_t *client, dns_name_t *name, + dns_rdatatype_t qtype, dns_rpz_type_t rpz_type, + dns_rdataset_t **ip_rdatasetp, bool resuming) { + dns_rpz_st_t *st; + dns_dbversion_t *ip_version; + dns_db_t *ip_db; + dns_rdataset_t *p_rdataset; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip_rrsets"); + + st = client->query.rpz_st; + ip_version = NULL; + ip_db = NULL; + p_rdataset = NULL; + if ((st->state & DNS_RPZ_DONE_IPv4) == 0 && + (qtype == dns_rdatatype_a || qtype == dns_rdatatype_any || + rpz_type == DNS_RPZ_TYPE_NSIP)) + { + /* + * Rewrite based on an IPv4 address that will appear + * in the ANSWER section or if we are checking IP addresses. + */ + result = rpz_rewrite_ip_rrset( + client, name, qtype, rpz_type, dns_rdatatype_a, &ip_db, + ip_version, ip_rdatasetp, &p_rdataset, resuming); + if (result == ISC_R_SUCCESS) { + st->state |= DNS_RPZ_DONE_IPv4; + } + } else { + result = ISC_R_SUCCESS; + } + if (result == ISC_R_SUCCESS && + (qtype == dns_rdatatype_aaaa || qtype == dns_rdatatype_any || + rpz_type == DNS_RPZ_TYPE_NSIP)) + { + /* + * Rewrite based on IPv6 addresses that will appear + * in the ANSWER section or if we are checking IP addresses. + */ + result = rpz_rewrite_ip_rrset(client, name, qtype, rpz_type, + dns_rdatatype_aaaa, &ip_db, + ip_version, ip_rdatasetp, + &p_rdataset, resuming); + } + if (ip_db != NULL) { + dns_db_detach(&ip_db); + } + ns_client_putrdataset(client, &p_rdataset); + return (result); +} + +/* + * Try to rewrite a request for a qtype rdataset based on the trigger name + * trig_name and rpz_type (DNS_RPZ_TYPE_QNAME or DNS_RPZ_TYPE_NSDNAME). + * Record the results including the replacement rdataset if any + * in client->query.rpz_st. + * *rdatasetp is a scratch rdataset. + */ +static isc_result_t +rpz_rewrite_name(ns_client_t *client, dns_name_t *trig_name, + dns_rdatatype_t qtype, dns_rpz_type_t rpz_type, + dns_rpz_zbits_t allowed_zbits, bool recursed, + dns_rdataset_t **rdatasetp) { + dns_rpz_zones_t *rpzs; + dns_rpz_zone_t *rpz; + dns_rpz_st_t *st; + dns_fixedname_t p_namef; + dns_name_t *p_name; + dns_rpz_zbits_t zbits; + dns_rpz_num_t rpz_num; + dns_zone_t *p_zone; + dns_db_t *p_db; + dns_dbversion_t *p_version; + dns_dbnode_t *p_node; + dns_rpz_policy_t policy; + isc_result_t result; + +#ifndef USE_DNSRPS + UNUSED(recursed); +#endif /* ifndef USE_DNSRPS */ + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_name"); + + rpzs = client->view->rpzs; + st = client->query.rpz_st; + +#ifdef USE_DNSRPS + if (st->popt.dnsrps_enabled) { + return (dnsrps_rewrite_name(client, trig_name, recursed, + rpz_type, rdatasetp)); + } +#endif /* ifdef USE_DNSRPS */ + + zbits = rpz_get_zbits(client, qtype, rpz_type); + zbits &= allowed_zbits; + if (zbits == 0) { + return (ISC_R_SUCCESS); + } + + /* + * Use the summary database to find the bit mask of policy zones + * with policies for this trigger name. We do this even if there + * is only one eligible policy zone so that wildcard triggers + * are matched correctly, and not into their parent. + */ + zbits = dns_rpz_find_name(rpzs, rpz_type, zbits, trig_name); + if (zbits == 0) { + return (ISC_R_SUCCESS); + } + + p_name = dns_fixedname_initname(&p_namef); + + p_zone = NULL; + p_db = NULL; + p_node = NULL; + + /* + * Check the trigger name in every policy zone that the summary data + * says has a hit for the trigger name. + * Most of the time there are no eligible zones and the summary data + * keeps us from getting this far. + * We check the most eligible zone first and so usually check only + * one policy zone. + */ + for (rpz_num = 0; zbits != 0; ++rpz_num, zbits >>= 1) { + if ((zbits & 1) == 0) { + continue; + } + + /* + * Do not check policy zones that cannot replace a previously + * found policy. + */ + rpz = rpzs->zones[rpz_num]; + if (st->m.policy != DNS_RPZ_POLICY_MISS) { + if (st->m.rpz->num < rpz->num) { + break; + } + if (st->m.rpz->num == rpz->num && st->m.type < rpz_type) + { + break; + } + } + + /* + * Get the next policy zone's record for this trigger name. + */ + result = rpz_get_p_name(client, p_name, rpz, rpz_type, + trig_name); + if (result != ISC_R_SUCCESS) { + continue; + } + result = rpz_find_p(client, trig_name, qtype, p_name, rpz, + rpz_type, &p_zone, &p_db, &p_version, + &p_node, rdatasetp, &policy); + switch (result) { + case DNS_R_NXDOMAIN: + /* + * Continue after a missing policy record + * contrary to the summary data. The summary + * data can out of date during races with and among + * policy zone updates. + */ + CTRACE(ISC_LOG_ERROR, "rpz_rewrite_name: mismatched " + "summary data; " + "continuing"); + continue; + case DNS_R_SERVFAIL: + rpz_clean(&p_zone, &p_db, &p_node, rdatasetp); + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (DNS_R_SERVFAIL); + default: + /* + * With more than one applicable policy, prefer + * the earliest configured policy, + * client-IP over QNAME over IP over NSDNAME over NSIP, + * and the smallest name. + * We known st->m.rpz->num >= rpz->num and either + * st->m.rpz->num > rpz->num or st->m.type >= rpz_type + */ + if (st->m.policy != DNS_RPZ_POLICY_MISS && + rpz->num == st->m.rpz->num && + (st->m.type < rpz_type || + (st->m.type == rpz_type && + 0 >= dns_name_compare(p_name, st->p_name)))) + { + continue; + } + + if (rpz->policy != DNS_RPZ_POLICY_DISABLED) { + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_name: " + "rpz_save_p"); + rpz_save_p(st, rpz, rpz_type, policy, p_name, 0, + result, &p_zone, &p_db, &p_node, + rdatasetp, p_version); + /* + * After a hit, higher numbered policy zones + * are irrelevant + */ + rpz_clean(&p_zone, &p_db, &p_node, rdatasetp); + return (ISC_R_SUCCESS); + } + /* + * Log DNS_RPZ_POLICY_DISABLED zones + * and try the next eligible policy zone. + */ + rpz_log_rewrite(client, true, policy, rpz_type, p_zone, + p_name, NULL, rpz_num); + break; + } + } + + rpz_clean(&p_zone, &p_db, &p_node, rdatasetp); + return (ISC_R_SUCCESS); +} + +static void +rpz_rewrite_ns_skip(ns_client_t *client, dns_name_t *nsname, + isc_result_t result, int level, const char *str) { + dns_rpz_st_t *st; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ns_skip"); + + st = client->query.rpz_st; + + if (str != NULL) { + rpz_log_fail_helper(client, level, nsname, DNS_RPZ_TYPE_NSIP, + DNS_RPZ_TYPE_NSDNAME, str, result); + } + if (st->r.ns_rdataset != NULL && + dns_rdataset_isassociated(st->r.ns_rdataset)) + { + dns_rdataset_disassociate(st->r.ns_rdataset); + } + + st->r.label--; +} + +/* + * RPZ query result types + */ +typedef enum { + qresult_type_done = 0, + qresult_type_restart = 1, + qresult_type_recurse = 2 +} qresult_type_t; + +/* + * Look for response policy zone QNAME, NSIP, and NSDNAME rewriting. + */ +static isc_result_t +rpz_rewrite(ns_client_t *client, dns_rdatatype_t qtype, isc_result_t qresult, + bool resuming, dns_rdataset_t *ordataset, dns_rdataset_t *osigset) { + dns_rpz_zones_t *rpzs; + dns_rpz_st_t *st; + dns_rdataset_t *rdataset; + dns_fixedname_t nsnamef; + dns_name_t *nsname; + qresult_type_t qresult_type; + dns_rpz_zbits_t zbits; + isc_result_t result = ISC_R_SUCCESS; + dns_rpz_have_t have; + dns_rpz_popt_t popt; + int rpz_ver; + unsigned int options; +#ifdef USE_DNSRPS + librpz_emsg_t emsg; +#endif /* ifdef USE_DNSRPS */ + + CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite"); + + rpzs = client->view->rpzs; + st = client->query.rpz_st; + + if (rpzs == NULL) { + return (ISC_R_NOTFOUND); + } + if (st != NULL && (st->state & DNS_RPZ_REWRITTEN) != 0) { + return (DNS_R_DISALLOWED); + } + if (RECURSING(client)) { + return (DNS_R_DISALLOWED); + } + + RWLOCK(&rpzs->search_lock, isc_rwlocktype_read); + if ((rpzs->p.num_zones == 0 && !rpzs->p.dnsrps_enabled) || + (!RECURSIONOK(client) && rpzs->p.no_rd_ok == 0) || + !rpz_ck_dnssec(client, qresult, ordataset, osigset)) + { + RWUNLOCK(&rpzs->search_lock, isc_rwlocktype_read); + return (DNS_R_DISALLOWED); + } + have = rpzs->have; + popt = rpzs->p; + rpz_ver = rpzs->rpz_ver; + RWUNLOCK(&rpzs->search_lock, isc_rwlocktype_read); + +#ifndef USE_DNSRPS + INSIST(!popt.dnsrps_enabled); +#endif /* ifndef USE_DNSRPS */ + + if (st == NULL) { + st = isc_mem_get(client->mctx, sizeof(*st)); + st->state = 0; + st->rpsdb = NULL; + } + if (st->state == 0) { + st->state |= DNS_RPZ_ACTIVE; + memset(&st->m, 0, sizeof(st->m)); + st->m.type = DNS_RPZ_TYPE_BAD; + st->m.policy = DNS_RPZ_POLICY_MISS; + st->m.ttl = ~0; + memset(&st->r, 0, sizeof(st->r)); + memset(&st->q, 0, sizeof(st->q)); + st->p_name = dns_fixedname_initname(&st->_p_namef); + st->r_name = dns_fixedname_initname(&st->_r_namef); + st->fname = dns_fixedname_initname(&st->_fnamef); + st->have = have; + st->popt = popt; + st->rpz_ver = rpz_ver; + client->query.rpz_st = st; +#ifdef USE_DNSRPS + if (popt.dnsrps_enabled) { + if (st->rpsdb != NULL) { + dns_db_detach(&st->rpsdb); + } + result = dns_dnsrps_rewrite_init( + &emsg, st, rpzs, client->query.qname, + client->mctx, RECURSIONOK(client)); + if (result != ISC_R_SUCCESS) { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, NULL, + DNS_RPZ_TYPE_QNAME, emsg.c, + result); + st->m.policy = DNS_RPZ_POLICY_ERROR; + return (ISC_R_SUCCESS); + } + } +#endif /* ifdef USE_DNSRPS */ + } + + /* + * There is nothing to rewrite if the main query failed. + */ + switch (qresult) { + case ISC_R_SUCCESS: + case DNS_R_GLUE: + case DNS_R_ZONECUT: + qresult_type = qresult_type_done; + break; + case DNS_R_EMPTYNAME: + case DNS_R_NXRRSET: + case DNS_R_NXDOMAIN: + case DNS_R_EMPTYWILD: + case DNS_R_NCACHENXDOMAIN: + case DNS_R_NCACHENXRRSET: + case DNS_R_COVERINGNSEC: + case DNS_R_CNAME: + case DNS_R_DNAME: + qresult_type = qresult_type_restart; + break; + case DNS_R_DELEGATION: + case ISC_R_NOTFOUND: + /* + * If recursion is on, do only tentative rewriting. + * If recursion is off, this the normal and only time we + * can rewrite. + */ + if (RECURSIONOK(client)) { + qresult_type = qresult_type_recurse; + } else { + qresult_type = qresult_type_restart; + } + break; + case ISC_R_FAILURE: + case ISC_R_TIMEDOUT: + case DNS_R_BROKENCHAIN: + rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL3, NULL, + DNS_RPZ_TYPE_QNAME, + "stop on qresult in rpz_rewrite()", qresult); + return (ISC_R_SUCCESS); + default: + rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, NULL, + DNS_RPZ_TYPE_QNAME, + "stop on unrecognized qresult in rpz_rewrite()", + qresult); + return (ISC_R_SUCCESS); + } + + rdataset = NULL; + + if ((st->state & (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME)) != + (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME)) + { + isc_netaddr_t netaddr; + dns_rpz_zbits_t allowed; + + if (!st->popt.dnsrps_enabled && + qresult_type == qresult_type_recurse) + { + /* + * This request needs recursion that has not been done. + * Get bits for the policy zones that do not need + * to wait for the results of recursion. + */ + allowed = st->have.qname_skip_recurse; + if (allowed == 0) { + return (ISC_R_SUCCESS); + } + } else { + allowed = DNS_RPZ_ALL_ZBITS; + } + + /* + * Check once for triggers for the client IP address. + */ + if ((st->state & DNS_RPZ_DONE_CLIENT_IP) == 0) { + zbits = rpz_get_zbits(client, dns_rdatatype_none, + DNS_RPZ_TYPE_CLIENT_IP); + zbits &= allowed; + if (zbits != 0) { + isc_netaddr_fromsockaddr(&netaddr, + &client->peeraddr); + result = rpz_rewrite_ip(client, &netaddr, qtype, + DNS_RPZ_TYPE_CLIENT_IP, + zbits, &rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + } + } + + /* + * Check triggers for the query name if this is the first time + * for the current qname. + * There is a first time for each name in a CNAME chain + */ + if ((st->state & DNS_RPZ_DONE_QNAME) == 0) { + bool norec = (qresult_type != qresult_type_recurse); + result = rpz_rewrite_name(client, client->query.qname, + qtype, DNS_RPZ_TYPE_QNAME, + allowed, norec, &rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + /* + * Check IPv4 addresses in A RRs next. + * Reset to the start of the NS names. + */ + st->r.label = dns_name_countlabels(client->query.qname); + st->state &= ~(DNS_RPZ_DONE_QNAME_IP | + DNS_RPZ_DONE_IPv4); + } + + /* + * Quit if this was an attempt to find a qname or + * client-IP trigger before recursion. + * We will be back if no pre-recursion triggers hit. + * For example, consider 2 policy zones, both with qname and + * IP address triggers. If the qname misses the 1st zone, + * then we cannot know whether a hit for the qname in the + * 2nd zone matters until after recursing to get the A RRs and + * testing them in the first zone. + * Do not bother saving the work from this attempt, + * because recursion is so slow. + */ + if (qresult_type == qresult_type_recurse) { + goto cleanup; + } + + /* + * DNS_RPZ_DONE_QNAME but not DNS_RPZ_DONE_CLIENT_IP + * is reset at the end of dealing with each CNAME. + */ + st->state |= (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME); + } + + /* + * Check known IP addresses for the query name if the database lookup + * resulted in some addresses (qresult_type == qresult_type_done) + * and if we have not already checked them. + * Any recursion required for the query has already happened. + * Do not check addresses that will not be in the ANSWER section. + */ + if ((st->state & DNS_RPZ_DONE_QNAME_IP) == 0 && + qresult_type == qresult_type_done && + rpz_get_zbits(client, qtype, DNS_RPZ_TYPE_IP) != 0) + { + result = rpz_rewrite_ip_rrsets(client, client->query.qname, + qtype, DNS_RPZ_TYPE_IP, + &rdataset, resuming); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + /* + * We are finished checking the IP addresses for the qname. + * Start with IPv4 if we will check NS IP addresses. + */ + st->state |= DNS_RPZ_DONE_QNAME_IP; + st->state &= ~DNS_RPZ_DONE_IPv4; + } + + /* + * Stop looking for rules if there are none of the other kinds + * that could override what we already have. + */ + if (rpz_get_zbits(client, dns_rdatatype_any, DNS_RPZ_TYPE_NSDNAME) == + 0 && + rpz_get_zbits(client, dns_rdatatype_any, DNS_RPZ_TYPE_NSIP) == 0) + { + result = ISC_R_SUCCESS; + goto cleanup; + } + + dns_fixedname_init(&nsnamef); + dns_name_clone(client->query.qname, dns_fixedname_name(&nsnamef)); + options = client->query.dboptions | DNS_DBFIND_GLUEOK; + while (st->r.label > st->popt.min_ns_labels) { + bool was_glue = false; + /* + * Get NS rrset for each domain in the current qname. + */ + if (st->r.label == dns_name_countlabels(client->query.qname)) { + nsname = client->query.qname; + } else { + nsname = dns_fixedname_name(&nsnamef); + dns_name_split(client->query.qname, st->r.label, NULL, + nsname); + } + if (st->r.ns_rdataset == NULL || + !dns_rdataset_isassociated(st->r.ns_rdataset)) + { + dns_db_t *db = NULL; + result = rpz_rrset_find(client, nsname, + dns_rdatatype_ns, options, + DNS_RPZ_TYPE_NSDNAME, &db, NULL, + &st->r.ns_rdataset, resuming); + if (db != NULL) { + dns_db_detach(&db); + } + if (st->m.policy == DNS_RPZ_POLICY_ERROR) { + goto cleanup; + } + switch (result) { + case DNS_R_GLUE: + was_glue = true; + FALLTHROUGH; + case ISC_R_SUCCESS: + result = dns_rdataset_first(st->r.ns_rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + st->state &= ~(DNS_RPZ_DONE_NSDNAME | + DNS_RPZ_DONE_IPv4); + break; + case DNS_R_DELEGATION: + case DNS_R_DUPLICATE: + case DNS_R_DROP: + goto cleanup; + case DNS_R_EMPTYNAME: + case DNS_R_NXRRSET: + case DNS_R_EMPTYWILD: + case DNS_R_NXDOMAIN: + case DNS_R_NCACHENXDOMAIN: + case DNS_R_NCACHENXRRSET: + case ISC_R_NOTFOUND: + case DNS_R_CNAME: + case DNS_R_DNAME: + rpz_rewrite_ns_skip(client, nsname, result, 0, + NULL); + continue; + case ISC_R_TIMEDOUT: + case DNS_R_BROKENCHAIN: + case ISC_R_FAILURE: + rpz_rewrite_ns_skip(client, nsname, result, + DNS_RPZ_DEBUG_LEVEL3, + " NS rpz_rrset_find()"); + continue; + default: + rpz_rewrite_ns_skip(client, nsname, result, + DNS_RPZ_INFO_LEVEL, + " unrecognized NS" + " rpz_rrset_find()"); + continue; + } + } + + /* + * Check all NS names. + */ + do { + dns_rdata_ns_t ns; + dns_rdata_t nsrdata = DNS_RDATA_INIT; + + dns_rdataset_current(st->r.ns_rdataset, &nsrdata); + result = dns_rdata_tostruct(&nsrdata, &ns, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&nsrdata); + + /* + * Do nothing about "NS ." + */ + if (dns_name_equal(&ns.name, dns_rootname)) { + dns_rdata_freestruct(&ns); + result = dns_rdataset_next(st->r.ns_rdataset); + continue; + } + /* + * Check this NS name if we did not handle it + * during a previous recursion. + */ + if ((st->state & DNS_RPZ_DONE_NSDNAME) == 0) { + result = rpz_rewrite_name( + client, &ns.name, qtype, + DNS_RPZ_TYPE_NSDNAME, DNS_RPZ_ALL_ZBITS, + true, &rdataset); + if (result != ISC_R_SUCCESS) { + dns_rdata_freestruct(&ns); + goto cleanup; + } + st->state |= DNS_RPZ_DONE_NSDNAME; + } + /* + * Check all IP addresses for this NS name. + */ + result = rpz_rewrite_ip_rrsets(client, &ns.name, qtype, + DNS_RPZ_TYPE_NSIP, + &rdataset, resuming); + dns_rdata_freestruct(&ns); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + st->state &= ~(DNS_RPZ_DONE_NSDNAME | + DNS_RPZ_DONE_IPv4); + result = dns_rdataset_next(st->r.ns_rdataset); + } while (result == ISC_R_SUCCESS); + dns_rdataset_disassociate(st->r.ns_rdataset); + + /* + * If we just checked a glue NS RRset retry without allowing + * glue responses, otherwise setup for the next name. + */ + if (was_glue) { + options = client->query.dboptions; + } else { + options = client->query.dboptions | DNS_DBFIND_GLUEOK; + st->r.label--; + } + + if (rpz_get_zbits(client, dns_rdatatype_any, + DNS_RPZ_TYPE_NSDNAME) == 0 && + rpz_get_zbits(client, dns_rdatatype_any, + DNS_RPZ_TYPE_NSIP) == 0) + { + break; + } + } + + /* + * Use the best hit, if any. + */ + result = ISC_R_SUCCESS; + +cleanup: +#ifdef USE_DNSRPS + if (st->popt.dnsrps_enabled && st->m.policy != DNS_RPZ_POLICY_ERROR && + !dnsrps_set_p(&emsg, client, st, qtype, &rdataset, + (qresult_type != qresult_type_recurse))) + { + rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, NULL, + DNS_RPZ_TYPE_BAD, emsg.c, DNS_R_SERVFAIL); + st->m.policy = DNS_RPZ_POLICY_ERROR; + } +#endif /* ifdef USE_DNSRPS */ + if (st->m.policy != DNS_RPZ_POLICY_MISS && + st->m.policy != DNS_RPZ_POLICY_ERROR && + st->m.rpz->policy != DNS_RPZ_POLICY_GIVEN) + { + st->m.policy = st->m.rpz->policy; + } + if (st->m.policy == DNS_RPZ_POLICY_MISS || + st->m.policy == DNS_RPZ_POLICY_PASSTHRU || + st->m.policy == DNS_RPZ_POLICY_ERROR) + { + if (st->m.policy == DNS_RPZ_POLICY_PASSTHRU && + result != DNS_R_DELEGATION) + { + rpz_log_rewrite(client, false, st->m.policy, st->m.type, + st->m.zone, st->p_name, NULL, + st->m.rpz->num); + } + rpz_match_clear(st); + } + if (st->m.policy == DNS_RPZ_POLICY_ERROR) { + CTRACE(ISC_LOG_ERROR, "SERVFAIL due to RPZ policy"); + st->m.type = DNS_RPZ_TYPE_BAD; + result = DNS_R_SERVFAIL; + } + ns_client_putrdataset(client, &rdataset); + if ((st->state & DNS_RPZ_RECURSING) == 0) { + rpz_clean(NULL, &st->r.db, NULL, &st->r.ns_rdataset); + } + + return (result); +} + +/* + * See if response policy zone rewriting is allowed by a lack of interest + * by the client in DNSSEC or a lack of signatures. + */ +static bool +rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset) { + dns_fixedname_t fixed; + dns_name_t *found; + dns_rdataset_t trdataset; + dns_rdatatype_t type; + isc_result_t result; + + CTRACE(ISC_LOG_DEBUG(3), "rpz_ck_dnssec"); + + if (client->view->rpzs->p.break_dnssec || !WANTDNSSEC(client)) { + return (true); + } + + /* + * We do not know if there are signatures if we have not recursed + * for them. + */ + if (qresult == DNS_R_DELEGATION || qresult == ISC_R_NOTFOUND) { + return (false); + } + + if (sigrdataset == NULL) { + return (true); + } + if (dns_rdataset_isassociated(sigrdataset)) { + return (false); + } + + /* + * We are happy to rewrite nothing. + */ + if (rdataset == NULL || !dns_rdataset_isassociated(rdataset)) { + return (true); + } + /* + * Do not rewrite if there is any sign of signatures. + */ + if (rdataset->type == dns_rdatatype_nsec || + rdataset->type == dns_rdatatype_nsec3 || + rdataset->type == dns_rdatatype_rrsig) + { + return (false); + } + + /* + * Look for a signature in a negative cache rdataset. + */ + if ((rdataset->attributes & DNS_RDATASETATTR_NEGATIVE) == 0) { + return (true); + } + found = dns_fixedname_initname(&fixed); + dns_rdataset_init(&trdataset); + for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + dns_ncache_current(rdataset, found, &trdataset); + type = trdataset.type; + dns_rdataset_disassociate(&trdataset); + if (type == dns_rdatatype_nsec || type == dns_rdatatype_nsec3 || + type == dns_rdatatype_rrsig) + { + return (false); + } + } + return (true); +} + +/* + * Extract a network address from the RDATA of an A or AAAA + * record. + * + * Returns: + * ISC_R_SUCCESS + * ISC_R_NOTIMPLEMENTED The rdata is not a known address type. + */ +static isc_result_t +rdata_tonetaddr(const dns_rdata_t *rdata, isc_netaddr_t *netaddr) { + struct in_addr ina; + struct in6_addr in6a; + + switch (rdata->type) { + case dns_rdatatype_a: + INSIST(rdata->length == 4); + memmove(&ina.s_addr, rdata->data, 4); + isc_netaddr_fromin(netaddr, &ina); + return (ISC_R_SUCCESS); + case dns_rdatatype_aaaa: + INSIST(rdata->length == 16); + memmove(in6a.s6_addr, rdata->data, 16); + isc_netaddr_fromin6(netaddr, &in6a); + return (ISC_R_SUCCESS); + default: + return (ISC_R_NOTIMPLEMENTED); + } +} + +static unsigned char inaddr10_offsets[] = { 0, 3, 11, 16 }; +static unsigned char inaddr172_offsets[] = { 0, 3, 7, 15, 20 }; +static unsigned char inaddr192_offsets[] = { 0, 4, 8, 16, 21 }; + +static unsigned char inaddr10[] = "\00210\007IN-ADDR\004ARPA"; + +static unsigned char inaddr16172[] = "\00216\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr17172[] = "\00217\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr18172[] = "\00218\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr19172[] = "\00219\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr20172[] = "\00220\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr21172[] = "\00221\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr22172[] = "\00222\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr23172[] = "\00223\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr24172[] = "\00224\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr25172[] = "\00225\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr26172[] = "\00226\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr27172[] = "\00227\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr28172[] = "\00228\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr29172[] = "\00229\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr30172[] = "\00230\003172\007IN-ADDR\004ARPA"; +static unsigned char inaddr31172[] = "\00231\003172\007IN-ADDR\004ARPA"; + +static unsigned char inaddr168192[] = "\003168\003192\007IN-ADDR\004ARPA"; + +static dns_name_t rfc1918names[] = { + DNS_NAME_INITABSOLUTE(inaddr10, inaddr10_offsets), + DNS_NAME_INITABSOLUTE(inaddr16172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr17172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr18172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr19172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr20172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr21172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr22172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr23172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr24172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr25172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr26172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr27172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr28172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr29172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr30172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr31172, inaddr172_offsets), + DNS_NAME_INITABSOLUTE(inaddr168192, inaddr192_offsets) +}; + +static unsigned char prisoner_data[] = "\010prisoner\004iana\003org"; +static unsigned char hostmaster_data[] = "\012hostmaster\014root-" + "servers\003org"; + +static unsigned char prisoner_offsets[] = { 0, 9, 14, 18 }; +static unsigned char hostmaster_offsets[] = { 0, 11, 24, 28 }; + +static dns_name_t const prisoner = DNS_NAME_INITABSOLUTE(prisoner_data, + prisoner_offsets); +static dns_name_t const hostmaster = DNS_NAME_INITABSOLUTE(hostmaster_data, + hostmaster_offsets); + +static void +warn_rfc1918(ns_client_t *client, dns_name_t *fname, dns_rdataset_t *rdataset) { + unsigned int i; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_soa_t soa; + dns_rdataset_t found; + isc_result_t result; + + for (i = 0; i < (sizeof(rfc1918names) / sizeof(*rfc1918names)); i++) { + if (dns_name_issubdomain(fname, &rfc1918names[i])) { + dns_rdataset_init(&found); + result = dns_ncache_getrdataset( + rdataset, &rfc1918names[i], dns_rdatatype_soa, + &found); + if (result != ISC_R_SUCCESS) { + return; + } + + result = dns_rdataset_first(&found); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdataset_current(&found, &rdata); + result = dns_rdata_tostruct(&rdata, &soa, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (dns_name_equal(&soa.origin, &prisoner) && + dns_name_equal(&soa.contact, &hostmaster)) + { + char buf[DNS_NAME_FORMATSIZE]; + dns_name_format(fname, buf, sizeof(buf)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, + ISC_LOG_WARNING, + "RFC 1918 response from " + "Internet for %s", + buf); + } + dns_rdataset_disassociate(&found); + return; + } + } +} + +static void +query_findclosestnsec3(dns_name_t *qname, dns_db_t *db, + dns_dbversion_t *version, ns_client_t *client, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset, + dns_name_t *fname, bool exact, dns_name_t *found) { + unsigned char salt[256]; + size_t salt_length; + uint16_t iterations; + isc_result_t result; + unsigned int dboptions; + dns_fixedname_t fixed; + dns_hash_t hash; + dns_name_t name; + unsigned int skip = 0, labels; + dns_rdata_nsec3_t nsec3; + dns_rdata_t rdata = DNS_RDATA_INIT; + bool optout; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + salt_length = sizeof(salt); + result = dns_db_getnsec3parameters(db, version, &hash, NULL, + &iterations, salt, &salt_length); + if (result != ISC_R_SUCCESS) { + return; + } + + dns_name_init(&name, NULL); + dns_name_clone(qname, &name); + labels = dns_name_countlabels(&name); + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Map unknown algorithm to known value. + */ + if (hash == DNS_NSEC3_UNKNOWNALG) { + hash = 1; + } + +again: + dns_fixedname_init(&fixed); + result = dns_nsec3_hashname(&fixed, NULL, NULL, &name, + dns_db_origin(db), hash, iterations, salt, + salt_length); + if (result != ISC_R_SUCCESS) { + return; + } + + dboptions = client->query.dboptions | DNS_DBFIND_FORCENSEC3; + result = dns_db_findext(db, dns_fixedname_name(&fixed), version, + dns_rdatatype_nsec3, dboptions, client->now, + NULL, fname, &cm, &ci, rdataset, sigrdataset); + + if (result == DNS_R_NXDOMAIN) { + if (!dns_rdataset_isassociated(rdataset)) { + return; + } + result = dns_rdataset_first(rdataset); + INSIST(result == ISC_R_SUCCESS); + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec3, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&rdata); + optout = ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) != 0); + if (found != NULL && optout && + dns_name_issubdomain(&name, dns_db_origin(db))) + { + dns_rdataset_disassociate(rdataset); + if (dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + skip++; + dns_name_getlabelsequence(qname, skip, labels - skip, + &name); + ns_client_log(client, DNS_LOGCATEGORY_DNSSEC, + NS_LOGMODULE_QUERY, ISC_LOG_DEBUG(3), + "looking for closest provable encloser"); + goto again; + } + if (exact) { + ns_client_log(client, DNS_LOGCATEGORY_DNSSEC, + NS_LOGMODULE_QUERY, ISC_LOG_WARNING, + "expected a exact match NSEC3, got " + "a covering record"); + } + } else if (result != ISC_R_SUCCESS) { + return; + } else if (!exact) { + ns_client_log(client, DNS_LOGCATEGORY_DNSSEC, + NS_LOGMODULE_QUERY, ISC_LOG_WARNING, + "expected covering NSEC3, got an exact match"); + } + if (found == qname) { + if (skip != 0U) { + dns_name_getlabelsequence(qname, skip, labels - skip, + found); + } + } else if (found != NULL) { + dns_name_copynf(&name, found); + } + return; +} + +static uint32_t +dns64_ttl(dns_db_t *db, dns_dbversion_t *version) { + dns_dbnode_t *node = NULL; + dns_rdata_soa_t soa; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_t rdataset; + isc_result_t result; + uint32_t ttl = UINT32_MAX; + + dns_rdataset_init(&rdataset); + + result = dns_db_getoriginnode(db, &node); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + result = dns_db_findrdataset(db, node, version, dns_rdatatype_soa, 0, 0, + &rdataset, NULL); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + result = dns_rdataset_first(&rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + dns_rdataset_current(&rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &soa, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + ttl = ISC_MIN(rdataset.ttl, soa.minimum); + +cleanup: + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + return (ttl); +} + +static bool +dns64_aaaaok(ns_client_t *client, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + isc_netaddr_t netaddr; + dns_aclenv_t *env = + ns_interfacemgr_getaclenv(client->manager->interface->mgr); + dns_dns64_t *dns64 = ISC_LIST_HEAD(client->view->dns64); + unsigned int flags = 0; + unsigned int i, count; + bool *aaaaok; + + INSIST(client->query.dns64_aaaaok == NULL); + INSIST(client->query.dns64_aaaaoklen == 0); + INSIST(client->query.dns64_aaaa == NULL); + INSIST(client->query.dns64_sigaaaa == NULL); + + if (dns64 == NULL) { + return (true); + } + + if (RECURSIONOK(client)) { + flags |= DNS_DNS64_RECURSIVE; + } + + if (WANTDNSSEC(client) && sigrdataset != NULL && + dns_rdataset_isassociated(sigrdataset)) + { + flags |= DNS_DNS64_DNSSEC; + } + + count = dns_rdataset_count(rdataset); + aaaaok = isc_mem_get(client->mctx, sizeof(bool) * count); + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + if (dns_dns64_aaaaok(dns64, &netaddr, client->signer, env, flags, + rdataset, aaaaok, count)) + { + for (i = 0; i < count; i++) { + if (aaaaok != NULL && !aaaaok[i]) { + SAVE(client->query.dns64_aaaaok, aaaaok); + client->query.dns64_aaaaoklen = count; + break; + } + } + if (aaaaok != NULL) { + isc_mem_put(client->mctx, aaaaok, sizeof(bool) * count); + } + return (true); + } + if (aaaaok != NULL) { + isc_mem_put(client->mctx, aaaaok, sizeof(bool) * count); + } + return (false); +} + +/* + * Look for the name and type in the redirection zone. If found update + * the arguments as appropriate. Return true if a update was + * performed. + * + * Only perform the update if the client is in the allow query acl and + * returning the update would not cause a DNSSEC validation failure. + */ +static isc_result_t +redirect(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset, + dns_dbnode_t **nodep, dns_db_t **dbp, dns_dbversion_t **versionp, + dns_rdatatype_t qtype) { + dns_db_t *db = NULL; + dns_dbnode_t *node = NULL; + dns_fixedname_t fixed; + dns_name_t *found; + dns_rdataset_t trdataset; + isc_result_t result; + dns_rdatatype_t type; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + ns_dbversion_t *dbversion; + + CTRACE(ISC_LOG_DEBUG(3), "redirect"); + + if (client->view->redirect == NULL) { + return (ISC_R_NOTFOUND); + } + + found = dns_fixedname_initname(&fixed); + dns_rdataset_init(&trdataset); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, &client->ecs, NULL); + + if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp)) + { + return (ISC_R_NOTFOUND); + } + + if (WANTDNSSEC(client) && dns_rdataset_isassociated(rdataset)) { + if (rdataset->trust == dns_trust_secure) { + return (ISC_R_NOTFOUND); + } + if (rdataset->trust == dns_trust_ultimate && + (rdataset->type == dns_rdatatype_nsec || + rdataset->type == dns_rdatatype_nsec3)) + { + return (ISC_R_NOTFOUND); + } + if ((rdataset->attributes & DNS_RDATASETATTR_NEGATIVE) != 0) { + for (result = dns_rdataset_first(rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + dns_ncache_current(rdataset, found, &trdataset); + type = trdataset.type; + dns_rdataset_disassociate(&trdataset); + if (type == dns_rdatatype_nsec || + type == dns_rdatatype_nsec3 || + type == dns_rdatatype_rrsig) + { + return (ISC_R_NOTFOUND); + } + } + } + } + + result = ns_client_checkaclsilent( + client, NULL, dns_zone_getqueryacl(client->view->redirect), + true); + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + result = dns_zone_getdb(client->view->redirect, &db); + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + + dbversion = ns_client_findversion(client, db); + if (dbversion == NULL) { + dns_db_detach(&db); + return (ISC_R_NOTFOUND); + } + + /* + * Lookup the requested data in the redirect zone. + */ + result = dns_db_findext(db, client->query.qname, dbversion->version, + qtype, DNS_DBFIND_NOZONECUT, client->now, &node, + found, &cm, &ci, &trdataset, NULL); + if (result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_disassociate(&trdataset); + } + goto nxrrset; + } else if (result != ISC_R_SUCCESS) { + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_disassociate(&trdataset); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + return (ISC_R_NOTFOUND); + } + + CTRACE(ISC_LOG_DEBUG(3), "redirect: found data: done"); + dns_name_copynf(found, name); + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_clone(&trdataset, rdataset); + dns_rdataset_disassociate(&trdataset); + } +nxrrset: + if (*nodep != NULL) { + dns_db_detachnode(*dbp, nodep); + } + dns_db_detach(dbp); + dns_db_attachnode(db, node, nodep); + dns_db_attach(db, dbp); + dns_db_detachnode(db, &node); + dns_db_detach(&db); + *versionp = dbversion->version; + + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + + return (result); +} + +static isc_result_t +redirect2(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset, + dns_dbnode_t **nodep, dns_db_t **dbp, dns_dbversion_t **versionp, + dns_rdatatype_t qtype, bool *is_zonep) { + dns_db_t *db = NULL; + dns_dbnode_t *node = NULL; + dns_fixedname_t fixed; + dns_fixedname_t fixedredirect; + dns_name_t *found, *redirectname; + dns_rdataset_t trdataset; + isc_result_t result; + dns_rdatatype_t type; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_dbversion_t *version = NULL; + dns_zone_t *zone = NULL; + bool is_zone; + unsigned int labels; + unsigned int options; + + CTRACE(ISC_LOG_DEBUG(3), "redirect2"); + + if (client->view->redirectzone == NULL) { + return (ISC_R_NOTFOUND); + } + + if (dns_name_issubdomain(name, client->view->redirectzone)) { + return (ISC_R_NOTFOUND); + } + + found = dns_fixedname_initname(&fixed); + dns_rdataset_init(&trdataset); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, &client->ecs, NULL); + + if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp)) + { + return (ISC_R_NOTFOUND); + } + + if (WANTDNSSEC(client) && dns_rdataset_isassociated(rdataset)) { + if (rdataset->trust == dns_trust_secure) { + return (ISC_R_NOTFOUND); + } + if (rdataset->trust == dns_trust_ultimate && + (rdataset->type == dns_rdatatype_nsec || + rdataset->type == dns_rdatatype_nsec3)) + { + return (ISC_R_NOTFOUND); + } + if ((rdataset->attributes & DNS_RDATASETATTR_NEGATIVE) != 0) { + for (result = dns_rdataset_first(rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + dns_ncache_current(rdataset, found, &trdataset); + type = trdataset.type; + dns_rdataset_disassociate(&trdataset); + if (type == dns_rdatatype_nsec || + type == dns_rdatatype_nsec3 || + type == dns_rdatatype_rrsig) + { + return (ISC_R_NOTFOUND); + } + } + } + } + + redirectname = dns_fixedname_initname(&fixedredirect); + labels = dns_name_countlabels(client->query.qname); + if (labels > 1U) { + dns_name_t prefix; + + dns_name_init(&prefix, NULL); + dns_name_getlabelsequence(client->query.qname, 0, labels - 1, + &prefix); + result = dns_name_concatenate(&prefix, + client->view->redirectzone, + redirectname, NULL); + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + } else { + dns_name_copynf(redirectname, client->view->redirectzone); + } + + options = 0; + result = query_getdb(client, redirectname, qtype, options, &zone, &db, + &version, &is_zone); + if (result != ISC_R_SUCCESS) { + return (ISC_R_NOTFOUND); + } + if (zone != NULL) { + dns_zone_detach(&zone); + } + + /* + * Lookup the requested data in the redirect zone. + */ + result = dns_db_findext(db, redirectname, version, qtype, 0, + client->now, &node, found, &cm, &ci, &trdataset, + NULL); + if (result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_disassociate(&trdataset); + } + goto nxrrset; + } else if (result == ISC_R_NOTFOUND || result == DNS_R_DELEGATION) { + /* + * Cleanup. + */ + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_disassociate(&trdataset); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + /* + * Don't loop forever if the lookup failed last time. + */ + if (!REDIRECT(client)) { + result = ns_query_recurse(client, qtype, redirectname, + NULL, NULL, true); + if (result == ISC_R_SUCCESS) { + client->query.attributes |= + NS_QUERYATTR_RECURSING; + client->query.attributes |= + NS_QUERYATTR_REDIRECT; + return (DNS_R_CONTINUE); + } + } + return (ISC_R_NOTFOUND); + } else if (result != ISC_R_SUCCESS) { + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_disassociate(&trdataset); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + return (ISC_R_NOTFOUND); + } + + CTRACE(ISC_LOG_DEBUG(3), "redirect2: found data: done"); + /* + * Adjust the found name to not include the redirectzone suffix. + */ + dns_name_split(found, dns_name_countlabels(client->view->redirectzone), + found, NULL); + /* + * Make the name absolute. + */ + result = dns_name_concatenate(found, dns_rootname, found, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + dns_name_copynf(found, name); + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (dns_rdataset_isassociated(&trdataset)) { + dns_rdataset_clone(&trdataset, rdataset); + dns_rdataset_disassociate(&trdataset); + } +nxrrset: + if (*nodep != NULL) { + dns_db_detachnode(*dbp, nodep); + } + dns_db_detach(dbp); + dns_db_attachnode(db, node, nodep); + dns_db_attach(db, dbp); + dns_db_detachnode(db, &node); + dns_db_detach(&db); + *is_zonep = is_zone; + *versionp = version; + + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + + return (result); +} + +/*% + * Initialize query context 'qctx'. Run by query_setup() when + * first handling a client query, and by query_resume() when + * returning from recursion. + * + * Whenever this function is called, qctx_destroy() must be called + * when leaving the scope or freeing the qctx. + */ +static void +qctx_init(ns_client_t *client, dns_fetchevent_t **eventp, dns_rdatatype_t qtype, + query_ctx_t *qctx) { + REQUIRE(qctx != NULL); + REQUIRE(client != NULL); + + memset(qctx, 0, sizeof(*qctx)); + + /* Set this first so CCTRACE will work */ + qctx->client = client; + + dns_view_attach(client->view, &qctx->view); + + CCTRACE(ISC_LOG_DEBUG(3), "qctx_init"); + + if (eventp != NULL) { + qctx->event = *eventp; + *eventp = NULL; + } else { + qctx->event = NULL; + } + qctx->qtype = qctx->type = qtype; + qctx->result = ISC_R_SUCCESS; + qctx->findcoveringnsec = qctx->view->synthfromdnssec; + + /* + * If it's an RRSIG or SIG query, we'll iterate the node. + */ + if (qctx->qtype == dns_rdatatype_rrsig || + qctx->qtype == dns_rdatatype_sig) + { + qctx->type = dns_rdatatype_any; + } + + CALL_HOOK_NORETURN(NS_QUERY_QCTX_INITIALIZED, qctx); +} + +/* + * Make 'dst' and exact copy of 'src', with exception of the + * option field, which is reset to zero. + * This function also attaches dst's view and db to the src's + * view and cachedb. + */ +static void +qctx_copy(const query_ctx_t *qctx, query_ctx_t *dst) { + REQUIRE(qctx != NULL); + REQUIRE(dst != NULL); + + memmove(dst, qctx, sizeof(*dst)); + dst->view = NULL; + dst->db = NULL; + dst->options = 0; + dns_view_attach(qctx->view, &dst->view); + dns_db_attach(qctx->view->cachedb, &dst->db); + CCTRACE(ISC_LOG_DEBUG(3), "qctx_copy"); +} + +/*% + * Clean up and disassociate the rdataset and node pointers in qctx. + */ +static void +qctx_clean(query_ctx_t *qctx) { + if (qctx->rdataset != NULL && dns_rdataset_isassociated(qctx->rdataset)) + { + dns_rdataset_disassociate(qctx->rdataset); + } + if (qctx->sigrdataset != NULL && + dns_rdataset_isassociated(qctx->sigrdataset)) + { + dns_rdataset_disassociate(qctx->sigrdataset); + } + if (qctx->db != NULL && qctx->node != NULL) { + dns_db_detachnode(qctx->db, &qctx->node); + } +} + +/*% + * Free any allocated memory associated with qctx. + */ +static void +qctx_freedata(query_ctx_t *qctx) { + if (qctx->rdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->rdataset); + } + + if (qctx->sigrdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->sigrdataset); + } + + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, &qctx->fname); + } + + if (qctx->db != NULL) { + INSIST(qctx->node == NULL); + dns_db_detach(&qctx->db); + } + + if (qctx->zone != NULL) { + dns_zone_detach(&qctx->zone); + } + + if (qctx->zdb != NULL) { + ns_client_putrdataset(qctx->client, &qctx->zsigrdataset); + ns_client_putrdataset(qctx->client, &qctx->zrdataset); + ns_client_releasename(qctx->client, &qctx->zfname); + dns_db_detachnode(qctx->zdb, &qctx->znode); + dns_db_detach(&qctx->zdb); + } + + if (qctx->event != NULL && !qctx->client->nodetach) { + free_devent(qctx->client, ISC_EVENT_PTR(&qctx->event), + &qctx->event); + } +} + +static void +qctx_destroy(query_ctx_t *qctx) { + CALL_HOOK_NORETURN(NS_QUERY_QCTX_DESTROYED, qctx); + + dns_view_detach(&qctx->view); +} + +/*% + * Log detailed information about the query immediately after + * the client request or a return from recursion. + */ +static void +query_trace(query_ctx_t *qctx) { +#ifdef WANT_QUERYTRACE + char mbuf[2 * DNS_NAME_FORMATSIZE]; + char qbuf[DNS_NAME_FORMATSIZE]; + + if (qctx->client->query.origqname != NULL) { + dns_name_format(qctx->client->query.origqname, qbuf, + sizeof(qbuf)); + } else { + snprintf(qbuf, sizeof(qbuf), "<unset>"); + } + + snprintf(mbuf, sizeof(mbuf) - 1, + "client attr:0x%x, query attr:0x%X, restarts:%u, " + "origqname:%s, timer:%d, authdb:%d, referral:%d", + qctx->client->attributes, qctx->client->query.attributes, + qctx->client->query.restarts, qbuf, + (int)qctx->client->query.timerset, + (int)qctx->client->query.authdbset, + (int)qctx->client->query.isreferral); + CCTRACE(ISC_LOG_DEBUG(3), mbuf); +#else /* ifdef WANT_QUERYTRACE */ + UNUSED(qctx); +#endif /* ifdef WANT_QUERYTRACE */ +} + +/* + * Set up query processing for the current query of 'client'. + * Calls qctx_init() to initialize a query context, checks + * the SERVFAIL cache, then hands off processing to ns__query_start(). + * + * This is called only from ns_query_start(), to begin a query + * for the first time. Restarting an existing query (for + * instance, to handle CNAME lookups), is done by calling + * ns__query_start() again with the same query context. Resuming from + * recursion is handled by query_resume(). + */ +static isc_result_t +query_setup(ns_client_t *client, dns_rdatatype_t qtype) { + isc_result_t result; + query_ctx_t qctx; + + qctx_init(client, NULL, qtype, &qctx); + query_trace(&qctx); + + CALL_HOOK(NS_QUERY_SETUP, &qctx); + + /* + * Check SERVFAIL cache + */ + result = ns__query_sfcache(&qctx); + if (result != ISC_R_COMPLETE) { + qctx_destroy(&qctx); + return (result); + } + + result = ns__query_start(&qctx); + +cleanup: + qctx_destroy(&qctx); + return (result); +} + +static bool +get_root_key_sentinel_id(query_ctx_t *qctx, const char *ndata) { + unsigned int v = 0; + int i; + + for (i = 0; i < 5; i++) { + if (!isdigit((unsigned char)ndata[i])) { + return (false); + } + v *= 10; + v += ndata[i] - '0'; + } + if (v > 65535U) { + return (false); + } + qctx->client->query.root_key_sentinel_keyid = v; + return (true); +} + +/*% + * Find out if the query is for a root key sentinel and if so, record the type + * of root key sentinel query and the key id that is being checked for. + * + * The code is assuming a zero padded decimal field of width 5. + */ +static void +root_key_sentinel_detect(query_ctx_t *qctx) { + const char *ndata = (const char *)qctx->client->query.qname->ndata; + + if (qctx->client->query.qname->length > 30 && ndata[0] == 29 && + strncasecmp(ndata + 1, "root-key-sentinel-is-ta-", 24) == 0) + { + if (!get_root_key_sentinel_id(qctx, ndata + 25)) { + return; + } + qctx->client->query.root_key_sentinel_is_ta = true; + /* + * Simplify processing by disabling aggressive + * negative caching. + */ + qctx->findcoveringnsec = false; + ns_client_log(qctx->client, NS_LOGCATEGORY_TAT, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "root-key-sentinel-is-ta query label found"); + } else if (qctx->client->query.qname->length > 31 && ndata[0] == 30 && + strncasecmp(ndata + 1, "root-key-sentinel-not-ta-", 25) == 0) + { + if (!get_root_key_sentinel_id(qctx, ndata + 26)) { + return; + } + qctx->client->query.root_key_sentinel_not_ta = true; + /* + * Simplify processing by disabling aggressive + * negative caching. + */ + qctx->findcoveringnsec = false; + ns_client_log(qctx->client, NS_LOGCATEGORY_TAT, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "root-key-sentinel-not-ta query label found"); + } +} + +/*% + * Starting point for a client query or a chaining query. + * + * Called first by query_setup(), and then again as often as needed to + * follow a CNAME chain. Determines which authoritative database to + * search, then hands off processing to query_lookup(). + */ +isc_result_t +ns__query_start(query_ctx_t *qctx) { + isc_result_t result; + CCTRACE(ISC_LOG_DEBUG(3), "ns__query_start"); + qctx->want_restart = false; + qctx->authoritative = false; + qctx->version = NULL; + qctx->zversion = NULL; + qctx->need_wildcardproof = false; + qctx->rpz = false; + + CALL_HOOK(NS_QUERY_START_BEGIN, qctx); + + /* + * If we require a server cookie then send back BADCOOKIE + * before we have done too much work. + */ + if (!TCP(qctx->client) && qctx->view->requireservercookie && + WANTCOOKIE(qctx->client) && !HAVECOOKIE(qctx->client)) + { + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA; + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; + qctx->client->message->rcode = dns_rcode_badcookie; + return (ns_query_done(qctx)); + } + + if (qctx->view->checknames && + !dns_rdata_checkowner(qctx->client->query.qname, + qctx->client->message->rdclass, qctx->qtype, + false)) + { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + + dns_name_format(qctx->client->query.qname, namebuf, + sizeof(namebuf)); + dns_rdatatype_format(qctx->qtype, typebuf, sizeof(typebuf)); + dns_rdataclass_format(qctx->client->message->rdclass, classbuf, + sizeof(classbuf)); + ns_client_log(qctx->client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_QUERY, ISC_LOG_ERROR, + "check-names failure %s/%s/%s", namebuf, typebuf, + classbuf); + QUERY_ERROR(qctx, DNS_R_REFUSED); + return (ns_query_done(qctx)); + } + + /* + * Setup for root key sentinel processing. + */ + if (qctx->view->root_key_sentinel && + qctx->client->query.restarts == 0 && + (qctx->qtype == dns_rdatatype_a || + qctx->qtype == dns_rdatatype_aaaa) && + (qctx->client->message->flags & DNS_MESSAGEFLAG_CD) == 0) + { + root_key_sentinel_detect(qctx); + } + + /* + * First we must find the right database. + */ + qctx->options &= DNS_GETDB_NOLOG; /* Preserve DNS_GETDB_NOLOG. */ + if (dns_rdatatype_atparent(qctx->qtype) && + !dns_name_equal(qctx->client->query.qname, dns_rootname)) + { + /* + * If authoritative data for this QTYPE is supposed to live in + * the parent zone, do not look for an exact match for QNAME, + * but rather for its containing zone (unless the QNAME is + * root). + */ + qctx->options |= DNS_GETDB_NOEXACT; + } + + result = query_getdb(qctx->client, qctx->client->query.qname, + qctx->qtype, qctx->options, &qctx->zone, &qctx->db, + &qctx->version, &qctx->is_zone); + if (ISC_UNLIKELY((result != ISC_R_SUCCESS || !qctx->is_zone) && + qctx->qtype == dns_rdatatype_ds && + !RECURSIONOK(qctx->client) && + (qctx->options & DNS_GETDB_NOEXACT) != 0)) + { + /* + * This is a non-recursive QTYPE=DS query with QNAME whose + * parent we are not authoritative for. Check whether we are + * authoritative for QNAME, because if so, we need to send a + * "no data" response as required by RFC 4035, section 3.1.4.1. + */ + dns_db_t *tdb = NULL; + dns_zone_t *tzone = NULL; + dns_dbversion_t *tversion = NULL; + isc_result_t tresult; + + tresult = query_getzonedb( + qctx->client, qctx->client->query.qname, qctx->qtype, + DNS_GETDB_PARTIAL, &tzone, &tdb, &tversion); + if (tresult == ISC_R_SUCCESS) { + /* + * We are authoritative for QNAME. Attach the relevant + * zone to query context, set result to ISC_R_SUCCESS. + */ + qctx->options &= ~DNS_GETDB_NOEXACT; + ns_client_putrdataset(qctx->client, &qctx->rdataset); + if (qctx->db != NULL) { + dns_db_detach(&qctx->db); + } + if (qctx->zone != NULL) { + dns_zone_detach(&qctx->zone); + } + qctx->version = NULL; + RESTORE(qctx->version, tversion); + RESTORE(qctx->db, tdb); + RESTORE(qctx->zone, tzone); + qctx->is_zone = true; + result = ISC_R_SUCCESS; + } else { + /* + * We are not authoritative for QNAME. Clean up and + * leave result as it was. + */ + if (tdb != NULL) { + dns_db_detach(&tdb); + } + if (tzone != NULL) { + dns_zone_detach(&tzone); + } + } + } + /* + * If we did not find a database from which we can answer the query, + * respond with either REFUSED or SERVFAIL, depending on what the + * result of query_getdb() was. + */ + if (result != ISC_R_SUCCESS) { + if (result == DNS_R_REFUSED) { + if (WANTRECURSION(qctx->client)) { + inc_stats(qctx->client, + ns_statscounter_recurserej); + } else { + inc_stats(qctx->client, + ns_statscounter_authrej); + } + if (!PARTIALANSWER(qctx->client)) { + QUERY_ERROR(qctx, DNS_R_REFUSED); + } + } else { + CCTRACE(ISC_LOG_ERROR, "ns__query_start: query_getdb " + "failed"); + QUERY_ERROR(qctx, result); + } + return (ns_query_done(qctx)); + } + + /* + * We found a database from which we can answer the query. Update + * relevant query context flags if the answer is to be prepared using + * authoritative data. + */ + qctx->is_staticstub_zone = false; + if (qctx->is_zone) { + qctx->authoritative = true; + if (qctx->zone != NULL) { + if (dns_zone_gettype(qctx->zone) == dns_zone_mirror) { + qctx->authoritative = false; + } + if (dns_zone_gettype(qctx->zone) == dns_zone_staticstub) + { + qctx->is_staticstub_zone = true; + } + } + } + + /* + * Attach to the database which will be used to prepare the answer. + * Update query statistics. + */ + if (qctx->event == NULL && qctx->client->query.restarts == 0) { + if (qctx->is_zone) { + if (qctx->zone != NULL) { + /* + * if is_zone = true, zone = NULL then this is + * a DLZ zone. Don't attempt to attach zone. + */ + dns_zone_attach(qctx->zone, + &qctx->client->query.authzone); + } + dns_db_attach(qctx->db, &qctx->client->query.authdb); + } + qctx->client->query.authdbset = true; + + /* Track TCP vs UDP stats per zone */ + if (TCP(qctx->client)) { + inc_stats(qctx->client, ns_statscounter_tcp); + } else { + inc_stats(qctx->client, ns_statscounter_udp); + } + } + + if (!qctx->is_zone && (qctx->view->staleanswerclienttimeout == 0) && + dns_view_staleanswerenabled(qctx->view)) + { + /* + * If stale answers are enabled and + * stale-answer-client-timeout is zero, then we can promptly + * answer with a stale RRset if one is available in cache. + */ + qctx->options |= DNS_GETDB_STALEFIRST; + } + + result = query_lookup(qctx); + + /* + * Clear "look-also-for-stale-data" flag. + * If a fetch is created to resolve this query, then, + * when it completes, this option is not expected to be set. + */ + qctx->options &= ~DNS_GETDB_STALEFIRST; + +cleanup: + return (result); +} + +/* + * Allocate buffers in 'qctx' used to store query results. + * + * 'buffer' must be a pointer to an object whose lifetime + * doesn't expire while 'qctx' is in use. + */ +static isc_result_t +qctx_prepare_buffers(query_ctx_t *qctx, isc_buffer_t *buffer) { + REQUIRE(qctx != NULL); + REQUIRE(qctx->client != NULL); + REQUIRE(buffer != NULL); + + qctx->dbuf = ns_client_getnamebuf(qctx->client); + if (ISC_UNLIKELY(qctx->dbuf == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_getnamebuf " + "failed"); + return (ISC_R_NOMEMORY); + } + + qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, buffer); + if (ISC_UNLIKELY(qctx->fname == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_newname failed"); + + return (ISC_R_NOMEMORY); + } + + qctx->rdataset = ns_client_newrdataset(qctx->client); + if (ISC_UNLIKELY(qctx->rdataset == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_newrdataset failed"); + goto error; + } + + if ((WANTDNSSEC(qctx->client) || qctx->findcoveringnsec) && + (!qctx->is_zone || dns_db_issecure(qctx->db))) + { + qctx->sigrdataset = ns_client_newrdataset(qctx->client); + if (qctx->sigrdataset == NULL) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: " + "ns_client_newrdataset failed (2)"); + goto error; + } + } + + return (ISC_R_SUCCESS); + +error: + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, &qctx->fname); + } + if (qctx->rdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->rdataset); + } + + return (ISC_R_NOMEMORY); +} + +/* + * Setup a new query context for resolving a query. + * + * This function is only called if both these conditions are met: + * 1. BIND is configured with stale-answer-client-timeout 0. + * 2. A stale RRset is found in cache during initial query + * database lookup. + * + * We continue with this function for refreshing/resolving an RRset + * after answering a client with stale data. + */ +static void +query_refresh_rrset(query_ctx_t *orig_qctx) { + isc_buffer_t buffer; + query_ctx_t qctx; + + REQUIRE(orig_qctx != NULL); + REQUIRE(orig_qctx->client != NULL); + + qctx_copy(orig_qctx, &qctx); + qctx.client->query.dboptions &= ~(DNS_DBFIND_STALETIMEOUT | + DNS_DBFIND_STALEOK | + DNS_DBFIND_STALEENABLED); + qctx.client->nodetach = false; + + /* + * We'll need some resources... + */ + if (qctx_prepare_buffers(&qctx, &buffer) != ISC_R_SUCCESS) { + dns_db_detach(&qctx.db); + qctx_destroy(&qctx); + return; + } + + /* + * Pretend we didn't find anything in cache. + */ + (void)query_gotanswer(&qctx, ISC_R_NOTFOUND); + + if (qctx.fname != NULL) { + ns_client_releasename(qctx.client, &qctx.fname); + } + if (qctx.rdataset != NULL) { + ns_client_putrdataset(qctx.client, &qctx.rdataset); + } + + qctx_destroy(&qctx); +} + +/*% + * Depending on the db lookup result, we can respond to the + * client this stale answer. + */ +static bool +stale_client_answer(isc_result_t result) { + switch (result) { + case ISC_R_SUCCESS: + case DNS_R_EMPTYNAME: + case DNS_R_NXRRSET: + case DNS_R_NCACHENXRRSET: + case DNS_R_CNAME: + case DNS_R_DNAME: + return (true); + default: + return (false); + } + + UNREACHABLE(); +} + +/*% + * Perform a local database lookup, in either an authoritative or + * cache database. If unable to answer, call ns_query_done(); otherwise + * hand off processing to query_gotanswer(). + */ +static isc_result_t +query_lookup(query_ctx_t *qctx) { + isc_buffer_t buffer; + isc_result_t result = ISC_R_UNSET; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_name_t *rpzqname = NULL; + char namebuf[DNS_NAME_FORMATSIZE]; + unsigned int dboptions; + dns_ttl_t stale_refresh = 0; + bool dbfind_stale = false; + bool stale_timeout = false; + bool answer_found = false; + bool stale_found = false; + bool stale_refresh_window = false; + + CCTRACE(ISC_LOG_DEBUG(3), "query_lookup"); + + CALL_HOOK(NS_QUERY_LOOKUP_BEGIN, qctx); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, qctx->client, + HAVEECS(qctx->client) ? &qctx->client->ecs : NULL, + NULL); + + /* + * We'll need some resources... + */ + result = qctx_prepare_buffers(qctx, &buffer); + if (result != ISC_R_SUCCESS) { + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + + /* + * Now look for an answer in the database. + */ + if (qctx->dns64 && qctx->rpz) { + rpzqname = qctx->client->query.rpz_st->p_name; + } else { + rpzqname = qctx->client->query.qname; + } + + if ((qctx->options & DNS_GETDB_STALEFIRST) != 0) { + /* + * If DNS_GETDB_STALEFIRST is set, it means that a stale + * RRset may be returned as part of this lookup. An attempt + * to refresh the RRset will still take place if an + * active RRset is not available. + */ + qctx->client->query.dboptions |= DNS_DBFIND_STALETIMEOUT; + } + + dboptions = qctx->client->query.dboptions; + if (!qctx->is_zone && qctx->findcoveringnsec && + (qctx->type != dns_rdatatype_null || !dns_name_istat(rpzqname))) + { + dboptions |= DNS_DBFIND_COVERINGNSEC; + } + + (void)dns_db_getservestalerefresh(qctx->client->view->cachedb, + &stale_refresh); + if (stale_refresh > 0 && + dns_view_staleanswerenabled(qctx->client->view)) + { + dboptions |= DNS_DBFIND_STALEENABLED; + } + + result = dns_db_findext(qctx->db, rpzqname, qctx->version, qctx->type, + dboptions, qctx->client->now, &qctx->node, + qctx->fname, &cm, &ci, qctx->rdataset, + qctx->sigrdataset); + + /* + * Fixup fname and sigrdataset. + */ + if (qctx->dns64 && qctx->rpz) { + dns_name_copynf(qctx->client->query.qname, qctx->fname); + if (qctx->sigrdataset != NULL && + dns_rdataset_isassociated(qctx->sigrdataset)) + { + dns_rdataset_disassociate(qctx->sigrdataset); + } + } + + if (!qctx->is_zone) { + dns_cache_updatestats(qctx->view->cache, result); + } + + /* + * If DNS_DBFIND_STALEOK is set this means we are dealing with a + * lookup following a failed lookup and it is okay to serve a stale + * answer. This will (re)start the 'stale-refresh-time' window in + * rbtdb, tracking the last time the RRset lookup failed. + */ + dbfind_stale = ((dboptions & DNS_DBFIND_STALEOK) != 0); + + /* + * If DNS_DBFIND_STALEENABLED is set, this may be a normal lookup, but + * we are allowed to immediately respond with a stale answer if the + * request is within the 'stale-refresh-time' window. + */ + stale_refresh_window = (STALE_WINDOW(qctx->rdataset) && + (dboptions & DNS_DBFIND_STALEENABLED) != 0); + + /* + * If DNS_DBFIND_STALETIMEOUT is set, a stale answer is requested. + * This can happen if 'stale-answer-client-timeout' is enabled. + * + * If 'stale-answer-client-timeout' is set to 0, and a stale + * answer is found, send it to the client, and try to refresh the + * RRset. + * + * If 'stale-answer-client-timeout' is non-zero, and a stale + * answer is found, send it to the client. Don't try to refresh the + * RRset because a fetch is already in progress. + */ + stale_timeout = ((dboptions & DNS_DBFIND_STALETIMEOUT) != 0); + + if (dns_rdataset_isassociated(qctx->rdataset) && + dns_rdataset_count(qctx->rdataset) > 0 && !STALE(qctx->rdataset)) + { + /* Found non-stale usable rdataset. */ + answer_found = true; + } + + if (dbfind_stale || stale_refresh_window || stale_timeout) { + dns_name_format(qctx->client->query.qname, namebuf, + sizeof(namebuf)); + + inc_stats(qctx->client, ns_statscounter_trystale); + + if (dns_rdataset_isassociated(qctx->rdataset) && + dns_rdataset_count(qctx->rdataset) > 0 && + STALE(qctx->rdataset)) + { + qctx->rdataset->ttl = qctx->view->staleanswerttl; + stale_found = true; + inc_stats(qctx->client, ns_statscounter_usedstale); + } else { + stale_found = false; + } + } + + if (dbfind_stale) { + isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s resolver failure, stale answer %s", namebuf, + stale_found ? "used" : "unavailable"); + if (!stale_found && !answer_found) { + /* + * Resolver failure, no stale data, nothing more we + * can do, return SERVFAIL. + */ + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + } else if (stale_refresh_window) { + /* + * A recent lookup failed, so during this time window we are + * allowed to return stale data immediately. + */ + isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s query within stale refresh time, stale " + "answer %s", + namebuf, stale_found ? "used" : "unavailable"); + + if (!stale_found && !answer_found) { + /* + * During the stale refresh window explicitly do not try + * to refresh the data, because a recent lookup failed. + */ + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + } else if (stale_timeout) { + if ((qctx->options & DNS_GETDB_STALEFIRST) != 0) { + if (!stale_found && !answer_found) { + /* + * We have nothing useful in cache to return + * immediately. + */ + qctx_clean(qctx); + qctx_freedata(qctx); + dns_db_attach(qctx->client->view->cachedb, + &qctx->db); + qctx->client->query.dboptions &= + ~DNS_DBFIND_STALETIMEOUT; + qctx->options &= ~DNS_GETDB_STALEFIRST; + if (qctx->client->query.fetch != NULL) { + dns_resolver_destroyfetch( + &qctx->client->query.fetch); + } + return (query_lookup(qctx)); + } else if (stale_client_answer(result)) { + /* + * Immediately return the stale answer, start a + * resolver fetch to refresh the data in cache. + */ + isc_log_write( + ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s stale answer used, an attempt to " + "refresh the RRset will still be made", + namebuf); + + qctx->refresh_rrset = STALE(qctx->rdataset); + + /* + * If we are refreshing the RRSet, we must not + * detach from the client in query_send(). + */ + qctx->client->nodetach = qctx->refresh_rrset; + } + } else { + /* + * The 'stale-answer-client-timeout' triggered, return + * the stale answer if available, otherwise wait until + * the resolver finishes. + */ + isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s client timeout, stale answer %s", + namebuf, + stale_found ? "used" : "unavailable"); + if (!stale_found && !answer_found) { + return (result); + } + + if (!stale_client_answer(result)) { + return (result); + } + + /* + * There still might be real answer later. Mark the + * query so we'll know we can skip answering. + */ + qctx->client->query.attributes |= + NS_QUERYATTR_STALEPENDING; + } + } + + if (stale_timeout && (answer_found || stale_found)) { + /* + * Mark RRsets that we are adding to the client message on a + * lookup during 'stale-answer-client-timeout', so we can + * clean it up if needed when we resume from recursion. + */ + qctx->client->query.attributes |= NS_QUERYATTR_STALEOK; + qctx->rdataset->attributes |= DNS_RDATASETATTR_STALE_ADDED; + } + + result = query_gotanswer(qctx, result); + +cleanup: + return (result); +} + +/* + * Clear all rdatasets from the message that are in the given section and + * that have the 'attr' attribute set. + */ +static void +message_clearrdataset(dns_message_t *msg, unsigned int attr) { + unsigned int i; + dns_name_t *name, *next_name; + dns_rdataset_t *rds, *next_rds; + + /* + * Clean up name lists by calling the rdataset disassociate function. + */ + for (i = DNS_SECTION_ANSWER; i < DNS_SECTION_MAX; i++) { + name = ISC_LIST_HEAD(msg->sections[i]); + while (name != NULL) { + next_name = ISC_LIST_NEXT(name, link); + + rds = ISC_LIST_HEAD(name->list); + while (rds != NULL) { + next_rds = ISC_LIST_NEXT(rds, link); + if ((rds->attributes & attr) != attr) { + rds = next_rds; + continue; + } + ISC_LIST_UNLINK(name->list, rds, link); + INSIST(dns_rdataset_isassociated(rds)); + dns_rdataset_disassociate(rds); + isc_mempool_put(msg->rdspool, rds); + rds = next_rds; + } + + if (ISC_LIST_EMPTY(name->list)) { + ISC_LIST_UNLINK(msg->sections[i], name, link); + if (dns_name_dynamic(name)) { + dns_name_free(name, msg->mctx); + } + isc_mempool_put(msg->namepool, name); + } + + name = next_name; + } + } +} + +/* + * Clear any rdatasets from the client's message that were added on a lookup + * due to a client timeout. + */ +static void +query_clear_stale(ns_client_t *client) { + message_clearrdataset(client->message, DNS_RDATASETATTR_STALE_ADDED); +} + +/* + * Create a new query context with the sole intent of looking up for a stale + * RRset in cache. If an entry is found, we mark the original query as + * answered, in order to avoid answering the query twice, when the original + * fetch finishes. + */ +static void +query_lookup_stale(ns_client_t *client) { + query_ctx_t qctx; + + qctx_init(client, NULL, client->query.qtype, &qctx); + dns_db_attach(client->view->cachedb, &qctx.db); + client->query.attributes &= ~NS_QUERYATTR_RECURSIONOK; + client->query.dboptions |= DNS_DBFIND_STALETIMEOUT; + client->nodetach = true; + (void)query_lookup(&qctx); + if (qctx.node != NULL) { + dns_db_detachnode(qctx.db, &qctx.node); + } + qctx_freedata(&qctx); + qctx_destroy(&qctx); +} + +/* + * Event handler to resume processing a query after recursion, or when a + * client timeout is triggered. If the query has timed out or been cancelled + * or the system is shutting down, clean up and exit. If a client timeout is + * triggered, see if we can respond with a stale answer from cache. Otherwise, + * call query_resume() to continue the ongoing work. + */ +static void +fetch_callback(isc_task_t *task, isc_event_t *event) { + dns_fetchevent_t *devent = (dns_fetchevent_t *)event; + dns_fetch_t *fetch = NULL; + ns_client_t *client = NULL; + bool fetch_canceled = false; + bool fetch_answered = false; + bool client_shuttingdown = false; + isc_logcategory_t *logcategory = NS_LOGCATEGORY_QUERY_ERRORS; + isc_result_t result; + int errorloglevel; + query_ctx_t qctx; + + UNUSED(task); + + REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE || + event->ev_type == DNS_EVENT_TRYSTALE); + + client = devent->ev_arg; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(task == client->task); + REQUIRE(RECURSING(client)); + + CTRACE(ISC_LOG_DEBUG(3), "fetch_callback"); + + if (event->ev_type == DNS_EVENT_TRYSTALE) { + if (devent->result != ISC_R_CANCELED) { + query_lookup_stale(client); + } + isc_event_free(ISC_EVENT_PTR(&event)); + return; + } + /* + * We are resuming from recursion. Reset any attributes, options + * that a lookup due to stale-answer-client-timeout may have set. + */ + if (client->view->cachedb != NULL && client->view->recursion) { + client->query.attributes |= NS_QUERYATTR_RECURSIONOK; + } + client->query.fetchoptions &= ~DNS_FETCHOPT_TRYSTALE_ONTIMEOUT; + client->query.dboptions &= ~DNS_DBFIND_STALETIMEOUT; + client->nodetach = false; + + LOCK(&client->query.fetchlock); + INSIST(client->query.fetch == devent->fetch || + client->query.fetch == NULL); + if (QUERY_STALEPENDING(&client->query)) { + /* + * We've gotten an authoritative answer to a query that + * was left pending after a stale timeout. We don't need + * to do anything with it; free all the data and go home. + */ + client->query.fetch = NULL; + fetch_answered = true; + } else if (client->query.fetch != NULL) { + /* + * This is the fetch we've been waiting for. + */ + INSIST(devent->fetch == client->query.fetch); + client->query.fetch = NULL; + + /* + * Update client->now. + */ + isc_stdtime_get(&client->now); + } else { + /* + * This is a fetch completion event for a canceled fetch. + * Clean up and don't resume the find. + */ + fetch_canceled = true; + } + UNLOCK(&client->query.fetchlock); + + SAVE(fetch, devent->fetch); + + /* + * We're done recursing, detach from quota and unlink from + * the manager's recursing-clients list. + */ + + if (client->recursionquota != NULL) { + isc_quota_detach(&client->recursionquota); + ns_stats_decrement(client->sctx->nsstats, + ns_statscounter_recursclients); + } + + LOCK(&client->manager->reclock); + if (ISC_LINK_LINKED(client, rlink)) { + ISC_LIST_UNLINK(client->manager->recursing, client, rlink); + } + UNLOCK(&client->manager->reclock); + + isc_nmhandle_detach(&client->fetchhandle); + + client->query.attributes &= ~NS_QUERYATTR_RECURSING; + client->state = NS_CLIENTSTATE_WORKING; + + /* + * Initialize a new qctx and use it to either resume from + * recursion or clean up after cancelation. Transfer + * ownership of devent to the new qctx in the process. + */ + qctx_init(client, &devent, 0, &qctx); + + client_shuttingdown = ns_client_shuttingdown(client); + if (fetch_canceled || fetch_answered || client_shuttingdown) { + /* + * We've timed out or are shutting down. We can now + * free the event and other resources held by qctx, but + * don't call qctx_destroy() yet: it might destroy the + * client, which we still need for a moment. + */ + qctx_freedata(&qctx); + + /* + * Return an error to the client, or just drop. + */ + if (fetch_canceled) { + CTRACE(ISC_LOG_ERROR, "fetch cancelled"); + query_error(client, DNS_R_SERVFAIL, __LINE__); + } else { + query_next(client, ISC_R_CANCELED); + } + + /* + * Free any persistent plugin data that was allocated to + * service the client, then detach the client object. + */ + qctx.detach_client = true; + qctx_destroy(&qctx); + } else { + /* + * Resume the find process. + */ + query_trace(&qctx); + + result = query_resume(&qctx); + if (result != ISC_R_SUCCESS) { + if (result == DNS_R_SERVFAIL) { + errorloglevel = ISC_LOG_DEBUG(2); + } else { + errorloglevel = ISC_LOG_DEBUG(4); + } + if (isc_log_wouldlog(ns_lctx, errorloglevel)) { + dns_resolver_logfetch(fetch, ns_lctx, + logcategory, + NS_LOGMODULE_QUERY, + errorloglevel, false); + } + } + + qctx_destroy(&qctx); + } + + dns_resolver_destroyfetch(&fetch); +} + +/*% + * Check whether the recursion parameters in 'param' match the current query's + * recursion parameters provided in 'qtype', 'qname', and 'qdomain'. + */ +static bool +recparam_match(const ns_query_recparam_t *param, dns_rdatatype_t qtype, + const dns_name_t *qname, const dns_name_t *qdomain) { + REQUIRE(param != NULL); + + return (param->qtype == qtype && param->qname != NULL && + qname != NULL && param->qdomain != NULL && qdomain != NULL && + dns_name_equal(param->qname, qname) && + dns_name_equal(param->qdomain, qdomain)); +} + +/*% + * Update 'param' with current query's recursion parameters provided in + * 'qtype', 'qname', and 'qdomain'. + */ +static void +recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype, + const dns_name_t *qname, const dns_name_t *qdomain) { + REQUIRE(param != NULL); + + param->qtype = qtype; + + if (qname == NULL) { + param->qname = NULL; + } else { + param->qname = dns_fixedname_initname(¶m->fqname); + dns_name_copynf(qname, param->qname); + } + + if (qdomain == NULL) { + param->qdomain = NULL; + } else { + param->qdomain = dns_fixedname_initname(¶m->fqdomain); + dns_name_copynf(qdomain, param->qdomain); + } +} +static atomic_uint_fast32_t last_soft, last_hard; + +isc_result_t +ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, + dns_name_t *qdomain, dns_rdataset_t *nameservers, + bool resuming) { + isc_result_t result; + dns_rdataset_t *rdataset, *sigrdataset; + isc_sockaddr_t *peeraddr = NULL; + + CTRACE(ISC_LOG_DEBUG(3), "ns_query_recurse"); + + /* + * Check recursion parameters from the previous query to see if they + * match. If not, update recursion parameters and proceed. + */ + if (recparam_match(&client->query.recparam, qtype, qname, qdomain)) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, + ISC_LOG_INFO, "recursion loop detected"); + return (ISC_R_ALREADYRUNNING); + } + + recparam_update(&client->query.recparam, qtype, qname, qdomain); + + if (!resuming) { + inc_stats(client, ns_statscounter_recursion); + } + + /* + * We are about to recurse, which means that this client will + * be unavailable for serving new requests for an indeterminate + * amount of time. If this client is currently responsible + * for handling incoming queries, set up a new client + * object to handle them while we are waiting for a + * response. There is no need to replace TCP clients + * because those have already been replaced when the + * connection was accepted (if allowed by the TCP quota). + */ + if (client->recursionquota == NULL) { + result = isc_quota_attach(&client->sctx->recursionquota, + &client->recursionquota); + if (result == ISC_R_SUCCESS || result == ISC_R_SOFTQUOTA) { + ns_stats_increment(client->sctx->nsstats, + ns_statscounter_recursclients); + } + + if (result == ISC_R_SOFTQUOTA) { + isc_stdtime_t now; + isc_stdtime_get(&now); + if (now != atomic_load_relaxed(&last_soft)) { + atomic_store_relaxed(&last_soft, now); + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, + ISC_LOG_WARNING, + "recursive-clients soft limit " + "exceeded (%u/%u/%u), " + "aborting oldest query", + isc_quota_getused( + client->recursionquota), + isc_quota_getsoft( + client->recursionquota), + isc_quota_getmax( + client->recursionquota)); + } + ns_client_killoldestquery(client); + result = ISC_R_SUCCESS; + } else if (result == ISC_R_QUOTA) { + isc_stdtime_t now; + isc_stdtime_get(&now); + if (now != atomic_load_relaxed(&last_hard)) { + ns_server_t *sctx = client->sctx; + atomic_store_relaxed(&last_hard, now); + ns_client_log( + client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, ISC_LOG_WARNING, + "no more recursive clients " + "(%u/%u/%u): %s", + isc_quota_getused( + &sctx->recursionquota), + isc_quota_getsoft( + &sctx->recursionquota), + isc_quota_getmax(&sctx->recursionquota), + isc_result_totext(result)); + } + ns_client_killoldestquery(client); + } + if (result != ISC_R_SUCCESS) { + return (result); + } + + dns_message_clonebuffer(client->message); + ns_client_recursing(client); + } + + /* + * Invoke the resolver. + */ + REQUIRE(nameservers == NULL || nameservers->type == dns_rdatatype_ns); + REQUIRE(client->query.fetch == NULL); + + rdataset = ns_client_newrdataset(client); + if (rdataset == NULL) { + return (ISC_R_NOMEMORY); + } + + if (WANTDNSSEC(client)) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + ns_client_putrdataset(client, &rdataset); + return (ISC_R_NOMEMORY); + } + } else { + sigrdataset = NULL; + } + + if (!client->query.timerset) { + ns_client_settimeout(client, 60); + } + + if (!TCP(client)) { + peeraddr = &client->peeraddr; + } + + if (client->view->staleanswerclienttimeout > 0 && + client->view->staleanswerclienttimeout != (uint32_t)-1 && + dns_view_staleanswerenabled(client->view)) + { + client->query.fetchoptions |= DNS_FETCHOPT_TRYSTALE_ONTIMEOUT; + } + + isc_nmhandle_attach(client->handle, &client->fetchhandle); + result = dns_resolver_createfetch( + client->view->resolver, qname, qtype, qdomain, nameservers, + NULL, peeraddr, client->message->id, client->query.fetchoptions, + 0, NULL, client->task, fetch_callback, client, rdataset, + sigrdataset, &client->query.fetch); + if (result != ISC_R_SUCCESS) { + isc_nmhandle_detach(&client->fetchhandle); + ns_client_putrdataset(client, &rdataset); + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + } + + /* + * We're now waiting for a fetch event. A client which is + * shutting down will not be destroyed until all the events + * have been received. + */ + + return (result); +} + +/*% + * Restores the query context after resuming from recursion, and + * continues the query processing if needed. + */ +static isc_result_t +query_resume(query_ctx_t *qctx) { + isc_result_t result; + dns_name_t *tname; + isc_buffer_t b; +#ifdef WANT_QUERYTRACE + char mbuf[4 * DNS_NAME_FORMATSIZE]; + char qbuf[DNS_NAME_FORMATSIZE]; + char tbuf[DNS_RDATATYPE_FORMATSIZE]; +#endif /* ifdef WANT_QUERYTRACE */ + + CCTRACE(ISC_LOG_DEBUG(3), "query_resume"); + + CALL_HOOK(NS_QUERY_RESUME_BEGIN, qctx); + + qctx->want_restart = false; + + qctx->rpz_st = qctx->client->query.rpz_st; + if (qctx->rpz_st != NULL && + (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0) + { + CCTRACE(ISC_LOG_DEBUG(3), "resume from RPZ recursion"); +#ifdef WANT_QUERYTRACE + { + char pbuf[DNS_NAME_FORMATSIZE] = "<unset>"; + char fbuf[DNS_NAME_FORMATSIZE] = "<unset>"; + if (qctx->rpz_st->r_name != NULL) { + dns_name_format(qctx->rpz_st->r_name, qbuf, + sizeof(qbuf)); + } else { + snprintf(qbuf, sizeof(qbuf), "<unset>"); + } + if (qctx->rpz_st->p_name != NULL) { + dns_name_format(qctx->rpz_st->p_name, pbuf, + sizeof(pbuf)); + } + if (qctx->rpz_st->fname != NULL) { + dns_name_format(qctx->rpz_st->fname, fbuf, + sizeof(fbuf)); + } + + snprintf(mbuf, sizeof(mbuf) - 1, + "rpz rname:%s, pname:%s, qctx->fname:%s", qbuf, + pbuf, fbuf); + CCTRACE(ISC_LOG_DEBUG(3), mbuf); + } +#endif /* ifdef WANT_QUERYTRACE */ + + qctx->is_zone = qctx->rpz_st->q.is_zone; + qctx->authoritative = qctx->rpz_st->q.authoritative; + RESTORE(qctx->zone, qctx->rpz_st->q.zone); + RESTORE(qctx->node, qctx->rpz_st->q.node); + RESTORE(qctx->db, qctx->rpz_st->q.db); + RESTORE(qctx->rdataset, qctx->rpz_st->q.rdataset); + RESTORE(qctx->sigrdataset, qctx->rpz_st->q.sigrdataset); + qctx->qtype = qctx->rpz_st->q.qtype; + + if (qctx->event->node != NULL) { + dns_db_detachnode(qctx->event->db, &qctx->event->node); + } + SAVE(qctx->rpz_st->r.db, qctx->event->db); + qctx->rpz_st->r.r_type = qctx->event->qtype; + SAVE(qctx->rpz_st->r.r_rdataset, qctx->event->rdataset); + ns_client_putrdataset(qctx->client, &qctx->event->sigrdataset); + } else if (REDIRECT(qctx->client)) { + /* + * Restore saved state. + */ + CCTRACE(ISC_LOG_DEBUG(3), "resume from redirect recursion"); +#ifdef WANT_QUERYTRACE + dns_name_format(qctx->client->query.redirect.fname, qbuf, + sizeof(qbuf)); + dns_rdatatype_format(qctx->client->query.redirect.qtype, tbuf, + sizeof(tbuf)); + snprintf(mbuf, sizeof(mbuf) - 1, + "redirect qctx->fname:%s, qtype:%s, auth:%d", qbuf, + tbuf, qctx->client->query.redirect.authoritative); + CCTRACE(ISC_LOG_DEBUG(3), mbuf); +#endif /* ifdef WANT_QUERYTRACE */ + qctx->qtype = qctx->client->query.redirect.qtype; + INSIST(qctx->client->query.redirect.rdataset != NULL); + RESTORE(qctx->rdataset, qctx->client->query.redirect.rdataset); + RESTORE(qctx->sigrdataset, + qctx->client->query.redirect.sigrdataset); + RESTORE(qctx->db, qctx->client->query.redirect.db); + RESTORE(qctx->node, qctx->client->query.redirect.node); + RESTORE(qctx->zone, qctx->client->query.redirect.zone); + qctx->authoritative = + qctx->client->query.redirect.authoritative; + + /* + * Free resources used while recursing. + */ + ns_client_putrdataset(qctx->client, &qctx->event->rdataset); + ns_client_putrdataset(qctx->client, &qctx->event->sigrdataset); + if (qctx->event->node != NULL) { + dns_db_detachnode(qctx->event->db, &qctx->event->node); + } + if (qctx->event->db != NULL) { + dns_db_detach(&qctx->event->db); + } + } else { + CCTRACE(ISC_LOG_DEBUG(3), "resume from normal recursion"); + qctx->authoritative = false; + + qctx->qtype = qctx->event->qtype; + SAVE(qctx->db, qctx->event->db); + SAVE(qctx->node, qctx->event->node); + SAVE(qctx->rdataset, qctx->event->rdataset); + SAVE(qctx->sigrdataset, qctx->event->sigrdataset); + } + INSIST(qctx->rdataset != NULL); + + if (qctx->qtype == dns_rdatatype_rrsig || + qctx->qtype == dns_rdatatype_sig) + { + qctx->type = dns_rdatatype_any; + } else { + qctx->type = qctx->qtype; + } + + CALL_HOOK(NS_QUERY_RESUME_RESTORED, qctx); + + if (DNS64(qctx->client)) { + qctx->client->query.attributes &= ~NS_QUERYATTR_DNS64; + qctx->dns64 = true; + } + + if (DNS64EXCLUDE(qctx->client)) { + qctx->client->query.attributes &= ~NS_QUERYATTR_DNS64EXCLUDE; + qctx->dns64_exclude = true; + } + + if (qctx->rpz_st != NULL && + (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0) + { + /* + * Has response policy changed out from under us? + */ + if (qctx->rpz_st->rpz_ver != qctx->view->rpzs->rpz_ver) { + ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, DNS_RPZ_INFO_LEVEL, + "query_resume: RPZ settings " + "out of date " + "(rpz_ver %d, expected %d)", + qctx->view->rpzs->rpz_ver, + qctx->rpz_st->rpz_ver); + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + } + + /* + * We'll need some resources... + */ + qctx->dbuf = ns_client_getnamebuf(qctx->client); + if (qctx->dbuf == NULL) { + CCTRACE(ISC_LOG_ERROR, "query_resume: ns_client_getnamebuf " + "failed (1)"); + QUERY_ERROR(qctx, ISC_R_NOMEMORY); + return (ns_query_done(qctx)); + } + + qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b); + if (qctx->fname == NULL) { + CCTRACE(ISC_LOG_ERROR, "query_resume: ns_client_newname failed " + "(1)"); + QUERY_ERROR(qctx, ISC_R_NOMEMORY); + return (ns_query_done(qctx)); + } + + if (qctx->rpz_st != NULL && + (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0) + { + tname = qctx->rpz_st->fname; + } else if (REDIRECT(qctx->client)) { + tname = qctx->client->query.redirect.fname; + } else { + tname = dns_fixedname_name(&qctx->event->foundname); + } + + dns_name_copynf(tname, qctx->fname); + + if (qctx->rpz_st != NULL && + (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0) + { + qctx->rpz_st->r.r_result = qctx->event->result; + result = qctx->rpz_st->q.result; + free_devent(qctx->client, ISC_EVENT_PTR(&qctx->event), + &qctx->event); + } else if (REDIRECT(qctx->client)) { + result = qctx->client->query.redirect.result; + } else { + result = qctx->event->result; + } + + qctx->resuming = true; + + return (query_gotanswer(qctx, result)); + +cleanup: + return (result); +} + +/*% + * If the query is recursive, check the SERVFAIL cache to see whether + * identical queries have failed recently. If we find a match, and it was + * from a query with CD=1, *or* if the current query has CD=0, then we just + * return SERVFAIL again. This prevents a validation failure from eliciting a + * SERVFAIL response to a CD=1 query. + */ +isc_result_t +ns__query_sfcache(query_ctx_t *qctx) { + bool failcache; + uint32_t flags; + + /* + * The SERVFAIL cache doesn't apply to authoritative queries. + */ + if (!RECURSIONOK(qctx->client)) { + return (ISC_R_COMPLETE); + } + + flags = 0; +#ifdef ENABLE_AFL + if (qctx->client->sctx->fuzztype == isc_fuzz_resolver) { + failcache = false; + } else { + failcache = dns_badcache_find( + qctx->view->failcache, qctx->client->query.qname, + qctx->qtype, &flags, &qctx->client->tnow); + } +#else /* ifdef ENABLE_AFL */ + failcache = dns_badcache_find(qctx->view->failcache, + qctx->client->query.qname, qctx->qtype, + &flags, &qctx->client->tnow); +#endif /* ifdef ENABLE_AFL */ + if (failcache && + (((flags & NS_FAILCACHE_CD) != 0) || + ((qctx->client->message->flags & DNS_MESSAGEFLAG_CD) == 0))) + { + if (isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1))) { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + + dns_name_format(qctx->client->query.qname, namebuf, + sizeof(namebuf)); + dns_rdatatype_format(qctx->qtype, typebuf, + sizeof(typebuf)); + ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_QUERY, ISC_LOG_DEBUG(1), + "servfail cache hit %s/%s (%s)", namebuf, + typebuf, + ((flags & NS_FAILCACHE_CD) != 0) ? "CD=1" + : "CD=" + "0"); + } + + qctx->client->attributes |= NS_CLIENTATTR_NOSETFC; + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + return (ISC_R_COMPLETE); +} + +/*% + * Handle response rate limiting (RRL). + */ +static isc_result_t +query_checkrrl(query_ctx_t *qctx, isc_result_t result) { + /* + * Rate limit these responses to this client. + * Do not delay counting and handling obvious referrals, + * since those won't come here again. + * Delay handling delegations for which we are certain to recurse and + * return here (DNS_R_DELEGATION, not a child of one of our + * own zones, and recursion enabled) + * Don't mess with responses rewritten by RPZ + * Count each response at most once. + */ + + /* + * XXXMPA the rrl system tests fails sometimes and RRL_CHECKED + * is set when we are called the second time preventing the + * response being dropped. + */ + ns_client_log( + qctx->client, DNS_LOGCATEGORY_RRL, NS_LOGMODULE_QUERY, + ISC_LOG_DEBUG(99), + "rrl=%p, HAVECOOKIE=%u, result=%s, " + "fname=%p(%u), is_zone=%u, RECURSIONOK=%u, " + "query.rpz_st=%p(%u), RRL_CHECKED=%u\n", + qctx->client->view->rrl, HAVECOOKIE(qctx->client), + isc_result_toid(result), qctx->fname, + qctx->fname != NULL ? dns_name_isabsolute(qctx->fname) : 0, + qctx->is_zone, RECURSIONOK(qctx->client), + qctx->client->query.rpz_st, + qctx->client->query.rpz_st != NULL + ? ((qctx->client->query.rpz_st->state & + DNS_RPZ_REWRITTEN) != 0) + : 0, + (qctx->client->query.attributes & NS_QUERYATTR_RRL_CHECKED) != + 0); + + if (qctx->view->rrl != NULL && !HAVECOOKIE(qctx->client) && + ((qctx->fname != NULL && dns_name_isabsolute(qctx->fname)) || + (result == ISC_R_NOTFOUND && !RECURSIONOK(qctx->client))) && + !(result == DNS_R_DELEGATION && !qctx->is_zone && + RECURSIONOK(qctx->client)) && + (qctx->client->query.rpz_st == NULL || + (qctx->client->query.rpz_st->state & DNS_RPZ_REWRITTEN) == 0) && + (qctx->client->query.attributes & NS_QUERYATTR_RRL_CHECKED) == 0) + { + dns_rdataset_t nc_rdataset; + bool wouldlog; + dns_fixedname_t fixed; + const dns_name_t *constname; + char log_buf[DNS_RRL_LOG_BUF_LEN]; + isc_result_t nc_result, resp_result; + dns_rrl_result_t rrl_result; + + qctx->client->query.attributes |= NS_QUERYATTR_RRL_CHECKED; + + wouldlog = isc_log_wouldlog(ns_lctx, DNS_RRL_LOG_DROP); + constname = qctx->fname; + if (result == DNS_R_NXDOMAIN) { + /* + * Use the database origin name to rate limit NXDOMAIN + */ + if (qctx->db != NULL) { + constname = dns_db_origin(qctx->db); + } + resp_result = result; + } else if (result == DNS_R_NCACHENXDOMAIN && + qctx->rdataset != NULL && + dns_rdataset_isassociated(qctx->rdataset) && + (qctx->rdataset->attributes & + DNS_RDATASETATTR_NEGATIVE) != 0) + { + /* + * Try to use owner name in the negative cache SOA. + */ + dns_fixedname_init(&fixed); + dns_rdataset_init(&nc_rdataset); + for (nc_result = dns_rdataset_first(qctx->rdataset); + nc_result == ISC_R_SUCCESS; + nc_result = dns_rdataset_next(qctx->rdataset)) + { + dns_ncache_current(qctx->rdataset, + dns_fixedname_name(&fixed), + &nc_rdataset); + if (nc_rdataset.type == dns_rdatatype_soa) { + dns_rdataset_disassociate(&nc_rdataset); + constname = dns_fixedname_name(&fixed); + break; + } + dns_rdataset_disassociate(&nc_rdataset); + } + resp_result = DNS_R_NXDOMAIN; + } else if (result == DNS_R_NXRRSET || result == DNS_R_EMPTYNAME) + { + resp_result = DNS_R_NXRRSET; + } else if (result == DNS_R_DELEGATION) { + resp_result = result; + } else if (result == ISC_R_NOTFOUND) { + /* + * Handle referral to ".", including when recursion + * is off or not requested and the hints have not + * been loaded or we have "additional-from-cache no". + */ + constname = dns_rootname; + resp_result = DNS_R_DELEGATION; + } else { + resp_result = ISC_R_SUCCESS; + } + + rrl_result = dns_rrl( + qctx->view, qctx->zone, &qctx->client->peeraddr, + TCP(qctx->client), qctx->client->message->rdclass, + qctx->qtype, constname, resp_result, qctx->client->now, + wouldlog, log_buf, sizeof(log_buf)); + if (rrl_result != DNS_RRL_RESULT_OK) { + /* + * Log dropped or slipped responses in the query + * category so that requests are not silently lost. + * Starts of rate-limited bursts are logged in + * DNS_LOGCATEGORY_RRL. + * + * Dropped responses are counted with dropped queries + * in QryDropped while slipped responses are counted + * with other truncated responses in RespTruncated. + */ + if (wouldlog) { + ns_client_log(qctx->client, DNS_LOGCATEGORY_RRL, + NS_LOGMODULE_QUERY, + DNS_RRL_LOG_DROP, "%s", log_buf); + } + + if (!qctx->view->rrl->log_only) { + if (rrl_result == DNS_RRL_RESULT_DROP) { + /* + * These will also be counted in + * ns_statscounter_dropped + */ + inc_stats(qctx->client, + ns_statscounter_ratedropped); + QUERY_ERROR(qctx, DNS_R_DROP); + } else { + /* + * These will also be counted in + * ns_statscounter_truncatedresp + */ + inc_stats(qctx->client, + ns_statscounter_rateslipped); + if (WANTCOOKIE(qctx->client)) { + qctx->client->message->flags &= + ~DNS_MESSAGEFLAG_AA; + qctx->client->message->flags &= + ~DNS_MESSAGEFLAG_AD; + qctx->client->message->rcode = + dns_rcode_badcookie; + } else { + qctx->client->message->flags |= + DNS_MESSAGEFLAG_TC; + if (resp_result == + DNS_R_NXDOMAIN) + { + qctx->client->message + ->rcode = + dns_rcode_nxdomain; + } + } + } + return (DNS_R_DROP); + } + } + } + + return (ISC_R_SUCCESS); +} + +/*% + * Do any RPZ rewriting that may be needed for this query. + */ +static isc_result_t +query_checkrpz(query_ctx_t *qctx, isc_result_t result) { + isc_result_t rresult; + + CCTRACE(ISC_LOG_DEBUG(3), "query_checkrpz"); + + rresult = rpz_rewrite(qctx->client, qctx->qtype, result, qctx->resuming, + qctx->rdataset, qctx->sigrdataset); + qctx->rpz_st = qctx->client->query.rpz_st; + switch (rresult) { + case ISC_R_SUCCESS: + break; + case ISC_R_NOTFOUND: + case DNS_R_DISALLOWED: + return (result); + case DNS_R_DELEGATION: + /* + * recursing for NS names or addresses, + * so save the main query state + */ + INSIST(!RECURSING(qctx->client)); + qctx->rpz_st->q.qtype = qctx->qtype; + qctx->rpz_st->q.is_zone = qctx->is_zone; + qctx->rpz_st->q.authoritative = qctx->authoritative; + SAVE(qctx->rpz_st->q.zone, qctx->zone); + SAVE(qctx->rpz_st->q.db, qctx->db); + SAVE(qctx->rpz_st->q.node, qctx->node); + SAVE(qctx->rpz_st->q.rdataset, qctx->rdataset); + SAVE(qctx->rpz_st->q.sigrdataset, qctx->sigrdataset); + dns_name_copynf(qctx->fname, qctx->rpz_st->fname); + qctx->rpz_st->q.result = result; + qctx->client->query.attributes |= NS_QUERYATTR_RECURSING; + return (ISC_R_COMPLETE); + default: + QUERY_ERROR(qctx, rresult); + return (ISC_R_COMPLETE); + } + + if (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_MISS) { + qctx->rpz_st->state |= DNS_RPZ_REWRITTEN; + } + + if (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_MISS && + qctx->rpz_st->m.policy != DNS_RPZ_POLICY_PASSTHRU && + (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_TCP_ONLY || + !TCP(qctx->client)) && + qctx->rpz_st->m.policy != DNS_RPZ_POLICY_ERROR) + { + /* + * We got a hit and are going to answer with our + * fiction. Ensure that we answer with the name + * we looked up even if we were stopped short + * in recursion or for a deferral. + */ + dns_name_copynf(qctx->client->query.qname, qctx->fname); + rpz_clean(&qctx->zone, &qctx->db, &qctx->node, NULL); + if (qctx->rpz_st->m.rdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->rdataset); + RESTORE(qctx->rdataset, qctx->rpz_st->m.rdataset); + } else { + qctx_clean(qctx); + } + qctx->version = NULL; + + RESTORE(qctx->node, qctx->rpz_st->m.node); + RESTORE(qctx->db, qctx->rpz_st->m.db); + RESTORE(qctx->version, qctx->rpz_st->m.version); + RESTORE(qctx->zone, qctx->rpz_st->m.zone); + + /* + * Add SOA record to additional section + */ + if (qctx->rpz_st->m.rpz->addsoa) { + bool override_ttl = + dns_rdataset_isassociated(qctx->rdataset); + rresult = query_addsoa(qctx, override_ttl, + DNS_SECTION_ADDITIONAL); + if (rresult != ISC_R_SUCCESS) { + QUERY_ERROR(qctx, result); + return (ISC_R_COMPLETE); + } + } + + switch (qctx->rpz_st->m.policy) { + case DNS_RPZ_POLICY_TCP_ONLY: + qctx->client->message->flags |= DNS_MESSAGEFLAG_TC; + if (result == DNS_R_NXDOMAIN || + result == DNS_R_NCACHENXDOMAIN) + { + qctx->client->message->rcode = + dns_rcode_nxdomain; + } + rpz_log_rewrite(qctx->client, false, + qctx->rpz_st->m.policy, + qctx->rpz_st->m.type, qctx->zone, + qctx->rpz_st->p_name, NULL, + qctx->rpz_st->m.rpz->num); + return (ISC_R_COMPLETE); + case DNS_RPZ_POLICY_DROP: + QUERY_ERROR(qctx, DNS_R_DROP); + rpz_log_rewrite(qctx->client, false, + qctx->rpz_st->m.policy, + qctx->rpz_st->m.type, qctx->zone, + qctx->rpz_st->p_name, NULL, + qctx->rpz_st->m.rpz->num); + return (ISC_R_COMPLETE); + case DNS_RPZ_POLICY_NXDOMAIN: + result = DNS_R_NXDOMAIN; + qctx->nxrewrite = true; + qctx->rpz = true; + break; + case DNS_RPZ_POLICY_NODATA: + qctx->nxrewrite = true; + FALLTHROUGH; + case DNS_RPZ_POLICY_DNS64: + result = DNS_R_NXRRSET; + qctx->rpz = true; + break; + case DNS_RPZ_POLICY_RECORD: + result = qctx->rpz_st->m.result; + if (qctx->qtype == dns_rdatatype_any && + result != DNS_R_CNAME) + { + /* + * We will add all of the rdatasets of + * the node by iterating later, + * and set the TTL then. + */ + if (dns_rdataset_isassociated(qctx->rdataset)) { + dns_rdataset_disassociate( + qctx->rdataset); + } + } else { + /* + * We will add this rdataset. + */ + qctx->rdataset->ttl = + ISC_MIN(qctx->rdataset->ttl, + qctx->rpz_st->m.ttl); + } + qctx->rpz = true; + break; + case DNS_RPZ_POLICY_WILDCNAME: { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_cname_t cname; + result = dns_rdataset_first(qctx->rdataset); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdataset_current(qctx->rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &cname, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&rdata); + result = query_rpzcname(qctx, &cname.cname); + if (result != ISC_R_SUCCESS) { + return (ISC_R_COMPLETE); + } + qctx->fname = NULL; + qctx->want_restart = true; + return (ISC_R_COMPLETE); + } + case DNS_RPZ_POLICY_CNAME: + /* + * Add overriding CNAME from a named.conf + * response-policy statement + */ + result = query_rpzcname(qctx, + &qctx->rpz_st->m.rpz->cname); + if (result != ISC_R_SUCCESS) { + return (ISC_R_COMPLETE); + } + qctx->fname = NULL; + qctx->want_restart = true; + return (ISC_R_COMPLETE); + default: + UNREACHABLE(); + } + + /* + * Turn off DNSSEC because the results of a + * response policy zone cannot verify. + */ + qctx->client->attributes &= ~(NS_CLIENTATTR_WANTDNSSEC | + NS_CLIENTATTR_WANTAD); + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; + ns_client_putrdataset(qctx->client, &qctx->sigrdataset); + qctx->rpz_st->q.is_zone = qctx->is_zone; + qctx->is_zone = true; + rpz_log_rewrite(qctx->client, false, qctx->rpz_st->m.policy, + qctx->rpz_st->m.type, qctx->zone, + qctx->rpz_st->p_name, NULL, + qctx->rpz_st->m.rpz->num); + } + + return (result); +} + +/*% + * Add a CNAME to a query response, including translating foo.evil.com and + * *.evil.com CNAME *.example.com + * to + * foo.evil.com CNAME foo.evil.com.example.com + */ +static isc_result_t +query_rpzcname(query_ctx_t *qctx, dns_name_t *cname) { + ns_client_t *client; + dns_fixedname_t prefix, suffix; + unsigned int labels; + isc_result_t result; + + REQUIRE(qctx != NULL && qctx->client != NULL); + + client = qctx->client; + + CTRACE(ISC_LOG_DEBUG(3), "query_rpzcname"); + + labels = dns_name_countlabels(cname); + if (labels > 2 && dns_name_iswildcard(cname)) { + dns_fixedname_init(&prefix); + dns_name_split(client->query.qname, 1, + dns_fixedname_name(&prefix), NULL); + dns_fixedname_init(&suffix); + dns_name_split(cname, labels - 1, NULL, + dns_fixedname_name(&suffix)); + result = dns_name_concatenate(dns_fixedname_name(&prefix), + dns_fixedname_name(&suffix), + qctx->fname, NULL); + if (result == DNS_R_NAMETOOLONG) { + client->message->rcode = dns_rcode_yxdomain; + } else if (result != ISC_R_SUCCESS) { + return (result); + } + } else { + dns_name_copynf(cname, qctx->fname); + } + + ns_client_keepname(client, qctx->fname, qctx->dbuf); + result = query_addcname(qctx, dns_trust_authanswer, + qctx->rpz_st->m.ttl); + if (result != ISC_R_SUCCESS) { + return (result); + } + + rpz_log_rewrite(client, false, qctx->rpz_st->m.policy, + qctx->rpz_st->m.type, qctx->rpz_st->m.zone, + qctx->rpz_st->p_name, qctx->fname, + qctx->rpz_st->m.rpz->num); + + ns_client_qnamereplace(client, qctx->fname); + + /* + * Turn off DNSSEC because the results of a + * response policy zone cannot verify. + */ + client->attributes &= ~(NS_CLIENTATTR_WANTDNSSEC | + NS_CLIENTATTR_WANTAD); + + return (ISC_R_SUCCESS); +} + +/*% + * Check the configured trust anchors for a root zone trust anchor + * with a key id that matches qctx->client->query.root_key_sentinel_keyid. + * + * Return true when found, otherwise return false. + */ +static bool +has_ta(query_ctx_t *qctx) { + dns_keytable_t *keytable = NULL; + dns_keynode_t *keynode = NULL; + dns_rdataset_t dsset; + dns_keytag_t sentinel = qctx->client->query.root_key_sentinel_keyid; + isc_result_t result; + + result = dns_view_getsecroots(qctx->view, &keytable); + if (result != ISC_R_SUCCESS) { + return (false); + } + + result = dns_keytable_find(keytable, dns_rootname, &keynode); + if (result != ISC_R_SUCCESS) { + if (keynode != NULL) { + dns_keytable_detachkeynode(keytable, &keynode); + } + dns_keytable_detach(&keytable); + return (false); + } + + dns_rdataset_init(&dsset); + if (dns_keynode_dsset(keynode, &dsset)) { + for (result = dns_rdataset_first(&dsset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(&dsset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_ds_t ds; + + dns_rdata_reset(&rdata); + dns_rdataset_current(&dsset, &rdata); + result = dns_rdata_tostruct(&rdata, &ds, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (ds.key_tag == sentinel) { + dns_keytable_detachkeynode(keytable, &keynode); + dns_keytable_detach(&keytable); + dns_rdataset_disassociate(&dsset); + return (true); + } + } + dns_rdataset_disassociate(&dsset); + } + + if (keynode != NULL) { + dns_keytable_detachkeynode(keytable, &keynode); + } + + dns_keytable_detach(&keytable); + + return (false); +} + +/*% + * Check if a root key sentinel SERVFAIL should be returned. + */ +static bool +root_key_sentinel_return_servfail(query_ctx_t *qctx, isc_result_t result) { + /* + * Are we looking at a "root-key-sentinel" query? + */ + if (!qctx->client->query.root_key_sentinel_is_ta && + !qctx->client->query.root_key_sentinel_not_ta) + { + return (false); + } + + /* + * We only care about the query if 'result' indicates we have a cached + * answer. + */ + switch (result) { + case ISC_R_SUCCESS: + case DNS_R_CNAME: + case DNS_R_DNAME: + case DNS_R_NCACHENXDOMAIN: + case DNS_R_NCACHENXRRSET: + break; + default: + return (false); + } + + /* + * Do we meet the specified conditions to return SERVFAIL? + */ + if (!qctx->is_zone && qctx->rdataset->trust == dns_trust_secure && + ((qctx->client->query.root_key_sentinel_is_ta && !has_ta(qctx)) || + (qctx->client->query.root_key_sentinel_not_ta && has_ta(qctx)))) + { + return (true); + } + + /* + * As special processing may only be triggered by the original QNAME, + * disable it after following a CNAME/DNAME. + */ + qctx->client->query.root_key_sentinel_is_ta = false; + qctx->client->query.root_key_sentinel_not_ta = false; + + return (false); +} + +/*% + * If serving stale answers is allowed, set up 'qctx' to look for one and + * return true; otherwise, return false. + */ +static bool +query_usestale(query_ctx_t *qctx, isc_result_t result) { + if ((qctx->client->query.dboptions & DNS_DBFIND_STALEOK) != 0) { + /* + * Query was already using stale, if that didn't work the + * last time, it won't work this time either. + */ + return (false); + } + + if (qctx->refresh_rrset) { + /* + * This is a refreshing query, we have already prioritized + * stale data, so don't enable serve-stale again. + */ + return (false); + } + + if (result == DNS_R_DUPLICATE || result == DNS_R_DROP || + result == ISC_R_ALREADYRUNNING) + { + /* + * Don't enable serve-stale if the result signals a duplicate + * query or a query that is being dropped or can't proceed + * because of a recursion loop. + */ + return (false); + } + + qctx_clean(qctx); + qctx_freedata(qctx); + + if (dns_view_staleanswerenabled(qctx->client->view)) { + dns_db_attach(qctx->client->view->cachedb, &qctx->db); + qctx->version = NULL; + qctx->client->query.dboptions |= DNS_DBFIND_STALEOK; + if (qctx->client->query.fetch != NULL) { + dns_resolver_destroyfetch(&qctx->client->query.fetch); + } + + /* + * Start the stale-refresh-time window in case there was a + * resolver query timeout. + */ + if (qctx->resuming && result == ISC_R_TIMEDOUT) { + qctx->client->query.dboptions |= DNS_DBFIND_STALESTART; + } + return (true); + } + + return (false); +} + +/*% + * Continue after doing a database lookup or returning from + * recursion, and call out to the next function depending on the + * result from the search. + */ +static isc_result_t +query_gotanswer(query_ctx_t *qctx, isc_result_t res) { + isc_result_t result = res; + char errmsg[256]; + + CCTRACE(ISC_LOG_DEBUG(3), "query_gotanswer"); + + CALL_HOOK(NS_QUERY_GOT_ANSWER_BEGIN, qctx); + + if (query_checkrrl(qctx, result) != ISC_R_SUCCESS) { + return (ns_query_done(qctx)); + } + + if (!dns_name_equal(qctx->client->query.qname, dns_rootname)) { + result = query_checkrpz(qctx, result); + if (result == ISC_R_NOTFOUND) { + /* + * RPZ not configured for this view. + */ + goto root_key_sentinel; + } + if (RECURSING(qctx->client) && result == DNS_R_DISALLOWED) { + /* + * We are recursing, and thus RPZ processing is not + * allowed at the moment. This could happen on a + * "stale-answer-client-timeout" lookup. In this case, + * bail out and wait for recursion to complete, as we + * we can't perform the RPZ rewrite rules. + */ + return (result); + } + if (result == ISC_R_COMPLETE) { + return (ns_query_done(qctx)); + } + } + +root_key_sentinel: + /* + * If required, handle special "root-key-sentinel-is-ta-<keyid>" and + * "root-key-sentinel-not-ta-<keyid>" labels by returning SERVFAIL. + */ + if (root_key_sentinel_return_servfail(qctx, result)) { + /* + * Don't record this response in the SERVFAIL cache. + */ + qctx->client->attributes |= NS_CLIENTATTR_NOSETFC; + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + switch (result) { + case ISC_R_SUCCESS: + return (query_prepresponse(qctx)); + + case DNS_R_GLUE: + case DNS_R_ZONECUT: + INSIST(qctx->is_zone); + qctx->authoritative = false; + return (query_prepresponse(qctx)); + + case ISC_R_NOTFOUND: + return (query_notfound(qctx)); + + case DNS_R_DELEGATION: + return (query_delegation(qctx)); + + case DNS_R_EMPTYNAME: + return (query_nodata(qctx, DNS_R_EMPTYNAME)); + case DNS_R_NXRRSET: + return (query_nodata(qctx, DNS_R_NXRRSET)); + + case DNS_R_EMPTYWILD: + return (query_nxdomain(qctx, true)); + + case DNS_R_NXDOMAIN: + return (query_nxdomain(qctx, false)); + + case DNS_R_COVERINGNSEC: + return (query_coveringnsec(qctx)); + + case DNS_R_NCACHENXDOMAIN: + result = query_redirect(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + return (query_ncache(qctx, DNS_R_NCACHENXDOMAIN)); + + case DNS_R_NCACHENXRRSET: + return (query_ncache(qctx, DNS_R_NCACHENXRRSET)); + + case DNS_R_CNAME: + return (query_cname(qctx)); + + case DNS_R_DNAME: + return (query_dname(qctx)); + + default: + /* + * Something has gone wrong. + */ + snprintf(errmsg, sizeof(errmsg) - 1, + "query_gotanswer: unexpected error: %s", + isc_result_totext(result)); + CCTRACE(ISC_LOG_ERROR, errmsg); + if (query_usestale(qctx, result)) { + /* + * If serve-stale is enabled, query_usestale() already + * set up 'qctx' for looking up a stale response. + */ + return (query_lookup(qctx)); + } + + /* + * Regardless of the triggering result, we definitely + * want to return SERVFAIL from here. + */ + qctx->client->rcode_override = dns_rcode_servfail; + + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + +cleanup: + return (result); +} + +static void +query_addnoqnameproof(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + isc_buffer_t *dbuf, b; + dns_name_t *fname = NULL; + dns_rdataset_t *neg = NULL, *negsig = NULL; + isc_result_t result = ISC_R_NOMEMORY; + + CTRACE(ISC_LOG_DEBUG(3), "query_addnoqnameproof"); + + if (qctx->noqname == NULL) { + return; + } + + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + + fname = ns_client_newname(client, dbuf, &b); + neg = ns_client_newrdataset(client); + negsig = ns_client_newrdataset(client); + if (fname == NULL || neg == NULL || negsig == NULL) { + goto cleanup; + } + + result = dns_rdataset_getnoqname(qctx->noqname, fname, neg, negsig); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + query_addrrset(qctx, &fname, &neg, &negsig, dbuf, + DNS_SECTION_AUTHORITY); + + if ((qctx->noqname->attributes & DNS_RDATASETATTR_CLOSEST) == 0) { + goto cleanup; + } + + if (fname == NULL) { + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + } + + if (neg == NULL) { + neg = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(neg)) { + dns_rdataset_disassociate(neg); + } + + if (negsig == NULL) { + negsig = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(negsig)) { + dns_rdataset_disassociate(negsig); + } + + if (fname == NULL || neg == NULL || negsig == NULL) { + goto cleanup; + } + result = dns_rdataset_getclosest(qctx->noqname, fname, neg, negsig); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + query_addrrset(qctx, &fname, &neg, &negsig, dbuf, + DNS_SECTION_AUTHORITY); + +cleanup: + if (neg != NULL) { + ns_client_putrdataset(client, &neg); + } + if (negsig != NULL) { + ns_client_putrdataset(client, &negsig); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } +} + +/*% + * Build the response for a query for type ANY. + */ +static isc_result_t +query_respond_any(query_ctx_t *qctx) { + bool found = false, hidden = false; + dns_rdatasetiter_t *rdsiter = NULL; + isc_result_t result; + dns_rdatatype_t onetype = 0; /* type to use for minimal-any */ + isc_buffer_t b; + + CCTRACE(ISC_LOG_DEBUG(3), "query_respond_any"); + + CALL_HOOK(NS_QUERY_RESPOND_ANY_BEGIN, qctx); + + result = dns_db_allrdatasets(qctx->db, qctx->node, qctx->version, 0, 0, + &rdsiter); + if (result != ISC_R_SUCCESS) { + CCTRACE(ISC_LOG_ERROR, "query_respond_any: allrdatasets " + "failed"); + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + + /* + * Calling query_addrrset() with a non-NULL dbuf is going + * to either keep or release the name. We don't want it to + * release fname, since we may have to call query_addrrset() + * more than once. That means we have to call ns_client_keepname() + * now, and pass a NULL dbuf to query_addrrset(). + * + * If we do a query_addrrset() below, we must set qctx->fname to + * NULL before leaving this block, otherwise we might try to + * cleanup qctx->fname even though we're using it! + */ + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + qctx->tname = qctx->fname; + + result = dns_rdatasetiter_first(rdsiter); + while (result == ISC_R_SUCCESS) { + dns_rdatasetiter_current(rdsiter, qctx->rdataset); + + /* + * We found an NS RRset; no need to add one later. + */ + if (qctx->qtype == dns_rdatatype_any && + qctx->rdataset->type == dns_rdatatype_ns) + { + qctx->answer_has_ns = true; + } + + /* + * Note: if we're in this function, then qctx->type + * is guaranteed to be ANY, but qctx->qtype (i.e. the + * original type requested) might have been RRSIG or + * SIG; we need to check for that. + */ + if (qctx->is_zone && qctx->qtype == dns_rdatatype_any && + !dns_db_issecure(qctx->db) && + dns_rdatatype_isdnssec(qctx->rdataset->type)) + { + /* + * The zone may be transitioning from insecure + * to secure. Hide DNSSEC records from ANY queries. + */ + dns_rdataset_disassociate(qctx->rdataset); + hidden = true; + } else if (qctx->view->minimal_any && !TCP(qctx->client) && + !WANTDNSSEC(qctx->client) && + qctx->qtype == dns_rdatatype_any && + (qctx->rdataset->type == dns_rdatatype_sig || + qctx->rdataset->type == dns_rdatatype_rrsig)) + { + CCTRACE(ISC_LOG_DEBUG(5), "query_respond_any: " + "minimal-any skip signature"); + dns_rdataset_disassociate(qctx->rdataset); + } else if (qctx->view->minimal_any && !TCP(qctx->client) && + onetype != 0 && qctx->rdataset->type != onetype && + qctx->rdataset->covers != onetype) + { + CCTRACE(ISC_LOG_DEBUG(5), "query_respond_any: " + "minimal-any skip rdataset"); + dns_rdataset_disassociate(qctx->rdataset); + } else if ((qctx->qtype == dns_rdatatype_any || + qctx->rdataset->type == qctx->qtype) && + qctx->rdataset->type != 0) + { + if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client)) + { + qctx->noqname = qctx->rdataset; + } else { + qctx->noqname = NULL; + } + + qctx->rpz_st = qctx->client->query.rpz_st; + if (qctx->rpz_st != NULL) { + qctx->rdataset->ttl = + ISC_MIN(qctx->rdataset->ttl, + qctx->rpz_st->m.ttl); + } + + if (!qctx->is_zone && RECURSIONOK(qctx->client)) { + dns_name_t *name; + name = (qctx->fname != NULL) ? qctx->fname + : qctx->tname; + query_prefetch(qctx->client, name, + qctx->rdataset); + } + + /* + * Remember the first RRtype we find so we + * can skip others with minimal-any. + */ + if (qctx->rdataset->type == dns_rdatatype_sig || + qctx->rdataset->type == dns_rdatatype_rrsig) + { + onetype = qctx->rdataset->covers; + } else { + onetype = qctx->rdataset->type; + } + + query_addrrset(qctx, + (qctx->fname != NULL) ? &qctx->fname + : &qctx->tname, + &qctx->rdataset, NULL, NULL, + DNS_SECTION_ANSWER); + + query_addnoqnameproof(qctx); + + found = true; + INSIST(qctx->tname != NULL); + + /* + * rdataset is non-NULL only in certain + * pathological cases involving DNAMEs. + */ + if (qctx->rdataset != NULL) { + ns_client_putrdataset(qctx->client, + &qctx->rdataset); + } + + qctx->rdataset = ns_client_newrdataset(qctx->client); + if (qctx->rdataset == NULL) { + break; + } + } else { + /* + * We're not interested in this rdataset. + */ + dns_rdataset_disassociate(qctx->rdataset); + } + + result = dns_rdatasetiter_next(rdsiter); + } + + dns_rdatasetiter_destroy(&rdsiter); + + if (result != ISC_R_NOMORE) { + CCTRACE(ISC_LOG_ERROR, "query_respond_any: rdataset iterator " + "failed"); + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + if (found) { + /* + * Call hook if any answers were found. + * Do this before releasing qctx->fname, in case + * the hook function needs it. + */ + CALL_HOOK(NS_QUERY_RESPOND_ANY_FOUND, qctx); + } + + if (qctx->fname != NULL) { + dns_message_puttempname(qctx->client->message, &qctx->fname); + } + + if (found) { + /* + * At least one matching rdataset was found + */ + query_addauth(qctx); + } else if (qctx->qtype == dns_rdatatype_rrsig || + qctx->qtype == dns_rdatatype_sig) + { + /* + * No matching rdatasets were found, but we got + * here on a search for RRSIG/SIG, so that's okay. + */ + if (!qctx->is_zone) { + qctx->authoritative = false; + qctx->client->attributes &= ~NS_CLIENTATTR_RA; + query_addauth(qctx); + return (ns_query_done(qctx)); + } + + if (qctx->qtype == dns_rdatatype_rrsig && + dns_db_issecure(qctx->db)) + { + char namebuf[DNS_NAME_FORMATSIZE]; + dns_name_format(qctx->client->query.qname, namebuf, + sizeof(namebuf)); + ns_client_log(qctx->client, DNS_LOGCATEGORY_DNSSEC, + NS_LOGMODULE_QUERY, ISC_LOG_WARNING, + "missing signature for %s", namebuf); + } + + qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b); + return (query_sign_nodata(qctx)); + } else if (!hidden) { + /* + * No matching rdatasets were found and nothing was + * deliberately hidden: something must have gone wrong. + */ + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + } + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/* + * Set the expire time, if requested, when answering from a slave, mirror, or + * master zone. + */ +static void +query_getexpire(query_ctx_t *qctx) { + dns_zone_t *raw = NULL, *mayberaw; + + CCTRACE(ISC_LOG_DEBUG(3), "query_getexpire"); + + if (qctx->zone == NULL || !qctx->is_zone || + qctx->qtype != dns_rdatatype_soa || + qctx->client->query.restarts != 0 || + (qctx->client->attributes & NS_CLIENTATTR_WANTEXPIRE) == 0) + { + return; + } + + dns_zone_getraw(qctx->zone, &raw); + mayberaw = (raw != NULL) ? raw : qctx->zone; + + if (dns_zone_gettype(mayberaw) == dns_zone_secondary || + dns_zone_gettype(mayberaw) == dns_zone_mirror) + { + isc_time_t expiretime; + uint32_t secs; + dns_zone_getexpiretime(qctx->zone, &expiretime); + secs = isc_time_seconds(&expiretime); + if (secs >= qctx->client->now && qctx->result == ISC_R_SUCCESS) + { + qctx->client->attributes |= NS_CLIENTATTR_HAVEEXPIRE; + qctx->client->expire = secs - qctx->client->now; + } + } else if (dns_zone_gettype(mayberaw) == dns_zone_primary) { + isc_result_t result; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_soa_t soa; + + result = dns_rdataset_first(qctx->rdataset); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + dns_rdataset_current(qctx->rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &soa, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + qctx->client->expire = soa.expire; + qctx->client->attributes |= NS_CLIENTATTR_HAVEEXPIRE; + } + + if (raw != NULL) { + dns_zone_detach(&raw); + } +} + +/*% + * Fill the ANSWER section of a positive response. + */ +static isc_result_t +query_addanswer(query_ctx_t *qctx) { + dns_rdataset_t **sigrdatasetp = NULL; + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_addanswer"); + + CALL_HOOK(NS_QUERY_ADDANSWER_BEGIN, qctx); + + /* + * On normal lookups, clear any rdatasets that were added on a + * lookup due to stale-answer-client-timeout. Do not clear if we + * are going to refresh the RRset, because the stale contents are + * prioritized. + */ + if (QUERY_STALEOK(&qctx->client->query) && + !QUERY_STALETIMEOUT(&qctx->client->query) && !qctx->refresh_rrset) + { + CCTRACE(ISC_LOG_DEBUG(3), "query_clear_stale"); + query_clear_stale(qctx->client); + /* + * We can clear the attribute to prevent redundant clearing + * in subsequent lookups. + */ + qctx->client->query.attributes &= ~NS_QUERYATTR_STALEOK; + } + + if (qctx->dns64) { + result = query_dns64(qctx); + qctx->noqname = NULL; + dns_rdataset_disassociate(qctx->rdataset); + dns_message_puttemprdataset(qctx->client->message, + &qctx->rdataset); + if (result == ISC_R_NOMORE) { +#ifndef dns64_bis_return_excluded_addresses + if (qctx->dns64_exclude) { + if (!qctx->is_zone) { + return (ns_query_done(qctx)); + } + /* + * Add a fake SOA record. + */ + (void)query_addsoa(qctx, 600, + DNS_SECTION_AUTHORITY); + return (ns_query_done(qctx)); + } +#endif /* ifndef dns64_bis_return_excluded_addresses */ + if (qctx->is_zone) { + return (query_nodata(qctx, DNS_R_NXDOMAIN)); + } else { + return (query_ncache(qctx, DNS_R_NXDOMAIN)); + } + } else if (result != ISC_R_SUCCESS) { + qctx->result = result; + return (ns_query_done(qctx)); + } + } else if (qctx->client->query.dns64_aaaaok != NULL) { + query_filter64(qctx); + ns_client_putrdataset(qctx->client, &qctx->rdataset); + } else { + if (!qctx->is_zone && RECURSIONOK(qctx->client) && + !QUERY_STALETIMEOUT(&qctx->client->query)) + { + query_prefetch(qctx->client, qctx->fname, + qctx->rdataset); + } + if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) { + sigrdatasetp = &qctx->sigrdataset; + } + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + sigrdatasetp, qctx->dbuf, DNS_SECTION_ANSWER); + } + + return (ISC_R_COMPLETE); + +cleanup: + return (result); +} + +/*% + * Build a response for a "normal" query, for a type other than ANY, + * for which we have an answer (either positive or negative). + */ +static isc_result_t +query_respond(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_respond"); + + /* + * Check to see if the AAAA RRset has non-excluded addresses + * in it. If not look for a A RRset. + */ + INSIST(qctx->client->query.dns64_aaaaok == NULL); + + if (qctx->qtype == dns_rdatatype_aaaa && !qctx->dns64_exclude && + !ISC_LIST_EMPTY(qctx->view->dns64) && + qctx->client->message->rdclass == dns_rdataclass_in && + !dns64_aaaaok(qctx->client, qctx->rdataset, qctx->sigrdataset)) + { + /* + * Look to see if there are A records for this name. + */ + qctx->client->query.dns64_ttl = qctx->rdataset->ttl; + SAVE(qctx->client->query.dns64_aaaa, qctx->rdataset); + SAVE(qctx->client->query.dns64_sigaaaa, qctx->sigrdataset); + ns_client_releasename(qctx->client, &qctx->fname); + dns_db_detachnode(qctx->db, &qctx->node); + qctx->type = qctx->qtype = dns_rdatatype_a; + qctx->dns64_exclude = qctx->dns64 = true; + + return (query_lookup(qctx)); + } + + /* + * XXX: This hook is meant to be at the top of this function, + * but is postponed until after DNS64 in order to avoid an + * assertion if the hook causes recursion. (When DNS64 also + * becomes a plugin, it will be necessary to find some + * other way to prevent that assertion, since the order in + * which plugins are configured can't be enforced.) + */ + CALL_HOOK(NS_QUERY_RESPOND_BEGIN, qctx); + + if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client)) { + qctx->noqname = qctx->rdataset; + } else { + qctx->noqname = NULL; + } + + /* + * Special case NS handling + */ + if (qctx->is_zone && qctx->qtype == dns_rdatatype_ns) { + /* + * We've already got an NS, no need to add one in + * the authority section + */ + if (dns_name_equal(qctx->client->query.qname, + dns_db_origin(qctx->db))) + { + qctx->answer_has_ns = true; + } + + /* + * Always add glue for root priming queries, regardless + * of "minimal-responses" setting. + */ + if (dns_name_equal(qctx->client->query.qname, dns_rootname)) { + qctx->client->query.attributes &= + ~NS_QUERYATTR_NOADDITIONAL; + dns_db_attach(qctx->db, &qctx->client->query.gluedb); + } + } + + /* + * Set expire time + */ + query_getexpire(qctx); + + result = query_addanswer(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + + query_addnoqnameproof(qctx); + + /* + * 'qctx->rdataset' will only be non-NULL here if the ANSWER section of + * the message to be sent to the client already contains an RRset with + * the same owner name and the same type as 'qctx->rdataset'. This + * should never happen, with one exception: when chasing DNAME records, + * one of the DNAME records placed in the ANSWER section may turn out + * to be the final answer to the client's query, but we have no way of + * knowing that until now. In such a case, 'qctx->rdataset' will be + * freed later, so we do not need to free it here. + */ + INSIST(qctx->rdataset == NULL || qctx->qtype == dns_rdatatype_dname); + + query_addauth(qctx); + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +static isc_result_t +query_dns64(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + dns_aclenv_t *env = + ns_interfacemgr_getaclenv(client->manager->interface->mgr); + dns_name_t *name, *mname; + dns_rdata_t *dns64_rdata; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdatalist_t *dns64_rdatalist; + dns_rdataset_t *dns64_rdataset; + dns_rdataset_t *mrdataset; + isc_buffer_t *buffer; + isc_region_t r; + isc_result_t result; + dns_view_t *view = client->view; + isc_netaddr_t netaddr; + dns_dns64_t *dns64; + unsigned int flags = 0; + const dns_section_t section = DNS_SECTION_ANSWER; + + /*% + * To the current response for 'qctx->client', add the answer RRset + * '*rdatasetp' and an optional signature set '*sigrdatasetp', with + * owner name '*namep', to the answer section, unless they are + * already there. Also add any pertinent additional data. + * + * If 'qctx->dbuf' is not NULL, then 'qctx->fname' is the name + * whose data is stored 'qctx->dbuf'. In this case, + * query_addrrset() guarantees that when it returns the name + * will either have been kept or released. + */ + CTRACE(ISC_LOG_DEBUG(3), "query_dns64"); + + qctx->qtype = qctx->type = dns_rdatatype_aaaa; + + name = qctx->fname; + mname = NULL; + mrdataset = NULL; + buffer = NULL; + dns64_rdata = NULL; + dns64_rdataset = NULL; + dns64_rdatalist = NULL; + result = dns_message_findname( + client->message, section, name, dns_rdatatype_aaaa, + qctx->rdataset->covers, &mname, &mrdataset); + if (result == ISC_R_SUCCESS) { + /* + * We've already got an RRset of the given name and type. + * There's nothing else to do; + */ + CTRACE(ISC_LOG_DEBUG(3), "query_dns64: dns_message_findname " + "succeeded: done"); + if (qctx->dbuf != NULL) { + ns_client_releasename(client, &qctx->fname); + } + return (ISC_R_SUCCESS); + } else if (result == DNS_R_NXDOMAIN) { + /* + * The name doesn't exist. + */ + if (qctx->dbuf != NULL) { + ns_client_keepname(client, name, qctx->dbuf); + } + dns_message_addname(client->message, name, section); + qctx->fname = NULL; + mname = name; + } else { + RUNTIME_CHECK(result == DNS_R_NXRRSET); + if (qctx->dbuf != NULL) { + ns_client_releasename(client, &qctx->fname); + } + } + + if (qctx->rdataset->trust != dns_trust_secure) { + client->query.attributes &= ~NS_QUERYATTR_SECURE; + } + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + + isc_buffer_allocate(client->mctx, &buffer, + view->dns64cnt * 16 * + dns_rdataset_count(qctx->rdataset)); + result = dns_message_gettemprdataset(client->message, &dns64_rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + result = dns_message_gettemprdatalist(client->message, + &dns64_rdatalist); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + dns_rdatalist_init(dns64_rdatalist); + dns64_rdatalist->rdclass = dns_rdataclass_in; + dns64_rdatalist->type = dns_rdatatype_aaaa; + if (client->query.dns64_ttl != UINT32_MAX) { + dns64_rdatalist->ttl = ISC_MIN(qctx->rdataset->ttl, + client->query.dns64_ttl); + } else { + dns64_rdatalist->ttl = ISC_MIN(qctx->rdataset->ttl, 600); + } + + if (RECURSIONOK(client)) { + flags |= DNS_DNS64_RECURSIVE; + } + + /* + * We use the signatures from the A lookup to set DNS_DNS64_DNSSEC + * as this provides a easy way to see if the answer was signed. + */ + if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL && + dns_rdataset_isassociated(qctx->sigrdataset)) + { + flags |= DNS_DNS64_DNSSEC; + } + + for (result = dns_rdataset_first(qctx->rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(qctx->rdataset)) + { + for (dns64 = ISC_LIST_HEAD(client->view->dns64); dns64 != NULL; + dns64 = dns_dns64_next(dns64)) + { + dns_rdataset_current(qctx->rdataset, &rdata); + isc_buffer_availableregion(buffer, &r); + INSIST(r.length >= 16); + result = dns_dns64_aaaafroma(dns64, &netaddr, + client->signer, env, flags, + rdata.data, r.base); + if (result != ISC_R_SUCCESS) { + dns_rdata_reset(&rdata); + continue; + } + isc_buffer_add(buffer, 16); + isc_buffer_remainingregion(buffer, &r); + isc_buffer_forward(buffer, 16); + result = dns_message_gettemprdata(client->message, + &dns64_rdata); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + dns_rdata_init(dns64_rdata); + dns_rdata_fromregion(dns64_rdata, dns_rdataclass_in, + dns_rdatatype_aaaa, &r); + ISC_LIST_APPEND(dns64_rdatalist->rdata, dns64_rdata, + link); + dns64_rdata = NULL; + dns_rdata_reset(&rdata); + } + } + if (result != ISC_R_NOMORE) { + goto cleanup; + } + + if (ISC_LIST_EMPTY(dns64_rdatalist->rdata)) { + goto cleanup; + } + + result = dns_rdatalist_tordataset(dns64_rdatalist, dns64_rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + dns_rdataset_setownercase(dns64_rdataset, mname); + client->query.attributes |= NS_QUERYATTR_NOADDITIONAL; + dns64_rdataset->trust = qctx->rdataset->trust; + + query_addtoname(mname, dns64_rdataset); + query_setorder(qctx, mname, dns64_rdataset); + + dns64_rdataset = NULL; + dns64_rdatalist = NULL; + dns_message_takebuffer(client->message, &buffer); + inc_stats(client, ns_statscounter_dns64); + result = ISC_R_SUCCESS; + +cleanup: + if (buffer != NULL) { + isc_buffer_free(&buffer); + } + + if (dns64_rdata != NULL) { + dns_message_puttemprdata(client->message, &dns64_rdata); + } + + if (dns64_rdataset != NULL) { + dns_message_puttemprdataset(client->message, &dns64_rdataset); + } + + if (dns64_rdatalist != NULL) { + for (dns64_rdata = ISC_LIST_HEAD(dns64_rdatalist->rdata); + dns64_rdata != NULL; + dns64_rdata = ISC_LIST_HEAD(dns64_rdatalist->rdata)) + { + ISC_LIST_UNLINK(dns64_rdatalist->rdata, dns64_rdata, + link); + dns_message_puttemprdata(client->message, &dns64_rdata); + } + dns_message_puttemprdatalist(client->message, &dns64_rdatalist); + } + + CTRACE(ISC_LOG_DEBUG(3), "query_dns64: done"); + return (result); +} + +static void +query_filter64(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + dns_name_t *name, *mname; + dns_rdata_t *myrdata; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdatalist_t *myrdatalist; + dns_rdataset_t *myrdataset; + isc_buffer_t *buffer; + isc_region_t r; + isc_result_t result; + unsigned int i; + const dns_section_t section = DNS_SECTION_ANSWER; + + CTRACE(ISC_LOG_DEBUG(3), "query_filter64"); + + INSIST(client->query.dns64_aaaaok != NULL); + INSIST(client->query.dns64_aaaaoklen == + dns_rdataset_count(qctx->rdataset)); + + name = qctx->fname; + mname = NULL; + buffer = NULL; + myrdata = NULL; + myrdataset = NULL; + myrdatalist = NULL; + result = dns_message_findname( + client->message, section, name, dns_rdatatype_aaaa, + qctx->rdataset->covers, &mname, &myrdataset); + if (result == ISC_R_SUCCESS) { + /* + * We've already got an RRset of the given name and type. + * There's nothing else to do; + */ + CTRACE(ISC_LOG_DEBUG(3), "query_filter64: dns_message_findname " + "succeeded: done"); + if (qctx->dbuf != NULL) { + ns_client_releasename(client, &qctx->fname); + } + return; + } else if (result == DNS_R_NXDOMAIN) { + mname = name; + qctx->fname = NULL; + } else { + RUNTIME_CHECK(result == DNS_R_NXRRSET); + if (qctx->dbuf != NULL) { + ns_client_releasename(client, &qctx->fname); + } + qctx->dbuf = NULL; + } + + if (qctx->rdataset->trust != dns_trust_secure) { + client->query.attributes &= ~NS_QUERYATTR_SECURE; + } + + isc_buffer_allocate(client->mctx, &buffer, + 16 * dns_rdataset_count(qctx->rdataset)); + result = dns_message_gettemprdataset(client->message, &myrdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + result = dns_message_gettemprdatalist(client->message, &myrdatalist); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + dns_rdatalist_init(myrdatalist); + myrdatalist->rdclass = dns_rdataclass_in; + myrdatalist->type = dns_rdatatype_aaaa; + myrdatalist->ttl = qctx->rdataset->ttl; + + i = 0; + for (result = dns_rdataset_first(qctx->rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(qctx->rdataset)) + { + if (!client->query.dns64_aaaaok[i++]) { + continue; + } + dns_rdataset_current(qctx->rdataset, &rdata); + INSIST(rdata.length == 16); + isc_buffer_putmem(buffer, rdata.data, rdata.length); + isc_buffer_remainingregion(buffer, &r); + isc_buffer_forward(buffer, rdata.length); + result = dns_message_gettemprdata(client->message, &myrdata); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + dns_rdata_init(myrdata); + dns_rdata_fromregion(myrdata, dns_rdataclass_in, + dns_rdatatype_aaaa, &r); + ISC_LIST_APPEND(myrdatalist->rdata, myrdata, link); + myrdata = NULL; + dns_rdata_reset(&rdata); + } + if (result != ISC_R_NOMORE) { + goto cleanup; + } + + result = dns_rdatalist_tordataset(myrdatalist, myrdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + dns_rdataset_setownercase(myrdataset, name); + client->query.attributes |= NS_QUERYATTR_NOADDITIONAL; + if (mname == name) { + if (qctx->dbuf != NULL) { + ns_client_keepname(client, name, qctx->dbuf); + } + dns_message_addname(client->message, name, section); + qctx->dbuf = NULL; + } + myrdataset->trust = qctx->rdataset->trust; + + query_addtoname(mname, myrdataset); + query_setorder(qctx, mname, myrdataset); + + myrdataset = NULL; + myrdatalist = NULL; + dns_message_takebuffer(client->message, &buffer); + +cleanup: + if (buffer != NULL) { + isc_buffer_free(&buffer); + } + + if (myrdata != NULL) { + dns_message_puttemprdata(client->message, &myrdata); + } + + if (myrdataset != NULL) { + dns_message_puttemprdataset(client->message, &myrdataset); + } + + if (myrdatalist != NULL) { + for (myrdata = ISC_LIST_HEAD(myrdatalist->rdata); + myrdata != NULL; + myrdata = ISC_LIST_HEAD(myrdatalist->rdata)) + { + ISC_LIST_UNLINK(myrdatalist->rdata, myrdata, link); + dns_message_puttemprdata(client->message, &myrdata); + } + dns_message_puttemprdatalist(client->message, &myrdatalist); + } + if (qctx->dbuf != NULL) { + ns_client_releasename(client, &name); + } + + CTRACE(ISC_LOG_DEBUG(3), "query_filter64: done"); +} + +/*% + * Handle the case of a name not being found in a database lookup. + * Called from query_gotanswer(). Passes off processing to + * query_delegation() for a root referral if appropriate. + */ +static isc_result_t +query_notfound(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_notfound"); + + CALL_HOOK(NS_QUERY_NOTFOUND_BEGIN, qctx); + + INSIST(!qctx->is_zone); + + if (qctx->db != NULL) { + dns_db_detach(&qctx->db); + } + + /* + * If the cache doesn't even have the root NS, + * try to get that from the hints DB. + */ + if (qctx->view->hints != NULL) { + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, qctx->client, NULL, NULL); + + dns_db_attach(qctx->view->hints, &qctx->db); + result = dns_db_findext(qctx->db, dns_rootname, NULL, + dns_rdatatype_ns, 0, qctx->client->now, + &qctx->node, qctx->fname, &cm, &ci, + qctx->rdataset, qctx->sigrdataset); + } else { + /* We have no hints. */ + result = ISC_R_FAILURE; + } + if (result != ISC_R_SUCCESS) { + /* + * Nonsensical root hints may require cleanup. + */ + qctx_clean(qctx); + + /* + * We don't have any root server hints, but + * we may have working forwarders, so try to + * recurse anyway. + */ + if (RECURSIONOK(qctx->client)) { + INSIST(!REDIRECT(qctx->client)); + result = ns_query_recurse(qctx->client, qctx->qtype, + qctx->client->query.qname, + NULL, NULL, qctx->resuming); + if (result == ISC_R_SUCCESS) { + CALL_HOOK(NS_QUERY_NOTFOUND_RECURSE, qctx); + qctx->client->query.attributes |= + NS_QUERYATTR_RECURSING; + + if (qctx->dns64) { + qctx->client->query.attributes |= + NS_QUERYATTR_DNS64; + } + if (qctx->dns64_exclude) { + qctx->client->query.attributes |= + NS_QUERYATTR_DNS64EXCLUDE; + } + } else if (query_usestale(qctx, result)) { + /* + * If serve-stale is enabled, query_usestale() + * already set up 'qctx' for looking up a + * stale response. + */ + return (query_lookup(qctx)); + } else { + QUERY_ERROR(qctx, result); + } + return (ns_query_done(qctx)); + } else { + /* Unable to give root server referral. */ + CCTRACE(ISC_LOG_ERROR, "unable to give root server " + "referral"); + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + } + + return (query_delegation(qctx)); + +cleanup: + return (result); +} + +/*% + * We have a delegation but recursion is not allowed, so return the delegation + * to the client. + */ +static isc_result_t +query_prepare_delegation_response(query_ctx_t *qctx) { + isc_result_t result; + dns_rdataset_t **sigrdatasetp = NULL; + bool detach = false; + + CALL_HOOK(NS_QUERY_PREP_DELEGATION_BEGIN, qctx); + + /* + * qctx->fname could be released in query_addrrset(), so save a copy of + * it here in case we need it. + */ + dns_fixedname_init(&qctx->dsname); + dns_name_copynf(qctx->fname, dns_fixedname_name(&qctx->dsname)); + + /* + * This is the best answer. + */ + qctx->client->query.isreferral = true; + + if (!dns_db_iscache(qctx->db) && qctx->client->query.gluedb == NULL) { + dns_db_attach(qctx->db, &qctx->client->query.gluedb); + detach = true; + } + + /* + * We must ensure NOADDITIONAL is off, because the generation of + * additional data is required in delegations. + */ + qctx->client->query.attributes &= ~NS_QUERYATTR_NOADDITIONAL; + if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) { + sigrdatasetp = &qctx->sigrdataset; + } + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp, + qctx->dbuf, DNS_SECTION_AUTHORITY); + if (detach) { + dns_db_detach(&qctx->client->query.gluedb); + } + + /* + * Add DS/NSEC(3) record(s) if needed. + */ + query_addds(qctx); + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/*% + * Handle a delegation response from an authoritative lookup. This + * may trigger additional lookups, e.g. from the cache database to + * see if we have a better answer; if that is not allowed, return the + * delegation to the client and call ns_query_done(). + */ +static isc_result_t +query_zone_delegation(query_ctx_t *qctx) { + isc_result_t result; + + CALL_HOOK(NS_QUERY_ZONE_DELEGATION_BEGIN, qctx); + + /* + * If the query type is DS, look to see if we are + * authoritative for the child zone + */ + if (!RECURSIONOK(qctx->client) && + (qctx->options & DNS_GETDB_NOEXACT) != 0 && + qctx->qtype == dns_rdatatype_ds) + { + dns_db_t *tdb = NULL; + dns_zone_t *tzone = NULL; + dns_dbversion_t *tversion = NULL; + result = query_getzonedb( + qctx->client, qctx->client->query.qname, qctx->qtype, + DNS_GETDB_PARTIAL, &tzone, &tdb, &tversion); + if (result != ISC_R_SUCCESS) { + if (tdb != NULL) { + dns_db_detach(&tdb); + } + if (tzone != NULL) { + dns_zone_detach(&tzone); + } + } else { + qctx->options &= ~DNS_GETDB_NOEXACT; + ns_client_putrdataset(qctx->client, &qctx->rdataset); + if (qctx->sigrdataset != NULL) { + ns_client_putrdataset(qctx->client, + &qctx->sigrdataset); + } + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, + &qctx->fname); + } + if (qctx->node != NULL) { + dns_db_detachnode(qctx->db, &qctx->node); + } + if (qctx->db != NULL) { + dns_db_detach(&qctx->db); + } + if (qctx->zone != NULL) { + dns_zone_detach(&qctx->zone); + } + qctx->version = NULL; + RESTORE(qctx->version, tversion); + RESTORE(qctx->db, tdb); + RESTORE(qctx->zone, tzone); + qctx->authoritative = true; + + return (query_lookup(qctx)); + } + } + + if (USECACHE(qctx->client) && + (RECURSIONOK(qctx->client) || + (qctx->zone != NULL && + dns_zone_gettype(qctx->zone) == dns_zone_mirror))) + { + /* + * We might have a better answer or delegation in the + * cache. We'll remember the current values of fname, + * rdataset, and sigrdataset. We'll then go looking for + * QNAME in the cache. If we find something better, we'll + * use it instead. If not, then query_lookup() calls + * query_notfound() which calls query_delegation(), and + * we'll restore these values there. + */ + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + SAVE(qctx->zdb, qctx->db); + SAVE(qctx->znode, qctx->node); + SAVE(qctx->zfname, qctx->fname); + SAVE(qctx->zversion, qctx->version); + SAVE(qctx->zrdataset, qctx->rdataset); + SAVE(qctx->zsigrdataset, qctx->sigrdataset); + dns_db_attach(qctx->view->cachedb, &qctx->db); + qctx->is_zone = false; + + return (query_lookup(qctx)); + } + + return (query_prepare_delegation_response(qctx)); + +cleanup: + return (result); +} + +/*% + * Handle delegation responses, including root referrals. + * + * If the delegation was returned from authoritative data, + * call query_zone_delgation(). Otherwise, we can start + * recursion if allowed; or else return the delegation to the + * client and call ns_query_done(). + */ +static isc_result_t +query_delegation(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_delegation"); + + CALL_HOOK(NS_QUERY_DELEGATION_BEGIN, qctx); + + qctx->authoritative = false; + + if (qctx->is_zone) { + return (query_zone_delegation(qctx)); + } + + if (qctx->zfname != NULL && + (!dns_name_issubdomain(qctx->fname, qctx->zfname) || + (qctx->is_staticstub_zone && + dns_name_equal(qctx->fname, qctx->zfname)))) + { + /* + * In the following cases use "authoritative" + * data instead of the cache delegation: + * 1. We've already got a delegation from + * authoritative data, and it is better + * than what we found in the cache. + * (See the comment above.) + * 2. The query name matches the origin name + * of a static-stub zone. This needs to be + * considered for the case where the NS of + * the static-stub zone and the cached NS + * are different. We still need to contact + * the nameservers configured in the + * static-stub zone. + */ + ns_client_releasename(qctx->client, &qctx->fname); + + /* + * We've already done ns_client_keepname() on + * qctx->zfname, so we must set dbuf to NULL to + * prevent query_addrrset() from trying to + * call ns_client_keepname() again. + */ + qctx->dbuf = NULL; + ns_client_putrdataset(qctx->client, &qctx->rdataset); + if (qctx->sigrdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->sigrdataset); + } + qctx->version = NULL; + + dns_db_detachnode(qctx->db, &qctx->node); + dns_db_detach(&qctx->db); + RESTORE(qctx->db, qctx->zdb); + RESTORE(qctx->node, qctx->znode); + RESTORE(qctx->fname, qctx->zfname); + RESTORE(qctx->version, qctx->zversion); + RESTORE(qctx->rdataset, qctx->zrdataset); + RESTORE(qctx->sigrdataset, qctx->zsigrdataset); + } + + result = query_delegation_recurse(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + + return (query_prepare_delegation_response(qctx)); + +cleanup: + return (result); +} + +/*% + * Handle recursive queries that are triggered as part of the + * delegation process. + */ +static isc_result_t +query_delegation_recurse(query_ctx_t *qctx) { + isc_result_t result; + dns_name_t *qname = qctx->client->query.qname; + + CCTRACE(ISC_LOG_DEBUG(3), "query_delegation_recurse"); + + if (!RECURSIONOK(qctx->client)) { + return (ISC_R_COMPLETE); + } + + CALL_HOOK(NS_QUERY_DELEGATION_RECURSE_BEGIN, qctx); + + /* + * We have a delegation and recursion is allowed, + * so we call ns_query_recurse() to follow it. + * This phase of the query processing is done; + * we'll resume via fetch_callback() and + * query_resume() when the recursion is complete. + */ + + INSIST(!REDIRECT(qctx->client)); + + if (dns_rdatatype_atparent(qctx->type)) { + /* + * Parent is authoritative for this RDATA type (i.e. DS). + */ + result = ns_query_recurse(qctx->client, qctx->qtype, qname, + NULL, NULL, qctx->resuming); + } else if (qctx->dns64) { + /* + * Look up an A record so we can synthesize DNS64. + */ + result = ns_query_recurse(qctx->client, dns_rdatatype_a, qname, + NULL, NULL, qctx->resuming); + } else { + /* + * Any other recursion. + */ + result = ns_query_recurse(qctx->client, qctx->qtype, qname, + qctx->fname, qctx->rdataset, + qctx->resuming); + } + + if (result == ISC_R_SUCCESS) { + qctx->client->query.attributes |= NS_QUERYATTR_RECURSING; + if (qctx->dns64) { + qctx->client->query.attributes |= NS_QUERYATTR_DNS64; + } + if (qctx->dns64_exclude) { + qctx->client->query.attributes |= + NS_QUERYATTR_DNS64EXCLUDE; + } + } else if (query_usestale(qctx, result)) { + /* + * If serve-stale is enabled, query_usestale() already set up + * 'qctx' for looking up a stale response. + */ + return (query_lookup(qctx)); + } else { + QUERY_ERROR(qctx, result); + } + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/*% + * Add DS/NSEC(3) record(s) if needed. + */ +static void +query_addds(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + dns_fixedname_t fixed; + dns_name_t *fname = NULL; + dns_name_t *rname = NULL; + dns_name_t *name; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + isc_buffer_t *dbuf, b; + isc_result_t result; + unsigned int count; + + CTRACE(ISC_LOG_DEBUG(3), "query_addds"); + + /* + * DS not needed. + */ + if (!WANTDNSSEC(client)) { + return; + } + + /* + * We'll need some resources... + */ + rdataset = ns_client_newrdataset(client); + sigrdataset = ns_client_newrdataset(client); + if (rdataset == NULL || sigrdataset == NULL) { + goto cleanup; + } + + /* + * Look for the DS record, which may or may not be present. + */ + result = dns_db_findrdataset(qctx->db, qctx->node, qctx->version, + dns_rdatatype_ds, 0, client->now, rdataset, + sigrdataset); + /* + * If we didn't find it, look for an NSEC. + */ + if (result == ISC_R_NOTFOUND) { + result = dns_db_findrdataset( + qctx->db, qctx->node, qctx->version, dns_rdatatype_nsec, + 0, client->now, rdataset, sigrdataset); + } + if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) { + goto addnsec3; + } + if (!dns_rdataset_isassociated(rdataset) || + !dns_rdataset_isassociated(sigrdataset)) + { + goto addnsec3; + } + + /* + * We've already added the NS record, so if the name's not there, + * we have other problems. + */ + result = dns_message_firstname(client->message, DNS_SECTION_AUTHORITY); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + /* + * Find the delegation in the response message - it is not necessarily + * the first name in the AUTHORITY section when wildcard processing is + * involved. + */ + while (result == ISC_R_SUCCESS) { + rname = NULL; + dns_message_currentname(client->message, DNS_SECTION_AUTHORITY, + &rname); + result = dns_message_findtype(rname, dns_rdatatype_ns, 0, NULL); + if (result == ISC_R_SUCCESS) { + break; + } + result = dns_message_nextname(client->message, + DNS_SECTION_AUTHORITY); + } + + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + /* + * Add the relevant RRset (DS or NSEC) to the delegation. + */ + query_addrrset(qctx, &rname, &rdataset, &sigrdataset, NULL, + DNS_SECTION_AUTHORITY); + goto cleanup; + +addnsec3: + if (!dns_db_iszone(qctx->db)) { + goto cleanup; + } + /* + * Add the NSEC3 which proves the DS does not exist. + */ + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + dns_fixedname_init(&fixed); + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + if (dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + name = dns_fixedname_name(&qctx->dsname); + query_findclosestnsec3(name, qctx->db, qctx->version, client, rdataset, + sigrdataset, fname, true, + dns_fixedname_name(&fixed)); + if (!dns_rdataset_isassociated(rdataset)) { + goto cleanup; + } + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + /* + * Did we find the closest provable encloser instead? + * If so add the nearest to the closest provable encloser. + */ + if (!dns_name_equal(name, dns_fixedname_name(&fixed))) { + count = dns_name_countlabels(dns_fixedname_name(&fixed)) + 1; + dns_name_getlabelsequence(name, + dns_name_countlabels(name) - count, + count, dns_fixedname_name(&fixed)); + fixfname(client, &fname, &dbuf, &b); + fixrdataset(client, &rdataset); + fixrdataset(client, &sigrdataset); + if (fname == NULL || rdataset == NULL || sigrdataset == NULL) { + goto cleanup; + } + query_findclosestnsec3(dns_fixedname_name(&fixed), qctx->db, + qctx->version, client, rdataset, + sigrdataset, fname, false, NULL); + if (!dns_rdataset_isassociated(rdataset)) { + goto cleanup; + } + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + } + +cleanup: + if (rdataset != NULL) { + ns_client_putrdataset(client, &rdataset); + } + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } +} + +/*% + * Handle authoritative NOERROR/NODATA responses. + */ +static isc_result_t +query_nodata(query_ctx_t *qctx, isc_result_t res) { + isc_result_t result = res; + + CCTRACE(ISC_LOG_DEBUG(3), "query_nodata"); + + CALL_HOOK(NS_QUERY_NODATA_BEGIN, qctx); + +#ifdef dns64_bis_return_excluded_addresses + if (qctx->dns64) +#else /* ifdef dns64_bis_return_excluded_addresses */ + if (qctx->dns64 && !qctx->dns64_exclude) +#endif /* ifdef dns64_bis_return_excluded_addresses */ + { + isc_buffer_t b; + /* + * Restore the answers from the previous AAAA lookup. + */ + if (qctx->rdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->rdataset); + } + if (qctx->sigrdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->sigrdataset); + } + RESTORE(qctx->rdataset, qctx->client->query.dns64_aaaa); + RESTORE(qctx->sigrdataset, qctx->client->query.dns64_sigaaaa); + if (qctx->fname == NULL) { + qctx->dbuf = ns_client_getnamebuf(qctx->client); + if (qctx->dbuf == NULL) { + CCTRACE(ISC_LOG_ERROR, "query_nodata: " + "ns_client_getnamebuf " + "failed (3)"); + QUERY_ERROR(qctx, ISC_R_NOMEMORY); + return (ns_query_done(qctx)); + } + qctx->fname = ns_client_newname(qctx->client, + qctx->dbuf, &b); + if (qctx->fname == NULL) { + CCTRACE(ISC_LOG_ERROR, "query_nodata: " + "ns_client_newname " + "failed (3)"); + QUERY_ERROR(qctx, ISC_R_NOMEMORY); + return (ns_query_done(qctx)); + } + } + dns_name_copynf(qctx->client->query.qname, qctx->fname); + qctx->dns64 = false; +#ifdef dns64_bis_return_excluded_addresses + /* + * Resume the diverted processing of the AAAA response? + */ + if (qctx->dns64_exclude) { + return (query_prepresponse(qctx)); + } +#endif /* ifdef dns64_bis_return_excluded_addresses */ + } else if ((result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) && + !ISC_LIST_EMPTY(qctx->view->dns64) && !qctx->nxrewrite && + qctx->client->message->rdclass == dns_rdataclass_in && + qctx->qtype == dns_rdatatype_aaaa) + { + /* + * Look to see if there are A records for this name. + */ + switch (result) { + case DNS_R_NCACHENXRRSET: + /* + * This is from the negative cache; if the ttl is + * zero, we need to work out whether we have just + * decremented to zero or there was no negative + * cache ttl in the answer. + */ + if (qctx->rdataset->ttl != 0) { + qctx->client->query.dns64_ttl = + qctx->rdataset->ttl; + break; + } + if (dns_rdataset_first(qctx->rdataset) == ISC_R_SUCCESS) + { + qctx->client->query.dns64_ttl = 0; + } + break; + case DNS_R_NXRRSET: + qctx->client->query.dns64_ttl = + dns64_ttl(qctx->db, qctx->version); + break; + default: + UNREACHABLE(); + } + + SAVE(qctx->client->query.dns64_aaaa, qctx->rdataset); + SAVE(qctx->client->query.dns64_sigaaaa, qctx->sigrdataset); + ns_client_releasename(qctx->client, &qctx->fname); + dns_db_detachnode(qctx->db, &qctx->node); + qctx->type = qctx->qtype = dns_rdatatype_a; + qctx->dns64 = true; + return (query_lookup(qctx)); + } + + if (qctx->is_zone) { + return (query_sign_nodata(qctx)); + } else { + /* + * We don't call query_addrrset() because we don't need any + * of its extra features (and things would probably break!). + */ + if (dns_rdataset_isassociated(qctx->rdataset)) { + ns_client_keepname(qctx->client, qctx->fname, + qctx->dbuf); + dns_message_addname(qctx->client->message, qctx->fname, + DNS_SECTION_AUTHORITY); + ISC_LIST_APPEND(qctx->fname->list, qctx->rdataset, + link); + qctx->fname = NULL; + qctx->rdataset = NULL; + } + } + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/*% + * Add RRSIGs for NOERROR/NODATA responses when answering authoritatively. + */ +isc_result_t +query_sign_nodata(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_sign_nodata"); + + /* + * Look for a NSEC3 record if we don't have a NSEC record. + */ + if (qctx->redirected) { + return (ns_query_done(qctx)); + } + if (!dns_rdataset_isassociated(qctx->rdataset) && + WANTDNSSEC(qctx->client)) + { + if ((qctx->fname->attributes & DNS_NAMEATTR_WILDCARD) == 0) { + dns_name_t *found; + dns_name_t *qname; + dns_fixedname_t fixed; + isc_buffer_t b; + + found = dns_fixedname_initname(&fixed); + qname = qctx->client->query.qname; + + query_findclosestnsec3(qname, qctx->db, qctx->version, + qctx->client, qctx->rdataset, + qctx->sigrdataset, qctx->fname, + true, found); + /* + * Did we find the closest provable encloser + * instead? If so add the nearest to the + * closest provable encloser. + */ + if (dns_rdataset_isassociated(qctx->rdataset) && + !dns_name_equal(qname, found) && + (((qctx->client->sctx->options & + NS_SERVER_NONEAREST) == 0) || + qctx->qtype == dns_rdatatype_ds)) + { + unsigned int count; + unsigned int skip; + + /* + * Add the closest provable encloser. + */ + query_addrrset(qctx, &qctx->fname, + &qctx->rdataset, + &qctx->sigrdataset, qctx->dbuf, + DNS_SECTION_AUTHORITY); + + count = dns_name_countlabels(found) + 1; + skip = dns_name_countlabels(qname) - count; + dns_name_getlabelsequence(qname, skip, count, + found); + + fixfname(qctx->client, &qctx->fname, + &qctx->dbuf, &b); + fixrdataset(qctx->client, &qctx->rdataset); + fixrdataset(qctx->client, &qctx->sigrdataset); + if (qctx->fname == NULL || + qctx->rdataset == NULL || + qctx->sigrdataset == NULL) + { + CCTRACE(ISC_LOG_ERROR, "query_sign_" + "nodata: " + "failure " + "getting " + "closest " + "encloser"); + QUERY_ERROR(qctx, ISC_R_NOMEMORY); + return (ns_query_done(qctx)); + } + /* + * 'nearest' doesn't exist so + * 'exist' is set to false. + */ + query_findclosestnsec3( + found, qctx->db, qctx->version, + qctx->client, qctx->rdataset, + qctx->sigrdataset, qctx->fname, false, + NULL); + } + } else { + ns_client_releasename(qctx->client, &qctx->fname); + query_addwildcardproof(qctx, false, true); + } + } + if (dns_rdataset_isassociated(qctx->rdataset)) { + /* + * If we've got a NSEC record, we need to save the + * name now because we're going call query_addsoa() + * below, and it needs to use the name buffer. + */ + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + } else if (qctx->fname != NULL) { + /* + * We're not going to use fname, and need to release + * our hold on the name buffer so query_addsoa() + * may use it. + */ + ns_client_releasename(qctx->client, &qctx->fname); + } + + /* + * The RPZ SOA has already been added to the additional section + * if this was an RPZ rewrite, but if it wasn't, add it now. + */ + if (!qctx->nxrewrite) { + result = query_addsoa(qctx, UINT32_MAX, DNS_SECTION_AUTHORITY); + if (result != ISC_R_SUCCESS) { + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + } + + /* + * Add NSEC record if we found one. + */ + if (WANTDNSSEC(qctx->client) && + dns_rdataset_isassociated(qctx->rdataset)) + { + query_addnxrrsetnsec(qctx); + } + + return (ns_query_done(qctx)); +} + +static void +query_addnxrrsetnsec(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + dns_rdata_t sigrdata; + dns_rdata_rrsig_t sig; + unsigned int labels; + isc_buffer_t *dbuf, b; + dns_name_t *fname; + isc_result_t result; + + INSIST(qctx->fname != NULL); + + if ((qctx->fname->attributes & DNS_NAMEATTR_WILDCARD) == 0) { + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY); + return; + } + + if (qctx->sigrdataset == NULL || + !dns_rdataset_isassociated(qctx->sigrdataset)) + { + return; + } + + if (dns_rdataset_first(qctx->sigrdataset) != ISC_R_SUCCESS) { + return; + } + + dns_rdata_init(&sigrdata); + dns_rdataset_current(qctx->sigrdataset, &sigrdata); + result = dns_rdata_tostruct(&sigrdata, &sig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + labels = dns_name_countlabels(qctx->fname); + if ((unsigned int)sig.labels + 1 >= labels) { + return; + } + + query_addwildcardproof(qctx, true, false); + + /* + * We'll need some resources... + */ + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + return; + } + + fname = ns_client_newname(client, dbuf, &b); + if (fname == NULL) { + return; + } + + dns_name_split(qctx->fname, sig.labels + 1, NULL, fname); + /* This will succeed, since we've stripped labels. */ + RUNTIME_CHECK(dns_name_concatenate(dns_wildcardname, fname, fname, + NULL) == ISC_R_SUCCESS); + query_addrrset(qctx, &fname, &qctx->rdataset, &qctx->sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); +} + +/*% + * Handle NXDOMAIN and empty wildcard responses. + */ +static isc_result_t +query_nxdomain(query_ctx_t *qctx, bool empty_wild) { + dns_section_t section; + uint32_t ttl; + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_nxdomain"); + + CALL_HOOK(NS_QUERY_NXDOMAIN_BEGIN, qctx); + + INSIST(qctx->is_zone || REDIRECT(qctx->client)); + + if (!empty_wild) { + result = query_redirect(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + } + + if (dns_rdataset_isassociated(qctx->rdataset)) { + /* + * If we've got a NSEC record, we need to save the + * name now because we're going call query_addsoa() + * below, and it needs to use the name buffer. + */ + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + } else if (qctx->fname != NULL) { + /* + * We're not going to use fname, and need to release + * our hold on the name buffer so query_addsoa() + * may use it. + */ + ns_client_releasename(qctx->client, &qctx->fname); + } + + /* + * Add SOA to the additional section if generated by a + * RPZ rewrite. + * + * If the query was for a SOA record force the + * ttl to zero so that it is possible for clients to find + * the containing zone of an arbitrary name with a stub + * resolver and not have it cached. + */ + section = qctx->nxrewrite ? DNS_SECTION_ADDITIONAL + : DNS_SECTION_AUTHORITY; + ttl = UINT32_MAX; + if (!qctx->nxrewrite && qctx->qtype == dns_rdatatype_soa && + qctx->zone != NULL && dns_zone_getzeronosoattl(qctx->zone)) + { + ttl = 0; + } + if (!qctx->nxrewrite || + (qctx->rpz_st != NULL && qctx->rpz_st->m.rpz->addsoa)) + { + result = query_addsoa(qctx, ttl, section); + if (result != ISC_R_SUCCESS) { + QUERY_ERROR(qctx, result); + return (ns_query_done(qctx)); + } + } + + if (WANTDNSSEC(qctx->client)) { + /* + * Add NSEC record if we found one. + */ + if (dns_rdataset_isassociated(qctx->rdataset)) { + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + &qctx->sigrdataset, NULL, + DNS_SECTION_AUTHORITY); + } + query_addwildcardproof(qctx, false, false); + } + + /* + * Set message rcode. + */ + if (empty_wild) { + qctx->client->message->rcode = dns_rcode_noerror; + } else { + qctx->client->message->rcode = dns_rcode_nxdomain; + } + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/* + * Handle both types of NXDOMAIN redirection, calling redirect() + * (which implements type redirect zones) and redirect2() (which + * implements recursive nxdomain-redirect lookups). + * + * Any result code other than ISC_R_COMPLETE means redirection was + * successful and the result code should be returned up the call stack. + * + * ISC_R_COMPLETE means we reached the end of this function without + * redirecting, so query processing should continue past it. + */ +static isc_result_t +query_redirect(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_redirect"); + + result = redirect(qctx->client, qctx->fname, qctx->rdataset, + &qctx->node, &qctx->db, &qctx->version, qctx->type); + switch (result) { + case ISC_R_SUCCESS: + inc_stats(qctx->client, ns_statscounter_nxdomainredirect); + return (query_prepresponse(qctx)); + case DNS_R_NXRRSET: + qctx->redirected = true; + qctx->is_zone = true; + return (query_nodata(qctx, DNS_R_NXRRSET)); + case DNS_R_NCACHENXRRSET: + qctx->redirected = true; + qctx->is_zone = false; + return (query_ncache(qctx, DNS_R_NCACHENXRRSET)); + default: + break; + } + + result = redirect2(qctx->client, qctx->fname, qctx->rdataset, + &qctx->node, &qctx->db, &qctx->version, qctx->type, + &qctx->is_zone); + switch (result) { + case ISC_R_SUCCESS: + inc_stats(qctx->client, ns_statscounter_nxdomainredirect); + return (query_prepresponse(qctx)); + case DNS_R_CONTINUE: + inc_stats(qctx->client, + ns_statscounter_nxdomainredirect_rlookup); + SAVE(qctx->client->query.redirect.db, qctx->db); + SAVE(qctx->client->query.redirect.node, qctx->node); + SAVE(qctx->client->query.redirect.zone, qctx->zone); + qctx->client->query.redirect.qtype = qctx->qtype; + INSIST(qctx->rdataset != NULL); + SAVE(qctx->client->query.redirect.rdataset, qctx->rdataset); + SAVE(qctx->client->query.redirect.sigrdataset, + qctx->sigrdataset); + qctx->client->query.redirect.result = DNS_R_NCACHENXDOMAIN; + dns_name_copynf(qctx->fname, + qctx->client->query.redirect.fname); + qctx->client->query.redirect.authoritative = + qctx->authoritative; + qctx->client->query.redirect.is_zone = qctx->is_zone; + return (ns_query_done(qctx)); + case DNS_R_NXRRSET: + qctx->redirected = true; + qctx->is_zone = true; + return (query_nodata(qctx, DNS_R_NXRRSET)); + case DNS_R_NCACHENXRRSET: + qctx->redirected = true; + qctx->is_zone = false; + return (query_ncache(qctx, DNS_R_NCACHENXRRSET)); + default: + break; + } + + return (ISC_R_COMPLETE); +} + +/*% + * Logging function to be passed to dns_nsec_noexistnodata. + */ +static void +log_noexistnodata(void *val, int level, const char *fmt, ...) { + query_ctx_t *qctx = val; + va_list ap; + + va_start(ap, fmt); + ns_client_logv(qctx->client, NS_LOGCATEGORY_QUERIES, NS_LOGMODULE_QUERY, + level, fmt, ap); + va_end(ap); +} + +static dns_ttl_t +query_synthttl(dns_rdataset_t *soardataset, dns_rdataset_t *sigsoardataset, + dns_rdataset_t *p1rdataset, dns_rdataset_t *sigp1rdataset, + dns_rdataset_t *p2rdataset, dns_rdataset_t *sigp2rdataset) { + dns_rdata_soa_t soa; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_ttl_t ttl; + isc_result_t result; + + REQUIRE(soardataset != NULL); + REQUIRE(sigsoardataset != NULL); + REQUIRE(p1rdataset != NULL); + REQUIRE(sigp1rdataset != NULL); + + result = dns_rdataset_first(soardataset); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdataset_current(soardataset, &rdata); + result = dns_rdata_tostruct(&rdata, &soa, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + ttl = ISC_MIN(soa.minimum, soardataset->ttl); + ttl = ISC_MIN(ttl, sigsoardataset->ttl); + ttl = ISC_MIN(ttl, p1rdataset->ttl); + ttl = ISC_MIN(ttl, sigp1rdataset->ttl); + if (p2rdataset != NULL) { + ttl = ISC_MIN(ttl, p2rdataset->ttl); + } + if (sigp2rdataset != NULL) { + ttl = ISC_MIN(ttl, sigp2rdataset->ttl); + } + + return (ttl); +} + +/* + * Synthesize a NODATA response from the SOA and covering NSEC in cache. + */ +static isc_result_t +query_synthnodata(query_ctx_t *qctx, const dns_name_t *signer, + dns_rdataset_t **soardatasetp, + dns_rdataset_t **sigsoardatasetp) { + dns_name_t *name = NULL; + dns_ttl_t ttl; + isc_buffer_t *dbuf, b; + isc_result_t result; + + /* + * Determine the correct TTL to use for the SOA and RRSIG + */ + ttl = query_synthttl(*soardatasetp, *sigsoardatasetp, qctx->rdataset, + qctx->sigrdataset, NULL, NULL); + (*soardatasetp)->ttl = (*sigsoardatasetp)->ttl = ttl; + + /* + * We want the SOA record to be first, so save the + * NODATA proof's name now or else discard it. + */ + if (WANTDNSSEC(qctx->client)) { + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + } else { + ns_client_releasename(qctx->client, &qctx->fname); + } + + dbuf = ns_client_getnamebuf(qctx->client); + if (dbuf == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + name = ns_client_newname(qctx->client, dbuf, &b); + if (name == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + dns_name_copynf(signer, name); + + /* + * Add SOA record. Omit the RRSIG if DNSSEC was not requested. + */ + if (!WANTDNSSEC(qctx->client)) { + sigsoardatasetp = NULL; + } + query_addrrset(qctx, &name, soardatasetp, sigsoardatasetp, dbuf, + DNS_SECTION_AUTHORITY); + + if (WANTDNSSEC(qctx->client)) { + /* + * Add NODATA proof. + */ + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY); + } + + result = ISC_R_SUCCESS; + inc_stats(qctx->client, ns_statscounter_nodatasynth); + +cleanup: + if (name != NULL) { + ns_client_releasename(qctx->client, &name); + } + return (result); +} + +/* + * Synthesize a wildcard answer using the contents of 'rdataset'. + * qctx contains the NODATA proof. + */ +static isc_result_t +query_synthwildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + dns_name_t *name = NULL; + isc_buffer_t *dbuf, b; + isc_result_t result; + dns_rdataset_t *cloneset = NULL, *clonesigset = NULL; + dns_rdataset_t **sigrdatasetp; + + CCTRACE(ISC_LOG_DEBUG(3), "query_synthwildcard"); + + /* + * We want the answer to be first, so save the + * NOQNAME proof's name now or else discard it. + */ + if (WANTDNSSEC(qctx->client)) { + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + } else { + ns_client_releasename(qctx->client, &qctx->fname); + } + + dbuf = ns_client_getnamebuf(qctx->client); + if (dbuf == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + name = ns_client_newname(qctx->client, dbuf, &b); + if (name == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + dns_name_copynf(qctx->client->query.qname, name); + + cloneset = ns_client_newrdataset(qctx->client); + if (cloneset == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + dns_rdataset_clone(rdataset, cloneset); + + /* + * Add answer RRset. Omit the RRSIG if DNSSEC was not requested. + */ + if (WANTDNSSEC(qctx->client)) { + clonesigset = ns_client_newrdataset(qctx->client); + if (clonesigset == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + dns_rdataset_clone(sigrdataset, clonesigset); + sigrdatasetp = &clonesigset; + } else { + sigrdatasetp = NULL; + } + + query_addrrset(qctx, &name, &cloneset, sigrdatasetp, dbuf, + DNS_SECTION_ANSWER); + + if (WANTDNSSEC(qctx->client)) { + /* + * Add NOQNAME proof. + */ + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY); + } + + result = ISC_R_SUCCESS; + inc_stats(qctx->client, ns_statscounter_wildcardsynth); + +cleanup: + if (name != NULL) { + ns_client_releasename(qctx->client, &name); + } + if (cloneset != NULL) { + ns_client_putrdataset(qctx->client, &cloneset); + } + if (clonesigset != NULL) { + ns_client_putrdataset(qctx->client, &clonesigset); + } + return (result); +} + +/* + * Add a synthesized CNAME record from the wildard RRset (rdataset) + * and NODATA proof by calling query_synthwildcard then setup to + * follow the CNAME. + */ +static isc_result_t +query_synthcnamewildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset, + dns_rdataset_t *sigrdataset) { + isc_result_t result; + dns_name_t *tname = NULL; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_cname_t cname; + + result = query_synthwildcard(qctx, rdataset, sigrdataset); + if (result != ISC_R_SUCCESS) { + return (result); + } + + qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER; + + /* + * Reset qname to be the target name of the CNAME and restart + * the query. + */ + result = dns_message_gettempname(qctx->client->message, &tname); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = dns_rdataset_first(rdataset); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(qctx->client->message, &tname); + return (result); + } + + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &cname, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&rdata); + + if (dns_name_equal(qctx->client->query.qname, &cname.cname)) { + dns_message_puttempname(qctx->client->message, &tname); + dns_rdata_freestruct(&cname); + return (ISC_R_SUCCESS); + } + + dns_name_copynf(&cname.cname, tname); + + dns_rdata_freestruct(&cname); + ns_client_qnamereplace(qctx->client, tname); + qctx->want_restart = true; + if (!WANTRECURSION(qctx->client)) { + qctx->options |= DNS_GETDB_NOLOG; + } + + return (result); +} + +/* + * Synthesize a NXDOMAIN response from qctx (which contains the + * NODATA proof), nowild + nowildrdataset + signowildrdataset (which + * contains the NOWILDCARD proof) and signer + soardatasetp + sigsoardatasetp + * which contain the SOA record + RRSIG for the negative answer. + */ +static isc_result_t +query_synthnxdomain(query_ctx_t *qctx, dns_name_t *nowild, + dns_rdataset_t *nowildrdataset, + dns_rdataset_t *signowildrdataset, dns_name_t *signer, + dns_rdataset_t **soardatasetp, + dns_rdataset_t **sigsoardatasetp) { + dns_name_t *name = NULL; + dns_ttl_t ttl; + isc_buffer_t *dbuf, b; + isc_result_t result; + dns_rdataset_t *cloneset = NULL, *clonesigset = NULL; + + CCTRACE(ISC_LOG_DEBUG(3), "query_synthnxdomain"); + + /* + * Determine the correct TTL to use for the SOA and RRSIG + */ + ttl = query_synthttl(*soardatasetp, *sigsoardatasetp, qctx->rdataset, + qctx->sigrdataset, nowildrdataset, + signowildrdataset); + (*soardatasetp)->ttl = (*sigsoardatasetp)->ttl = ttl; + + /* + * We want the SOA record to be first, so save the + * NOQNAME proof's name now or else discard it. + */ + if (WANTDNSSEC(qctx->client)) { + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + } else { + ns_client_releasename(qctx->client, &qctx->fname); + } + + dbuf = ns_client_getnamebuf(qctx->client); + if (dbuf == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + name = ns_client_newname(qctx->client, dbuf, &b); + if (name == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + dns_name_copynf(signer, name); + + /* + * Add SOA record. Omit the RRSIG if DNSSEC was not requested. + */ + if (!WANTDNSSEC(qctx->client)) { + sigsoardatasetp = NULL; + } + query_addrrset(qctx, &name, soardatasetp, sigsoardatasetp, dbuf, + DNS_SECTION_AUTHORITY); + + if (WANTDNSSEC(qctx->client)) { + /* + * Add NOQNAME proof. + */ + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, + &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY); + + dbuf = ns_client_getnamebuf(qctx->client); + if (dbuf == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + name = ns_client_newname(qctx->client, dbuf, &b); + if (name == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + dns_name_copynf(nowild, name); + + cloneset = ns_client_newrdataset(qctx->client); + clonesigset = ns_client_newrdataset(qctx->client); + if (cloneset == NULL || clonesigset == NULL) { + result = ISC_R_NOMEMORY; + goto cleanup; + } + + dns_rdataset_clone(nowildrdataset, cloneset); + dns_rdataset_clone(signowildrdataset, clonesigset); + + /* + * Add NOWILDCARD proof. + */ + query_addrrset(qctx, &name, &cloneset, &clonesigset, dbuf, + DNS_SECTION_AUTHORITY); + } + + qctx->client->message->rcode = dns_rcode_nxdomain; + result = ISC_R_SUCCESS; + inc_stats(qctx->client, ns_statscounter_nxdomainsynth); + +cleanup: + if (name != NULL) { + ns_client_releasename(qctx->client, &name); + } + if (cloneset != NULL) { + ns_client_putrdataset(qctx->client, &cloneset); + } + if (clonesigset != NULL) { + ns_client_putrdataset(qctx->client, &clonesigset); + } + return (result); +} + +/* + * Check that all signer names in sigrdataset match the expected signer. + */ +static isc_result_t +checksignames(dns_name_t *signer, dns_rdataset_t *sigrdataset) { + isc_result_t result; + + for (result = dns_rdataset_first(sigrdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(sigrdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_rrsig_t rrsig; + + dns_rdataset_current(sigrdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &rrsig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (dns_name_countlabels(signer) == 0) { + dns_name_copynf(&rrsig.signer, signer); + } else if (!dns_name_equal(signer, &rrsig.signer)) { + return (ISC_R_FAILURE); + } + } + + return (ISC_R_SUCCESS); +} + +/*% + * Handle covering NSEC responses. + * + * Verify the NSEC record is appropriate for the QNAME; if not, + * redo the initial query without DNS_DBFIND_COVERINGNSEC. + * + * If the covering NSEC proves that the name exists but not the type, + * synthesize a NODATA response. + * + * If the name doesn't exist, compute the wildcard record and check whether + * the wildcard name exists or not. If we can't determine this, redo the + * initial query without DNS_DBFIND_COVERINGNSEC. + * + * If the wildcard name does not exist, compute the SOA name and look that + * up. If the SOA record does not exist, redo the initial query without + * DNS_DBFIND_COVERINGNSEC. If the SOA record exists, synthesize an + * NXDOMAIN response from the found records. + * + * If the wildcard name does exist, perform a lookup for the requested + * type at the wildcard name. + */ +static isc_result_t +query_coveringnsec(query_ctx_t *qctx) { + dns_db_t *db = NULL; + dns_clientinfo_t ci; + dns_clientinfomethods_t cm; + dns_dbnode_t *node = NULL; + dns_fixedname_t fixed; + dns_fixedname_t fnowild; + dns_fixedname_t fsigner; + dns_fixedname_t fwild; + dns_name_t *fname = NULL; + dns_name_t *nowild = NULL; + dns_name_t *signer = NULL; + dns_name_t *wild = NULL; + dns_rdataset_t *soardataset = NULL, *sigsoardataset = NULL; + dns_rdataset_t rdataset, sigrdataset; + bool done = false; + bool exists = true, data = true; + bool redirected = false; + isc_result_t result = ISC_R_SUCCESS; + unsigned int dboptions = qctx->client->query.dboptions; + + CCTRACE(ISC_LOG_DEBUG(3), "query_coveringnsec"); + + dns_rdataset_init(&rdataset); + dns_rdataset_init(&sigrdataset); + + /* + * If we have no signer name, stop immediately. + */ + if (!dns_rdataset_isassociated(qctx->sigrdataset)) { + goto cleanup; + } + + wild = dns_fixedname_initname(&fwild); + fname = dns_fixedname_initname(&fixed); + signer = dns_fixedname_initname(&fsigner); + nowild = dns_fixedname_initname(&fnowild); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, qctx->client, NULL, NULL); + + /* + * All signer names must be the same to accept. + */ + result = checksignames(signer, qctx->sigrdataset); + if (result != ISC_R_SUCCESS) { + result = ISC_R_SUCCESS; + goto cleanup; + } + + /* + * Check that we have the correct NOQNAME NSEC record. + */ + result = dns_nsec_noexistnodata(qctx->qtype, qctx->client->query.qname, + qctx->fname, qctx->rdataset, &exists, + &data, wild, log_noexistnodata, qctx); + + if (result != ISC_R_SUCCESS || (exists && data)) { + goto cleanup; + } + + if (exists) { + if (qctx->type == dns_rdatatype_any) { /* XXX not yet */ + goto cleanup; + } + if (!ISC_LIST_EMPTY(qctx->view->dns64) && + (qctx->type == dns_rdatatype_a || + qctx->type == dns_rdatatype_aaaa)) /* XXX not yet */ + { + goto cleanup; + } + if (!qctx->resuming && !STALE(qctx->rdataset) && + qctx->rdataset->ttl == 0 && RECURSIONOK(qctx->client)) + { + goto cleanup; + } + + soardataset = ns_client_newrdataset(qctx->client); + sigsoardataset = ns_client_newrdataset(qctx->client); + if (soardataset == NULL || sigsoardataset == NULL) { + goto cleanup; + } + + /* + * Look for SOA record to construct NODATA response. + */ + dns_db_attach(qctx->db, &db); + result = dns_db_findext(db, signer, qctx->version, + dns_rdatatype_soa, dboptions, + qctx->client->now, &node, fname, &cm, + &ci, soardataset, sigsoardataset); + + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + (void)query_synthnodata(qctx, signer, &soardataset, + &sigsoardataset); + done = true; + goto cleanup; + } + + /* + * Look up the no-wildcard proof. + */ + dns_db_attach(qctx->db, &db); + result = dns_db_findext(db, wild, qctx->version, qctx->type, + dboptions | DNS_DBFIND_COVERINGNSEC, + qctx->client->now, &node, nowild, &cm, &ci, + &rdataset, &sigrdataset); + + if (rdataset.trust != dns_trust_secure || + sigrdataset.trust != dns_trust_secure) + { + goto cleanup; + } + + /* + * Zero TTL handling of wildcard record. + * + * We don't yet have code to handle synthesis and type ANY or dns64 + * processing so we abort the synthesis here if there would be a + * interaction. + */ + switch (result) { + case ISC_R_SUCCESS: + if (qctx->type == dns_rdatatype_any) { /* XXX not yet */ + goto cleanup; + } + if (!ISC_LIST_EMPTY(qctx->view->dns64) && + (qctx->type == dns_rdatatype_a || + qctx->type == dns_rdatatype_aaaa)) /* XXX not yet */ + { + goto cleanup; + } + FALLTHROUGH; + case DNS_R_CNAME: + if (!qctx->resuming && !STALE(&rdataset) && rdataset.ttl == 0 && + RECURSIONOK(qctx->client)) + { + goto cleanup; + } + default: + break; + } + + switch (result) { + case DNS_R_COVERINGNSEC: + result = dns_nsec_noexistnodata(qctx->qtype, wild, nowild, + &rdataset, &exists, &data, NULL, + log_noexistnodata, qctx); + if (result != ISC_R_SUCCESS || exists) { + goto cleanup; + } + break; + case ISC_R_SUCCESS: /* wild card match */ + (void)query_synthwildcard(qctx, &rdataset, &sigrdataset); + done = true; + goto cleanup; + case DNS_R_CNAME: /* wild card cname */ + (void)query_synthcnamewildcard(qctx, &rdataset, &sigrdataset); + done = true; + goto cleanup; + case DNS_R_NCACHENXRRSET: /* wild card nodata */ + case DNS_R_NCACHENXDOMAIN: /* direct nxdomain */ + default: + goto cleanup; + } + + /* + * We now have the proof that we have an NXDOMAIN. Apply + * NXDOMAIN redirection if configured. + */ + result = query_redirect(qctx); + if (result != ISC_R_COMPLETE) { + redirected = true; + goto cleanup; + } + + /* + * Must be signed to accept. + */ + if (!dns_rdataset_isassociated(&sigrdataset)) { + goto cleanup; + } + + /* + * Check signer signer names again. + */ + result = checksignames(signer, &sigrdataset); + if (result != ISC_R_SUCCESS) { + result = ISC_R_SUCCESS; + goto cleanup; + } + + if (node != NULL) { + dns_db_detachnode(db, &node); + } + + soardataset = ns_client_newrdataset(qctx->client); + sigsoardataset = ns_client_newrdataset(qctx->client); + if (soardataset == NULL || sigsoardataset == NULL) { + goto cleanup; + } + + /* + * Look for SOA record to construct NXDOMAIN response. + */ + result = dns_db_findext(db, signer, qctx->version, dns_rdatatype_soa, + dboptions, qctx->client->now, &node, fname, &cm, + &ci, soardataset, sigsoardataset); + + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + (void)query_synthnxdomain(qctx, nowild, &rdataset, &sigrdataset, signer, + &soardataset, &sigsoardataset); + done = true; + +cleanup: + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + if (dns_rdataset_isassociated(&sigrdataset)) { + dns_rdataset_disassociate(&sigrdataset); + } + if (soardataset != NULL) { + ns_client_putrdataset(qctx->client, &soardataset); + } + if (sigsoardataset != NULL) { + ns_client_putrdataset(qctx->client, &sigsoardataset); + } + if (db != NULL) { + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + } + + if (redirected) { + return (result); + } + + if (!done) { + /* + * No covering NSEC was found; proceed with recursion. + */ + qctx->findcoveringnsec = false; + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, &qctx->fname); + } + if (qctx->node != NULL) { + dns_db_detachnode(qctx->db, &qctx->node); + } + ns_client_putrdataset(qctx->client, &qctx->rdataset); + if (qctx->sigrdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->sigrdataset); + } + return (query_lookup(qctx)); + } + + return (ns_query_done(qctx)); +} + +/*% + * Handle negative cache responses, DNS_R_NCACHENXRRSET or + * DNS_R_NCACHENXDOMAIN. (Note: may also be called with result + * set to DNS_R_NXDOMAIN when handling DNS64 lookups.) + */ +static isc_result_t +query_ncache(query_ctx_t *qctx, isc_result_t result) { + INSIST(!qctx->is_zone); + INSIST(result == DNS_R_NCACHENXDOMAIN || + result == DNS_R_NCACHENXRRSET || result == DNS_R_NXDOMAIN); + + CCTRACE(ISC_LOG_DEBUG(3), "query_ncache"); + + CALL_HOOK(NS_QUERY_NCACHE_BEGIN, qctx); + + qctx->authoritative = false; + + if (result == DNS_R_NCACHENXDOMAIN) { + /* + * Set message rcode. (This is not done when + * result == DNS_R_NXDOMAIN because that means we're + * being called after a DNS64 lookup and don't want + * to update the rcode now.) + */ + qctx->client->message->rcode = dns_rcode_nxdomain; + + /* Look for RFC 1918 leakage from Internet. */ + if (qctx->qtype == dns_rdatatype_ptr && + qctx->client->message->rdclass == dns_rdataclass_in && + dns_name_countlabels(qctx->fname) == 7) + { + warn_rfc1918(qctx->client, qctx->fname, qctx->rdataset); + } + } + + return (query_nodata(qctx, result)); + +cleanup: + return (result); +} + +/* + * If we have a zero ttl from the cache, refetch. + */ +static isc_result_t +query_zerottl_refetch(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_zerottl_refetch"); + + if (qctx->is_zone || qctx->resuming || STALE(qctx->rdataset) || + qctx->rdataset->ttl != 0 || !RECURSIONOK(qctx->client)) + { + return (ISC_R_COMPLETE); + } + + qctx_clean(qctx); + + INSIST(!REDIRECT(qctx->client)); + + result = ns_query_recurse(qctx->client, qctx->qtype, + qctx->client->query.qname, NULL, NULL, + qctx->resuming); + if (result == ISC_R_SUCCESS) { + CALL_HOOK(NS_QUERY_ZEROTTL_RECURSE, qctx); + qctx->client->query.attributes |= NS_QUERYATTR_RECURSING; + + if (qctx->dns64) { + qctx->client->query.attributes |= NS_QUERYATTR_DNS64; + } + if (qctx->dns64_exclude) { + qctx->client->query.attributes |= + NS_QUERYATTR_DNS64EXCLUDE; + } + } else { + /* + * There was a zero ttl from the cache, don't fallback to + * serve-stale lookup. + */ + QUERY_ERROR(qctx, result); + } + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/* + * Handle CNAME responses. + */ +static isc_result_t +query_cname(query_ctx_t *qctx) { + isc_result_t result = ISC_R_UNSET; + dns_name_t *tname = NULL; + dns_rdataset_t *trdataset = NULL; + dns_rdataset_t **sigrdatasetp = NULL; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_cname_t cname; + + CCTRACE(ISC_LOG_DEBUG(3), "query_cname"); + + CALL_HOOK(NS_QUERY_CNAME_BEGIN, qctx); + + result = query_zerottl_refetch(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + + /* + * Keep a copy of the rdataset. We have to do this because + * query_addrrset may clear 'rdataset' (to prevent the + * cleanup code from cleaning it up). + */ + trdataset = qctx->rdataset; + + /* + * Add the CNAME to the answer section. + */ + if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) { + sigrdatasetp = &qctx->sigrdataset; + } + + if (WANTDNSSEC(qctx->client) && + (qctx->fname->attributes & DNS_NAMEATTR_WILDCARD) != 0) + { + dns_fixedname_init(&qctx->wildcardname); + dns_name_copynf(qctx->fname, + dns_fixedname_name(&qctx->wildcardname)); + qctx->need_wildcardproof = true; + } + + if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client)) { + qctx->noqname = qctx->rdataset; + } else { + qctx->noqname = NULL; + } + + if (!qctx->is_zone && RECURSIONOK(qctx->client)) { + query_prefetch(qctx->client, qctx->fname, qctx->rdataset); + } + + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp, + qctx->dbuf, DNS_SECTION_ANSWER); + + query_addnoqnameproof(qctx); + + /* + * We set the PARTIALANSWER attribute so that if anything goes + * wrong later on, we'll return what we've got so far. + */ + qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER; + + /* + * Reset qname to be the target name of the CNAME and restart + * the query. + */ + result = dns_message_gettempname(qctx->client->message, &tname); + if (result != ISC_R_SUCCESS) { + return (ns_query_done(qctx)); + } + + result = dns_rdataset_first(trdataset); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(qctx->client->message, &tname); + return (ns_query_done(qctx)); + } + + dns_rdataset_current(trdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &cname, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&rdata); + + dns_name_copynf(&cname.cname, tname); + + dns_rdata_freestruct(&cname); + ns_client_qnamereplace(qctx->client, tname); + qctx->want_restart = true; + if (!WANTRECURSION(qctx->client)) { + qctx->options |= DNS_GETDB_NOLOG; + } + + query_addauth(qctx); + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/* + * Handle DNAME responses. + */ +static isc_result_t +query_dname(query_ctx_t *qctx) { + dns_name_t *tname, *prefix; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_dname_t dname; + dns_fixedname_t fixed; + dns_rdataset_t *trdataset; + dns_rdataset_t **sigrdatasetp = NULL; + dns_namereln_t namereln; + isc_buffer_t b; + int order; + isc_result_t result; + unsigned int nlabels; + + CCTRACE(ISC_LOG_DEBUG(3), "query_dname"); + + CALL_HOOK(NS_QUERY_DNAME_BEGIN, qctx); + + /* + * Compare the current qname to the found name. We need + * to know how many labels and bits are in common because + * we're going to have to split qname later on. + */ + namereln = dns_name_fullcompare(qctx->client->query.qname, qctx->fname, + &order, &nlabels); + INSIST(namereln == dns_namereln_subdomain); + + /* + * Keep a copy of the rdataset. We have to do this because + * query_addrrset may clear 'rdataset' (to prevent the + * cleanup code from cleaning it up). + */ + trdataset = qctx->rdataset; + + /* + * Add the DNAME to the answer section. + */ + if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) { + sigrdatasetp = &qctx->sigrdataset; + } + + if (WANTDNSSEC(qctx->client) && + (qctx->fname->attributes & DNS_NAMEATTR_WILDCARD) != 0) + { + dns_fixedname_init(&qctx->wildcardname); + dns_name_copynf(qctx->fname, + dns_fixedname_name(&qctx->wildcardname)); + qctx->need_wildcardproof = true; + } + + if (!qctx->is_zone && RECURSIONOK(qctx->client)) { + query_prefetch(qctx->client, qctx->fname, qctx->rdataset); + } + query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp, + qctx->dbuf, DNS_SECTION_ANSWER); + + /* + * We set the PARTIALANSWER attribute so that if anything goes + * wrong later on, we'll return what we've got so far. + */ + qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER; + + /* + * Get the target name of the DNAME. + */ + tname = NULL; + result = dns_message_gettempname(qctx->client->message, &tname); + if (result != ISC_R_SUCCESS) { + return (ns_query_done(qctx)); + } + + result = dns_rdataset_first(trdataset); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(qctx->client->message, &tname); + return (ns_query_done(qctx)); + } + + dns_rdataset_current(trdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &dname, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdata_reset(&rdata); + + dns_name_copynf(&dname.dname, tname); + dns_rdata_freestruct(&dname); + + /* + * Construct the new qname consisting of + * <found name prefix>.<dname target> + */ + prefix = dns_fixedname_initname(&fixed); + dns_name_split(qctx->client->query.qname, nlabels, prefix, NULL); + INSIST(qctx->fname == NULL); + qctx->dbuf = ns_client_getnamebuf(qctx->client); + if (qctx->dbuf == NULL) { + dns_message_puttempname(qctx->client->message, &tname); + return (ns_query_done(qctx)); + } + qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b); + if (qctx->fname == NULL) { + dns_message_puttempname(qctx->client->message, &tname); + return (ns_query_done(qctx)); + } + result = dns_name_concatenate(prefix, tname, qctx->fname, NULL); + dns_message_puttempname(qctx->client->message, &tname); + + /* + * RFC2672, section 4.1, subsection 3c says + * we should return YXDOMAIN if the constructed + * name would be too long. + */ + if (result == DNS_R_NAMETOOLONG) { + qctx->client->message->rcode = dns_rcode_yxdomain; + } + if (result != ISC_R_SUCCESS) { + return (ns_query_done(qctx)); + } + + ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf); + + /* + * Synthesize a CNAME consisting of + * <old qname> <dname ttl> CNAME <new qname> + * with <dname trust value> + * + * Synthesize a CNAME so old old clients that don't understand + * DNAME can chain. + * + * We do not try to synthesize a signature because we hope + * that security aware servers will understand DNAME. Also, + * even if we had an online key, making a signature + * on-the-fly is costly, and not really legitimate anyway + * since the synthesized CNAME is NOT in the zone. + */ + result = query_addcname(qctx, trdataset->trust, trdataset->ttl); + if (result != ISC_R_SUCCESS) { + return (ns_query_done(qctx)); + } + + /* + * If the original query was not for a CNAME or ANY then follow the + * CNAME. + */ + if (qctx->qtype != dns_rdatatype_cname && + qctx->qtype != dns_rdatatype_any) + { + /* + * Switch to the new qname and restart. + */ + ns_client_qnamereplace(qctx->client, qctx->fname); + qctx->fname = NULL; + qctx->want_restart = true; + if (!WANTRECURSION(qctx->client)) { + qctx->options |= DNS_GETDB_NOLOG; + } + } + + query_addauth(qctx); + + return (ns_query_done(qctx)); + +cleanup: + return (result); +} + +/*% + * Add CNAME to response. + */ +static isc_result_t +query_addcname(query_ctx_t *qctx, dns_trust_t trust, dns_ttl_t ttl) { + ns_client_t *client = qctx->client; + dns_rdataset_t *rdataset = NULL; + dns_rdatalist_t *rdatalist = NULL; + dns_rdata_t *rdata = NULL; + isc_region_t r; + dns_name_t *aname = NULL; + isc_result_t result; + + result = dns_message_gettempname(client->message, &aname); + if (result != ISC_R_SUCCESS) { + return (result); + } + + dns_name_copynf(client->query.qname, aname); + + result = dns_message_gettemprdatalist(client->message, &rdatalist); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(client->message, &aname); + return (result); + } + + result = dns_message_gettemprdata(client->message, &rdata); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(client->message, &aname); + dns_message_puttemprdatalist(client->message, &rdatalist); + return (result); + } + + result = dns_message_gettemprdataset(client->message, &rdataset); + if (result != ISC_R_SUCCESS) { + dns_message_puttempname(client->message, &aname); + dns_message_puttemprdatalist(client->message, &rdatalist); + dns_message_puttemprdata(client->message, &rdata); + return (result); + } + + rdatalist->type = dns_rdatatype_cname; + rdatalist->rdclass = client->message->rdclass; + rdatalist->ttl = ttl; + + dns_name_toregion(qctx->fname, &r); + rdata->data = r.base; + rdata->length = r.length; + rdata->rdclass = client->message->rdclass; + rdata->type = dns_rdatatype_cname; + + ISC_LIST_APPEND(rdatalist->rdata, rdata, link); + RUNTIME_CHECK(dns_rdatalist_tordataset(rdatalist, rdataset) == + ISC_R_SUCCESS); + rdataset->trust = trust; + dns_rdataset_setownercase(rdataset, aname); + + query_addrrset(qctx, &aname, &rdataset, NULL, NULL, DNS_SECTION_ANSWER); + if (rdataset != NULL) { + if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + dns_message_puttemprdataset(client->message, &rdataset); + } + if (aname != NULL) { + dns_message_puttempname(client->message, &aname); + } + + return (ISC_R_SUCCESS); +} + +/*% + * Prepare to respond: determine whether a wildcard proof is needed, + * then hand off to query_respond() or (for type ANY queries) + * query_respond_any(). + */ +static isc_result_t +query_prepresponse(query_ctx_t *qctx) { + isc_result_t result; + + CCTRACE(ISC_LOG_DEBUG(3), "query_prepresponse"); + + CALL_HOOK(NS_QUERY_PREP_RESPONSE_BEGIN, qctx); + + if (WANTDNSSEC(qctx->client) && + (qctx->fname->attributes & DNS_NAMEATTR_WILDCARD) != 0) + { + dns_fixedname_init(&qctx->wildcardname); + dns_name_copynf(qctx->fname, + dns_fixedname_name(&qctx->wildcardname)); + qctx->need_wildcardproof = true; + } + + if (qctx->type == dns_rdatatype_any) { + return (query_respond_any(qctx)); + } + + result = query_zerottl_refetch(qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + + return (query_respond(qctx)); + +cleanup: + return (result); +} + +/*% + * Add SOA to the authority section when sending negative responses + * (or to the additional section if sending negative responses triggered + * by RPZ rewriting.) + */ +static isc_result_t +query_addsoa(query_ctx_t *qctx, unsigned int override_ttl, + dns_section_t section) { + ns_client_t *client = qctx->client; + dns_name_t *name; + dns_dbnode_t *node; + isc_result_t result, eresult; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + dns_rdataset_t **sigrdatasetp = NULL; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + CTRACE(ISC_LOG_DEBUG(3), "query_addsoa"); + /* + * Initialization. + */ + eresult = ISC_R_SUCCESS; + name = NULL; + rdataset = NULL; + node = NULL; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Don't add the SOA record for test which set "-T nosoa". + */ + if (((client->sctx->options & NS_SERVER_NOSOA) != 0) && + (!WANTDNSSEC(client) || !dns_rdataset_isassociated(qctx->rdataset))) + { + return (ISC_R_SUCCESS); + } + + /* + * Get resources and make 'name' be the database origin. + */ + result = dns_message_gettempname(client->message, &name); + if (result != ISC_R_SUCCESS) { + return (result); + } + + /* + * We'll be releasing 'name' before returning, so it's safe to + * use clone instead of copying here. + */ + dns_name_clone(dns_db_origin(qctx->db), name); + + rdataset = ns_client_newrdataset(client); + if (rdataset == NULL) { + CTRACE(ISC_LOG_ERROR, "unable to allocate rdataset"); + eresult = DNS_R_SERVFAIL; + goto cleanup; + } + if (WANTDNSSEC(client) && dns_db_issecure(qctx->db)) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + CTRACE(ISC_LOG_ERROR, "unable to allocate sigrdataset"); + eresult = DNS_R_SERVFAIL; + goto cleanup; + } + } + + /* + * Find the SOA. + */ + result = dns_db_getoriginnode(qctx->db, &node); + if (result == ISC_R_SUCCESS) { + result = dns_db_findrdataset(qctx->db, node, qctx->version, + dns_rdatatype_soa, 0, client->now, + rdataset, sigrdataset); + } else { + dns_fixedname_t foundname; + dns_name_t *fname; + + fname = dns_fixedname_initname(&foundname); + + result = dns_db_findext(qctx->db, name, qctx->version, + dns_rdatatype_soa, + client->query.dboptions, 0, &node, + fname, &cm, &ci, rdataset, sigrdataset); + } + if (result != ISC_R_SUCCESS) { + /* + * This is bad. We tried to get the SOA RR at the zone top + * and it didn't work! + */ + CTRACE(ISC_LOG_ERROR, "unable to find SOA RR at zone apex"); + eresult = DNS_R_SERVFAIL; + } else { + /* + * Extract the SOA MINIMUM. + */ + dns_rdata_soa_t soa; + dns_rdata_t rdata = DNS_RDATA_INIT; + result = dns_rdataset_first(rdataset); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &soa, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + if (override_ttl != UINT32_MAX && override_ttl < rdataset->ttl) + { + rdataset->ttl = override_ttl; + if (sigrdataset != NULL) { + sigrdataset->ttl = override_ttl; + } + } + + /* + * Add the SOA and its SIG to the response, with the + * TTLs adjusted per RFC2308 section 3. + */ + if (rdataset->ttl > soa.minimum) { + rdataset->ttl = soa.minimum; + } + if (sigrdataset != NULL && sigrdataset->ttl > soa.minimum) { + sigrdataset->ttl = soa.minimum; + } + + if (sigrdataset != NULL) { + sigrdatasetp = &sigrdataset; + } else { + sigrdatasetp = NULL; + } + + if (section == DNS_SECTION_ADDITIONAL) { + rdataset->attributes |= DNS_RDATASETATTR_REQUIRED; + } + query_addrrset(qctx, &name, &rdataset, sigrdatasetp, NULL, + section); + } + +cleanup: + ns_client_putrdataset(client, &rdataset); + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (name != NULL) { + ns_client_releasename(client, &name); + } + if (node != NULL) { + dns_db_detachnode(qctx->db, &node); + } + + return (eresult); +} + +/*% + * Add NS to authority section (used when the zone apex is already known). + */ +static isc_result_t +query_addns(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + isc_result_t result, eresult; + dns_name_t *name = NULL, *fname; + dns_dbnode_t *node = NULL; + dns_fixedname_t foundname; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + dns_rdataset_t **sigrdatasetp = NULL; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + CTRACE(ISC_LOG_DEBUG(3), "query_addns"); + + /* + * Initialization. + */ + eresult = ISC_R_SUCCESS; + fname = dns_fixedname_initname(&foundname); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Get resources and make 'name' be the database origin. + */ + result = dns_message_gettempname(client->message, &name); + if (result != ISC_R_SUCCESS) { + CTRACE(ISC_LOG_DEBUG(3), "query_addns: dns_message_gettempname " + "failed: done"); + return (result); + } + dns_name_clone(dns_db_origin(qctx->db), name); + rdataset = ns_client_newrdataset(client); + if (rdataset == NULL) { + CTRACE(ISC_LOG_ERROR, "query_addns: ns_client_newrdataset " + "failed"); + eresult = DNS_R_SERVFAIL; + goto cleanup; + } + + if (WANTDNSSEC(client) && dns_db_issecure(qctx->db)) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + CTRACE(ISC_LOG_ERROR, "query_addns: " + "ns_client_newrdataset failed"); + eresult = DNS_R_SERVFAIL; + goto cleanup; + } + } + + /* + * Find the NS rdataset. + */ + result = dns_db_getoriginnode(qctx->db, &node); + if (result == ISC_R_SUCCESS) { + result = dns_db_findrdataset(qctx->db, node, qctx->version, + dns_rdatatype_ns, 0, client->now, + rdataset, sigrdataset); + } else { + CTRACE(ISC_LOG_DEBUG(3), "query_addns: calling dns_db_find"); + result = dns_db_findext(qctx->db, name, NULL, dns_rdatatype_ns, + client->query.dboptions, 0, &node, + fname, &cm, &ci, rdataset, sigrdataset); + CTRACE(ISC_LOG_DEBUG(3), "query_addns: dns_db_find complete"); + } + if (result != ISC_R_SUCCESS) { + CTRACE(ISC_LOG_ERROR, "query_addns: " + "dns_db_findrdataset or dns_db_find " + "failed"); + /* + * This is bad. We tried to get the NS rdataset at the zone + * top and it didn't work! + */ + eresult = DNS_R_SERVFAIL; + } else { + if (sigrdataset != NULL) { + sigrdatasetp = &sigrdataset; + } + query_addrrset(qctx, &name, &rdataset, sigrdatasetp, NULL, + DNS_SECTION_AUTHORITY); + } + +cleanup: + CTRACE(ISC_LOG_DEBUG(3), "query_addns: cleanup"); + ns_client_putrdataset(client, &rdataset); + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (name != NULL) { + ns_client_releasename(client, &name); + } + if (node != NULL) { + dns_db_detachnode(qctx->db, &node); + } + + CTRACE(ISC_LOG_DEBUG(3), "query_addns: done"); + return (eresult); +} + +/*% + * Find the zone cut and add the best NS rrset to the authority section. + */ +static void +query_addbestns(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + dns_db_t *db = NULL, *zdb = NULL; + dns_dbnode_t *node = NULL; + dns_name_t *fname = NULL, *zfname = NULL; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + dns_rdataset_t *zrdataset = NULL, *zsigrdataset = NULL; + bool is_zone = false, use_zone = false; + isc_buffer_t *dbuf = NULL; + isc_result_t result; + dns_dbversion_t *version = NULL; + dns_zone_t *zone = NULL; + isc_buffer_t b; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + CTRACE(ISC_LOG_DEBUG(3), "query_addbestns"); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * Find the right database. + */ + result = query_getdb(client, client->query.qname, dns_rdatatype_ns, 0, + &zone, &db, &version, &is_zone); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + +db_find: + /* + * We'll need some resources... + */ + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + rdataset = ns_client_newrdataset(client); + if (fname == NULL || rdataset == NULL) { + goto cleanup; + } + + /* + * Get the RRSIGs if the client requested them or if we may + * need to validate answers from the cache. + */ + if (WANTDNSSEC(client) || !is_zone) { + sigrdataset = ns_client_newrdataset(client); + if (sigrdataset == NULL) { + goto cleanup; + } + } + + /* + * Now look for the zonecut. + */ + if (is_zone) { + result = dns_db_findext( + db, client->query.qname, version, dns_rdatatype_ns, + client->query.dboptions, client->now, &node, fname, &cm, + &ci, rdataset, sigrdataset); + if (result != DNS_R_DELEGATION) { + goto cleanup; + } + if (USECACHE(client)) { + ns_client_keepname(client, fname, dbuf); + dns_db_detachnode(db, &node); + SAVE(zdb, db); + SAVE(zfname, fname); + SAVE(zrdataset, rdataset); + SAVE(zsigrdataset, sigrdataset); + version = NULL; + dns_db_attach(client->view->cachedb, &db); + is_zone = false; + goto db_find; + } + } else { + result = dns_db_findzonecut( + db, client->query.qname, client->query.dboptions, + client->now, &node, fname, NULL, rdataset, sigrdataset); + if (result == ISC_R_SUCCESS) { + if (zfname != NULL && + !dns_name_issubdomain(fname, zfname)) + { + /* + * We found a zonecut in the cache, but our + * zone delegation is better. + */ + use_zone = true; + } + } else if (result == ISC_R_NOTFOUND && zfname != NULL) { + /* + * We didn't find anything in the cache, but we + * have a zone delegation, so use it. + */ + use_zone = true; + } else { + goto cleanup; + } + } + + if (use_zone) { + ns_client_releasename(client, &fname); + /* + * We've already done ns_client_keepname() on + * zfname, so we must set dbuf to NULL to + * prevent query_addrrset() from trying to + * call ns_client_keepname() again. + */ + dbuf = NULL; + ns_client_putrdataset(client, &rdataset); + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + + if (node != NULL) { + dns_db_detachnode(db, &node); + } + dns_db_detach(&db); + + RESTORE(db, zdb); + RESTORE(fname, zfname); + RESTORE(rdataset, zrdataset); + RESTORE(sigrdataset, zsigrdataset); + } + + /* + * Attempt to validate RRsets that are pending or that are glue. + */ + if ((DNS_TRUST_PENDING(rdataset->trust) || + (sigrdataset != NULL && DNS_TRUST_PENDING(sigrdataset->trust))) && + !validate(client, db, fname, rdataset, sigrdataset) && + !PENDINGOK(client->query.dboptions)) + { + goto cleanup; + } + + if ((DNS_TRUST_GLUE(rdataset->trust) || + (sigrdataset != NULL && DNS_TRUST_GLUE(sigrdataset->trust))) && + !validate(client, db, fname, rdataset, sigrdataset) && + SECURE(client) && WANTDNSSEC(client)) + { + goto cleanup; + } + + /* + * If the answer is secure only add NS records if they are secure + * when the client may be looking for AD in the response. + */ + if (SECURE(client) && (WANTDNSSEC(client) || WANTAD(client)) && + ((rdataset->trust != dns_trust_secure) || + (sigrdataset != NULL && sigrdataset->trust != dns_trust_secure))) + { + goto cleanup; + } + + /* + * If the client doesn't want DNSSEC we can discard the sigrdataset + * now. + */ + if (!WANTDNSSEC(client)) { + ns_client_putrdataset(client, &sigrdataset); + } + + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + +cleanup: + if (rdataset != NULL) { + ns_client_putrdataset(client, &rdataset); + } + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } + if (node != NULL) { + dns_db_detachnode(db, &node); + } + if (db != NULL) { + dns_db_detach(&db); + } + if (zone != NULL) { + dns_zone_detach(&zone); + } + if (zdb != NULL) { + ns_client_putrdataset(client, &zrdataset); + if (zsigrdataset != NULL) { + ns_client_putrdataset(client, &zsigrdataset); + } + if (zfname != NULL) { + ns_client_releasename(client, &zfname); + } + dns_db_detach(&zdb); + } +} + +static void +query_addwildcardproof(query_ctx_t *qctx, bool ispositive, bool nodata) { + ns_client_t *client = qctx->client; + isc_buffer_t *dbuf, b; + dns_name_t *name; + dns_name_t *fname = NULL; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; + dns_fixedname_t wfixed; + dns_name_t *wname; + dns_dbnode_t *node = NULL; + unsigned int options; + unsigned int olabels, nlabels, labels; + isc_result_t result; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_nsec_t nsec; + bool have_wname; + int order; + dns_fixedname_t cfixed; + dns_name_t *cname; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + + CTRACE(ISC_LOG_DEBUG(3), "query_addwildcardproof"); + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + dns_clientinfo_init(&ci, client, NULL, NULL); + + /* + * If a name has been specifically flagged as needing + * a wildcard proof then it will have been copied to + * qctx->wildcardname. Otherwise we just use the client + * QNAME. + */ + if (qctx->need_wildcardproof) { + name = dns_fixedname_name(&qctx->wildcardname); + } else { + name = client->query.qname; + } + + /* + * Get the NOQNAME proof then if !ispositive + * get the NOWILDCARD proof. + * + * DNS_DBFIND_NOWILD finds the NSEC records that covers the + * name ignoring any wildcard. From the owner and next names + * of this record you can compute which wildcard (if it exists) + * will match by finding the longest common suffix of the + * owner name and next names with the qname and prefixing that + * with the wildcard label. + * + * e.g. + * Given: + * example SOA + * example NSEC b.example + * b.example A + * b.example NSEC a.d.example + * a.d.example A + * a.d.example NSEC g.f.example + * g.f.example A + * g.f.example NSEC z.i.example + * z.i.example A + * z.i.example NSEC example + * + * QNAME: + * a.example -> example NSEC b.example + * owner common example + * next common example + * wild *.example + * d.b.example -> b.example NSEC a.d.example + * owner common b.example + * next common example + * wild *.b.example + * a.f.example -> a.d.example NSEC g.f.example + * owner common example + * next common f.example + * wild *.f.example + * j.example -> z.i.example NSEC example + * owner common example + * next common example + * wild *.example + */ + options = client->query.dboptions | DNS_DBFIND_NOWILD; + wname = dns_fixedname_initname(&wfixed); +again: + have_wname = false; + /* + * We'll need some resources... + */ + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + rdataset = ns_client_newrdataset(client); + sigrdataset = ns_client_newrdataset(client); + if (fname == NULL || rdataset == NULL || sigrdataset == NULL) { + goto cleanup; + } + + result = dns_db_findext(qctx->db, name, qctx->version, + dns_rdatatype_nsec, options, 0, &node, fname, + &cm, &ci, rdataset, sigrdataset); + if (node != NULL) { + dns_db_detachnode(qctx->db, &node); + } + + if (!dns_rdataset_isassociated(rdataset)) { + /* + * No NSEC proof available, return NSEC3 proofs instead. + */ + cname = dns_fixedname_initname(&cfixed); + /* + * Find the closest encloser. + */ + dns_name_copynf(name, cname); + while (result == DNS_R_NXDOMAIN) { + labels = dns_name_countlabels(cname) - 1; + /* + * Sanity check. + */ + if (labels == 0U) { + goto cleanup; + } + dns_name_split(cname, labels, NULL, cname); + result = dns_db_findext(qctx->db, cname, qctx->version, + dns_rdatatype_nsec, options, 0, + NULL, fname, &cm, &ci, NULL, + NULL); + } + /* + * Add closest (provable) encloser NSEC3. + */ + query_findclosestnsec3(cname, qctx->db, qctx->version, client, + rdataset, sigrdataset, fname, true, + cname); + if (!dns_rdataset_isassociated(rdataset)) { + goto cleanup; + } + if (!ispositive) { + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, + dbuf, DNS_SECTION_AUTHORITY); + } + + /* + * Replace resources which were consumed by query_addrrset. + */ + if (fname == NULL) { + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + } + + if (rdataset == NULL) { + rdataset = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + + if (sigrdataset == NULL) { + sigrdataset = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + + if (fname == NULL || rdataset == NULL || sigrdataset == NULL) { + goto cleanup; + } + /* + * Add no qname proof. + */ + labels = dns_name_countlabels(cname) + 1; + if (dns_name_countlabels(name) == labels) { + dns_name_copynf(name, wname); + } else { + dns_name_split(name, labels, NULL, wname); + } + + query_findclosestnsec3(wname, qctx->db, qctx->version, client, + rdataset, sigrdataset, fname, false, + NULL); + if (!dns_rdataset_isassociated(rdataset)) { + goto cleanup; + } + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + + if (ispositive) { + goto cleanup; + } + + /* + * Replace resources which were consumed by query_addrrset. + */ + if (fname == NULL) { + dbuf = ns_client_getnamebuf(client); + if (dbuf == NULL) { + goto cleanup; + } + fname = ns_client_newname(client, dbuf, &b); + } + + if (rdataset == NULL) { + rdataset = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(rdataset)) { + dns_rdataset_disassociate(rdataset); + } + + if (sigrdataset == NULL) { + sigrdataset = ns_client_newrdataset(client); + } else if (dns_rdataset_isassociated(sigrdataset)) { + dns_rdataset_disassociate(sigrdataset); + } + + if (fname == NULL || rdataset == NULL || sigrdataset == NULL) { + goto cleanup; + } + /* + * Add the no wildcard proof. + */ + result = dns_name_concatenate(dns_wildcardname, cname, wname, + NULL); + if (result != ISC_R_SUCCESS) { + goto cleanup; + } + + query_findclosestnsec3(wname, qctx->db, qctx->version, client, + rdataset, sigrdataset, fname, nodata, + NULL); + if (!dns_rdataset_isassociated(rdataset)) { + goto cleanup; + } + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + + goto cleanup; + } else if (result == DNS_R_NXDOMAIN) { + if (!ispositive) { + result = dns_rdataset_first(rdataset); + } + if (result == ISC_R_SUCCESS) { + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + (void)dns_name_fullcompare(name, fname, &order, + &olabels); + (void)dns_name_fullcompare(name, &nsec.next, &order, + &nlabels); + /* + * Check for a pathological condition created when + * serving some malformed signed zones and bail out. + */ + if (dns_name_countlabels(name) == nlabels) { + goto cleanup; + } + + if (olabels > nlabels) { + dns_name_split(name, olabels, NULL, wname); + } else { + dns_name_split(name, nlabels, NULL, wname); + } + result = dns_name_concatenate(dns_wildcardname, wname, + wname, NULL); + if (result == ISC_R_SUCCESS) { + have_wname = true; + } + dns_rdata_freestruct(&nsec); + } + query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf, + DNS_SECTION_AUTHORITY); + } + if (rdataset != NULL) { + ns_client_putrdataset(client, &rdataset); + } + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } + if (have_wname) { + ispositive = true; /* prevent loop */ + if (!dns_name_equal(name, wname)) { + name = wname; + goto again; + } + } +cleanup: + if (rdataset != NULL) { + ns_client_putrdataset(client, &rdataset); + } + if (sigrdataset != NULL) { + ns_client_putrdataset(client, &sigrdataset); + } + if (fname != NULL) { + ns_client_releasename(client, &fname); + } +} + +/*% + * Add NS records, and NSEC/NSEC3 wildcard proof records if needed, + * to the authority section. + */ +static void +query_addauth(query_ctx_t *qctx) { + CCTRACE(ISC_LOG_DEBUG(3), "query_addauth"); + /* + * Add NS records to the authority section (if we haven't already + * added them to the answer section). + */ + if (!qctx->want_restart && !NOAUTHORITY(qctx->client)) { + if (qctx->is_zone) { + if (!qctx->answer_has_ns) { + (void)query_addns(qctx); + } + } else if (!qctx->answer_has_ns && + qctx->qtype != dns_rdatatype_ns) + { + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, + &qctx->fname); + } + query_addbestns(qctx); + } + } + + /* + * Add NSEC records to the authority section if they're needed for + * DNSSEC wildcard proofs. + */ + if (qctx->need_wildcardproof && dns_db_issecure(qctx->db)) { + query_addwildcardproof(qctx, true, false); + } +} + +/* + * Find the sort order of 'rdata' in the topology-like + * ACL forming the second element in a 2-element top-level + * sortlist statement. + */ +static int +query_sortlist_order_2element(const dns_rdata_t *rdata, const void *arg) { + isc_netaddr_t netaddr; + + if (rdata_tonetaddr(rdata, &netaddr) != ISC_R_SUCCESS) { + return (INT_MAX); + } + return (ns_sortlist_addrorder2(&netaddr, arg)); +} + +/* + * Find the sort order of 'rdata' in the matching element + * of a 1-element top-level sortlist statement. + */ +static int +query_sortlist_order_1element(const dns_rdata_t *rdata, const void *arg) { + isc_netaddr_t netaddr; + + if (rdata_tonetaddr(rdata, &netaddr) != ISC_R_SUCCESS) { + return (INT_MAX); + } + return (ns_sortlist_addrorder1(&netaddr, arg)); +} + +/* + * Find the sortlist statement that applies to 'client' and set up + * the sortlist info in in client->message appropriately. + */ +static void +query_setup_sortlist(query_ctx_t *qctx) { + isc_netaddr_t netaddr; + ns_client_t *client = qctx->client; + dns_aclenv_t *env = + ns_interfacemgr_getaclenv(client->manager->interface->mgr); + const void *order_arg = NULL; + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + switch (ns_sortlist_setup(client->view->sortlist, env, &netaddr, + &order_arg)) + { + case NS_SORTLISTTYPE_1ELEMENT: + dns_message_setsortorder(client->message, + query_sortlist_order_1element, env, + NULL, order_arg); + break; + case NS_SORTLISTTYPE_2ELEMENT: + dns_message_setsortorder(client->message, + query_sortlist_order_2element, env, + order_arg, NULL); + break; + case NS_SORTLISTTYPE_NONE: + break; + default: + UNREACHABLE(); + } +} + +/* + * When sending a referral, if the answer to the question is + * in the glue, sort it to the start of the additional section. + */ +static void +query_glueanswer(query_ctx_t *qctx) { + const dns_namelist_t *secs = qctx->client->message->sections; + const dns_section_t section = DNS_SECTION_ADDITIONAL; + dns_name_t *name; + dns_message_t *msg; + dns_rdataset_t *rdataset = NULL; + + if (!ISC_LIST_EMPTY(secs[DNS_SECTION_ANSWER]) || + qctx->client->message->rcode != dns_rcode_noerror || + (qctx->qtype != dns_rdatatype_a && + qctx->qtype != dns_rdatatype_aaaa)) + { + return; + } + + msg = qctx->client->message; + for (name = ISC_LIST_HEAD(msg->sections[section]); name != NULL; + name = ISC_LIST_NEXT(name, link)) + { + if (dns_name_equal(name, qctx->client->query.qname)) { + for (rdataset = ISC_LIST_HEAD(name->list); + rdataset != NULL; + rdataset = ISC_LIST_NEXT(rdataset, link)) + { + if (rdataset->type == qctx->qtype) { + break; + } + } + break; + } + } + if (rdataset != NULL) { + ISC_LIST_UNLINK(msg->sections[section], name, link); + ISC_LIST_PREPEND(msg->sections[section], name, link); + ISC_LIST_UNLINK(name->list, rdataset, link); + ISC_LIST_PREPEND(name->list, rdataset, link); + rdataset->attributes |= DNS_RDATASETATTR_REQUIRED; + } +} + +isc_result_t +ns_query_done(query_ctx_t *qctx) { + isc_result_t result; + const dns_namelist_t *secs = qctx->client->message->sections; + bool nodetach; + + CCTRACE(ISC_LOG_DEBUG(3), "ns_query_done"); + + CALL_HOOK(NS_QUERY_DONE_BEGIN, qctx); + + /* + * General cleanup. + */ + qctx->rpz_st = qctx->client->query.rpz_st; + if (qctx->rpz_st != NULL && + (qctx->rpz_st->state & DNS_RPZ_RECURSING) == 0) + { + rpz_match_clear(qctx->rpz_st); + qctx->rpz_st->state &= ~DNS_RPZ_DONE_QNAME; + } + + qctx_clean(qctx); + qctx_freedata(qctx); + + if (qctx->client->query.gluedb != NULL) { + dns_db_detach(&qctx->client->query.gluedb); + } + + /* + * Clear the AA bit if we're not authoritative. + */ + if (qctx->client->query.restarts == 0 && !qctx->authoritative) { + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA; + } + + /* + * Do we need to restart the query (e.g. for CNAME chaining)? + */ + if (qctx->want_restart && qctx->client->query.restarts < MAX_RESTARTS) { + qctx->client->query.restarts++; + return (ns__query_start(qctx)); + } + + if (qctx->result != ISC_R_SUCCESS && + (!PARTIALANSWER(qctx->client) || WANTRECURSION(qctx->client) || + qctx->result == DNS_R_DROP)) + { + if (qctx->result == DNS_R_DUPLICATE || + qctx->result == DNS_R_DROP) + { + /* + * This was a duplicate query that we are + * recursing on or the result of rate limiting. + * Don't send a response now for a duplicate query, + * because the original will still cause a response. + */ + query_next(qctx->client, qctx->result); + } else { + /* + * If we don't have any answer to give the client, + * or if the client requested recursion and thus wanted + * the complete answer, send an error response. + */ + INSIST(qctx->line >= 0); + query_error(qctx->client, qctx->result, qctx->line); + } + + qctx->detach_client = true; + return (qctx->result); + } + + /* + * If we're recursing then just return; the query will + * resume when recursion ends. + */ + if (RECURSING(qctx->client) && + (!QUERY_STALETIMEOUT(&qctx->client->query) || + ((qctx->options & DNS_GETDB_STALEFIRST) != 0))) + { + return (qctx->result); + } + + /* + * We are done. Set up sortlist data for the message + * rendering code, sort the answer to the front of the + * additional section if necessary, make a final tweak + * to the AA bit if the auth-nxdomain config option + * says so, then render and send the response. + */ + query_setup_sortlist(qctx); + query_glueanswer(qctx); + + if (qctx->client->message->rcode == dns_rcode_nxdomain && + qctx->view->auth_nxdomain) + { + qctx->client->message->flags |= DNS_MESSAGEFLAG_AA; + } + + /* + * If the response is somehow unexpected for the client and this + * is a result of recursion, return an error to the caller + * to indicate it may need to be logged. + */ + if (qctx->resuming && + (ISC_LIST_EMPTY(secs[DNS_SECTION_ANSWER]) || + qctx->client->message->rcode != dns_rcode_noerror)) + { + qctx->result = ISC_R_FAILURE; + } + + CALL_HOOK(NS_QUERY_DONE_SEND, qctx); + + /* + * Client may have been detached after query_send(), so + * we test and store the flag state here, for safety. + */ + nodetach = qctx->client->nodetach; + query_send(qctx->client); + + if (qctx->refresh_rrset) { + /* + * If we reached this point then it means that we have found a + * stale RRset entry in cache and BIND is configured to allow + * queries to be answered with stale data if no active RRset + * is available, i.e. "stale-anwer-client-timeout 0". But, we + * still need to refresh the RRset. To prevent adding duplicate + * RRsets, clear the RRsets from the message before doing the + * refresh. + */ + message_clearrdataset(qctx->client->message, 0); + query_refresh_rrset(qctx); + } + + if (!nodetach) { + qctx->detach_client = true; + } + return (qctx->result); + +cleanup: + return (result); +} + +static void +log_tat(ns_client_t *client) { + char namebuf[DNS_NAME_FORMATSIZE]; + char clientbuf[ISC_NETADDR_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + isc_netaddr_t netaddr; + char *tags = NULL; + size_t taglen = 0; + + if (!isc_log_wouldlog(ns_lctx, ISC_LOG_INFO)) { + return; + } + + if ((client->query.qtype != dns_rdatatype_null || + !dns_name_istat(client->query.qname)) && + (client->keytag == NULL || + client->query.qtype != dns_rdatatype_dnskey)) + { + return; + } + + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + dns_name_format(client->query.qname, namebuf, sizeof(namebuf)); + isc_netaddr_format(&netaddr, clientbuf, sizeof(clientbuf)); + dns_rdataclass_format(client->view->rdclass, classbuf, + sizeof(classbuf)); + + if (client->query.qtype == dns_rdatatype_dnskey) { + uint16_t keytags = client->keytag_len / 2; + size_t len = taglen = sizeof("65000") * keytags + 1; + char *cp = tags = isc_mem_get(client->mctx, taglen); + int i = 0; + + INSIST(client->keytag != NULL); + if (tags != NULL) { + while (keytags-- > 0U) { + int n; + uint16_t keytag; + keytag = (client->keytag[i * 2] << 8) | + client->keytag[i * 2 + 1]; + n = snprintf(cp, len, " %u", keytag); + if (n > 0 && (size_t)n <= len) { + cp += n; + len -= n; + i++; + } else { + break; + } + } + } + } + + isc_log_write(ns_lctx, NS_LOGCATEGORY_TAT, NS_LOGMODULE_QUERY, + ISC_LOG_INFO, "trust-anchor-telemetry '%s/%s' from %s%s", + namebuf, classbuf, clientbuf, tags != NULL ? tags : ""); + if (tags != NULL) { + isc_mem_put(client->mctx, tags, taglen); + } +} + +static void +log_query(ns_client_t *client, unsigned int flags, unsigned int extflags) { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + char onbuf[ISC_NETADDR_FORMATSIZE]; + char ecsbuf[DNS_ECS_FORMATSIZE + sizeof(" [ECS ]") - 1] = { 0 }; + char ednsbuf[sizeof("E(65535)")] = { 0 }; + dns_rdataset_t *rdataset; + int level = ISC_LOG_INFO; + + if (!isc_log_wouldlog(ns_lctx, level)) { + return; + } + + rdataset = ISC_LIST_HEAD(client->query.qname->list); + INSIST(rdataset != NULL); + dns_name_format(client->query.qname, namebuf, sizeof(namebuf)); + dns_rdataclass_format(rdataset->rdclass, classbuf, sizeof(classbuf)); + dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf)); + isc_netaddr_format(&client->destaddr, onbuf, sizeof(onbuf)); + + if (client->ednsversion >= 0) { + snprintf(ednsbuf, sizeof(ednsbuf), "E(%hd)", + client->ednsversion); + } + + if (HAVEECS(client)) { + strlcpy(ecsbuf, " [ECS ", sizeof(ecsbuf)); + dns_ecs_format(&client->ecs, ecsbuf + 6, sizeof(ecsbuf) - 6); + strlcat(ecsbuf, "]", sizeof(ecsbuf)); + } + + ns_client_log(client, NS_LOGCATEGORY_QUERIES, NS_LOGMODULE_QUERY, level, + "query: %s %s %s %s%s%s%s%s%s%s (%s)%s", namebuf, + classbuf, typebuf, WANTRECURSION(client) ? "+" : "-", + (client->signer != NULL) ? "S" : "", ednsbuf, + TCP(client) ? "T" : "", + ((extflags & DNS_MESSAGEEXTFLAG_DO) != 0) ? "D" : "", + ((flags & DNS_MESSAGEFLAG_CD) != 0) ? "C" : "", + HAVECOOKIE(client) ? "V" + : WANTCOOKIE(client) ? "K" + : "", + onbuf, ecsbuf); +} + +static void +log_queryerror(ns_client_t *client, isc_result_t result, int line, int level) { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + const char *namep, *typep, *classp, *sep1, *sep2; + dns_rdataset_t *rdataset; + + if (!isc_log_wouldlog(ns_lctx, level)) { + return; + } + + namep = typep = classp = sep1 = sep2 = ""; + + /* + * Query errors can happen for various reasons. In some cases we cannot + * even assume the query contains a valid question section, so we should + * expect exceptional cases. + */ + if (client->query.origqname != NULL) { + dns_name_format(client->query.origqname, namebuf, + sizeof(namebuf)); + namep = namebuf; + sep1 = " for "; + + rdataset = ISC_LIST_HEAD(client->query.origqname->list); + if (rdataset != NULL) { + dns_rdataclass_format(rdataset->rdclass, classbuf, + sizeof(classbuf)); + classp = classbuf; + dns_rdatatype_format(rdataset->type, typebuf, + sizeof(typebuf)); + typep = typebuf; + sep2 = "/"; + } + } + + ns_client_log(client, NS_LOGCATEGORY_QUERY_ERRORS, NS_LOGMODULE_QUERY, + level, "query failed (%s)%s%s%s%s%s%s at %s:%d", + isc_result_totext(result), sep1, namep, sep2, classp, + sep2, typep, __FILE__, line); +} + +void +ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) { + isc_result_t result; + dns_message_t *message; + dns_rdataset_t *rdataset; + dns_rdatatype_t qtype; + unsigned int saved_extflags; + unsigned int saved_flags; + + REQUIRE(NS_CLIENT_VALID(client)); + + /* + * Attach to the request handle + */ + isc_nmhandle_attach(handle, &client->reqhandle); + + message = client->message; + saved_extflags = client->extflags; + saved_flags = client->message->flags; + + CTRACE(ISC_LOG_DEBUG(3), "ns_query_start"); + + /* + * Ensure that appropriate cleanups occur. + */ + client->cleanup = query_cleanup; + + if ((message->flags & DNS_MESSAGEFLAG_RD) != 0) { + client->query.attributes |= NS_QUERYATTR_WANTRECURSION; + } + + if ((client->extflags & DNS_MESSAGEEXTFLAG_DO) != 0) { + client->attributes |= NS_CLIENTATTR_WANTDNSSEC; + } + + switch (client->view->minimalresponses) { + case dns_minimal_no: + break; + case dns_minimal_yes: + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + break; + case dns_minimal_noauth: + client->query.attributes |= NS_QUERYATTR_NOAUTHORITY; + break; + case dns_minimal_noauthrec: + if ((message->flags & DNS_MESSAGEFLAG_RD) != 0) { + client->query.attributes |= NS_QUERYATTR_NOAUTHORITY; + } + break; + } + + if (client->view->cachedb == NULL || !client->view->recursion) { + /* + * We don't have a cache. Turn off cache support and + * recursion. + */ + client->query.attributes &= ~(NS_QUERYATTR_RECURSIONOK | + NS_QUERYATTR_CACHEOK); + client->attributes |= NS_CLIENTATTR_NOSETFC; + } else if ((client->attributes & NS_CLIENTATTR_RA) == 0 || + (message->flags & DNS_MESSAGEFLAG_RD) == 0) + { + /* + * If the client isn't allowed to recurse (due to + * "recursion no", the allow-recursion ACL, or the + * lack of a resolver in this view), or if it + * doesn't want recursion, turn recursion off. + */ + client->query.attributes &= ~NS_QUERYATTR_RECURSIONOK; + client->attributes |= NS_CLIENTATTR_NOSETFC; + } + + /* + * Check for multiple question queries, since edns1 is dead. + */ + if (message->counts[DNS_SECTION_QUESTION] > 1) { + query_error(client, DNS_R_FORMERR, __LINE__); + return; + } + + /* + * Get the question name. + */ + result = dns_message_firstname(message, DNS_SECTION_QUESTION); + if (result != ISC_R_SUCCESS) { + query_error(client, result, __LINE__); + return; + } + dns_message_currentname(message, DNS_SECTION_QUESTION, + &client->query.qname); + client->query.origqname = client->query.qname; + result = dns_message_nextname(message, DNS_SECTION_QUESTION); + if (result != ISC_R_NOMORE) { + if (result == ISC_R_SUCCESS) { + /* + * There's more than one QNAME in the question + * section. + */ + query_error(client, DNS_R_FORMERR, __LINE__); + } else { + query_error(client, result, __LINE__); + } + return; + } + + if ((client->sctx->options & NS_SERVER_LOGQUERIES) != 0) { + log_query(client, saved_flags, saved_extflags); + } + + /* + * Check for meta-queries like IXFR and AXFR. + */ + rdataset = ISC_LIST_HEAD(client->query.qname->list); + INSIST(rdataset != NULL); + client->query.qtype = qtype = rdataset->type; + dns_rdatatypestats_increment(client->sctx->rcvquerystats, qtype); + + log_tat(client); + + if (dns_rdatatype_ismeta(qtype)) { + switch (qtype) { + case dns_rdatatype_any: + break; /* Let the query logic handle it. */ + case dns_rdatatype_ixfr: + case dns_rdatatype_axfr: + ns_xfr_start(client, rdataset->type); + return; + case dns_rdatatype_maila: + case dns_rdatatype_mailb: + query_error(client, DNS_R_NOTIMP, __LINE__); + return; + case dns_rdatatype_tkey: + result = dns_tkey_processquery( + client->message, client->sctx->tkeyctx, + client->view->dynamickeys); + if (result == ISC_R_SUCCESS) { + query_send(client); + } else { + query_error(client, result, __LINE__); + } + return; + default: /* TSIG, etc. */ + query_error(client, DNS_R_FORMERR, __LINE__); + return; + } + } + + /* + * Turn on minimal response for (C)DNSKEY and (C)DS queries. + */ + if (qtype == dns_rdatatype_dnskey || qtype == dns_rdatatype_ds || + qtype == dns_rdatatype_cdnskey || qtype == dns_rdatatype_cds) + { + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + } else if (qtype == dns_rdatatype_ns) { + /* + * Always turn on additional records for NS queries. + */ + client->query.attributes &= ~(NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + } + + /* + * Maybe turn on minimal responses for ANY queries. + */ + if (qtype == dns_rdatatype_any && client->view->minimal_any && + !TCP(client)) + { + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + } + + /* + * Turn on minimal responses for EDNS/UDP bufsize 512 queries. + */ + if (client->ednsversion >= 0 && client->udpsize <= 512U && !TCP(client)) + { + client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | + NS_QUERYATTR_NOADDITIONAL); + } + + /* + * If the client has requested that DNSSEC checking be disabled, + * allow lookups to return pending data and instruct the resolver + * to return data before validation has completed. + * + * We don't need to set DNS_DBFIND_PENDINGOK when validation is + * disabled as there will be no pending data. + */ + if ((message->flags & DNS_MESSAGEFLAG_CD) != 0 || + qtype == dns_rdatatype_rrsig) + { + client->query.dboptions |= DNS_DBFIND_PENDINGOK; + client->query.fetchoptions |= DNS_FETCHOPT_NOVALIDATE; + } else if (!client->view->enablevalidation) { + client->query.fetchoptions |= DNS_FETCHOPT_NOVALIDATE; + } + + if (client->view->qminimization) { + client->query.fetchoptions |= DNS_FETCHOPT_QMINIMIZE | + DNS_FETCHOPT_QMIN_SKIP_IP6A; + if (client->view->qmin_strict) { + client->query.fetchoptions |= DNS_FETCHOPT_QMIN_STRICT; + } else { + client->query.fetchoptions |= DNS_FETCHOPT_QMIN_USE_A; + } + } + + /* + * Allow glue NS records to be added to the authority section + * if the answer is secure. + */ + if ((message->flags & DNS_MESSAGEFLAG_CD) != 0) { + client->query.attributes &= ~NS_QUERYATTR_SECURE; + } + + /* + * Set NS_CLIENTATTR_WANTAD if the client has set AD in the query. + * This allows AD to be returned on queries without DO set. + */ + if ((message->flags & DNS_MESSAGEFLAG_AD) != 0) { + client->attributes |= NS_CLIENTATTR_WANTAD; + } + + /* + * This is an ordinary query. + */ + result = dns_message_reply(message, true); + if (result != ISC_R_SUCCESS) { + query_next(client, result); + return; + } + + /* + * Assume authoritative response until it is known to be + * otherwise. + * + * If "-T noaa" has been set on the command line don't set + * AA on authoritative answers. + */ + if ((client->sctx->options & NS_SERVER_NOAA) == 0) { + message->flags |= DNS_MESSAGEFLAG_AA; + } + + /* + * Set AD. We must clear it if we add non-validated data to a + * response. + */ + if (WANTDNSSEC(client) || WANTAD(client)) { + message->flags |= DNS_MESSAGEFLAG_AD; + } + + (void)query_setup(client, qtype); +} diff --git a/lib/ns/server.c b/lib/ns/server.c new file mode 100644 index 0000000..63bea5d --- /dev/null +++ b/lib/ns/server.c @@ -0,0 +1,233 @@ +/* + * 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. + */ + +/*! \file */ + +#include <stdbool.h> + +#include <isc/mem.h> +#include <isc/stats.h> +#include <isc/util.h> + +#include <dns/stats.h> +#include <dns/tkey.h> + +#include <ns/query.h> +#include <ns/server.h> +#include <ns/stats.h> + +#define SCTX_MAGIC ISC_MAGIC('S', 'c', 't', 'x') +#define SCTX_VALID(s) ISC_MAGIC_VALID(s, SCTX_MAGIC) + +#define CHECKFATAL(op) \ + do { \ + result = (op); \ + RUNTIME_CHECK(result == ISC_R_SUCCESS); \ + } while (0) + +isc_result_t +ns_server_create(isc_mem_t *mctx, ns_matchview_t matchingview, + ns_server_t **sctxp) { + ns_server_t *sctx; + isc_result_t result; + + REQUIRE(sctxp != NULL && *sctxp == NULL); + + sctx = isc_mem_get(mctx, sizeof(*sctx)); + + memset(sctx, 0, sizeof(*sctx)); + + isc_mem_attach(mctx, &sctx->mctx); + + isc_refcount_init(&sctx->references, 1); + + isc_quota_init(&sctx->xfroutquota, 10); + isc_quota_init(&sctx->tcpquota, 10); + isc_quota_init(&sctx->recursionquota, 100); + isc_quota_init(&sctx->updquota, 100); + + CHECKFATAL(dns_tkeyctx_create(mctx, &sctx->tkeyctx)); + + CHECKFATAL(ns_stats_create(mctx, ns_statscounter_max, &sctx->nsstats)); + + CHECKFATAL(dns_rdatatypestats_create(mctx, &sctx->rcvquerystats)); + + CHECKFATAL(dns_opcodestats_create(mctx, &sctx->opcodestats)); + + CHECKFATAL(dns_rcodestats_create(mctx, &sctx->rcodestats)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->udpinstats4, + dns_sizecounter_in_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->udpoutstats4, + dns_sizecounter_out_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->udpinstats6, + dns_sizecounter_in_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->udpoutstats6, + dns_sizecounter_out_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->tcpinstats4, + dns_sizecounter_in_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->tcpoutstats4, + dns_sizecounter_out_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->tcpinstats6, + dns_sizecounter_in_max)); + + CHECKFATAL(isc_stats_create(mctx, &sctx->tcpoutstats6, + dns_sizecounter_out_max)); + + sctx->udpsize = 1232; + sctx->transfer_tcp_message_size = 20480; + + sctx->fuzztype = isc_fuzz_none; + sctx->fuzznotify = NULL; + sctx->gethostname = NULL; + + sctx->matchingview = matchingview; + sctx->answercookie = true; + + ISC_LIST_INIT(sctx->altsecrets); + + sctx->magic = SCTX_MAGIC; + *sctxp = sctx; + + return (ISC_R_SUCCESS); +} + +void +ns_server_attach(ns_server_t *src, ns_server_t **dest) { + REQUIRE(SCTX_VALID(src)); + REQUIRE(dest != NULL && *dest == NULL); + + isc_refcount_increment(&src->references); + + *dest = src; +} + +void +ns_server_detach(ns_server_t **sctxp) { + ns_server_t *sctx; + + REQUIRE(sctxp != NULL && SCTX_VALID(*sctxp)); + sctx = *sctxp; + *sctxp = NULL; + + if (isc_refcount_decrement(&sctx->references) == 1) { + ns_altsecret_t *altsecret; + + while ((altsecret = ISC_LIST_HEAD(sctx->altsecrets)) != NULL) { + ISC_LIST_UNLINK(sctx->altsecrets, altsecret, link); + isc_mem_put(sctx->mctx, altsecret, sizeof(*altsecret)); + } + + isc_quota_destroy(&sctx->updquota); + isc_quota_destroy(&sctx->recursionquota); + isc_quota_destroy(&sctx->tcpquota); + isc_quota_destroy(&sctx->xfroutquota); + + if (sctx->server_id != NULL) { + isc_mem_free(sctx->mctx, sctx->server_id); + } + + if (sctx->blackholeacl != NULL) { + dns_acl_detach(&sctx->blackholeacl); + } + if (sctx->keepresporder != NULL) { + dns_acl_detach(&sctx->keepresporder); + } + if (sctx->tkeyctx != NULL) { + dns_tkeyctx_destroy(&sctx->tkeyctx); + } + + if (sctx->nsstats != NULL) { + ns_stats_detach(&sctx->nsstats); + } + + if (sctx->rcvquerystats != NULL) { + dns_stats_detach(&sctx->rcvquerystats); + } + if (sctx->opcodestats != NULL) { + dns_stats_detach(&sctx->opcodestats); + } + if (sctx->rcodestats != NULL) { + dns_stats_detach(&sctx->rcodestats); + } + + if (sctx->udpinstats4 != NULL) { + isc_stats_detach(&sctx->udpinstats4); + } + if (sctx->tcpinstats4 != NULL) { + isc_stats_detach(&sctx->tcpinstats4); + } + if (sctx->udpoutstats4 != NULL) { + isc_stats_detach(&sctx->udpoutstats4); + } + if (sctx->tcpoutstats4 != NULL) { + isc_stats_detach(&sctx->tcpoutstats4); + } + + if (sctx->udpinstats6 != NULL) { + isc_stats_detach(&sctx->udpinstats6); + } + if (sctx->tcpinstats6 != NULL) { + isc_stats_detach(&sctx->tcpinstats6); + } + if (sctx->udpoutstats6 != NULL) { + isc_stats_detach(&sctx->udpoutstats6); + } + if (sctx->tcpoutstats6 != NULL) { + isc_stats_detach(&sctx->tcpoutstats6); + } + + sctx->magic = 0; + + isc_mem_putanddetach(&sctx->mctx, sctx, sizeof(*sctx)); + } +} + +isc_result_t +ns_server_setserverid(ns_server_t *sctx, const char *serverid) { + REQUIRE(SCTX_VALID(sctx)); + + if (sctx->server_id != NULL) { + isc_mem_free(sctx->mctx, sctx->server_id); + sctx->server_id = NULL; + } + + if (serverid != NULL) { + sctx->server_id = isc_mem_strdup(sctx->mctx, serverid); + } + + return (ISC_R_SUCCESS); +} + +void +ns_server_setoption(ns_server_t *sctx, unsigned int option, bool value) { + REQUIRE(SCTX_VALID(sctx)); + if (value) { + sctx->options |= option; + } else { + sctx->options &= ~option; + } +} + +bool +ns_server_getoption(ns_server_t *sctx, unsigned int option) { + REQUIRE(SCTX_VALID(sctx)); + + return ((sctx->options & option) != 0); +} diff --git a/lib/ns/sortlist.c b/lib/ns/sortlist.c new file mode 100644 index 0000000..5b7932f --- /dev/null +++ b/lib/ns/sortlist.c @@ -0,0 +1,167 @@ +/* + * 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. + */ + +/*! \file */ + +#include <isc/mem.h> +#include <isc/util.h> + +#include <dns/acl.h> +#include <dns/message.h> +#include <dns/result.h> + +#include <ns/server.h> +#include <ns/sortlist.h> + +ns_sortlisttype_t +ns_sortlist_setup(dns_acl_t *acl, dns_aclenv_t *env, isc_netaddr_t *clientaddr, + const void **argp) { + unsigned int i; + + if (acl == NULL) { + goto dont_sort; + } + + for (i = 0; i < acl->length; i++) { + /* + * 'e' refers to the current 'top level statement' + * in the sortlist (see ARM). + */ + dns_aclelement_t *e = &acl->elements[i]; + dns_aclelement_t *try_elt; + dns_aclelement_t *order_elt = NULL; + const dns_aclelement_t *matched_elt = NULL; + + if (e->type == dns_aclelementtype_nestedacl) { + dns_acl_t *inner = e->nestedacl; + + if (inner->length == 0) { + try_elt = e; + } else if (inner->length > 2) { + goto dont_sort; + } else if (inner->elements[0].negative) { + goto dont_sort; + } else { + try_elt = &inner->elements[0]; + if (inner->length == 2) { + order_elt = &inner->elements[1]; + } + } + } else { + /* + * BIND 8 allows bare elements at the top level + * as an undocumented feature. + */ + try_elt = e; + } + + if (dns_aclelement_match(clientaddr, NULL, try_elt, env, + &matched_elt)) + { + if (order_elt != NULL) { + if (order_elt->type == + dns_aclelementtype_nestedacl) + { + *argp = order_elt->nestedacl; + return (NS_SORTLISTTYPE_2ELEMENT); + } else if (order_elt->type == + dns_aclelementtype_localhost && + env->localhost != NULL) + { + *argp = env->localhost; + return (NS_SORTLISTTYPE_2ELEMENT); + } else if (order_elt->type == + dns_aclelementtype_localnets && + env->localnets != NULL) + { + *argp = env->localnets; + return (NS_SORTLISTTYPE_2ELEMENT); + } else { + /* + * BIND 8 allows a bare IP prefix as + * the 2nd element of a 2-element + * sortlist statement. + */ + *argp = order_elt; + return (NS_SORTLISTTYPE_1ELEMENT); + } + } else { + INSIST(matched_elt != NULL); + *argp = matched_elt; + return (NS_SORTLISTTYPE_1ELEMENT); + } + } + } + + /* No match; don't sort. */ +dont_sort: + *argp = NULL; + return (NS_SORTLISTTYPE_NONE); +} + +int +ns_sortlist_addrorder2(const isc_netaddr_t *addr, const void *arg) { + const dns_sortlist_arg_t *sla = (const dns_sortlist_arg_t *)arg; + const dns_aclenv_t *env = sla->env; + const dns_acl_t *sortacl = sla->acl; + int match; + + (void)dns_acl_match(addr, NULL, sortacl, env, &match, NULL); + if (match > 0) { + return (match); + } else if (match < 0) { + return (INT_MAX - (-match)); + } else { + return (INT_MAX / 2); + } +} + +int +ns_sortlist_addrorder1(const isc_netaddr_t *addr, const void *arg) { + const dns_sortlist_arg_t *sla = (const dns_sortlist_arg_t *)arg; + const dns_aclenv_t *env = sla->env; + const dns_aclelement_t *element = sla->element; + + if (dns_aclelement_match(addr, NULL, element, env, NULL)) { + return (0); + } + + return (INT_MAX); +} + +void +ns_sortlist_byaddrsetup(dns_acl_t *sortlist_acl, dns_aclenv_t *env, + isc_netaddr_t *client_addr, + dns_addressorderfunc_t *orderp, const void **argp) { + ns_sortlisttype_t sortlisttype; + + sortlisttype = ns_sortlist_setup(sortlist_acl, env, client_addr, argp); + + switch (sortlisttype) { + case NS_SORTLISTTYPE_1ELEMENT: + *orderp = ns_sortlist_addrorder1; + break; + case NS_SORTLISTTYPE_2ELEMENT: + *orderp = ns_sortlist_addrorder2; + break; + case NS_SORTLISTTYPE_NONE: + *orderp = NULL; + break; + default: + UNEXPECTED_ERROR(__FILE__, __LINE__, + "unexpected return from ns_sortlist_setup(): " + "%d", + sortlisttype); + break; + } +} diff --git a/lib/ns/stats.c b/lib/ns/stats.c new file mode 100644 index 0000000..de5b083 --- /dev/null +++ b/lib/ns/stats.c @@ -0,0 +1,128 @@ +/* + * 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. + */ + +/*! \file */ + +#include <isc/magic.h> +#include <isc/mem.h> +#include <isc/refcount.h> +#include <isc/stats.h> +#include <isc/util.h> + +#include <ns/stats.h> + +#define NS_STATS_MAGIC ISC_MAGIC('N', 's', 't', 't') +#define NS_STATS_VALID(x) ISC_MAGIC_VALID(x, NS_STATS_MAGIC) + +struct ns_stats { + /*% Unlocked */ + unsigned int magic; + isc_mem_t *mctx; + isc_stats_t *counters; + isc_refcount_t references; +}; + +void +ns_stats_attach(ns_stats_t *stats, ns_stats_t **statsp) { + REQUIRE(NS_STATS_VALID(stats)); + REQUIRE(statsp != NULL && *statsp == NULL); + + isc_refcount_increment(&stats->references); + + *statsp = stats; +} + +void +ns_stats_detach(ns_stats_t **statsp) { + ns_stats_t *stats; + + REQUIRE(statsp != NULL && NS_STATS_VALID(*statsp)); + + stats = *statsp; + *statsp = NULL; + + if (isc_refcount_decrement(&stats->references) == 1) { + isc_stats_detach(&stats->counters); + isc_refcount_destroy(&stats->references); + isc_mem_putanddetach(&stats->mctx, stats, sizeof(*stats)); + } +} + +isc_result_t +ns_stats_create(isc_mem_t *mctx, int ncounters, ns_stats_t **statsp) { + ns_stats_t *stats; + isc_result_t result; + + REQUIRE(statsp != NULL && *statsp == NULL); + + stats = isc_mem_get(mctx, sizeof(*stats)); + stats->counters = NULL; + + isc_refcount_init(&stats->references, 1); + + result = isc_stats_create(mctx, &stats->counters, ncounters); + if (result != ISC_R_SUCCESS) { + goto clean_mem; + } + + stats->magic = NS_STATS_MAGIC; + stats->mctx = NULL; + isc_mem_attach(mctx, &stats->mctx); + *statsp = stats; + + return (ISC_R_SUCCESS); + +clean_mem: + isc_mem_put(mctx, stats, sizeof(*stats)); + + return (result); +} + +/*% + * Increment/Decrement methods + */ +void +ns_stats_increment(ns_stats_t *stats, isc_statscounter_t counter) { + REQUIRE(NS_STATS_VALID(stats)); + + isc_stats_increment(stats->counters, counter); +} + +void +ns_stats_decrement(ns_stats_t *stats, isc_statscounter_t counter) { + REQUIRE(NS_STATS_VALID(stats)); + + isc_stats_decrement(stats->counters, counter); +} + +isc_stats_t * +ns_stats_get(ns_stats_t *stats) { + REQUIRE(NS_STATS_VALID(stats)); + + return (stats->counters); +} + +void +ns_stats_update_if_greater(ns_stats_t *stats, isc_statscounter_t counter, + isc_statscounter_t value) { + REQUIRE(NS_STATS_VALID(stats)); + + isc_stats_update_if_greater(stats->counters, counter, value); +} + +isc_statscounter_t +ns_stats_get_counter(ns_stats_t *stats, isc_statscounter_t counter) { + REQUIRE(NS_STATS_VALID(stats)); + + return (isc_stats_get_counter(stats->counters, counter)); +} diff --git a/lib/ns/tests/Kyuafile b/lib/ns/tests/Kyuafile new file mode 100644 index 0000000..22cdcff --- /dev/null +++ b/lib/ns/tests/Kyuafile @@ -0,0 +1,18 @@ +-- 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. + +syntax(2) +test_suite('bind9') + +tap_test_program{name='listenlist_test'} +tap_test_program{name='notify_test'} +tap_test_program{name='plugin_test'} +tap_test_program{name='query_test'} diff --git a/lib/ns/tests/Makefile.in b/lib/ns/tests/Makefile.in new file mode 100644 index 0000000..9adcb37 --- /dev/null +++ b/lib/ns/tests/Makefile.in @@ -0,0 +1,85 @@ +# 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. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +VERSION=@BIND9_VERSION@ + +@BIND9_MAKE_INCLUDES@ + +WRAP_OPTIONS = -Wl,--wrap=isc__nmhandle_detach -Wl,--wrap=isc__nmhandle_attach + +CINCLUDES = -I. -Iinclude ${NS_INCLUDES} ${DNS_INCLUDES} ${ISC_INCLUDES} \ + ${OPENSSL_CFLAGS} \ + @CMOCKA_CFLAGS@ +CDEFINES = -DTESTS="\"${top_builddir}/lib/ns/tests/\"" -DNAMED_PLUGINDIR=\"${plugindir}\" + +ISCLIBS = ../../isc/libisc.@A@ @NO_LIBTOOL_ISCLIBS@ +ISCDEPLIBS = ../../isc/libisc.@A@ +DNSLIBS = ../../dns/libdns.@A@ @NO_LIBTOOL_DNSLIBS@ +DNSDEPLIBS = ../../dns/libdns.@A@ +NSLIBS = ../libns.@A@ +NSDEPLIBS = ../libns.@A@ + +LIBS = @LIBS@ @CMOCKA_LIBS@ + +SO_CFLAGS = @CFLAGS@ @SO_CFLAGS@ +SO_LDFLAGS = @LDFLAGS@ @SO_LDFLAGS@ + +OBJS = nstest.@O@ +SRCS = nstest.c \ + listenlist_test.c \ + notify_test.c \ + plugin_test.c \ + query_test.c + +SUBDIRS = +TARGETS = listenlist_test@EXEEXT@ \ + notify_test@EXEEXT@ \ + plugin_test@EXEEXT@ \ + query_test@EXEEXT@ + +LD_WRAP_TESTS=@LD_WRAP_TESTS@ + +@BIND9_MAKE_RULES@ + +listenlist_test@EXEEXT@: listenlist_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS} + if test "${LD_WRAP_TESTS}" = true -a -z "${LIBTOOL}"; then WRAP="${WRAP_OPTIONS}"; fi; \ + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} \ + ${LDFLAGS} $${WRAP} -o $@ listenlist_test.@O@ nstest.@O@ \ + ${NSLIBS} ${DNSLIBS} ${ISCLIBS} ${LIBS} + +notify_test@EXEEXT@: notify_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS} + if test "${LD_WRAP_TESTS}" = true -a -z "${LIBTOOL}"; then WRAP="${WRAP_OPTIONS}"; fi; \ + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} \ + ${LDFLAGS} $${WRAP} -o $@ notify_test.@O@ nstest.@O@ \ + ${NSLIBS} ${DNSLIBS} ${ISCLIBS} ${LIBS} + +plugin_test@EXEEXT@: plugin_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS} + if test "${LD_WRAP_TESTS}" = true -a -z "${LIBTOOL}"; then WRAP="${WRAP_OPTIONS}"; fi; \ + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} \ + ${LDFLAGS} $${WRAP} -o $@ plugin_test.@O@ nstest.@O@ \ + ${NSLIBS} ${DNSLIBS} ${ISCLIBS} ${LIBS} + +query_test@EXEEXT@: query_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS} + if test "${LD_WRAP_TESTS}" = true -a -z "${LIBTOOL}"; then WRAP="${WRAP_OPTIONS}"; fi; \ + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} \ + ${LDFLAGS} $${WRAP} -o $@ query_test.@O@ nstest.@O@ \ + ${NSLIBS} ${DNSLIBS} ${ISCLIBS} ${LIBS} + +unit:: + sh ${top_builddir}/unit/unittest.sh + +clean distclean:: + rm -f ${TARGETS} + rm -f atf.out diff --git a/lib/ns/tests/listenlist_test.c b/lib/ns/tests/listenlist_test.c new file mode 100644 index 0000000..72cb36e --- /dev/null +++ b/lib/ns/tests/listenlist_test.c @@ -0,0 +1,140 @@ +/* + * 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 <isc/util.h> + +#if HAVE_CMOCKA + +#include <sched.h> /* IWYU pragma: keep */ +#include <setjmp.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define UNIT_TESTING +#include <cmocka.h> + +#include <isc/list.h> +#include <isc/print.h> +#include <isc/random.h> + +#include <dns/acl.h> + +#include <ns/listenlist.h> + +#include "nstest.h" + +static int +_setup(void **state) { + isc_result_t result; + + UNUSED(state); + + result = ns_test_begin(NULL, true); + assert_int_equal(result, ISC_R_SUCCESS); + + return (0); +} + +static int +_teardown(void **state) { + UNUSED(state); + + ns_test_end(); + + return (0); +} + +/* test that ns_listenlist_default() works */ +static void +ns_listenlist_default_test(void **state) { + isc_result_t result; + in_port_t port = 5300 + isc_random8(); + ns_listenlist_t *list = NULL; + ns_listenelt_t *elt; + int count; + + UNUSED(state); + + result = ns_listenlist_default(mctx, port, -1, false, &list); + assert_int_equal(result, ISC_R_SUCCESS); + assert_non_null(list); + + assert_false(ISC_LIST_EMPTY(list->elts)); + + count = 0; + elt = ISC_LIST_HEAD(list->elts); + while (elt != NULL) { + ns_listenelt_t *next = ISC_LIST_NEXT(elt, link); + dns_acl_t *acl = NULL; + + dns_acl_attach(elt->acl, &acl); + ISC_LIST_UNLINK(list->elts, elt, link); + ns_listenelt_destroy(elt); + elt = next; + + assert_true(dns_acl_isnone(acl)); + dns_acl_detach(&acl); + count++; + } + + assert_true(ISC_LIST_EMPTY(list->elts)); + assert_int_equal(count, 1); + + ns_listenlist_detach(&list); + + result = ns_listenlist_default(mctx, port, -1, true, &list); + assert_int_equal(result, ISC_R_SUCCESS); + + assert_false(ISC_LIST_EMPTY(list->elts)); + + /* This time just use ns_listenlist_detach() to destroy elements */ + count = 0; + elt = ISC_LIST_HEAD(list->elts); + while (elt != NULL) { + ns_listenelt_t *next = ISC_LIST_NEXT(elt, link); + assert_true(dns_acl_isany(elt->acl)); + elt = next; + count++; + } + + assert_int_equal(count, 1); + + ns_listenlist_detach(&list); +} + +int +main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(ns_listenlist_default_test, + _setup, _teardown), + }; + + return (cmocka_run_group_tests(tests, NULL, NULL)); +} + +#else /* HAVE_CMOCKA */ + +#include <stdio.h> + +int +main(void) { + printf("1..0 # Skipped: cmocka not available\n"); + + return (SKIPPED_TEST_EXIT_CODE); +} + +#endif /* HAVE_CMOCKA */ diff --git a/lib/ns/tests/notify_test.c b/lib/ns/tests/notify_test.c new file mode 100644 index 0000000..82880fb --- /dev/null +++ b/lib/ns/tests/notify_test.c @@ -0,0 +1,172 @@ +/* + * 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 <isc/util.h> + +#if HAVE_CMOCKA + +#include <sched.h> /* IWYU pragma: keep */ +#include <setjmp.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define UNIT_TESTING +#include <cmocka.h> + +#include <isc/event.h> +#include <isc/print.h> +#include <isc/task.h> + +#include <dns/acl.h> +#include <dns/rcode.h> +#include <dns/view.h> + +#include <ns/client.h> +#include <ns/notify.h> + +#include "nstest.h" + +#if defined(USE_LIBTOOL) || LD_WRAP +static int +_setup(void **state) { + isc_result_t result; + + UNUSED(state); + + result = ns_test_begin(NULL, true); + assert_int_equal(result, ISC_R_SUCCESS); + + return (0); +} + +static int +_teardown(void **state) { + UNUSED(state); + + ns_test_end(); + + return (0); +} + +static void +check_response(isc_buffer_t *buf) { + isc_result_t result; + dns_message_t *message = NULL; + char rcodebuf[20]; + isc_buffer_t b; + + dns_message_create(mctx, DNS_MESSAGE_INTENTPARSE, &message); + + result = dns_message_parse(message, buf, 0); + assert_int_equal(result, ISC_R_SUCCESS); + + isc_buffer_init(&b, rcodebuf, sizeof(rcodebuf)); + result = dns_rcode_totext(message->rcode, &b); + assert_int_equal(result, ISC_R_SUCCESS); + + assert_int_equal(message->rcode, dns_rcode_noerror); + + dns_message_detach(&message); +} + +/* test ns_notify_start() */ +static void +notify_start(void **state) { + isc_result_t result; + ns_client_t *client = NULL; + isc_nmhandle_t *handle = NULL; + dns_message_t *nmsg = NULL; + unsigned char ndata[4096]; + isc_buffer_t nbuf; + size_t nsize; + + UNUSED(state); + + result = ns_test_getclient(NULL, false, &client); + assert_int_equal(result, ISC_R_SUCCESS); + + result = ns_test_makeview("view", false, &client->view); + assert_int_equal(result, ISC_R_SUCCESS); + + result = ns_test_serve_zone("example.com", "testdata/notify/zone1.db", + client->view); + assert_int_equal(result, ISC_R_SUCCESS); + + /* + * Create a NOTIFY message by parsing a file in testdata. + * (XXX: use better message mocking method when available.) + */ + + result = ns_test_getdata("testdata/notify/notify1.msg", ndata, + sizeof(ndata), &nsize); + assert_int_equal(result, ISC_R_SUCCESS); + isc_buffer_init(&nbuf, ndata, nsize); + isc_buffer_add(&nbuf, nsize); + + dns_message_create(mctx, DNS_MESSAGE_INTENTPARSE, &nmsg); + + result = dns_message_parse(nmsg, &nbuf, 0); + assert_int_equal(result, ISC_R_SUCCESS); + + /* + * Set up client object with this message and test the NOTIFY + * handler. + */ + if (client->message != NULL) { + dns_message_detach(&client->message); + } + client->message = nmsg; + nmsg = NULL; + client->sendcb = check_response; + ns_notify_start(client, client->handle); + + /* + * Clean up + */ + ns_test_cleanup_zone(); + + handle = client->handle; + isc_nmhandle_detach(&client->handle); + isc_nmhandle_detach(&handle); +} +#endif /* if defined(USE_LIBTOOL) || LD_WRAP */ + +int +main(void) { +#if defined(USE_LIBTOOL) || LD_WRAP + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(notify_start, _setup, + _teardown), + }; + + return (cmocka_run_group_tests(tests, NULL, NULL)); +#else /* if defined(USE_LIBTOOL) || LD_WRAP */ + print_message("1..0 # Skip notify_test requires libtool or LD_WRAP\n"); +#endif /* if defined(USE_LIBTOOL) || LD_WRAP */ +} +#else /* HAVE_CMOCKA */ + +#include <stdio.h> + +int +main(void) { + printf("1..0 # Skipped: cmocka not available\n"); + + return (SKIPPED_TEST_EXIT_CODE); +} + +#endif /* HAVE_CMOCKA */ diff --git a/lib/ns/tests/nstest.c b/lib/ns/tests/nstest.c new file mode 100644 index 0000000..df15408 --- /dev/null +++ b/lib/ns/tests/nstest.c @@ -0,0 +1,1018 @@ +/* + * 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. + */ + +/*! \file */ + +#include "nstest.h" +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> + +#include <isc/app.h> +#include <isc/buffer.h> +#include <isc/file.h> +#include <isc/hash.h> +#include <isc/managers.h> +#include <isc/mem.h> +#include <isc/netmgr.h> +#include <isc/os.h> +#include <isc/print.h> +#include <isc/random.h> +#include <isc/resource.h> +#include <isc/socket.h> +#include <isc/stdio.h> +#include <isc/string.h> +#include <isc/task.h> +#include <isc/timer.h> +#include <isc/util.h> + +#include <dns/cache.h> +#include <dns/db.h> +#include <dns/dispatch.h> +#include <dns/fixedname.h> +#include <dns/log.h> +#include <dns/name.h> +#include <dns/result.h> +#include <dns/view.h> +#include <dns/zone.h> + +#include <ns/client.h> +#include <ns/hooks.h> +#include <ns/interfacemgr.h> +#include <ns/server.h> + +isc_mem_t *mctx = NULL; +isc_log_t *lctx = NULL; +isc_taskmgr_t *taskmgr = NULL; +isc_task_t *maintask = NULL; +isc_timermgr_t *timermgr = NULL; +isc_socketmgr_t *socketmgr = NULL; +isc_nm_t *netmgr = NULL; +dns_zonemgr_t *zonemgr = NULL; +dns_dispatchmgr_t *dispatchmgr = NULL; +ns_clientmgr_t *clientmgr = NULL; +ns_interfacemgr_t *interfacemgr = NULL; +ns_server_t *sctx = NULL; +bool app_running = false; +int ncpus; +bool debug_mem_record = true; +static atomic_bool run_managers = false; + +static bool dst_active = false; +static bool test_running = false; + +static dns_zone_t *served_zone = NULL; + +/* + * We don't want to use netmgr-based client accounting, we need to emulate it. + */ +atomic_uint_fast32_t client_refs[32]; +atomic_uintptr_t client_addrs[32]; + +void +__wrap_isc__nmhandle_attach(isc_nmhandle_t *source, isc_nmhandle_t **targetp); +void +__wrap_isc__nmhandle_detach(isc_nmhandle_t **handlep); + +void +__wrap_isc__nmhandle_attach(isc_nmhandle_t *source, isc_nmhandle_t **targetp) { + ns_client_t *client = (ns_client_t *)source; + int i; + + for (i = 0; i < 32; i++) { + if (atomic_load(&client_addrs[i]) == (uintptr_t)client) { + break; + } + } + INSIST(i < 32); + INSIST(atomic_load(&client_refs[i]) > 0); + + atomic_fetch_add(&client_refs[i], 1); + + *targetp = source; + return; +} + +void +__wrap_isc__nmhandle_detach(isc_nmhandle_t **handlep) { + isc_nmhandle_t *handle = *handlep; + ns_client_t *client = (ns_client_t *)handle; + int i; + + *handlep = NULL; + + for (i = 0; i < 32; i++) { + if (atomic_load(&client_addrs[i]) == (uintptr_t)client) { + break; + } + } + INSIST(i < 32); + + if (atomic_fetch_sub(&client_refs[i], 1) == 1) { + dns_view_detach(&client->view); + client->state = 4; + ns__client_reset_cb(client); + ns__client_put_cb(client); + isc_mem_put(mctx, client, sizeof(ns_client_t)); + } + + return; +} + +#ifdef USE_LIBTOOL +void +isc__nmhandle_attach(isc_nmhandle_t *source, isc_nmhandle_t **targetp) { + __wrap_isc__nmhandle_attach(source, targetp); +} +void +isc__nmhandle_detach(isc_nmhandle_t **handle) { + __wrap_isc__nmhandle_detach(handle); +} +#endif /* USE_LIBTOOL */ + +/* + * Logging categories: this needs to match the list in lib/ns/log.c. + */ +static isc_logcategory_t categories[] = { { "", 0 }, + { "client", 0 }, + { "network", 0 }, + { "update", 0 }, + { "queries", 0 }, + { "unmatched", 0 }, + { "update-security", 0 }, + { "query-errors", 0 }, + { NULL, 0 } }; + +static isc_result_t +matchview(isc_netaddr_t *srcaddr, isc_netaddr_t *destaddr, + dns_message_t *message, dns_aclenv_t *env, isc_result_t *sigresultp, + dns_view_t **viewp) { + UNUSED(srcaddr); + UNUSED(destaddr); + UNUSED(message); + UNUSED(env); + UNUSED(sigresultp); + UNUSED(viewp); + + return (ISC_R_NOTIMPLEMENTED); +} + +/* + * These need to be shut down from a running task. + */ +static atomic_bool shutdown_done = false; +static void +shutdown_managers(isc_task_t *task, isc_event_t *event) { + UNUSED(task); + + if (interfacemgr != NULL) { + ns_interfacemgr_shutdown(interfacemgr); + ns_interfacemgr_detach(&interfacemgr); + } + + if (dispatchmgr != NULL) { + dns_dispatchmgr_destroy(&dispatchmgr); + } + + atomic_store(&shutdown_done, true); + atomic_store(&run_managers, false); + + isc_event_free(&event); +} + +static void +cleanup_managers(void) { + atomic_store(&shutdown_done, false); + + if (maintask != NULL) { + isc_task_shutdown(maintask); + isc_task_destroy(&maintask); + } + + while (atomic_load(&run_managers) && !atomic_load(&shutdown_done)) { + /* + * There's no straightforward way to determine + * whether all the clients have shut down, so + * we'll just sleep for a bit and hope. + */ + ns_test_nap(500000); + } + + if (sctx != NULL) { + ns_server_detach(&sctx); + } + if (interfacemgr != NULL) { + ns_interfacemgr_detach(&interfacemgr); + } + if (socketmgr != NULL) { + isc_socketmgr_destroy(&socketmgr); + } + + isc_managers_destroy(netmgr == NULL ? NULL : &netmgr, + taskmgr == NULL ? NULL : &taskmgr); + + if (timermgr != NULL) { + isc_timermgr_destroy(&timermgr); + } + if (app_running) { + isc_app_finish(); + } +} + +static void +scan_interfaces(isc_task_t *task, isc_event_t *event) { + UNUSED(task); + + ns_interfacemgr_scan(interfacemgr, true); + isc_event_free(&event); +} + +static isc_result_t +create_managers(void) { + isc_result_t result; + in_port_t port = 5300 + isc_random8(); + ns_listenlist_t *listenon = NULL; + isc_event_t *event = NULL; + ncpus = isc_os_ncpus(); + + CHECK(isc_managers_create(mctx, ncpus, 0, &netmgr, &taskmgr)); + CHECK(isc_task_create_bound(taskmgr, 0, &maintask, 0)); + isc_taskmgr_setexcltask(taskmgr, maintask); + CHECK(isc_task_onshutdown(maintask, shutdown_managers, NULL)); + + CHECK(isc_timermgr_create(mctx, &timermgr)); + + CHECK(isc_socketmgr_create(mctx, &socketmgr)); + + CHECK(ns_server_create(mctx, matchview, &sctx)); + + CHECK(dns_dispatchmgr_create(mctx, &dispatchmgr)); + + CHECK(ns_interfacemgr_create(mctx, sctx, taskmgr, timermgr, socketmgr, + netmgr, dispatchmgr, maintask, ncpus, NULL, + ncpus, &interfacemgr)); + + CHECK(ns_listenlist_default(mctx, port, -1, true, &listenon)); + ns_interfacemgr_setlistenon4(interfacemgr, listenon); + ns_listenlist_detach(&listenon); + + event = isc_event_allocate(mctx, maintask, ISC_TASKEVENT_TEST, + scan_interfaces, NULL, sizeof(isc_event_t)); + isc_task_send(maintask, &event); + + /* + * There's no straightforward way to determine + * whether the interfaces have been scanned, + * we'll just sleep for a bit and hope. + */ + ns_test_nap(500000); + ns_interface_t *ifp = ns__interfacemgr_getif(interfacemgr); + clientmgr = ifp->clientmgr; + + atomic_store(&run_managers, true); + + return (ISC_R_SUCCESS); + +cleanup: + cleanup_managers(); + return (result); +} + +isc_result_t +ns_test_begin(FILE *logfile, bool start_managers) { + isc_result_t result; + + INSIST(!test_running); + test_running = true; + + if (start_managers) { + isc_resourcevalue_t files; + + /* + * The 'listenlist_test', 'notify_test', and 'query_test' + * tests need more than 256 descriptors with 8 cpus. + * Bump up to at least 1024. + */ + result = isc_resource_getcurlimit(isc_resource_openfiles, + &files); + if (result == ISC_R_SUCCESS) { + if (files < 1024) { + files = 1024; + (void)isc_resource_setlimit( + isc_resource_openfiles, files); + } + } + CHECK(isc_app_start()); + } + if (debug_mem_record) { + isc_mem_debugging |= ISC_MEM_DEBUGRECORD; + } + + INSIST(mctx == NULL); + isc_mem_create(&mctx); + + if (!dst_active) { + CHECK(dst_lib_init(mctx, NULL)); + dst_active = true; + } + + if (logfile != NULL) { + isc_logdestination_t destination; + isc_logconfig_t *logconfig = NULL; + + INSIST(lctx == NULL); + isc_log_create(mctx, &lctx, &logconfig); + + isc_log_registercategories(lctx, categories); + isc_log_setcontext(lctx); + dns_log_init(lctx); + dns_log_setcontext(lctx); + + destination.file.stream = logfile; + destination.file.name = NULL; + destination.file.versions = ISC_LOG_ROLLNEVER; + destination.file.maximum_size = 0; + isc_log_createchannel(logconfig, "stderr", ISC_LOG_TOFILEDESC, + ISC_LOG_DYNAMIC, &destination, 0); + CHECK(isc_log_usechannel(logconfig, "stderr", NULL, NULL)); + } + + dns_result_register(); + + if (start_managers) { + CHECK(create_managers()); + } + + /* + * atf-run changes us to a /tmp directory, so tests + * that access test data files must first chdir to the proper + * location. + */ + if (chdir(TESTS) == -1) { + CHECK(ISC_R_FAILURE); + } + + return (ISC_R_SUCCESS); + +cleanup: + ns_test_end(); + return (result); +} + +void +ns_test_end(void) { + cleanup_managers(); + + dst_lib_destroy(); + dst_active = false; + + if (lctx != NULL) { + isc_log_destroy(&lctx); + } + + if (mctx != NULL) { + isc_mem_destroy(&mctx); + } + + test_running = false; +} + +isc_result_t +ns_test_makeview(const char *name, bool with_cache, dns_view_t **viewp) { + dns_cache_t *cache = NULL; + dns_view_t *view = NULL; + isc_result_t result; + + CHECK(dns_view_create(mctx, dns_rdataclass_in, name, &view)); + + if (with_cache) { + CHECK(dns_cache_create(mctx, mctx, taskmgr, timermgr, + dns_rdataclass_in, "", "rbt", 0, NULL, + &cache)); + dns_view_setcache(view, cache, false); + /* + * Reference count for "cache" is now at 2, so decrement it in + * order for the cache to be automatically freed when "view" + * gets freed. + */ + dns_cache_detach(&cache); + } + + *viewp = view; + + return (ISC_R_SUCCESS); + +cleanup: + if (view != NULL) { + dns_view_detach(&view); + } + return (result); +} + +/* + * Create a zone with origin 'name', return a pointer to the zone object in + * 'zonep'. If 'view' is set, add the zone to that view; otherwise, create + * a new view for the purpose. + * + * If the created view is going to be needed by the caller subsequently, + * then 'keepview' should be set to true; this will prevent the view + * from being detached. In this case, the caller is responsible for + * detaching the view. + */ +isc_result_t +ns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view, + bool keepview) { + isc_result_t result; + dns_zone_t *zone = NULL; + isc_buffer_t buffer; + dns_fixedname_t fixorigin; + dns_name_t *origin; + + if (view == NULL) { + CHECK(dns_view_create(mctx, dns_rdataclass_in, "view", &view)); + } else if (!keepview) { + keepview = true; + } + + zone = *zonep; + if (zone == NULL) { + CHECK(dns_zone_create(&zone, mctx)); + } + + isc_buffer_constinit(&buffer, name, strlen(name)); + isc_buffer_add(&buffer, strlen(name)); + origin = dns_fixedname_initname(&fixorigin); + CHECK(dns_name_fromtext(origin, &buffer, dns_rootname, 0, NULL)); + CHECK(dns_zone_setorigin(zone, origin)); + dns_zone_setview(zone, view); + dns_zone_settype(zone, dns_zone_primary); + dns_zone_setclass(zone, view->rdclass); + dns_view_addzone(view, zone); + + if (!keepview) { + dns_view_detach(&view); + } + + *zonep = zone; + + return (ISC_R_SUCCESS); + +cleanup: + if (zone != NULL) { + dns_zone_detach(&zone); + } + if (view != NULL) { + dns_view_detach(&view); + } + return (result); +} + +isc_result_t +ns_test_setupzonemgr(void) { + isc_result_t result; + REQUIRE(zonemgr == NULL); + + result = dns_zonemgr_create(mctx, taskmgr, timermgr, socketmgr, + &zonemgr); + return (result); +} + +isc_result_t +ns_test_managezone(dns_zone_t *zone) { + isc_result_t result; + REQUIRE(zonemgr != NULL); + + result = dns_zonemgr_setsize(zonemgr, 1); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = dns_zonemgr_managezone(zonemgr, zone); + return (result); +} + +void +ns_test_releasezone(dns_zone_t *zone) { + REQUIRE(zonemgr != NULL); + dns_zonemgr_releasezone(zonemgr, zone); +} + +void +ns_test_closezonemgr(void) { + REQUIRE(zonemgr != NULL); + + dns_zonemgr_shutdown(zonemgr); + dns_zonemgr_detach(&zonemgr); +} + +isc_result_t +ns_test_serve_zone(const char *zonename, const char *filename, + dns_view_t *view) { + isc_result_t result; + dns_db_t *db = NULL; + + /* + * Prepare zone structure for further processing. + */ + result = ns_test_makezone(zonename, &served_zone, view, true); + if (result != ISC_R_SUCCESS) { + return (result); + } + + /* + * Start zone manager. + */ + result = ns_test_setupzonemgr(); + if (result != ISC_R_SUCCESS) { + goto free_zone; + } + + /* + * Add the zone to the zone manager. + */ + result = ns_test_managezone(served_zone); + if (result != ISC_R_SUCCESS) { + goto close_zonemgr; + } + + view->nocookieudp = 512; + + /* + * Set path to the master file for the zone and then load it. + */ + dns_zone_setfile(served_zone, filename, dns_masterformat_text, + &dns_master_style_default); + result = dns_zone_load(served_zone, false); + if (result != ISC_R_SUCCESS) { + goto release_zone; + } + + /* + * The zone should now be loaded; test it. + */ + result = dns_zone_getdb(served_zone, &db); + if (result != ISC_R_SUCCESS) { + goto release_zone; + } + if (db != NULL) { + dns_db_detach(&db); + } + + return (ISC_R_SUCCESS); + +release_zone: + ns_test_releasezone(served_zone); +close_zonemgr: + ns_test_closezonemgr(); +free_zone: + dns_zone_detach(&served_zone); + + return (result); +} + +void +ns_test_cleanup_zone(void) { + ns_test_releasezone(served_zone); + ns_test_closezonemgr(); + + dns_zone_detach(&served_zone); +} + +isc_result_t +ns_test_getclient(ns_interface_t *ifp0, bool tcp, ns_client_t **clientp) { + isc_result_t result; + ns_client_t *client = isc_mem_get(mctx, sizeof(ns_client_t)); + int i; + + UNUSED(ifp0); + UNUSED(tcp); + + result = ns__client_setup(client, clientmgr, true); + + for (i = 0; i < 32; i++) { + if (atomic_load(&client_addrs[i]) == (uintptr_t)NULL || + atomic_load(&client_addrs[i]) == (uintptr_t)client) + { + break; + } + } + REQUIRE(i < 32); + + atomic_store(&client_refs[i], 2); + atomic_store(&client_addrs[i], (uintptr_t)client); + client->handle = (isc_nmhandle_t *)client; /* Hack */ + *clientp = client; + + return (result); +} + +/*% + * Synthesize a DNS message based on supplied QNAME, QTYPE and flags, then + * parse it and store the results in client->message. + */ +static isc_result_t +attach_query_msg_to_client(ns_client_t *client, const char *qnamestr, + dns_rdatatype_t qtype, unsigned int qflags) { + dns_rdataset_t *qrdataset = NULL; + dns_message_t *message = NULL; + unsigned char query[65536]; + dns_name_t *qname = NULL; + isc_buffer_t querybuf; + dns_compress_t cctx; + isc_result_t result; + + REQUIRE(client != NULL); + REQUIRE(qnamestr != NULL); + + /* + * Create a new DNS message holding a query. + */ + dns_message_create(mctx, DNS_MESSAGE_INTENTRENDER, &message); + + /* + * Set query ID to a random value. + */ + message->id = isc_random16(); + + /* + * Set query flags as requested by the caller. + */ + message->flags = qflags; + + /* + * Allocate structures required to construct the query. + */ + result = dns_message_gettemprdataset(message, &qrdataset); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_gettempname(message, &qname); + if (result != ISC_R_SUCCESS) { + goto put_rdataset; + } + + /* + * Convert "qnamestr" to a DNS name, create a question rdataset of + * class IN and type "qtype", link the two and add the result to the + * QUESTION section of the query. + */ + result = dns_name_fromstring(qname, qnamestr, 0, mctx); + if (result != ISC_R_SUCCESS) { + goto put_name; + } + dns_rdataset_makequestion(qrdataset, dns_rdataclass_in, qtype); + ISC_LIST_APPEND(qname->list, qrdataset, link); + dns_message_addname(message, qname, DNS_SECTION_QUESTION); + + /* + * Render the query. + */ + dns_compress_init(&cctx, -1, mctx); + isc_buffer_init(&querybuf, query, sizeof(query)); + result = dns_message_renderbegin(message, &cctx, &querybuf); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_rendersection(message, DNS_SECTION_QUESTION, 0); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_renderend(message); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + dns_compress_invalidate(&cctx); + + /* + * Destroy the created message as it was rendered into "querybuf" and + * the latter is all we are going to need from now on. + */ + dns_message_detach(&message); + + /* + * Parse the rendered query, storing results in client->message. + */ + isc_buffer_first(&querybuf); + return (dns_message_parse(client->message, &querybuf, 0)); + +put_name: + dns_message_puttempname(message, &qname); +put_rdataset: + dns_message_puttemprdataset(message, &qrdataset); +destroy_message: + dns_message_detach(&message); + + return (result); +} + +/*% + * A hook action which stores the query context pointed to by "arg" at + * "data". Causes execution to be interrupted at hook insertion + * point. + */ +static ns_hookresult_t +extract_qctx(void *arg, void *data, isc_result_t *resultp) { + query_ctx_t **qctxp; + query_ctx_t *qctx; + + REQUIRE(arg != NULL); + REQUIRE(data != NULL); + REQUIRE(resultp != NULL); + + /* + * qctx is a stack variable in lib/ns/query.c. Its contents need to be + * duplicated or otherwise they will become invalidated once the stack + * gets unwound. + */ + qctx = isc_mem_get(mctx, sizeof(*qctx)); + if (qctx != NULL) { + memmove(qctx, (query_ctx_t *)arg, sizeof(*qctx)); + } + + qctxp = (query_ctx_t **)data; + /* + * If memory allocation failed, the supplied pointer will simply be set + * to NULL. We rely on the user of this hook to react properly. + */ + *qctxp = qctx; + *resultp = ISC_R_UNSET; + + return (NS_HOOK_RETURN); +} + +/*% + * Initialize a query context for "client" and store it in "qctxp". + * + * Requires: + * + * \li "client->message" to hold a parsed DNS query. + */ +static isc_result_t +create_qctx_for_client(ns_client_t *client, query_ctx_t **qctxp) { + ns_hooktable_t *saved_hook_table = NULL, *query_hooks = NULL; + const ns_hook_t hook = { + .action = extract_qctx, + .action_data = qctxp, + }; + + REQUIRE(client != NULL); + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp == NULL); + + /* + * Call ns_query_start() to initialize a query context for given + * client, but first hook into query_setup() so that we can just + * extract an initialized query context, without kicking off any + * further processing. Make sure we do not overwrite any previously + * set hooks. + */ + + ns_hooktable_create(mctx, &query_hooks); + ns_hook_add(query_hooks, mctx, NS_QUERY_SETUP, &hook); + + saved_hook_table = ns__hook_table; + ns__hook_table = query_hooks; + + ns_query_start(client, client->handle); + + ns__hook_table = saved_hook_table; + ns_hooktable_free(mctx, (void **)&query_hooks); + + isc_nmhandle_detach(&client->reqhandle); + + if (*qctxp == NULL) { + return (ISC_R_NOMEMORY); + } else { + return (ISC_R_SUCCESS); + } +} + +isc_result_t +ns_test_qctx_create(const ns_test_qctx_create_params_t *params, + query_ctx_t **qctxp) { + ns_client_t *client = NULL; + isc_result_t result; + isc_nmhandle_t *handle = NULL; + + REQUIRE(params != NULL); + REQUIRE(params->qname != NULL); + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp == NULL); + + /* + * Allocate and initialize a client structure. + */ + result = ns_test_getclient(NULL, false, &client); + if (result != ISC_R_SUCCESS) { + return (result); + } + TIME_NOW(&client->tnow); + + /* + * Every client needs to belong to a view. + */ + result = ns_test_makeview("view", params->with_cache, &client->view); + if (result != ISC_R_SUCCESS) { + goto detach_client; + } + + /* + * Synthesize a DNS query using given QNAME, QTYPE and flags, storing + * it in client->message. + */ + result = attach_query_msg_to_client(client, params->qname, + params->qtype, params->qflags); + if (result != ISC_R_SUCCESS) { + goto detach_view; + } + + /* + * Allow recursion for the client. As NS_CLIENTATTR_RA normally gets + * set in ns__client_request(), i.e. earlier than the unit tests hook + * into the call chain, just set it manually. + */ + client->attributes |= NS_CLIENTATTR_RA; + + /* + * Create a query context for a client sending the previously + * synthesized query. + */ + result = create_qctx_for_client(client, qctxp); + if (result != ISC_R_SUCCESS) { + goto detach_query; + } + + /* + * The reference count for "client" is now at 2, so we need to + * decrement it in order for it to drop to zero when "qctx" gets + * destroyed. + */ + handle = client->handle; + isc_nmhandle_detach(&handle); + + return (ISC_R_SUCCESS); + +detach_query: + dns_message_detach(&client->message); +detach_view: + dns_view_detach(&client->view); +detach_client: + isc_nmhandle_detach(&client->handle); + + return (result); +} + +void +ns_test_qctx_destroy(query_ctx_t **qctxp) { + query_ctx_t *qctx; + + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp != NULL); + + qctx = *qctxp; + *qctxp = NULL; + + if (qctx->zone != NULL) { + dns_zone_detach(&qctx->zone); + } + if (qctx->db != NULL) { + dns_db_detach(&qctx->db); + } + if (qctx->client != NULL) { + isc_nmhandle_detach(&qctx->client->handle); + } + + isc_mem_put(mctx, qctx, sizeof(*qctx)); +} + +ns_hookresult_t +ns_test_hook_catch_call(void *arg, void *data, isc_result_t *resultp) { + UNUSED(arg); + UNUSED(data); + + *resultp = ISC_R_UNSET; + + return (NS_HOOK_RETURN); +} + +/* + * Sleep for 'usec' microseconds. + */ +void +ns_test_nap(uint32_t usec) { + struct timespec ts; + + ts.tv_sec = usec / 1000000; + ts.tv_nsec = (usec % 1000000) * 1000; + nanosleep(&ts, NULL); +} + +isc_result_t +ns_test_loaddb(dns_db_t **db, dns_dbtype_t dbtype, const char *origin, + const char *testfile) { + isc_result_t result; + dns_fixedname_t fixed; + dns_name_t *name; + + name = dns_fixedname_initname(&fixed); + + result = dns_name_fromstring(name, origin, 0, NULL); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = dns_db_create(mctx, "rbt", name, dbtype, dns_rdataclass_in, 0, + NULL, db); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = dns_db_load(*db, testfile, dns_masterformat_text, 0); + return (result); +} + +static int +fromhex(char c) { + if (c >= '0' && c <= '9') { + return (c - '0'); + } else if (c >= 'a' && c <= 'f') { + return (c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + return (c - 'A' + 10); + } + + printf("bad input format: %02x\n", c); + exit(3); +} + +isc_result_t +ns_test_getdata(const char *file, unsigned char *buf, size_t bufsiz, + size_t *sizep) { + isc_result_t result; + unsigned char *bp; + char *rp, *wp; + char s[BUFSIZ]; + size_t len, i; + FILE *f = NULL; + int n; + + result = isc_stdio_open(file, "r", &f); + if (result != ISC_R_SUCCESS) { + return (result); + } + + bp = buf; + while (fgets(s, sizeof(s), f) != NULL) { + rp = s; + wp = s; + len = 0; + while (*rp != '\0') { + if (*rp == '#') { + break; + } + if (*rp != ' ' && *rp != '\t' && *rp != '\r' && + *rp != '\n') + { + *wp++ = *rp; + len++; + } + rp++; + } + if (len == 0U) { + continue; + } + if (len % 2 != 0U) { + CHECK(ISC_R_UNEXPECTEDEND); + } + if (len > bufsiz * 2) { + CHECK(ISC_R_NOSPACE); + } + rp = s; + for (i = 0; i < len; i += 2) { + n = fromhex(*rp++); + n *= 16; + n += fromhex(*rp++); + *bp++ = n; + } + } + + *sizep = bp - buf; + + result = ISC_R_SUCCESS; + +cleanup: + isc_stdio_close(f); + return (result); +} diff --git a/lib/ns/tests/nstest.h b/lib/ns/tests/nstest.h new file mode 100644 index 0000000..87a26a6 --- /dev/null +++ b/lib/ns/tests/nstest.h @@ -0,0 +1,165 @@ +/* + * 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. + */ + +/*! \file */ + +#include <inttypes.h> +#include <stdbool.h> + +#include <isc/buffer.h> +#include <isc/hash.h> +#include <isc/log.h> +#include <isc/mem.h> +#include <isc/string.h> +#include <isc/task.h> +#include <isc/timer.h> +#include <isc/util.h> + +#include <dns/result.h> +#include <dns/zone.h> + +#include <ns/client.h> +#include <ns/hooks.h> +#include <ns/interfacemgr.h> + +typedef struct ns_test_id { + const char *description; + int lineno; +} ns_test_id_t; + +#define NS_TEST_ID(desc) \ + { \ + .description = desc, .lineno = __LINE__ \ + } + +#define CHECK(r) \ + do { \ + result = (r); \ + if (result != ISC_R_SUCCESS) \ + goto cleanup; \ + } while (0) + +extern isc_mem_t *mctx; +extern isc_log_t *lctx; +extern isc_taskmgr_t *taskmgr; +extern isc_task_t *maintask; +extern isc_timermgr_t *timermgr; +extern isc_socketmgr_t *socketmgr; +extern dns_zonemgr_t *zonemgr; +extern dns_dispatchmgr_t *dispatchmgr; +extern ns_clientmgr_t *clientmgr; +extern ns_interfacemgr_t *interfacemgr; +extern ns_server_t *sctx; +extern bool app_running; +extern int ncpus; +extern bool debug_mem_record; + +#ifdef NETMGR_TRACE +#define FLARG \ + , const char *file __attribute__((unused)), \ + unsigned int line __attribute__((unused)), \ + const char *func __attribute__((unused)) +#else +#define FLARG +#endif + +isc_result_t +ns_test_begin(FILE *logfile, bool create_managers); + +void +ns_test_end(void); + +/*% + * Create a view. If "with_cache" is set to true, a cache database will + * also be created and attached to the created view. + */ +isc_result_t +ns_test_makeview(const char *name, bool with_cache, dns_view_t **viewp); + +isc_result_t +ns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view, + bool keepview); + +isc_result_t +ns_test_setupzonemgr(void); + +isc_result_t +ns_test_managezone(dns_zone_t *zone); + +void +ns_test_releasezone(dns_zone_t *zone); + +void +ns_test_closezonemgr(void); + +/*% + * Load data for zone "zonename" from file "filename" and start serving it to + * clients matching "view". Only one zone loaded using this function can be + * served at any given time. + */ +isc_result_t +ns_test_serve_zone(const char *zonename, const char *filename, + dns_view_t *view); + +/*% + * Release the zone loaded by ns_test_serve_zone(). + */ +void +ns_test_cleanup_zone(void); + +void +ns_test_nap(uint32_t usec); + +isc_result_t +ns_test_loaddb(dns_db_t **db, dns_dbtype_t dbtype, const char *origin, + const char *testfile); + +isc_result_t +ns_test_getdata(const char *file, unsigned char *buf, size_t bufsiz, + size_t *sizep); + +isc_result_t +ns_test_getclient(ns_interface_t *ifp0, bool tcp, ns_client_t **clientp); + +/*% + * Structure containing parameters for ns_test_qctx_create(). + */ +typedef struct ns_test_qctx_create_params { + const char *qname; + dns_rdatatype_t qtype; + unsigned int qflags; + bool with_cache; +} ns_test_qctx_create_params_t; + +/*% + * Prepare a query context identical with one that would be prepared if a query + * with given QNAME, QTYPE and flags was received from a client. Recursion is + * assumed to be allowed for this client. If "with_cache" is set to true, + * a cache database will be created and associated with the view matching the + * incoming query. + */ +isc_result_t +ns_test_qctx_create(const ns_test_qctx_create_params_t *params, + query_ctx_t **qctxp); + +/*% + * Destroy a query context created by ns_test_qctx_create(). + */ +void +ns_test_qctx_destroy(query_ctx_t **qctxp); + +/*% + * A hook callback interrupting execution at given hook's insertion point. + */ +ns_hookresult_t +ns_test_hook_catch_call(void *arg, void *data, isc_result_t *resultp); diff --git a/lib/ns/tests/plugin_test.c b/lib/ns/tests/plugin_test.c new file mode 100644 index 0000000..7870f7a --- /dev/null +++ b/lib/ns/tests/plugin_test.c @@ -0,0 +1,209 @@ +/* + * 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. + */ + +#if HAVE_CMOCKA + +#include <limits.h> +#include <sched.h> /* IWYU pragma: keep */ +#include <setjmp.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#define UNIT_TESTING +#include <cmocka.h> + +#include <isc/mem.h> +#include <isc/platform.h> +#include <isc/result.h> +#include <isc/types.h> +#include <isc/util.h> + +ISC_PLATFORM_NORETURN_PRE void +_fail(const char *const file, const int line) ISC_PLATFORM_NORETURN_POST; + +#include <ns/hooks.h> + +#include "nstest.h" + +static int +_setup(void **state) { + isc_result_t result; + + UNUSED(state); + + result = ns_test_begin(NULL, false); + assert_int_equal(result, ISC_R_SUCCESS); + + return (0); +} + +static int +_teardown(void **state) { + if (*state != NULL) { + isc_mem_free(mctx, *state); + } + + ns_test_end(); + + return (0); +} + +/*% + * Structure containing parameters for run_full_path_test(). + */ +typedef struct { + const ns_test_id_t id; /* libns test identifier */ + const char *input; /* source string - plugin name or path + * */ + size_t output_size; /* size of target char array to + * allocate */ + isc_result_t result; /* expected return value */ + const char *output; /* expected output string */ +} ns_plugin_expandpath_test_params_t; + +/*% + * Perform a single ns_plugin_expandpath() check using given parameters. + */ +static void +run_full_path_test(const ns_plugin_expandpath_test_params_t *test, + void **state) { + char **target = (char **)state; + isc_result_t result; + + REQUIRE(test != NULL); + REQUIRE(test->id.description != NULL); + REQUIRE(test->input != NULL); + REQUIRE(test->result != ISC_R_SUCCESS || test->output != NULL); + + /* + * Prepare a target buffer of given size. Store it in 'state' so that + * it can get cleaned up by _teardown() if the test fails. + */ + *target = isc_mem_allocate(mctx, test->output_size); + + /* + * Call ns_plugin_expandpath(). + */ + result = ns_plugin_expandpath(test->input, *target, test->output_size); + + /* + * Check return value. + */ + if (result != test->result) { + fail_msg("# test \"%s\" on line %d: " + "expected result %d (%s), got %d (%s)", + test->id.description, test->id.lineno, test->result, + isc_result_totext(test->result), result, + isc_result_totext(result)); + } + + /* + * Check output string if return value indicates success. + */ + if (result == ISC_R_SUCCESS && strcmp(*target, test->output) != 0) { + fail_msg("# test \"%s\" on line %d: " + "expected output \"%s\", got \"%s\"", + test->id.description, test->id.lineno, test->output, + *target); + } + + isc_mem_free(mctx, *target); +} + +/* test ns_plugin_expandpath() */ +static void +ns_plugin_expandpath_test(void **state) { + size_t i; + + const ns_plugin_expandpath_test_params_t tests[] = { + { + NS_TEST_ID("correct use with an absolute path"), + .input = "/usr/lib/named/foo.so", + .output_size = PATH_MAX, + .result = ISC_R_SUCCESS, + .output = "/usr/lib/named/foo.so", + }, + { + NS_TEST_ID("correct use with a relative path"), + .input = "../../foo.so", + .output_size = PATH_MAX, + .result = ISC_R_SUCCESS, + .output = "../../foo.so", + }, + { + NS_TEST_ID("correct use with a filename"), + .input = "foo.so", + .output_size = PATH_MAX, + .result = ISC_R_SUCCESS, +#ifndef WIN32 + .output = NAMED_PLUGINDIR "/foo.so", +#else /* ifndef WIN32 */ + .output = "foo.so", +#endif /* ifndef WIN32 */ + }, + { + NS_TEST_ID("no space at all in target buffer"), + .input = "/usr/lib/named/foo.so", + .output_size = 0, + .result = ISC_R_NOSPACE, + }, + { + NS_TEST_ID("target buffer too small to fit input"), + .input = "/usr/lib/named/foo.so", + .output_size = 1, + .result = ISC_R_NOSPACE, + }, + { + NS_TEST_ID("target buffer too small to fit NULL byte"), + .input = "/foo.so", + .output_size = 7, + .result = ISC_R_NOSPACE, + }, +#ifndef WIN32 + { + NS_TEST_ID("target buffer too small to fit full path"), + .input = "foo.so", + .output_size = 7, + .result = ISC_R_NOSPACE, + }, +#endif /* ifndef WIN32 */ + }; + + for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + run_full_path_test(&tests[i], state); + } +} + +int +main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(ns_plugin_expandpath_test, + _setup, _teardown), + }; + + return (cmocka_run_group_tests(tests, NULL, NULL)); +} +#else /* HAVE_CMOCKA */ + +#include <stdio.h> + +int +main(void) { + printf("1..0 # Skipped: cmocka not available\n"); + return (SKIPPED_TEST_EXIT_CODE); +} + +#endif /* if HAVE_CMOCKA */ diff --git a/lib/ns/tests/query_test.c b/lib/ns/tests/query_test.c new file mode 100644 index 0000000..b3e8fc1 --- /dev/null +++ b/lib/ns/tests/query_test.c @@ -0,0 +1,632 @@ +/* + * 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 <isc/util.h> + +#if HAVE_CMOCKA + +#include <inttypes.h> +#include <sched.h> /* IWYU pragma: keep */ +#include <setjmp.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#define UNIT_TESTING +#include <cmocka.h> + +#include <dns/badcache.h> +#include <dns/view.h> + +#include <ns/client.h> +#include <ns/hooks.h> +#include <ns/query.h> + +#include "nstest.h" + +#if defined(USE_LIBTOOL) || LD_WRAP +static int +_setup(void **state) { + isc_result_t result; + + UNUSED(state); + + result = ns_test_begin(NULL, true); + assert_int_equal(result, ISC_R_SUCCESS); + + return (0); +} + +static int +_teardown(void **state) { + UNUSED(state); + + ns_test_end(); + + return (0); +} + +/***** +***** ns__query_sfcache() tests +*****/ + +/*% + * Structure containing parameters for ns__query_sfcache_test(). + */ +typedef struct { + const ns_test_id_t id; /* libns test identifier */ + unsigned int qflags; /* query flags */ + bool cache_entry_present; /* whether a SERVFAIL + * cache entry + * matching the query + * should be + * present */ + uint32_t cache_entry_flags; /* NS_FAILCACHE_* flags to + * set for + * the SERVFAIL cache entry + * */ + bool servfail_expected; /* whether a cached + * SERVFAIL is + * expected to be returned + * */ +} ns__query_sfcache_test_params_t; + +/*% + * Perform a single ns__query_sfcache() check using given parameters. + */ +static void +run_sfcache_test(const ns__query_sfcache_test_params_t *test) { + ns_hooktable_t *query_hooks = NULL; + query_ctx_t *qctx = NULL; + isc_result_t result; + const ns_hook_t hook = { + .action = ns_test_hook_catch_call, + }; + + REQUIRE(test != NULL); + REQUIRE(test->id.description != NULL); + REQUIRE(test->cache_entry_present || test->cache_entry_flags == 0); + + /* + * Interrupt execution if ns_query_done() is called. + */ + + ns_hooktable_create(mctx, &query_hooks); + ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook); + ns__hook_table = query_hooks; + + /* + * Construct a query context for a ./NS query with given flags. + */ + { + const ns_test_qctx_create_params_t qctx_params = { + .qname = ".", + .qtype = dns_rdatatype_ns, + .qflags = test->qflags, + .with_cache = true, + }; + + result = ns_test_qctx_create(&qctx_params, &qctx); + assert_int_equal(result, ISC_R_SUCCESS); + } + + /* + * If this test wants a SERVFAIL cache entry matching the query to + * exist, create it. + */ + if (test->cache_entry_present) { + isc_interval_t hour; + isc_time_t expire; + + isc_interval_set(&hour, 3600, 0); + result = isc_time_nowplusinterval(&expire, &hour); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_badcache_add(qctx->client->view->failcache, dns_rootname, + dns_rdatatype_ns, true, + test->cache_entry_flags, &expire); + } + + /* + * Check whether ns__query_sfcache() behaves as expected. + */ + ns__query_sfcache(qctx); + + if (test->servfail_expected) { + if (qctx->result != DNS_R_SERVFAIL) { + fail_msg("# test \"%s\" on line %d: " + "expected SERVFAIL, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + } else { + if (qctx->result != ISC_R_SUCCESS) { + fail_msg("# test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + } + + /* + * Clean up. + */ + ns_test_qctx_destroy(&qctx); + ns_hooktable_free(mctx, (void **)&query_hooks); +} + +/* test ns__query_sfcache() */ +static void +ns__query_sfcache_test(void **state) { + size_t i; + + const ns__query_sfcache_test_params_t tests[] = { + /* + * Sanity check for an empty SERVFAIL cache. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: empty"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = false, + .servfail_expected = false, + }, + /* + * Query: RD=1, CD=0. Cache entry: CD=0. Should SERVFAIL. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: CD=0"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = true, + .cache_entry_flags = 0, + .servfail_expected = true, + }, + /* + * Query: RD=1, CD=1. Cache entry: CD=0. Should not SERVFAIL: + * failed validation should not influence CD=1 queries. + */ + { + NS_TEST_ID("query: RD=1, CD=1; cache: CD=0"), + .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD, + .cache_entry_present = true, + .cache_entry_flags = 0, + .servfail_expected = false, + }, + /* + * Query: RD=1, CD=1. Cache entry: CD=1. Should SERVFAIL: + * SERVFAIL responses elicited by CD=1 queries can be + * "replayed" for other CD=1 queries during the lifetime of the + * SERVFAIL cache entry. + */ + { + NS_TEST_ID("query: RD=1, CD=1; cache: CD=1"), + .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD, + .cache_entry_present = true, + .cache_entry_flags = NS_FAILCACHE_CD, + .servfail_expected = true, + }, + /* + * Query: RD=1, CD=0. Cache entry: CD=1. Should SERVFAIL: if + * a CD=1 query elicited a SERVFAIL, a CD=0 query for the same + * QNAME and QTYPE will SERVFAIL as well. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: CD=1"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = true, + .cache_entry_flags = NS_FAILCACHE_CD, + .servfail_expected = true, + }, + /* + * Query: RD=0, CD=0. Cache entry: CD=0. Should not SERVFAIL + * despite a matching entry being present as the SERVFAIL cache + * should not be consulted for non-recursive queries. + */ + { + NS_TEST_ID("query: RD=0, CD=0; cache: CD=0"), + .qflags = 0, + .cache_entry_present = true, + .cache_entry_flags = 0, + .servfail_expected = false, + }, + }; + + UNUSED(state); + + for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + run_sfcache_test(&tests[i]); + } +} + +/***** +***** ns__query_start() tests +*****/ + +/*% + * Structure containing parameters for ns__query_start_test(). + */ +typedef struct { + const ns_test_id_t id; /* libns test identifier */ + const char *qname; /* QNAME */ + dns_rdatatype_t qtype; /* QTYPE */ + unsigned int qflags; /* query flags */ + bool disable_name_checks; /* if set to true, owner + * name + * checks will + * be disabled for the + * view created + * */ + bool recursive_service; /* if set to true, the view + * created will + * have a cache + * database + * attached */ + const char *auth_zone_origin; /* origin name of the zone + * the + * created view will be + * authoritative for */ + const char *auth_zone_path; /* path to load the + * authoritative + * zone from */ + enum { /* expected result: */ + NS__QUERY_START_R_INVALID, + NS__QUERY_START_R_REFUSE, /* query should be REFUSED */ + NS__QUERY_START_R_CACHE, /* query should be answered from + * cache */ + NS__QUERY_START_R_AUTH, /* query should be answered using + * authoritative data */ + } expected_result; +} ns__query_start_test_params_t; + +/*% + * Perform a single ns__query_start() check using given parameters. + */ +static void +run_start_test(const ns__query_start_test_params_t *test) { + ns_hooktable_t *query_hooks = NULL; + query_ctx_t *qctx = NULL; + isc_result_t result; + const ns_hook_t hook = { + .action = ns_test_hook_catch_call, + }; + + REQUIRE(test != NULL); + REQUIRE(test->id.description != NULL); + REQUIRE((test->auth_zone_origin == NULL && + test->auth_zone_path == NULL) || + (test->auth_zone_origin != NULL && + test->auth_zone_path != NULL)); + + /* + * Interrupt execution if query_lookup() or ns_query_done() is called. + */ + ns_hooktable_create(mctx, &query_hooks); + ns_hook_add(query_hooks, mctx, NS_QUERY_LOOKUP_BEGIN, &hook); + ns_hook_add(query_hooks, mctx, NS_QUERY_DONE_BEGIN, &hook); + ns__hook_table = query_hooks; + + /* + * Construct a query context using the supplied parameters. + */ + { + const ns_test_qctx_create_params_t qctx_params = { + .qname = test->qname, + .qtype = test->qtype, + .qflags = test->qflags, + .with_cache = test->recursive_service, + }; + result = ns_test_qctx_create(&qctx_params, &qctx); + assert_int_equal(result, ISC_R_SUCCESS); + } + + /* + * Enable view->checknames by default, disable if requested. + */ + qctx->client->view->checknames = !test->disable_name_checks; + + /* + * Load zone from file and attach it to the client's view, if + * requested. + */ + if (test->auth_zone_path != NULL) { + result = ns_test_serve_zone(test->auth_zone_origin, + test->auth_zone_path, + qctx->client->view); + assert_int_equal(result, ISC_R_SUCCESS); + } + + /* + * Check whether ns__query_start() behaves as expected. + */ + ns__query_start(qctx); + + switch (test->expected_result) { + case NS__QUERY_START_R_REFUSE: + if (qctx->result != DNS_R_REFUSED) { + fail_msg("# test \"%s\" on line %d: " + "expected REFUSED, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + if (qctx->zone != NULL) { + fail_msg("# test \"%s\" on line %d: " + "no zone was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + } + if (qctx->db != NULL) { + fail_msg("# test \"%s\" on line %d: " + "no database was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + } + break; + case NS__QUERY_START_R_CACHE: + if (qctx->result != ISC_R_SUCCESS) { + fail_msg("# test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + if (qctx->zone != NULL) { + fail_msg("# test \"%s\" on line %d: " + "no zone was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + } + if (qctx->db == NULL || qctx->db != qctx->client->view->cachedb) + { + fail_msg("# test \"%s\" on line %d: " + "cache database was expected to be " + "attached to query context, but it was not", + test->id.description, test->id.lineno); + } + break; + case NS__QUERY_START_R_AUTH: + if (qctx->result != ISC_R_SUCCESS) { + fail_msg("# test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + if (qctx->zone == NULL) { + fail_msg("# test \"%s\" on line %d: " + "a zone was expected to be attached to query " + "context, but it was not", + test->id.description, test->id.lineno); + } + if (qctx->db == qctx->client->view->cachedb) { + fail_msg("# test \"%s\" on line %d: " + "cache database was not expected to be " + "attached to query context, but it is", + test->id.description, test->id.lineno); + } + break; + case NS__QUERY_START_R_INVALID: + fail_msg("# test \"%s\" on line %d has no expected result set", + test->id.description, test->id.lineno); + break; + default: + UNREACHABLE(); + } + + /* + * Clean up. + */ + if (test->auth_zone_path != NULL) { + ns_test_cleanup_zone(); + } + ns_test_qctx_destroy(&qctx); + ns_hooktable_free(mctx, (void **)&query_hooks); +} + +/* test ns__query_start() */ +static void +ns__query_start_test(void **state) { + size_t i; + + const ns__query_start_test_params_t tests[] = { + /* + * Recursive foo/A query to a server without recursive service + * and no zones configured. Query should be REFUSED. + */ + { + NS_TEST_ID("foo/A, no cache, no auth"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = false, + .expected_result = NS__QUERY_START_R_REFUSE, + }, + /* + * Recursive foo/A query to a server with recursive service and + * no zones configured. Query should be answered from cache. + */ + { + NS_TEST_ID("foo/A, cache, no auth"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .recursive_service = true, + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive foo/A query to a server with recursive service and + * zone "foo" configured. Query should be answered from + * authoritative data. + */ + { + NS_TEST_ID("foo/A, RD=1, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive bar/A query to a server without recursive service + * and zone "foo" configured. Query should be REFUSED. + */ + { + NS_TEST_ID("bar/A, RD=1, no cache, auth for foo"), + .qname = "bar", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = false, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_REFUSE, + }, + /* + * Recursive bar/A query to a server with recursive service and + * zone "foo" configured. Query should be answered from + * cache. + */ + { + NS_TEST_ID("bar/A, RD=1, cache, auth for foo"), + .qname = "bar", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive bar.foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("bar.foo/DS, RD=1, cache, auth for foo"), + .qname = "bar.foo", + .qtype = dns_rdatatype_ds, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Non-recursive bar.foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("bar.foo/DS, RD=0, cache, auth for foo"), + .qname = "bar.foo", + .qtype = dns_rdatatype_ds, + .qflags = 0, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive foo/DS query to a server with recursive service + * and zone "foo" configured. Query should be answered from + * cache. + */ + { + NS_TEST_ID("foo/DS, RD=1, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_ds, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Non-recursive foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("foo/DS, RD=0, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_ds, + .qflags = 0, + .recursive_service = true, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive _foo/A query to a server with recursive service, + * no zones configured and owner name checks disabled. Query + * should be answered from cache. + */ + { + NS_TEST_ID("_foo/A, cache, no auth, name checks off"), + .qname = "_foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .disable_name_checks = true, + .recursive_service = true, + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive _foo/A query to a server with recursive service, + * no zones configured and owner name checks enabled. Query + * should be REFUSED. + */ + { + NS_TEST_ID("_foo/A, cache, no auth, name checks on"), + .qname = "_foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .disable_name_checks = false, + .recursive_service = true, + .expected_result = NS__QUERY_START_R_REFUSE, + }, + }; + + UNUSED(state); + + for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + run_start_test(&tests[i]); + } +} +#endif /* if defined(USE_LIBTOOL) || LD_WRAP */ + +int +main(void) { +#if defined(USE_LIBTOOL) || LD_WRAP + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(ns__query_sfcache_test, _setup, + _teardown), + cmocka_unit_test_setup_teardown(ns__query_start_test, _setup, + _teardown), + }; + + return (cmocka_run_group_tests(tests, NULL, NULL)); +#else /* if defined(USE_LIBTOOL) || LD_WRAP */ + print_message("1..0 # Skip query_test requires libtool or LD_WRAP\n"); +#endif /* if defined(USE_LIBTOOL) || LD_WRAP */ +} + +#else /* HAVE_CMOCKA */ + +#include <stdio.h> + +int +main(void) { + printf("1..0 # Skipped: cmocka not available\n"); + + return (SKIPPED_TEST_EXIT_CODE); +} + +#endif /* HAVE_CMOCKA */ diff --git a/lib/ns/tests/testdata/notify/notify1.msg b/lib/ns/tests/testdata/notify/notify1.msg new file mode 100644 index 0000000..a84193b --- /dev/null +++ b/lib/ns/tests/testdata/notify/notify1.msg @@ -0,0 +1,3 @@ +# notify for example.com +10 a6 10 10 00 01 00 00 00 00 00 00 07 65 78 61 +6d 70 6c 65 03 63 6f 6d 00 00 06 00 01 diff --git a/lib/ns/tests/testdata/notify/zone1.db b/lib/ns/tests/testdata/notify/zone1.db new file mode 100644 index 0000000..b218d6d --- /dev/null +++ b/lib/ns/tests/testdata/notify/zone1.db @@ -0,0 +1,26 @@ +; 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. + +$TTL 1000 +@ in soa localhost. postmaster.localhost. ( + 1993050801 ;serial + 3600 ;refresh + 1800 ;retry + 604800 ;expiration + 3600 ) ;minimum + in ns ns.example.com. + in ns ns2.example.com. + in ns ns3.example.com. +ns in a 10.0.0.1 +ns2 in a 10.0.0.2 +ns3 in a 10.0.0.3 + +a in a 1.2.3.4 diff --git a/lib/ns/tests/testdata/query/foo.db b/lib/ns/tests/testdata/query/foo.db new file mode 100644 index 0000000..5fb2c42 --- /dev/null +++ b/lib/ns/tests/testdata/query/foo.db @@ -0,0 +1,20 @@ +; 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. + +$TTL 3600 +@ IN SOA localhost. postmaster.localhost. ( + 1 ;serial + 3600 ;refresh + 1800 ;retry + 604800 ;expiration + 3600 ) ;minimum + IN NS ns +ns IN A 127.0.0.1 diff --git a/lib/ns/update.c b/lib/ns/update.c new file mode 100644 index 0000000..4e51cdc --- /dev/null +++ b/lib/ns/update.c @@ -0,0 +1,3696 @@ +/* + * 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 <inttypes.h> +#include <stdbool.h> + +#include <isc/netaddr.h> +#include <isc/print.h> +#include <isc/serial.h> +#include <isc/stats.h> +#include <isc/string.h> +#include <isc/taskpool.h> +#include <isc/util.h> + +#include <dns/db.h> +#include <dns/dbiterator.h> +#include <dns/diff.h> +#include <dns/dnssec.h> +#include <dns/events.h> +#include <dns/fixedname.h> +#include <dns/journal.h> +#include <dns/keyvalues.h> +#include <dns/message.h> +#include <dns/nsec.h> +#include <dns/nsec3.h> +#include <dns/private.h> +#include <dns/rdataclass.h> +#include <dns/rdataset.h> +#include <dns/rdatasetiter.h> +#include <dns/rdatastruct.h> +#include <dns/rdatatype.h> +#include <dns/soa.h> +#include <dns/ssu.h> +#include <dns/tsig.h> +#include <dns/update.h> +#include <dns/view.h> +#include <dns/zone.h> +#include <dns/zt.h> + +#include <ns/client.h> +#include <ns/interfacemgr.h> +#include <ns/log.h> +#include <ns/server.h> +#include <ns/stats.h> +#include <ns/update.h> + +/*! \file + * \brief + * This module implements dynamic update as in RFC2136. + */ + +/* + * XXX TODO: + * - document strict minimality + */ + +/**************************************************************************/ + +/*% + * Log level for tracing dynamic update protocol requests. + */ +#define LOGLEVEL_PROTOCOL ISC_LOG_INFO + +/*% + * Log level for low-level debug tracing. + */ +#define LOGLEVEL_DEBUG ISC_LOG_DEBUG(8) + +/*% + * Check an operation for failure. These macros all assume that + * the function using them has a 'result' variable and a 'failure' + * label. + */ +#define CHECK(op) \ + do { \ + result = (op); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +/*% + * Fail unconditionally with result 'code', which must not + * be ISC_R_SUCCESS. The reason for failure presumably has + * been logged already. + * + * The test against ISC_R_SUCCESS is there to keep the Solaris compiler + * from complaining about "end-of-loop code not reached". + */ + +#define FAIL(code) \ + do { \ + result = (code); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +/*% + * Fail unconditionally and log as a client error. + * The test against ISC_R_SUCCESS is there to keep the Solaris compiler + * from complaining about "end-of-loop code not reached". + */ +#define FAILC(code, msg) \ + do { \ + const char *_what = "failed"; \ + result = (code); \ + switch (result) { \ + case DNS_R_NXDOMAIN: \ + case DNS_R_YXDOMAIN: \ + case DNS_R_YXRRSET: \ + case DNS_R_NXRRSET: \ + _what = "unsuccessful"; \ + } \ + update_log(client, zone, LOGLEVEL_PROTOCOL, \ + "update %s: %s (%s)", _what, msg, \ + isc_result_totext(result)); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) +#define PREREQFAILC(code, msg) \ + do { \ + inc_stats(client, zone, ns_statscounter_updatebadprereq); \ + FAILC(code, msg); \ + } while (0) + +#define FAILN(code, name, msg) \ + do { \ + const char *_what = "failed"; \ + result = (code); \ + switch (result) { \ + case DNS_R_NXDOMAIN: \ + case DNS_R_YXDOMAIN: \ + case DNS_R_YXRRSET: \ + case DNS_R_NXRRSET: \ + _what = "unsuccessful"; \ + } \ + if (isc_log_wouldlog(ns_lctx, LOGLEVEL_PROTOCOL)) { \ + char _nbuf[DNS_NAME_FORMATSIZE]; \ + dns_name_format(name, _nbuf, sizeof(_nbuf)); \ + update_log(client, zone, LOGLEVEL_PROTOCOL, \ + "update %s: %s: %s (%s)", _what, _nbuf, \ + msg, isc_result_totext(result)); \ + } \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) +#define PREREQFAILN(code, name, msg) \ + do { \ + inc_stats(client, zone, ns_statscounter_updatebadprereq); \ + FAILN(code, name, msg); \ + } while (0) + +#define FAILNT(code, name, type, msg) \ + do { \ + const char *_what = "failed"; \ + result = (code); \ + switch (result) { \ + case DNS_R_NXDOMAIN: \ + case DNS_R_YXDOMAIN: \ + case DNS_R_YXRRSET: \ + case DNS_R_NXRRSET: \ + _what = "unsuccessful"; \ + } \ + if (isc_log_wouldlog(ns_lctx, LOGLEVEL_PROTOCOL)) { \ + char _nbuf[DNS_NAME_FORMATSIZE]; \ + char _tbuf[DNS_RDATATYPE_FORMATSIZE]; \ + dns_name_format(name, _nbuf, sizeof(_nbuf)); \ + dns_rdatatype_format(type, _tbuf, sizeof(_tbuf)); \ + update_log(client, zone, LOGLEVEL_PROTOCOL, \ + "update %s: %s/%s: %s (%s)", _what, _nbuf, \ + _tbuf, msg, isc_result_totext(result)); \ + } \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) +#define PREREQFAILNT(code, name, type, msg) \ + do { \ + inc_stats(client, zone, ns_statscounter_updatebadprereq); \ + FAILNT(code, name, type, msg); \ + } while (0) + +/*% + * Fail unconditionally and log as a server error. + * The test against ISC_R_SUCCESS is there to keep the Solaris compiler + * from complaining about "end-of-loop code not reached". + */ +#define FAILS(code, msg) \ + do { \ + result = (code); \ + update_log(client, zone, LOGLEVEL_PROTOCOL, "error: %s: %s", \ + msg, isc_result_totext(result)); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +/* + * Return TRUE if NS_CLIENTATTR_TCP is set in the attributes other FALSE. + */ +#define TCPCLIENT(client) (((client)->attributes & NS_CLIENTATTR_TCP) != 0) + +/**************************************************************************/ + +typedef struct rr rr_t; + +struct rr { + /* dns_name_t name; */ + uint32_t ttl; + dns_rdata_t rdata; +}; + +typedef struct update_event update_event_t; + +struct update_event { + ISC_EVENT_COMMON(update_event_t); + dns_zone_t *zone; + isc_result_t result; + dns_message_t *answer; +}; + +/*% + * Prepare an RR for the addition of the new RR 'ctx->update_rr', + * with TTL 'ctx->update_rr_ttl', to its rdataset, by deleting + * the RRs if it is replaced by the new RR or has a conflicting TTL. + * The necessary changes are appended to ctx->del_diff and ctx->add_diff; + * we need to do all deletions before any additions so that we don't run + * into transient states with conflicting TTLs. + */ + +typedef struct { + dns_db_t *db; + dns_dbversion_t *ver; + dns_diff_t *diff; + dns_name_t *name; + dns_name_t *oldname; + dns_rdata_t *update_rr; + dns_ttl_t update_rr_ttl; + bool ignore_add; + dns_diff_t del_diff; + dns_diff_t add_diff; +} add_rr_prepare_ctx_t; + +/**************************************************************************/ +/* + * Forward declarations. + */ + +static void +update_action(isc_task_t *task, isc_event_t *event); +static void +updatedone_action(isc_task_t *task, isc_event_t *event); +static isc_result_t +send_forward_event(ns_client_t *client, dns_zone_t *zone); +static void +forward_done(isc_task_t *task, isc_event_t *event); +static isc_result_t +add_rr_prepare_action(void *data, rr_t *rr); +static isc_result_t +rr_exists(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + const dns_rdata_t *rdata, bool *flag); + +/**************************************************************************/ + +static void +update_log(ns_client_t *client, dns_zone_t *zone, int level, const char *fmt, + ...) ISC_FORMAT_PRINTF(4, 5); + +static void +update_log(ns_client_t *client, dns_zone_t *zone, int level, const char *fmt, + ...) { + va_list ap; + char message[4096]; + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + + if (client == NULL) { + return; + } + + if (!isc_log_wouldlog(ns_lctx, level)) { + return; + } + + va_start(ap, fmt); + vsnprintf(message, sizeof(message), fmt, ap); + va_end(ap); + + if (zone != NULL) { + dns_name_format(dns_zone_getorigin(zone), namebuf, + sizeof(namebuf)); + dns_rdataclass_format(dns_zone_getclass(zone), classbuf, + sizeof(classbuf)); + + ns_client_log(client, NS_LOGCATEGORY_UPDATE, + NS_LOGMODULE_UPDATE, level, + "updating zone '%s/%s': %s", namebuf, classbuf, + message); + } else { + ns_client_log(client, NS_LOGCATEGORY_UPDATE, + NS_LOGMODULE_UPDATE, level, "%s", message); + } +} + +static void +update_log_cb(void *arg, dns_zone_t *zone, int level, const char *message) { + update_log(arg, zone, level, "%s", message); +} + +/*% + * Increment updated-related statistics counters. + */ +static void +inc_stats(ns_client_t *client, dns_zone_t *zone, isc_statscounter_t counter) { + ns_stats_increment(client->sctx->nsstats, counter); + + if (zone != NULL) { + isc_stats_t *zonestats = dns_zone_getrequeststats(zone); + if (zonestats != NULL) { + isc_stats_increment(zonestats, counter); + } + } +} + +/*% + * Check if we could have queried for the contents of this zone or + * if the zone is potentially updateable. + * If the zone can potentially be updated and the check failed then + * log a error otherwise we log a informational message. + */ +static isc_result_t +checkqueryacl(ns_client_t *client, dns_acl_t *queryacl, dns_name_t *zonename, + dns_acl_t *updateacl, dns_ssutable_t *ssutable) { + isc_result_t result; + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + bool update_possible = + ((updateacl != NULL && !dns_acl_isnone(updateacl)) || + ssutable != NULL); + + result = ns_client_checkaclsilent(client, NULL, queryacl, true); + if (result != ISC_R_SUCCESS) { + int level = update_possible ? ISC_LOG_ERROR : ISC_LOG_INFO; + + dns_name_format(zonename, namebuf, sizeof(namebuf)); + dns_rdataclass_format(client->view->rdclass, classbuf, + sizeof(classbuf)); + + ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, + NS_LOGMODULE_UPDATE, level, + "update '%s/%s' denied due to allow-query", + namebuf, classbuf); + } else if (!update_possible) { + dns_name_format(zonename, namebuf, sizeof(namebuf)); + dns_rdataclass_format(client->view->rdclass, classbuf, + sizeof(classbuf)); + + result = DNS_R_REFUSED; + ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, + NS_LOGMODULE_UPDATE, ISC_LOG_INFO, + "update '%s/%s' denied", namebuf, classbuf); + } + return (result); +} + +/*% + * Override the default acl logging when checking whether a client + * can update the zone or whether we can forward the request to the + * master based on IP address. + * + * 'message' contains the type of operation that is being attempted. + * 'slave' indicates if this is a slave zone. If 'acl' is NULL then + * log at debug=3. + * If the zone has no access controls configured ('acl' == NULL && + * 'has_ssutable == ISC_FALS) log the attempt at info, otherwise + * at error. + * + * If the request was signed log that we received it. + */ +static isc_result_t +checkupdateacl(ns_client_t *client, dns_acl_t *acl, const char *message, + dns_name_t *zonename, bool slave, bool has_ssutable) { + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + int level = ISC_LOG_ERROR; + const char *msg = "denied"; + isc_result_t result; + + if (slave && acl == NULL) { + result = DNS_R_NOTIMP; + level = ISC_LOG_DEBUG(3); + msg = "disabled"; + } else { + result = ns_client_checkaclsilent(client, NULL, acl, false); + if (result == ISC_R_SUCCESS) { + level = ISC_LOG_DEBUG(3); + msg = "approved"; + } else if (acl == NULL && !has_ssutable) { + level = ISC_LOG_INFO; + } + } + + if (client->signer != NULL) { + dns_name_format(client->signer, namebuf, sizeof(namebuf)); + ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, + NS_LOGMODULE_UPDATE, ISC_LOG_INFO, + "signer \"%s\" %s", namebuf, msg); + } + + dns_name_format(zonename, namebuf, sizeof(namebuf)); + dns_rdataclass_format(client->view->rdclass, classbuf, + sizeof(classbuf)); + + ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, + NS_LOGMODULE_UPDATE, level, "%s '%s/%s' %s", message, + namebuf, classbuf, msg); + return (result); +} + +/*% + * Update a single RR in version 'ver' of 'db' and log the + * update in 'diff'. + * + * Ensures: + * \li '*tuple' == NULL. Either the tuple is freed, or its + * ownership has been transferred to the diff. + */ +static isc_result_t +do_one_tuple(dns_difftuple_t **tuple, dns_db_t *db, dns_dbversion_t *ver, + dns_diff_t *diff) { + dns_diff_t temp_diff; + isc_result_t result; + + /* + * Create a singleton diff. + */ + dns_diff_init(diff->mctx, &temp_diff); + ISC_LIST_APPEND(temp_diff.tuples, *tuple, link); + + /* + * Apply it to the database. + */ + result = dns_diff_apply(&temp_diff, db, ver); + ISC_LIST_UNLINK(temp_diff.tuples, *tuple, link); + if (result != ISC_R_SUCCESS) { + dns_difftuple_free(tuple); + return (result); + } + + /* + * Merge it into the current pending journal entry. + */ + dns_diff_appendminimal(diff, tuple); + + /* + * Do not clear temp_diff. + */ + return (ISC_R_SUCCESS); +} + +/*% + * Perform the updates in 'updates' in version 'ver' of 'db' and log the + * update in 'diff'. + * + * Ensures: + * \li 'updates' is empty. + */ +static isc_result_t +do_diff(dns_diff_t *updates, dns_db_t *db, dns_dbversion_t *ver, + dns_diff_t *diff) { + isc_result_t result; + while (!ISC_LIST_EMPTY(updates->tuples)) { + dns_difftuple_t *t = ISC_LIST_HEAD(updates->tuples); + ISC_LIST_UNLINK(updates->tuples, t, link); + CHECK(do_one_tuple(&t, db, ver, diff)); + } + return (ISC_R_SUCCESS); + +failure: + dns_diff_clear(diff); + return (result); +} + +static isc_result_t +update_one_rr(dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff, + dns_diffop_t op, dns_name_t *name, dns_ttl_t ttl, + dns_rdata_t *rdata) { + dns_difftuple_t *tuple = NULL; + isc_result_t result; + result = dns_difftuple_create(diff->mctx, op, name, ttl, rdata, &tuple); + if (result != ISC_R_SUCCESS) { + return (result); + } + return (do_one_tuple(&tuple, db, ver, diff)); +} + +/**************************************************************************/ +/* + * Callback-style iteration over rdatasets and rdatas. + * + * foreach_rrset() can be used to iterate over the RRsets + * of a name and call a callback function with each + * one. Similarly, foreach_rr() can be used to iterate + * over the individual RRs at name, optionally restricted + * to RRs of a given type. + * + * The callback functions are called "actions" and take + * two arguments: a void pointer for passing arbitrary + * context information, and a pointer to the current RRset + * or RR. By convention, their names end in "_action". + */ + +/* + * XXXRTH We might want to make this public somewhere in libdns. + */ + +/*% + * Function type for foreach_rrset() iterator actions. + */ +typedef isc_result_t +rrset_func(void *data, dns_rdataset_t *rrset); + +/*% + * Function type for foreach_rr() iterator actions. + */ +typedef isc_result_t +rr_func(void *data, rr_t *rr); + +/*% + * Internal context struct for foreach_node_rr(). + */ +typedef struct { + rr_func *rr_action; + void *rr_action_data; +} foreach_node_rr_ctx_t; + +/*% + * Internal helper function for foreach_node_rr(). + */ +static isc_result_t +foreach_node_rr_action(void *data, dns_rdataset_t *rdataset) { + isc_result_t result; + foreach_node_rr_ctx_t *ctx = data; + for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + rr_t rr = { 0, DNS_RDATA_INIT }; + + dns_rdataset_current(rdataset, &rr.rdata); + rr.ttl = rdataset->ttl; + result = (*ctx->rr_action)(ctx->rr_action_data, &rr); + if (result != ISC_R_SUCCESS) { + return (result); + } + } + if (result != ISC_R_NOMORE) { + return (result); + } + return (ISC_R_SUCCESS); +} + +/*% + * For each rdataset of 'name' in 'ver' of 'db', call 'action' + * with the rdataset and 'action_data' as arguments. If the name + * does not exist, do nothing. + * + * If 'action' returns an error, abort iteration and return the error. + */ +static isc_result_t +foreach_rrset(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + rrset_func *action, void *action_data) { + isc_result_t result; + dns_dbnode_t *node; + dns_rdatasetiter_t *iter; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_dbversion_t *oldver = NULL; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + + /* + * Only set the clientinfo 'versionp' if the new version is + * different from the current version + */ + dns_db_currentversion(db, &oldver); + dns_clientinfo_init(&ci, NULL, NULL, (ver != oldver) ? ver : NULL); + dns_db_closeversion(db, &oldver, false); + + node = NULL; + result = dns_db_findnodeext(db, name, false, &cm, &ci, &node); + if (result == ISC_R_NOTFOUND) { + return (ISC_R_SUCCESS); + } + if (result != ISC_R_SUCCESS) { + return (result); + } + + iter = NULL; + result = dns_db_allrdatasets(db, node, ver, 0, (isc_stdtime_t)0, &iter); + if (result != ISC_R_SUCCESS) { + goto cleanup_node; + } + + for (result = dns_rdatasetiter_first(iter); result == ISC_R_SUCCESS; + result = dns_rdatasetiter_next(iter)) + { + dns_rdataset_t rdataset; + + dns_rdataset_init(&rdataset); + dns_rdatasetiter_current(iter, &rdataset); + + result = (*action)(action_data, &rdataset); + + dns_rdataset_disassociate(&rdataset); + if (result != ISC_R_SUCCESS) { + goto cleanup_iterator; + } + } + if (result == ISC_R_NOMORE) { + result = ISC_R_SUCCESS; + } + +cleanup_iterator: + dns_rdatasetiter_destroy(&iter); + +cleanup_node: + dns_db_detachnode(db, &node); + + return (result); +} + +/*% + * For each RR of 'name' in 'ver' of 'db', call 'action' + * with the RR and 'action_data' as arguments. If the name + * does not exist, do nothing. + * + * If 'action' returns an error, abort iteration + * and return the error. + */ +static isc_result_t +foreach_node_rr(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + rr_func *rr_action, void *rr_action_data) { + foreach_node_rr_ctx_t ctx; + ctx.rr_action = rr_action; + ctx.rr_action_data = rr_action_data; + return (foreach_rrset(db, ver, name, foreach_node_rr_action, &ctx)); +} + +/*% + * For each of the RRs specified by 'db', 'ver', 'name', 'type', + * (which can be dns_rdatatype_any to match any type), and 'covers', call + * 'action' with the RR and 'action_data' as arguments. If the name + * does not exist, or if no RRset of the given type exists at the name, + * do nothing. + * + * If 'action' returns an error, abort iteration and return the error. + */ +static isc_result_t +foreach_rr(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + dns_rdatatype_t type, dns_rdatatype_t covers, rr_func *rr_action, + void *rr_action_data) { + isc_result_t result; + dns_dbnode_t *node; + dns_rdataset_t rdataset; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; + dns_dbversion_t *oldver = NULL; + dns_fixedname_t fixed; + + dns_clientinfomethods_init(&cm, ns_client_sourceip); + + /* + * Only set the clientinfo 'versionp' if the new version is + * different from the current version + */ + dns_db_currentversion(db, &oldver); + dns_clientinfo_init(&ci, NULL, NULL, (ver != oldver) ? ver : NULL); + dns_db_closeversion(db, &oldver, false); + + if (type == dns_rdatatype_any) { + return (foreach_node_rr(db, ver, name, rr_action, + rr_action_data)); + } + + node = NULL; + if (type == dns_rdatatype_nsec3 || + (type == dns_rdatatype_rrsig && covers == dns_rdatatype_nsec3)) + { + result = dns_db_findnsec3node(db, name, false, &node); + } else { + result = dns_db_findnodeext(db, name, false, &cm, &ci, &node); + } + if (result == ISC_R_NOTFOUND) { + return (ISC_R_SUCCESS); + } + if (result != ISC_R_SUCCESS) { + return (result); + } + + dns_rdataset_init(&rdataset); + result = dns_db_findrdataset(db, node, ver, type, covers, + (isc_stdtime_t)0, &rdataset, NULL); + if (result == ISC_R_NOTFOUND) { + result = ISC_R_SUCCESS; + goto cleanup_node; + } + if (result != ISC_R_SUCCESS) { + goto cleanup_node; + } + + if (rr_action == add_rr_prepare_action) { + add_rr_prepare_ctx_t *ctx = rr_action_data; + + ctx->oldname = dns_fixedname_initname(&fixed); + dns_name_copynf(name, ctx->oldname); + dns_rdataset_getownercase(&rdataset, ctx->oldname); + } + + for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + rr_t rr = { 0, DNS_RDATA_INIT }; + dns_rdataset_current(&rdataset, &rr.rdata); + rr.ttl = rdataset.ttl; + result = (*rr_action)(rr_action_data, &rr); + if (result != ISC_R_SUCCESS) { + goto cleanup_rdataset; + } + } + if (result != ISC_R_NOMORE) { + goto cleanup_rdataset; + } + result = ISC_R_SUCCESS; + +cleanup_rdataset: + dns_rdataset_disassociate(&rdataset); +cleanup_node: + dns_db_detachnode(db, &node); + + return (result); +} + +/**************************************************************************/ +/* + * Various tests on the database contents (for prerequisites, etc). + */ + +/*% + * Function type for predicate functions that compare a database RR 'db_rr' + * against an update RR 'update_rr'. + */ +typedef bool +rr_predicate(dns_rdata_t *update_rr, dns_rdata_t *db_rr); + +/*% + * Helper function for rrset_exists(). + */ +static isc_result_t +rrset_exists_action(void *data, rr_t *rr) { + UNUSED(data); + UNUSED(rr); + return (ISC_R_EXISTS); +} + +/*% + * Utility macro for RR existence checking functions. + * + * If the variable 'result' has the value ISC_R_EXISTS or + * ISC_R_SUCCESS, set *exists to true or false, + * respectively, and return success. + * + * If 'result' has any other value, there was a failure. + * Return the failure result code and do not set *exists. + * + * This would be more readable as "do { if ... } while(0)", + * but that form generates tons of warnings on Solaris 2.6. + */ +#define RETURN_EXISTENCE_FLAG \ + return ((result == ISC_R_EXISTS) \ + ? (*exists = true, ISC_R_SUCCESS) \ + : ((result == ISC_R_SUCCESS) \ + ? (*exists = false, ISC_R_SUCCESS) \ + : result)) + +/*% + * Set '*exists' to true iff an rrset of the given type exists, + * to false otherwise. + */ +static isc_result_t +rrset_exists(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + dns_rdatatype_t type, dns_rdatatype_t covers, bool *exists) { + isc_result_t result; + result = foreach_rr(db, ver, name, type, covers, rrset_exists_action, + NULL); + RETURN_EXISTENCE_FLAG; +} + +/*% + * Helper function for cname_incompatible_rrset_exists. + */ +static isc_result_t +cname_compatibility_action(void *data, dns_rdataset_t *rrset) { + UNUSED(data); + if (rrset->type != dns_rdatatype_cname && + !dns_rdatatype_atcname(rrset->type)) + { + return (ISC_R_EXISTS); + } + return (ISC_R_SUCCESS); +} + +/*% + * Check whether there is an rrset incompatible with adding a CNAME RR, + * i.e., anything but another CNAME (which can be replaced) or a + * DNSSEC RR (which can coexist). + * + * If such an incompatible rrset exists, set '*exists' to true. + * Otherwise, set it to false. + */ +static isc_result_t +cname_incompatible_rrset_exists(dns_db_t *db, dns_dbversion_t *ver, + dns_name_t *name, bool *exists) { + isc_result_t result; + result = foreach_rrset(db, ver, name, cname_compatibility_action, NULL); + RETURN_EXISTENCE_FLAG; +} + +/*% + * Helper function for rr_count(). + */ +static isc_result_t +count_rr_action(void *data, rr_t *rr) { + int *countp = data; + UNUSED(rr); + (*countp)++; + return (ISC_R_SUCCESS); +} + +/*% + * Count the number of RRs of 'type' belonging to 'name' in 'ver' of 'db'. + */ +static isc_result_t +rr_count(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + dns_rdatatype_t type, dns_rdatatype_t covers, int *countp) { + *countp = 0; + return (foreach_rr(db, ver, name, type, covers, count_rr_action, + countp)); +} + +/*% + * Context struct and helper function for name_exists(). + */ + +static isc_result_t +name_exists_action(void *data, dns_rdataset_t *rrset) { + UNUSED(data); + UNUSED(rrset); + return (ISC_R_EXISTS); +} + +/*% + * Set '*exists' to true iff the given name exists, to false otherwise. + */ +static isc_result_t +name_exists(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + bool *exists) { + isc_result_t result; + result = foreach_rrset(db, ver, name, name_exists_action, NULL); + RETURN_EXISTENCE_FLAG; +} + +/* + * 'ssu_check_t' is used to pass the arguments to + * dns_ssutable_checkrules() to the callback function + * ssu_checkrule(). + */ +typedef struct { + /* The ownername of the record to be updated. */ + dns_name_t *name; + + /* The signature's name if the request was signed. */ + dns_name_t *signer; + + /* The address of the client. */ + isc_netaddr_t *addr; + + /* The ACL environment */ + dns_aclenv_t *aclenv; + + /* Whether the request was sent via TCP. */ + bool tcp; + + /* The ssu table to check against. */ + dns_ssutable_t *table; + + /* the key used for TKEY requests */ + dst_key_t *key; +} ssu_check_t; + +static isc_result_t +ssu_checkrule(void *data, dns_rdataset_t *rrset) { + ssu_check_t *ssuinfo = data; + bool rule_ok; + + /* + * If we're deleting all records, it's ok to delete RRSIG and NSEC even + * if we're normally not allowed to. + */ + if (rrset->type == dns_rdatatype_rrsig || + rrset->type == dns_rdatatype_nsec) + { + return (ISC_R_SUCCESS); + } + + rule_ok = dns_ssutable_checkrules( + ssuinfo->table, ssuinfo->signer, ssuinfo->name, ssuinfo->addr, + ssuinfo->tcp, ssuinfo->aclenv, rrset->type, ssuinfo->key); + return (rule_ok ? ISC_R_SUCCESS : ISC_R_FAILURE); +} + +static bool +ssu_checkall(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + dns_ssutable_t *ssutable, dns_name_t *signer, isc_netaddr_t *addr, + dns_aclenv_t *aclenv, bool tcp, dst_key_t *key) { + isc_result_t result; + ssu_check_t ssuinfo; + + ssuinfo.name = name; + ssuinfo.table = ssutable; + ssuinfo.signer = signer; + ssuinfo.addr = addr; + ssuinfo.aclenv = aclenv; + ssuinfo.tcp = tcp; + ssuinfo.key = key; + result = foreach_rrset(db, ver, name, ssu_checkrule, &ssuinfo); + return (result == ISC_R_SUCCESS); +} + +/**************************************************************************/ +/* + * Checking of "RRset exists (value dependent)" prerequisites. + * + * In the RFC2136 section 3.2.5, this is the pseudocode involving + * a variable called "temp", a mapping of <name, type> tuples to rrsets. + * + * Here, we represent the "temp" data structure as (non-minimal) "dns_diff_t" + * where each tuple has op==DNS_DIFFOP_EXISTS. + */ + +/*% + * Append a tuple asserting the existence of the RR with + * 'name' and 'rdata' to 'diff'. + */ +static isc_result_t +temp_append(dns_diff_t *diff, dns_name_t *name, dns_rdata_t *rdata) { + isc_result_t result; + dns_difftuple_t *tuple = NULL; + + REQUIRE(DNS_DIFF_VALID(diff)); + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_EXISTS, name, 0, + rdata, &tuple)); + ISC_LIST_APPEND(diff->tuples, tuple, link); +failure: + return (result); +} + +/*% + * Compare two rdatasets represented as sorted lists of tuples. + * All list elements must have the same owner name and type. + * Return ISC_R_SUCCESS if the rdatasets are equal, rcode(dns_rcode_nxrrset) + * if not. + */ +static isc_result_t +temp_check_rrset(dns_difftuple_t *a, dns_difftuple_t *b) { + for (;;) { + if (a == NULL || b == NULL) { + break; + } + INSIST(a->op == DNS_DIFFOP_EXISTS && + b->op == DNS_DIFFOP_EXISTS); + INSIST(a->rdata.type == b->rdata.type); + INSIST(dns_name_equal(&a->name, &b->name)); + if (dns_rdata_casecompare(&a->rdata, &b->rdata) != 0) { + return (DNS_R_NXRRSET); + } + a = ISC_LIST_NEXT(a, link); + b = ISC_LIST_NEXT(b, link); + } + if (a != NULL || b != NULL) { + return (DNS_R_NXRRSET); + } + return (ISC_R_SUCCESS); +} + +/*% + * A comparison function defining the sorting order for the entries + * in the "temp" data structure. The major sort key is the owner name, + * followed by the type and rdata. + */ +static int +temp_order(const void *av, const void *bv) { + dns_difftuple_t const *const *ap = av; + dns_difftuple_t const *const *bp = bv; + dns_difftuple_t const *a = *ap; + dns_difftuple_t const *b = *bp; + int r; + r = dns_name_compare(&a->name, &b->name); + if (r != 0) { + return (r); + } + r = (b->rdata.type - a->rdata.type); + if (r != 0) { + return (r); + } + r = dns_rdata_casecompare(&a->rdata, &b->rdata); + return (r); +} + +/*% + * Check the "RRset exists (value dependent)" prerequisite information + * in 'temp' against the contents of the database 'db'. + * + * Return ISC_R_SUCCESS if the prerequisites are satisfied, + * rcode(dns_rcode_nxrrset) if not. + * + * 'temp' must be pre-sorted. + */ + +static isc_result_t +temp_check(isc_mem_t *mctx, dns_diff_t *temp, dns_db_t *db, + dns_dbversion_t *ver, dns_name_t *tmpname, dns_rdatatype_t *typep) { + isc_result_t result; + dns_name_t *name; + dns_dbnode_t *node; + dns_difftuple_t *t; + dns_diff_t trash; + + dns_diff_init(mctx, &trash); + + /* + * For each name and type in the prerequisites, + * construct a sorted rdata list of the corresponding + * database contents, and compare the lists. + */ + t = ISC_LIST_HEAD(temp->tuples); + while (t != NULL) { + name = &t->name; + dns_name_copynf(name, tmpname); + *typep = t->rdata.type; + + /* A new unique name begins here. */ + node = NULL; + result = dns_db_findnode(db, name, false, &node); + if (result == ISC_R_NOTFOUND) { + dns_diff_clear(&trash); + return (DNS_R_NXRRSET); + } + if (result != ISC_R_SUCCESS) { + dns_diff_clear(&trash); + return (result); + } + + /* A new unique type begins here. */ + while (t != NULL && dns_name_equal(&t->name, name)) { + dns_rdatatype_t type, covers; + dns_rdataset_t rdataset; + dns_diff_t d_rrs; /* Database RRs with + * this name and type */ + dns_diff_t u_rrs; /* Update RRs with + * this name and type */ + + *typep = type = t->rdata.type; + if (type == dns_rdatatype_rrsig || + type == dns_rdatatype_sig) + { + covers = dns_rdata_covers(&t->rdata); + } else if (type == dns_rdatatype_any) { + dns_db_detachnode(db, &node); + dns_diff_clear(&trash); + return (DNS_R_NXRRSET); + } else { + covers = 0; + } + + /* + * Collect all database RRs for this name and type + * onto d_rrs and sort them. + */ + dns_rdataset_init(&rdataset); + result = dns_db_findrdataset(db, node, ver, type, + covers, (isc_stdtime_t)0, + &rdataset, NULL); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(db, &node); + dns_diff_clear(&trash); + return (DNS_R_NXRRSET); + } + + dns_diff_init(mctx, &d_rrs); + dns_diff_init(mctx, &u_rrs); + + for (result = dns_rdataset_first(&rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(&rdataset, &rdata); + result = temp_append(&d_rrs, name, &rdata); + if (result != ISC_R_SUCCESS) { + goto failure; + } + } + if (result != ISC_R_NOMORE) { + goto failure; + } + result = dns_diff_sort(&d_rrs, temp_order); + if (result != ISC_R_SUCCESS) { + goto failure; + } + + /* + * Collect all update RRs for this name and type + * onto u_rrs. No need to sort them here - + * they are already sorted. + */ + while (t != NULL && dns_name_equal(&t->name, name) && + t->rdata.type == type) + { + dns_difftuple_t *next = ISC_LIST_NEXT(t, link); + ISC_LIST_UNLINK(temp->tuples, t, link); + ISC_LIST_APPEND(u_rrs.tuples, t, link); + t = next; + } + + /* Compare the two sorted lists. */ + result = temp_check_rrset(ISC_LIST_HEAD(u_rrs.tuples), + ISC_LIST_HEAD(d_rrs.tuples)); + if (result != ISC_R_SUCCESS) { + goto failure; + } + + /* + * We are done with the tuples, but we can't free + * them yet because "name" still points into one + * of them. Move them on a temporary list. + */ + ISC_LIST_APPENDLIST(trash.tuples, u_rrs.tuples, link); + ISC_LIST_APPENDLIST(trash.tuples, d_rrs.tuples, link); + dns_rdataset_disassociate(&rdataset); + + continue; + + failure: + dns_diff_clear(&d_rrs); + dns_diff_clear(&u_rrs); + dns_diff_clear(&trash); + dns_rdataset_disassociate(&rdataset); + dns_db_detachnode(db, &node); + return (result); + } + + dns_db_detachnode(db, &node); + } + + dns_diff_clear(&trash); + return (ISC_R_SUCCESS); +} + +/**************************************************************************/ +/* + * Conditional deletion of RRs. + */ + +/*% + * Context structure for delete_if(). + */ + +typedef struct { + rr_predicate *predicate; + dns_db_t *db; + dns_dbversion_t *ver; + dns_diff_t *diff; + dns_name_t *name; + dns_rdata_t *update_rr; +} conditional_delete_ctx_t; + +/*% + * Predicate functions for delete_if(). + */ + +/*% + * Return true iff 'db_rr' is neither a SOA nor an NS RR nor + * an RRSIG nor an NSEC3PARAM nor a NSEC. + */ +static bool +type_not_soa_nor_ns_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { + UNUSED(update_rr); + return ((db_rr->type != dns_rdatatype_soa && + db_rr->type != dns_rdatatype_ns && + db_rr->type != dns_rdatatype_nsec3param && + db_rr->type != dns_rdatatype_rrsig && + db_rr->type != dns_rdatatype_nsec) + ? true + : false); +} + +/*% + * Return true iff 'db_rr' is neither a RRSIG nor a NSEC. + */ +static bool +type_not_dnssec(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { + UNUSED(update_rr); + return ((db_rr->type != dns_rdatatype_rrsig && + db_rr->type != dns_rdatatype_nsec) + ? true + : false); +} + +/*% + * Return true always. + */ +static bool +true_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { + UNUSED(update_rr); + UNUSED(db_rr); + return (true); +} + +/*% + * Return true iff the two RRs have identical rdata. + */ +static bool +rr_equal_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { + /* + * XXXRTH This is not a problem, but we should consider creating + * dns_rdata_equal() (that used dns_name_equal()), since it + * would be faster. Not a priority. + */ + return (dns_rdata_casecompare(update_rr, db_rr) == 0 ? true : false); +} + +/*% + * Return true iff 'update_rr' should replace 'db_rr' according + * to the special RFC2136 rules for CNAME, SOA, and WKS records. + * + * RFC2136 does not mention NSEC or DNAME, but multiple NSECs or DNAMEs + * make little sense, so we replace those, too. + * + * Additionally replace RRSIG that have been generated by the same key + * for the same type. This simplifies refreshing a offline KSK by not + * requiring that the old RRSIG be deleted. It also simplifies key + * rollover by only requiring that the new RRSIG be added. + */ +static bool +replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { + dns_rdata_rrsig_t updatesig, dbsig; + isc_result_t result; + + if (db_rr->type != update_rr->type) { + return (false); + } + if (db_rr->type == dns_rdatatype_cname) { + return (true); + } + if (db_rr->type == dns_rdatatype_dname) { + return (true); + } + if (db_rr->type == dns_rdatatype_soa) { + return (true); + } + if (db_rr->type == dns_rdatatype_nsec) { + return (true); + } + if (db_rr->type == dns_rdatatype_rrsig) { + /* + * Replace existing RRSIG with the same keyid, + * covered and algorithm. + */ + result = dns_rdata_tostruct(db_rr, &dbsig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + result = dns_rdata_tostruct(update_rr, &updatesig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (dbsig.keyid == updatesig.keyid && + dbsig.covered == updatesig.covered && + dbsig.algorithm == updatesig.algorithm) + { + return (true); + } + } + if (db_rr->type == dns_rdatatype_wks) { + /* + * Compare the address and protocol fields only. These + * form the first five bytes of the RR data. Do a + * raw binary comparison; unpacking the WKS RRs using + * dns_rdata_tostruct() might be cleaner in some ways. + */ + INSIST(db_rr->length >= 5 && update_rr->length >= 5); + return (memcmp(db_rr->data, update_rr->data, 5) == 0 ? true + : false); + } + + if (db_rr->type == dns_rdatatype_nsec3param) { + if (db_rr->length != update_rr->length) { + return (false); + } + INSIST(db_rr->length >= 4 && update_rr->length >= 4); + /* + * Replace NSEC3PARAM records that only differ by the + * flags field. + */ + if (db_rr->data[0] == update_rr->data[0] && + memcmp(db_rr->data + 2, update_rr->data + 2, + update_rr->length - 2) == 0) + { + return (true); + } + } + return (false); +} + +/*% + * Internal helper function for delete_if(). + */ +static isc_result_t +delete_if_action(void *data, rr_t *rr) { + conditional_delete_ctx_t *ctx = data; + if ((*ctx->predicate)(ctx->update_rr, &rr->rdata)) { + isc_result_t result; + result = update_one_rr(ctx->db, ctx->ver, ctx->diff, + DNS_DIFFOP_DEL, ctx->name, rr->ttl, + &rr->rdata); + return (result); + } else { + return (ISC_R_SUCCESS); + } +} + +/*% + * Conditionally delete RRs. Apply 'predicate' to the RRs + * specified by 'db', 'ver', 'name', and 'type' (which can + * be dns_rdatatype_any to match any type). Delete those + * RRs for which the predicate returns true, and log the + * deletions in 'diff'. + */ +static isc_result_t +delete_if(rr_predicate *predicate, dns_db_t *db, dns_dbversion_t *ver, + dns_name_t *name, dns_rdatatype_t type, dns_rdatatype_t covers, + dns_rdata_t *update_rr, dns_diff_t *diff) { + conditional_delete_ctx_t ctx; + ctx.predicate = predicate; + ctx.db = db; + ctx.ver = ver; + ctx.diff = diff; + ctx.name = name; + ctx.update_rr = update_rr; + return (foreach_rr(db, ver, name, type, covers, delete_if_action, + &ctx)); +} + +/**************************************************************************/ + +static isc_result_t +add_rr_prepare_action(void *data, rr_t *rr) { + isc_result_t result = ISC_R_SUCCESS; + add_rr_prepare_ctx_t *ctx = data; + dns_difftuple_t *tuple = NULL; + bool equal, case_equal, ttl_equal; + + /* + * Are the new and old cases equal? + */ + case_equal = dns_name_caseequal(ctx->name, ctx->oldname); + + /* + * Are the ttl's equal? + */ + ttl_equal = rr->ttl == ctx->update_rr_ttl; + + /* + * If the update RR is a "duplicate" of a existing RR, + * the update should be silently ignored. + */ + equal = (dns_rdata_casecompare(&rr->rdata, ctx->update_rr) == 0); + if (equal && case_equal && ttl_equal) { + ctx->ignore_add = true; + return (ISC_R_SUCCESS); + } + + /* + * If this RR is "equal" to the update RR, it should + * be deleted before the update RR is added. + */ + if (replaces_p(ctx->update_rr, &rr->rdata)) { + CHECK(dns_difftuple_create(ctx->del_diff.mctx, DNS_DIFFOP_DEL, + ctx->oldname, rr->ttl, &rr->rdata, + &tuple)); + dns_diff_append(&ctx->del_diff, &tuple); + return (ISC_R_SUCCESS); + } + + /* + * If this RR differs in TTL or case from the update RR, + * its TTL and case must be adjusted. + */ + if (!ttl_equal || !case_equal) { + CHECK(dns_difftuple_create(ctx->del_diff.mctx, DNS_DIFFOP_DEL, + ctx->oldname, rr->ttl, &rr->rdata, + &tuple)); + dns_diff_append(&ctx->del_diff, &tuple); + if (!equal) { + CHECK(dns_difftuple_create( + ctx->add_diff.mctx, DNS_DIFFOP_ADD, ctx->name, + ctx->update_rr_ttl, &rr->rdata, &tuple)); + dns_diff_append(&ctx->add_diff, &tuple); + } + } +failure: + return (result); +} + +/**************************************************************************/ +/* + * Miscellaneous subroutines. + */ + +/*% + * Extract a single update RR from 'section' of dynamic update message + * 'msg', with consistency checking. + * + * Stores the owner name, rdata, and TTL of the update RR at 'name', + * 'rdata', and 'ttl', respectively. + */ +static void +get_current_rr(dns_message_t *msg, dns_section_t section, + dns_rdataclass_t zoneclass, dns_name_t **name, + dns_rdata_t *rdata, dns_rdatatype_t *covers, dns_ttl_t *ttl, + dns_rdataclass_t *update_class) { + dns_rdataset_t *rdataset; + isc_result_t result; + dns_message_currentname(msg, section, name); + rdataset = ISC_LIST_HEAD((*name)->list); + INSIST(rdataset != NULL); + INSIST(ISC_LIST_NEXT(rdataset, link) == NULL); + *covers = rdataset->covers; + *ttl = rdataset->ttl; + result = dns_rdataset_first(rdataset); + INSIST(result == ISC_R_SUCCESS); + dns_rdataset_current(rdataset, rdata); + INSIST(dns_rdataset_next(rdataset) == ISC_R_NOMORE); + *update_class = rdata->rdclass; + rdata->rdclass = zoneclass; +} + +/*% + * Increment the SOA serial number of database 'db', version 'ver'. + * Replace the SOA record in the database, and log the + * change in 'diff'. + */ + +/* + * XXXRTH Failures in this routine will be worth logging, when + * we have a logging system. Failure to find the zonename + * or the SOA rdataset warrant at least an UNEXPECTED_ERROR(). + */ + +static isc_result_t +update_soa_serial(dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff, + isc_mem_t *mctx, dns_updatemethod_t method) { + dns_difftuple_t *deltuple = NULL; + dns_difftuple_t *addtuple = NULL; + uint32_t serial; + isc_result_t result; + + CHECK(dns_db_createsoatuple(db, ver, mctx, DNS_DIFFOP_DEL, &deltuple)); + CHECK(dns_difftuple_copy(deltuple, &addtuple)); + addtuple->op = DNS_DIFFOP_ADD; + + serial = dns_soa_getserial(&addtuple->rdata); + serial = dns_update_soaserial(serial, method, NULL); + dns_soa_setserial(serial, &addtuple->rdata); + CHECK(do_one_tuple(&deltuple, db, ver, diff)); + CHECK(do_one_tuple(&addtuple, db, ver, diff)); + result = ISC_R_SUCCESS; + +failure: + if (addtuple != NULL) { + dns_difftuple_free(&addtuple); + } + if (deltuple != NULL) { + dns_difftuple_free(&deltuple); + } + return (result); +} + +/*% + * Check that the new SOA record at 'update_rdata' does not + * illegally cause the SOA serial number to decrease or stay + * unchanged relative to the existing SOA in 'db'. + * + * Sets '*ok' to true if the update is legal, false if not. + * + * William King points out that RFC2136 is inconsistent about + * the case where the serial number stays unchanged: + * + * section 3.4.2.2 requires a server to ignore a SOA update request + * if the serial number on the update SOA is less_than_or_equal to + * the zone SOA serial. + * + * section 3.6 requires a server to ignore a SOA update request if + * the serial is less_than the zone SOA serial. + * + * Paul says 3.4.2.2 is correct. + * + */ +static isc_result_t +check_soa_increment(dns_db_t *db, dns_dbversion_t *ver, + dns_rdata_t *update_rdata, bool *ok) { + uint32_t db_serial; + uint32_t update_serial; + isc_result_t result; + + update_serial = dns_soa_getserial(update_rdata); + + result = dns_db_getsoaserial(db, ver, &db_serial); + if (result != ISC_R_SUCCESS) { + return (result); + } + + if (DNS_SERIAL_GE(db_serial, update_serial)) { + *ok = false; + } else { + *ok = true; + } + + return (ISC_R_SUCCESS); +} + +/**************************************************************************/ +/*% + * The actual update code in all its glory. We try to follow + * the RFC2136 pseudocode as closely as possible. + */ + +static isc_result_t +send_update_event(ns_client_t *client, dns_zone_t *zone) { + isc_result_t result = ISC_R_SUCCESS; + update_event_t *event = NULL; + isc_task_t *zonetask = NULL; + dns_ssutable_t *ssutable = NULL; + dns_message_t *request = client->message; + dns_aclenv_t *env = + ns_interfacemgr_getaclenv(client->manager->interface->mgr); + dns_rdataclass_t zoneclass; + dns_rdatatype_t covers; + dns_name_t *zonename = NULL; + dns_db_t *db = NULL; + dns_dbversion_t *ver = NULL; + + CHECK(dns_zone_getdb(zone, &db)); + zonename = dns_db_origin(db); + zoneclass = dns_db_class(db); + dns_zone_getssutable(zone, &ssutable); + dns_db_currentversion(db, &ver); + + /* + * Update message processing can leak record existence information + * so check that we are allowed to query this zone. Additionally, + * if we would refuse all updates for this zone, we bail out here. + */ + CHECK(checkqueryacl(client, dns_zone_getqueryacl(zone), + dns_zone_getorigin(zone), + dns_zone_getupdateacl(zone), ssutable)); + + /* + * Check requestor's permissions. + */ + if (ssutable == NULL) { + CHECK(checkupdateacl(client, dns_zone_getupdateacl(zone), + "update", dns_zone_getorigin(zone), false, + false)); + } else if (client->signer == NULL && !TCPCLIENT(client)) { + CHECK(checkupdateacl(client, NULL, "update", + dns_zone_getorigin(zone), false, true)); + } + + if (dns_zone_getupdatedisabled(zone)) { + FAILC(DNS_R_REFUSED, "dynamic update temporarily disabled " + "because the zone is frozen. Use " + "'rndc thaw' to re-enable updates."); + } + + /* + * Prescan the update section, checking for updates that + * are illegal or violate policy. + */ + for (result = dns_message_firstname(request, DNS_SECTION_UPDATE); + result == ISC_R_SUCCESS; + result = dns_message_nextname(request, DNS_SECTION_UPDATE)) + { + dns_name_t *name = NULL; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_ttl_t ttl; + dns_rdataclass_t update_class; + + get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, + &rdata, &covers, &ttl, &update_class); + + if (!dns_name_issubdomain(name, zonename)) { + FAILC(DNS_R_NOTZONE, "update RR is outside zone"); + } + if (update_class == zoneclass) { + /* + * Check for meta-RRs. The RFC2136 pseudocode says + * check for ANY|AXFR|MAILA|MAILB, but the text adds + * "or any other QUERY metatype" + */ + if (dns_rdatatype_ismeta(rdata.type)) { + FAILC(DNS_R_FORMERR, "meta-RR in update"); + } + result = dns_zone_checknames(zone, name, &rdata); + if (result != ISC_R_SUCCESS) { + FAIL(DNS_R_REFUSED); + } + } else if (update_class == dns_rdataclass_any) { + if (ttl != 0 || rdata.length != 0 || + (dns_rdatatype_ismeta(rdata.type) && + rdata.type != dns_rdatatype_any)) + { + FAILC(DNS_R_FORMERR, "meta-RR in update"); + } + } else if (update_class == dns_rdataclass_none) { + if (ttl != 0 || dns_rdatatype_ismeta(rdata.type)) { + FAILC(DNS_R_FORMERR, "meta-RR in update"); + } + } else { + update_log(client, zone, ISC_LOG_WARNING, + "update RR has incorrect class %d", + update_class); + FAIL(DNS_R_FORMERR); + } + + /* + * draft-ietf-dnsind-simple-secure-update-01 says + * "Unlike traditional dynamic update, the client + * is forbidden from updating NSEC records." + */ + if (rdata.type == dns_rdatatype_nsec3) { + FAILC(DNS_R_REFUSED, "explicit NSEC3 updates are not " + "allowed " + "in secure zones"); + } else if (rdata.type == dns_rdatatype_nsec) { + FAILC(DNS_R_REFUSED, "explicit NSEC updates are not " + "allowed " + "in secure zones"); + } else if (rdata.type == dns_rdatatype_rrsig && + !dns_name_equal(name, zonename)) + { + FAILC(DNS_R_REFUSED, "explicit RRSIG updates are " + "currently " + "not supported in secure zones " + "except " + "at the apex"); + } + + if (ssutable != NULL) { + isc_netaddr_t netaddr; + dst_key_t *tsigkey = NULL; + isc_netaddr_fromsockaddr(&netaddr, &client->peeraddr); + + if (client->message->tsigkey != NULL) { + tsigkey = client->message->tsigkey->key; + } + + if (rdata.type != dns_rdatatype_any) { + if (!dns_ssutable_checkrules( + ssutable, client->signer, name, + &netaddr, TCPCLIENT(client), env, + rdata.type, tsigkey)) + { + FAILC(DNS_R_REFUSED, "rejected by " + "secure update"); + } + } else { + if (!ssu_checkall(db, ver, name, ssutable, + client->signer, &netaddr, env, + TCPCLIENT(client), tsigkey)) + { + FAILC(DNS_R_REFUSED, "rejected by " + "secure update"); + } + } + } + } + if (result != ISC_R_NOMORE) { + FAIL(result); + } + + update_log(client, zone, LOGLEVEL_DEBUG, "update section prescan OK"); + + result = isc_quota_attach(&client->manager->sctx->updquota, + &(isc_quota_t *){ NULL }); + if (result != ISC_R_SUCCESS) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "update failed: too many DNS UPDATEs queued (%s)", + isc_result_totext(result)); + ns_stats_increment(client->manager->sctx->nsstats, + ns_statscounter_updatequota); + CHECK(DNS_R_DROP); + } + + event = (update_event_t *)isc_event_allocate( + client->mctx, client, DNS_EVENT_UPDATE, update_action, NULL, + sizeof(*event)); + event->zone = zone; + event->result = ISC_R_SUCCESS; + + INSIST(client->nupdates == 0); + client->nupdates++; + event->ev_arg = client; + + isc_nmhandle_attach(client->handle, &client->updatehandle); + dns_zone_gettask(zone, &zonetask); + isc_task_send(zonetask, ISC_EVENT_PTR(&event)); + +failure: + if (db != NULL) { + dns_db_closeversion(db, &ver, false); + dns_db_detach(&db); + } + + if (ssutable != NULL) { + dns_ssutable_detach(&ssutable); + } + + return (result); +} + +static void +respond(ns_client_t *client, isc_result_t result) { + isc_result_t msg_result; + + msg_result = dns_message_reply(client->message, true); + if (msg_result != ISC_R_SUCCESS) { + isc_log_write(ns_lctx, NS_LOGCATEGORY_UPDATE, + NS_LOGMODULE_UPDATE, ISC_LOG_ERROR, + "could not create update response message: %s", + isc_result_totext(msg_result)); + ns_client_drop(client, msg_result); + isc_nmhandle_detach(&client->reqhandle); + return; + } + + client->message->rcode = dns_result_torcode(result); + ns_client_send(client); + isc_nmhandle_detach(&client->reqhandle); +} + +void +ns_update_start(ns_client_t *client, isc_nmhandle_t *handle, + isc_result_t sigresult) { + dns_message_t *request = client->message; + isc_result_t result; + dns_name_t *zonename; + dns_rdataset_t *zone_rdataset; + dns_zone_t *zone = NULL, *raw = NULL; + + /* + * Attach to the request handle. This will be held until + * we respond, or drop the request. + */ + isc_nmhandle_attach(handle, &client->reqhandle); + + /* + * Interpret the zone section. + */ + result = dns_message_firstname(request, DNS_SECTION_ZONE); + if (result != ISC_R_SUCCESS) { + FAILC(DNS_R_FORMERR, "update zone section empty"); + } + + /* + * The zone section must contain exactly one "question", and + * it must be of type SOA. + */ + zonename = NULL; + dns_message_currentname(request, DNS_SECTION_ZONE, &zonename); + zone_rdataset = ISC_LIST_HEAD(zonename->list); + if (zone_rdataset->type != dns_rdatatype_soa) { + FAILC(DNS_R_FORMERR, "update zone section contains non-SOA"); + } + if (ISC_LIST_NEXT(zone_rdataset, link) != NULL) { + FAILC(DNS_R_FORMERR, "update zone section contains multiple " + "RRs"); + } + + /* The zone section must have exactly one name. */ + result = dns_message_nextname(request, DNS_SECTION_ZONE); + if (result != ISC_R_NOMORE) { + FAILC(DNS_R_FORMERR, "update zone section contains multiple " + "RRs"); + } + + result = dns_zt_find(client->view->zonetable, zonename, 0, NULL, &zone); + if (result != ISC_R_SUCCESS) { + /* + * If we found a zone that is a parent of the update zonename, + * detach it so it isn't mentioned in log - it is irrelevant. + */ + if (zone != NULL) { + dns_zone_detach(&zone); + } + FAILN(DNS_R_NOTAUTH, zonename, + "not authoritative for update zone"); + } + + /* + * If there is a raw (unsigned) zone associated with this + * zone then it processes the UPDATE request. + */ + dns_zone_getraw(zone, &raw); + if (raw != NULL) { + dns_zone_detach(&zone); + dns_zone_attach(raw, &zone); + dns_zone_detach(&raw); + } + + switch (dns_zone_gettype(zone)) { + case dns_zone_primary: + case dns_zone_dlz: + /* + * We can now fail due to a bad signature as we now know + * that we are the master. + */ + if (sigresult != ISC_R_SUCCESS) { + FAIL(sigresult); + } + dns_message_clonebuffer(client->message); + CHECK(send_update_event(client, zone)); + break; + case dns_zone_secondary: + case dns_zone_mirror: + dns_message_clonebuffer(client->message); + CHECK(send_forward_event(client, zone)); + break; + default: + FAILC(DNS_R_NOTAUTH, "not authoritative for update zone"); + } + return; + +failure: + if (result == DNS_R_REFUSED) { + inc_stats(client, zone, ns_statscounter_updaterej); + } + + /* + * We failed without having sent an update event to the zone. + * We are still in the client task context, so we can + * simply give an error response without switching tasks. + */ + if (result == DNS_R_DROP) { + ns_client_drop(client, result); + isc_nmhandle_detach(&client->reqhandle); + } else { + respond(client, result); + } + + if (zone != NULL) { + dns_zone_detach(&zone); + } +} + +/*% + * DS records are not allowed to exist without corresponding NS records, + * RFC 3658, 2.2 Protocol Change, + * "DS RRsets MUST NOT appear at non-delegation points or at a zone's apex". + */ + +static isc_result_t +remove_orphaned_ds(dns_db_t *db, dns_dbversion_t *newver, dns_diff_t *diff) { + isc_result_t result; + bool ns_exists; + dns_difftuple_t *tuple; + dns_diff_t temp_diff; + + dns_diff_init(diff->mctx, &temp_diff); + + for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; + tuple = ISC_LIST_NEXT(tuple, link)) + { + if (!((tuple->op == DNS_DIFFOP_DEL && + tuple->rdata.type == dns_rdatatype_ns) || + (tuple->op == DNS_DIFFOP_ADD && + tuple->rdata.type == dns_rdatatype_ds))) + { + continue; + } + CHECK(rrset_exists(db, newver, &tuple->name, dns_rdatatype_ns, + 0, &ns_exists)); + if (ns_exists && + !dns_name_equal(&tuple->name, dns_db_origin(db))) + { + continue; + } + CHECK(delete_if(true_p, db, newver, &tuple->name, + dns_rdatatype_ds, 0, NULL, &temp_diff)); + } + result = ISC_R_SUCCESS; + +failure: + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = ISC_LIST_HEAD(temp_diff.tuples)) + { + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + dns_diff_appendminimal(diff, &tuple); + } + return (result); +} + +/* + * This implements the post load integrity checks for mx records. + */ +static isc_result_t +check_mx(ns_client_t *client, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *newver, dns_diff_t *diff) { + char tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:123.123.123.123.")]; + char ownerbuf[DNS_NAME_FORMATSIZE]; + char namebuf[DNS_NAME_FORMATSIZE]; + char altbuf[DNS_NAME_FORMATSIZE]; + dns_difftuple_t *t; + dns_fixedname_t fixed; + dns_name_t *foundname; + dns_rdata_mx_t mx; + dns_rdata_t rdata; + bool ok = true; + bool isaddress; + isc_result_t result; + struct in6_addr addr6; + struct in_addr addr; + dns_zoneopt_t options; + + foundname = dns_fixedname_initname(&fixed); + dns_rdata_init(&rdata); + options = dns_zone_getoptions(zone); + + for (t = ISC_LIST_HEAD(diff->tuples); t != NULL; + t = ISC_LIST_NEXT(t, link)) + { + if (t->op != DNS_DIFFOP_ADD || + t->rdata.type != dns_rdatatype_mx) + { + continue; + } + + result = dns_rdata_tostruct(&t->rdata, &mx, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + /* + * Check if we will error out if we attempt to reload the + * zone. + */ + dns_name_format(&mx.mx, namebuf, sizeof(namebuf)); + dns_name_format(&t->name, ownerbuf, sizeof(ownerbuf)); + isaddress = false; + if ((options & DNS_ZONEOPT_CHECKMX) != 0 && + strlcpy(tmp, namebuf, sizeof(tmp)) < sizeof(tmp)) + { + if (tmp[strlen(tmp) - 1] == '.') { + tmp[strlen(tmp) - 1] = '\0'; + } + if (inet_pton(AF_INET, tmp, &addr) == 1 || + inet_pton(AF_INET6, tmp, &addr6) == 1) + { + isaddress = true; + } + } + + if (isaddress && (options & DNS_ZONEOPT_CHECKMXFAIL) != 0) { + update_log(client, zone, ISC_LOG_ERROR, + "%s/MX: '%s': %s", ownerbuf, namebuf, + dns_result_totext(DNS_R_MXISADDRESS)); + ok = false; + } else if (isaddress) { + update_log(client, zone, ISC_LOG_WARNING, + "%s/MX: warning: '%s': %s", ownerbuf, + namebuf, + dns_result_totext(DNS_R_MXISADDRESS)); + } + + /* + * Check zone integrity checks. + */ + if ((options & DNS_ZONEOPT_CHECKINTEGRITY) == 0) { + continue; + } + result = dns_db_find(db, &mx.mx, newver, dns_rdatatype_a, 0, 0, + NULL, foundname, NULL, NULL); + if (result == ISC_R_SUCCESS) { + continue; + } + + if (result == DNS_R_NXRRSET) { + result = dns_db_find(db, &mx.mx, newver, + dns_rdatatype_aaaa, 0, 0, NULL, + foundname, NULL, NULL); + if (result == ISC_R_SUCCESS) { + continue; + } + } + + if (result == DNS_R_NXRRSET || result == DNS_R_NXDOMAIN) { + update_log(client, zone, ISC_LOG_ERROR, + "%s/MX '%s' has no address records " + "(A or AAAA)", + ownerbuf, namebuf); + ok = false; + } else if (result == DNS_R_CNAME) { + update_log(client, zone, ISC_LOG_ERROR, + "%s/MX '%s' is a CNAME (illegal)", ownerbuf, + namebuf); + ok = false; + } else if (result == DNS_R_DNAME) { + dns_name_format(foundname, altbuf, sizeof altbuf); + update_log(client, zone, ISC_LOG_ERROR, + "%s/MX '%s' is below a DNAME '%s' (illegal)", + ownerbuf, namebuf, altbuf); + ok = false; + } + } + return (ok ? ISC_R_SUCCESS : DNS_R_REFUSED); +} + +static isc_result_t +rr_exists(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, + const dns_rdata_t *rdata, bool *flag) { + dns_rdataset_t rdataset; + dns_dbnode_t *node = NULL; + isc_result_t result; + + dns_rdataset_init(&rdataset); + if (rdata->type == dns_rdatatype_nsec3) { + CHECK(dns_db_findnsec3node(db, name, false, &node)); + } else { + CHECK(dns_db_findnode(db, name, false, &node)); + } + result = dns_db_findrdataset(db, node, ver, rdata->type, 0, + (isc_stdtime_t)0, &rdataset, NULL); + if (result == ISC_R_NOTFOUND) { + *flag = false; + result = ISC_R_SUCCESS; + goto failure; + } + + for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + dns_rdata_t myrdata = DNS_RDATA_INIT; + dns_rdataset_current(&rdataset, &myrdata); + if (!dns_rdata_casecompare(&myrdata, rdata)) { + break; + } + } + dns_rdataset_disassociate(&rdataset); + if (result == ISC_R_SUCCESS) { + *flag = true; + } else if (result == ISC_R_NOMORE) { + *flag = false; + result = ISC_R_SUCCESS; + } + +failure: + if (node != NULL) { + dns_db_detachnode(db, &node); + } + return (result); +} + +static isc_result_t +get_iterations(dns_db_t *db, dns_dbversion_t *ver, dns_rdatatype_t privatetype, + unsigned int *iterationsp) { + dns_dbnode_t *node = NULL; + dns_rdata_nsec3param_t nsec3param; + dns_rdataset_t rdataset; + isc_result_t result; + unsigned int iterations = 0; + + dns_rdataset_init(&rdataset); + + result = dns_db_getoriginnode(db, &node); + if (result != ISC_R_SUCCESS) { + return (result); + } + result = dns_db_findrdataset(db, node, ver, dns_rdatatype_nsec3param, 0, + (isc_stdtime_t)0, &rdataset, NULL); + if (result == ISC_R_NOTFOUND) { + goto try_private; + } + if (result != ISC_R_SUCCESS) { + goto failure; + } + + for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(&rdataset, &rdata); + CHECK(dns_rdata_tostruct(&rdata, &nsec3param, NULL)); + if ((nsec3param.flags & DNS_NSEC3FLAG_REMOVE) != 0) { + continue; + } + if (nsec3param.iterations > iterations) { + iterations = nsec3param.iterations; + } + } + if (result != ISC_R_NOMORE) { + goto failure; + } + + dns_rdataset_disassociate(&rdataset); + +try_private: + if (privatetype == 0) { + goto success; + } + + result = dns_db_findrdataset(db, node, ver, privatetype, 0, + (isc_stdtime_t)0, &rdataset, NULL); + if (result == ISC_R_NOTFOUND) { + goto success; + } + if (result != ISC_R_SUCCESS) { + goto failure; + } + + for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + unsigned char buf[DNS_NSEC3PARAM_BUFFERSIZE]; + dns_rdata_t private = DNS_RDATA_INIT; + dns_rdata_t rdata = DNS_RDATA_INIT; + + dns_rdataset_current(&rdataset, &rdata); + if (!dns_nsec3param_fromprivate(&private, &rdata, buf, + sizeof(buf))) + { + continue; + } + CHECK(dns_rdata_tostruct(&rdata, &nsec3param, NULL)); + if ((nsec3param.flags & DNS_NSEC3FLAG_REMOVE) != 0) { + continue; + } + if (nsec3param.iterations > iterations) { + iterations = nsec3param.iterations; + } + } + if (result != ISC_R_NOMORE) { + goto failure; + } + +success: + *iterationsp = iterations; + result = ISC_R_SUCCESS; + +failure: + if (node != NULL) { + dns_db_detachnode(db, &node); + } + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + return (result); +} + +/* + * Prevent the zone entering a inconsistent state where + * NSEC only DNSKEYs are present with NSEC3 chains. + */ +static isc_result_t +check_dnssec(ns_client_t *client, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff) { + dns_difftuple_t *tuple; + bool nseconly = false, nsec3 = false; + isc_result_t result; + unsigned int iterations = 0; + dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); + + /* Scan the tuples for an NSEC-only DNSKEY or an NSEC3PARAM */ + for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; + tuple = ISC_LIST_NEXT(tuple, link)) + { + if (tuple->op != DNS_DIFFOP_ADD) { + continue; + } + + if (tuple->rdata.type == dns_rdatatype_dnskey) { + uint8_t alg; + alg = tuple->rdata.data[3]; + if (alg == DST_ALG_RSASHA1) { + nseconly = true; + break; + } + } else if (tuple->rdata.type == dns_rdatatype_nsec3param) { + nsec3 = true; + break; + } + } + + /* Check existing DB for NSEC-only DNSKEY */ + if (!nseconly) { + result = dns_nsec_nseconly(db, ver, &nseconly); + + /* + * An NSEC3PARAM update can proceed without a DNSKEY (it + * will trigger a delayed change), so we can ignore + * ISC_R_NOTFOUND here. + */ + if (result == ISC_R_NOTFOUND) { + result = ISC_R_SUCCESS; + } + + CHECK(result); + } + + /* Check existing DB for NSEC3 */ + if (!nsec3) { + CHECK(dns_nsec3_activex(db, ver, false, privatetype, &nsec3)); + } + + /* Refuse to allow NSEC3 with NSEC-only keys */ + if (nseconly && nsec3) { + update_log(client, zone, ISC_LOG_ERROR, + "NSEC only DNSKEYs and NSEC3 chains not allowed"); + result = DNS_R_REFUSED; + goto failure; + } + + /* Verify NSEC3 params */ + CHECK(get_iterations(db, ver, privatetype, &iterations)); + if (iterations > dns_nsec3_maxiterations()) { + update_log(client, zone, ISC_LOG_ERROR, + "too many NSEC3 iterations (%u)", iterations); + result = DNS_R_REFUSED; + goto failure; + } + +failure: + return (result); +} + +/* + * Delay NSEC3PARAM changes as they need to be applied to the whole zone. + */ +static isc_result_t +add_nsec3param_records(ns_client_t *client, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff) { + isc_result_t result = ISC_R_SUCCESS; + dns_difftuple_t *tuple, *newtuple = NULL, *next; + dns_rdata_t rdata = DNS_RDATA_INIT; + unsigned char buf[DNS_NSEC3PARAM_BUFFERSIZE + 1]; + dns_diff_t temp_diff; + dns_diffop_t op; + bool flag; + dns_name_t *name = dns_zone_getorigin(zone); + dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); + uint32_t ttl = 0; + bool ttl_good = false; + + update_log(client, zone, ISC_LOG_DEBUG(3), + "checking for NSEC3PARAM changes"); + + dns_diff_init(diff->mctx, &temp_diff); + + /* + * Extract NSEC3PARAM tuples from list. + */ + for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; tuple = next) { + next = ISC_LIST_NEXT(tuple, link); + + if (tuple->rdata.type != dns_rdatatype_nsec3param || + !dns_name_equal(name, &tuple->name)) + { + continue; + } + ISC_LIST_UNLINK(diff->tuples, tuple, link); + ISC_LIST_APPEND(temp_diff.tuples, tuple, link); + } + + /* + * Extract TTL changes pairs, we don't need to convert these to + * delayed changes. + */ + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = next) + { + if (tuple->op == DNS_DIFFOP_ADD) { + if (!ttl_good) { + /* + * Any adds here will contain the final + * NSEC3PARAM RRset TTL. + */ + ttl = tuple->ttl; + ttl_good = true; + } + /* + * Walk the temp_diff list looking for the + * corresponding delete. + */ + next = ISC_LIST_HEAD(temp_diff.tuples); + while (next != NULL) { + unsigned char *next_data = next->rdata.data; + unsigned char *tuple_data = tuple->rdata.data; + if (next->op == DNS_DIFFOP_DEL && + next->rdata.length == tuple->rdata.length && + !memcmp(next_data, tuple_data, + next->rdata.length)) + { + ISC_LIST_UNLINK(temp_diff.tuples, next, + link); + ISC_LIST_APPEND(diff->tuples, next, + link); + break; + } + next = ISC_LIST_NEXT(next, link); + } + /* + * If we have not found a pair move onto the next + * tuple. + */ + if (next == NULL) { + next = ISC_LIST_NEXT(tuple, link); + continue; + } + /* + * Find the next tuple to be processed before + * unlinking then complete moving the pair to 'diff'. + */ + next = ISC_LIST_NEXT(tuple, link); + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + ISC_LIST_APPEND(diff->tuples, tuple, link); + } else { + next = ISC_LIST_NEXT(tuple, link); + } + } + + /* + * Preserve any ongoing changes from a BIND 9.6.x upgrade. + * + * Any NSEC3PARAM records with flags other than OPTOUT named + * in managing and should not be touched so revert such changes + * taking into account any TTL change of the NSEC3PARAM RRset. + */ + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = next) + { + next = ISC_LIST_NEXT(tuple, link); + if ((tuple->rdata.data[1] & ~DNS_NSEC3FLAG_OPTOUT) != 0) { + /* + * If we haven't had any adds then the tuple->ttl must + * be the original ttl and should be used for any + * future changes. + */ + if (!ttl_good) { + ttl = tuple->ttl; + ttl_good = true; + } + op = (tuple->op == DNS_DIFFOP_DEL) ? DNS_DIFFOP_ADD + : DNS_DIFFOP_DEL; + CHECK(dns_difftuple_create(diff->mctx, op, name, ttl, + &tuple->rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + dns_diff_appendminimal(diff, &tuple); + } + } + + /* + * We now have just the actual changes to the NSEC3PARAM RRset. + * Convert the adds to delayed adds and the deletions into delayed + * deletions. + */ + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = next) + { + /* + * If we haven't had any adds then the tuple->ttl must be the + * original ttl and should be used for any future changes. + */ + if (!ttl_good) { + ttl = tuple->ttl; + ttl_good = true; + } + if (tuple->op == DNS_DIFFOP_ADD) { + bool nseconly = false; + + /* + * Look for any deletes which match this ADD ignoring + * flags. We don't need to explicitly remove them as + * they will be removed a side effect of processing + * the add. + */ + next = ISC_LIST_HEAD(temp_diff.tuples); + while (next != NULL) { + unsigned char *next_data = next->rdata.data; + unsigned char *tuple_data = tuple->rdata.data; + if (next->op != DNS_DIFFOP_DEL || + next->rdata.length != tuple->rdata.length || + next_data[0] != tuple_data[0] || + next_data[2] != tuple_data[2] || + next_data[3] != tuple_data[3] || + memcmp(next_data + 4, tuple_data + 4, + tuple->rdata.length - 4)) + { + next = ISC_LIST_NEXT(next, link); + continue; + } + ISC_LIST_UNLINK(temp_diff.tuples, next, link); + ISC_LIST_APPEND(diff->tuples, next, link); + next = ISC_LIST_HEAD(temp_diff.tuples); + } + + /* + * Create a private-type record to signal that + * we want a delayed NSEC3 chain add/delete + */ + dns_nsec3param_toprivate(&tuple->rdata, &rdata, + privatetype, buf, sizeof(buf)); + buf[2] |= DNS_NSEC3FLAG_CREATE; + + /* + * If the zone is not currently capable of + * supporting an NSEC3 chain, then we set the + * INITIAL flag to indicate that these parameters + * are to be used later. + */ + result = dns_nsec_nseconly(db, ver, &nseconly); + if (result == ISC_R_NOTFOUND || nseconly) { + buf[2] |= DNS_NSEC3FLAG_INITIAL; + } + + /* + * See if this CREATE request already exists. + */ + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + + if (!flag) { + CHECK(dns_difftuple_create( + diff->mctx, DNS_DIFFOP_ADD, name, 0, + &rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + } + + /* + * Remove any existing CREATE request to add an + * otherwise identical chain with a reversed + * OPTOUT state. + */ + buf[2] ^= DNS_NSEC3FLAG_OPTOUT; + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + + if (flag) { + CHECK(dns_difftuple_create( + diff->mctx, DNS_DIFFOP_DEL, name, 0, + &rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + } + + /* + * Find the next tuple to be processed and remove the + * temporary add record. + */ + next = ISC_LIST_NEXT(tuple, link); + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, + name, ttl, &tuple->rdata, + &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + dns_diff_appendminimal(diff, &tuple); + dns_rdata_reset(&rdata); + } else { + next = ISC_LIST_NEXT(tuple, link); + } + } + + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = next) + { + INSIST(ttl_good); + + next = ISC_LIST_NEXT(tuple, link); + /* + * See if we already have a REMOVE request in progress. + */ + dns_nsec3param_toprivate(&tuple->rdata, &rdata, privatetype, + buf, sizeof(buf)); + + buf[2] |= DNS_NSEC3FLAG_REMOVE | DNS_NSEC3FLAG_NONSEC; + + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + if (!flag) { + buf[2] &= ~DNS_NSEC3FLAG_NONSEC; + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + } + + if (!flag) { + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, + name, 0, &rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + } + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, + ttl, &tuple->rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + dns_diff_appendminimal(diff, &tuple); + dns_rdata_reset(&rdata); + } + + result = ISC_R_SUCCESS; +failure: + dns_diff_clear(&temp_diff); + return (result); +} + +static isc_result_t +rollback_private(dns_db_t *db, dns_rdatatype_t privatetype, + dns_dbversion_t *ver, dns_diff_t *diff) { + dns_diff_t temp_diff; + dns_diffop_t op; + dns_difftuple_t *tuple, *newtuple = NULL, *next; + dns_name_t *name = dns_db_origin(db); + isc_mem_t *mctx = diff->mctx; + isc_result_t result; + + if (privatetype == 0) { + return (ISC_R_SUCCESS); + } + + dns_diff_init(mctx, &temp_diff); + + /* + * Extract the changes to be rolled back. + */ + for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; tuple = next) { + next = ISC_LIST_NEXT(tuple, link); + + if (tuple->rdata.type != privatetype || + !dns_name_equal(name, &tuple->name)) + { + continue; + } + + /* + * Allow records which indicate that a zone has been + * signed with a DNSKEY to be removed. + */ + if (tuple->op == DNS_DIFFOP_DEL && tuple->rdata.length == 5 && + tuple->rdata.data[0] != 0 && tuple->rdata.data[4] != 0) + { + continue; + } + + ISC_LIST_UNLINK(diff->tuples, tuple, link); + ISC_LIST_PREPEND(temp_diff.tuples, tuple, link); + } + + /* + * Rollback the changes. + */ + while ((tuple = ISC_LIST_HEAD(temp_diff.tuples)) != NULL) { + op = (tuple->op == DNS_DIFFOP_DEL) ? DNS_DIFFOP_ADD + : DNS_DIFFOP_DEL; + CHECK(dns_difftuple_create(mctx, op, name, tuple->ttl, + &tuple->rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, &temp_diff)); + } + result = ISC_R_SUCCESS; + +failure: + dns_diff_clear(&temp_diff); + return (result); +} + +/* + * Add records to cause the delayed signing of the zone by added DNSKEY + * to remove the RRSIG records generated by a deleted DNSKEY. + */ +static isc_result_t +add_signing_records(dns_db_t *db, dns_rdatatype_t privatetype, + dns_dbversion_t *ver, dns_diff_t *diff) { + dns_difftuple_t *tuple, *newtuple = NULL, *next; + dns_rdata_dnskey_t dnskey; + dns_rdata_t rdata = DNS_RDATA_INIT; + bool flag; + isc_region_t r; + isc_result_t result = ISC_R_SUCCESS; + uint16_t keyid; + unsigned char buf[5]; + dns_name_t *name = dns_db_origin(db); + dns_diff_t temp_diff; + + dns_diff_init(diff->mctx, &temp_diff); + + /* + * Extract the DNSKEY tuples from the list. + */ + for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; tuple = next) { + next = ISC_LIST_NEXT(tuple, link); + + if (tuple->rdata.type != dns_rdatatype_dnskey) { + continue; + } + + ISC_LIST_UNLINK(diff->tuples, tuple, link); + ISC_LIST_APPEND(temp_diff.tuples, tuple, link); + } + + /* + * Extract TTL changes pairs, we don't need signing records for these. + */ + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = next) + { + if (tuple->op == DNS_DIFFOP_ADD) { + /* + * Walk the temp_diff list looking for the + * corresponding delete. + */ + next = ISC_LIST_HEAD(temp_diff.tuples); + while (next != NULL) { + unsigned char *next_data = next->rdata.data; + unsigned char *tuple_data = tuple->rdata.data; + if (next->op == DNS_DIFFOP_DEL && + dns_name_equal(&tuple->name, &next->name) && + next->rdata.length == tuple->rdata.length && + !memcmp(next_data, tuple_data, + next->rdata.length)) + { + ISC_LIST_UNLINK(temp_diff.tuples, next, + link); + ISC_LIST_APPEND(diff->tuples, next, + link); + break; + } + next = ISC_LIST_NEXT(next, link); + } + /* + * If we have not found a pair move onto the next + * tuple. + */ + if (next == NULL) { + next = ISC_LIST_NEXT(tuple, link); + continue; + } + /* + * Find the next tuple to be processed before + * unlinking then complete moving the pair to 'diff'. + */ + next = ISC_LIST_NEXT(tuple, link); + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + ISC_LIST_APPEND(diff->tuples, tuple, link); + } else { + next = ISC_LIST_NEXT(tuple, link); + } + } + + /* + * Process the remaining DNSKEY entries. + */ + for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; + tuple = ISC_LIST_HEAD(temp_diff.tuples)) + { + ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + ISC_LIST_APPEND(diff->tuples, tuple, link); + + result = dns_rdata_tostruct(&tuple->rdata, &dnskey, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if ((dnskey.flags & (DNS_KEYFLAG_OWNERMASK | + DNS_KEYTYPE_NOAUTH)) != DNS_KEYOWNER_ZONE) + { + continue; + } + + dns_rdata_toregion(&tuple->rdata, &r); + + keyid = dst_region_computeid(&r); + + buf[0] = dnskey.algorithm; + buf[1] = (keyid & 0xff00) >> 8; + buf[2] = (keyid & 0xff); + buf[3] = (tuple->op == DNS_DIFFOP_ADD) ? 0 : 1; + buf[4] = 0; + rdata.data = buf; + rdata.length = sizeof(buf); + rdata.type = privatetype; + rdata.rdclass = tuple->rdata.rdclass; + + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + if (flag) { + continue; + } + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, 0, + &rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + INSIST(newtuple == NULL); + /* + * Remove any record which says this operation has already + * completed. + */ + buf[4] = 1; + CHECK(rr_exists(db, ver, name, &rdata, &flag)); + if (flag) { + CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, + name, 0, &rdata, &newtuple)); + CHECK(do_one_tuple(&newtuple, db, ver, diff)); + INSIST(newtuple == NULL); + } + } + +failure: + dns_diff_clear(&temp_diff); + return (result); +} + +static bool +isdnssec(dns_db_t *db, dns_dbversion_t *ver, dns_rdatatype_t privatetype) { + isc_result_t result; + bool build_nsec, build_nsec3; + + if (dns_db_issecure(db)) { + return (true); + } + + result = dns_private_chains(db, ver, privatetype, &build_nsec, + &build_nsec3); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + return (build_nsec || build_nsec3); +} + +static void +update_action(isc_task_t *task, isc_event_t *event) { + update_event_t *uev = (update_event_t *)event; + dns_zone_t *zone = uev->zone; + ns_client_t *client = (ns_client_t *)event->ev_arg; + isc_result_t result; + dns_db_t *db = NULL; + dns_dbversion_t *oldver = NULL; + dns_dbversion_t *ver = NULL; + dns_diff_t diff; /* Pending updates. */ + dns_diff_t temp; /* Pending RR existence assertions. */ + bool soa_serial_changed = false; + isc_mem_t *mctx = client->mctx; + dns_rdatatype_t covers; + dns_message_t *request = client->message; + dns_rdataclass_t zoneclass; + dns_name_t *zonename = NULL; + dns_ssutable_t *ssutable = NULL; + dns_fixedname_t tmpnamefixed; + dns_name_t *tmpname = NULL; + dns_zoneopt_t options; + dns_difftuple_t *tuple; + dns_rdata_dnskey_t dnskey; + bool had_dnskey; + dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); + dns_ttl_t maxttl = 0; + uint32_t maxrecords; + uint64_t records; + + INSIST(event->ev_type == DNS_EVENT_UPDATE); + + dns_diff_init(mctx, &diff); + dns_diff_init(mctx, &temp); + + CHECK(dns_zone_getdb(zone, &db)); + zonename = dns_db_origin(db); + zoneclass = dns_db_class(db); + dns_zone_getssutable(zone, &ssutable); + options = dns_zone_getoptions(zone); + + /* + * Get old and new versions now that queryacl has been checked. + */ + dns_db_currentversion(db, &oldver); + CHECK(dns_db_newversion(db, &ver)); + + /* + * Check prerequisites. + */ + + for (result = dns_message_firstname(request, DNS_SECTION_PREREQUISITE); + result == ISC_R_SUCCESS; + result = dns_message_nextname(request, DNS_SECTION_PREREQUISITE)) + { + dns_name_t *name = NULL; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_ttl_t ttl; + dns_rdataclass_t update_class; + bool flag; + + get_current_rr(request, DNS_SECTION_PREREQUISITE, zoneclass, + &name, &rdata, &covers, &ttl, &update_class); + + if (ttl != 0) { + PREREQFAILC(DNS_R_FORMERR, "prerequisite TTL is not " + "zero"); + } + + if (!dns_name_issubdomain(name, zonename)) { + PREREQFAILN(DNS_R_NOTZONE, name, + "prerequisite name is out of zone"); + } + + if (update_class == dns_rdataclass_any) { + if (rdata.length != 0) { + PREREQFAILC(DNS_R_FORMERR, "class ANY " + "prerequisite " + "RDATA is not " + "empty"); + } + if (rdata.type == dns_rdatatype_any) { + CHECK(name_exists(db, ver, name, &flag)); + if (!flag) { + PREREQFAILN(DNS_R_NXDOMAIN, name, + "'name in use' " + "prerequisite not " + "satisfied"); + } + } else { + CHECK(rrset_exists(db, ver, name, rdata.type, + covers, &flag)); + if (!flag) { + /* RRset does not exist. */ + PREREQFAILNT(DNS_R_NXRRSET, name, + rdata.type, + "'rrset exists (value " + "independent)' " + "prerequisite not " + "satisfied"); + } + } + } else if (update_class == dns_rdataclass_none) { + if (rdata.length != 0) { + PREREQFAILC(DNS_R_FORMERR, "class NONE " + "prerequisite " + "RDATA is not " + "empty"); + } + if (rdata.type == dns_rdatatype_any) { + CHECK(name_exists(db, ver, name, &flag)); + if (flag) { + PREREQFAILN(DNS_R_YXDOMAIN, name, + "'name not in use' " + "prerequisite not " + "satisfied"); + } + } else { + CHECK(rrset_exists(db, ver, name, rdata.type, + covers, &flag)); + if (flag) { + /* RRset exists. */ + PREREQFAILNT(DNS_R_YXRRSET, name, + rdata.type, + "'rrset does not exist' " + "prerequisite not " + "satisfied"); + } + } + } else if (update_class == zoneclass) { + /* "temp<rr.name, rr.type> += rr;" */ + result = temp_append(&temp, name, &rdata); + if (result != ISC_R_SUCCESS) { + UNEXPECTED_ERROR(__FILE__, __LINE__, + "temp entry creation failed: " + "%s", + dns_result_totext(result)); + FAIL(ISC_R_UNEXPECTED); + } + } else { + PREREQFAILC(DNS_R_FORMERR, "malformed prerequisite"); + } + } + if (result != ISC_R_NOMORE) { + FAIL(result); + } + + /* + * Perform the final check of the "rrset exists (value dependent)" + * prerequisites. + */ + if (ISC_LIST_HEAD(temp.tuples) != NULL) { + dns_rdatatype_t type; + + /* + * Sort the prerequisite records by owner name, + * type, and rdata. + */ + result = dns_diff_sort(&temp, temp_order); + if (result != ISC_R_SUCCESS) { + FAILC(result, "'RRset exists (value dependent)' " + "prerequisite not satisfied"); + } + + tmpname = dns_fixedname_initname(&tmpnamefixed); + result = temp_check(mctx, &temp, db, ver, tmpname, &type); + if (result != ISC_R_SUCCESS) { + FAILNT(result, tmpname, type, + "'RRset exists (value dependent)' " + "prerequisite not satisfied"); + } + } + + update_log(client, zone, LOGLEVEL_DEBUG, "prerequisites are OK"); + + /* + * Process the Update Section. + */ + for (result = dns_message_firstname(request, DNS_SECTION_UPDATE); + result == ISC_R_SUCCESS; + result = dns_message_nextname(request, DNS_SECTION_UPDATE)) + { + dns_name_t *name = NULL; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_ttl_t ttl; + dns_rdataclass_t update_class; + bool flag; + + get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, + &rdata, &covers, &ttl, &update_class); + + if (update_class == zoneclass) { + /* + * RFC1123 doesn't allow MF and MD in master zones. + */ + if (rdata.type == dns_rdatatype_md || + rdata.type == dns_rdatatype_mf) + { + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + + dns_rdatatype_format(rdata.type, typebuf, + sizeof(typebuf)); + update_log(client, zone, LOGLEVEL_PROTOCOL, + "attempt to add %s ignored", + typebuf); + continue; + } + if ((rdata.type == dns_rdatatype_ns || + rdata.type == dns_rdatatype_dname) && + dns_name_iswildcard(name)) + { + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + + dns_rdatatype_format(rdata.type, typebuf, + sizeof(typebuf)); + update_log(client, zone, LOGLEVEL_PROTOCOL, + "attempt to add wildcard %s record " + "ignored", + typebuf); + continue; + } + if (rdata.type == dns_rdatatype_cname) { + CHECK(cname_incompatible_rrset_exists( + db, ver, name, &flag)); + if (flag) { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to add CNAME " + "alongside non-CNAME " + "ignored"); + continue; + } + } else { + CHECK(rrset_exists(db, ver, name, + dns_rdatatype_cname, 0, + &flag)); + if (flag && !dns_rdatatype_atcname(rdata.type)) + { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to add non-CNAME " + "alongside CNAME ignored"); + continue; + } + } + if (rdata.type == dns_rdatatype_soa) { + bool ok; + CHECK(rrset_exists(db, ver, name, + dns_rdatatype_soa, 0, + &flag)); + if (!flag) { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to create 2nd " + "SOA ignored"); + continue; + } + CHECK(check_soa_increment(db, ver, &rdata, + &ok)); + if (!ok) { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "SOA update failed to " + "increment serial, " + "ignoring it"); + continue; + } + soa_serial_changed = true; + } + + if (dns_rdatatype_atparent(rdata.type) && + dns_name_equal(name, zonename)) + { + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + + dns_rdatatype_format(rdata.type, typebuf, + sizeof(typebuf)); + update_log(client, zone, LOGLEVEL_PROTOCOL, + "attempt to add a %s record at " + "zone apex ignored", + typebuf); + continue; + } + + if (rdata.type == privatetype) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "attempt to add a private type " + "(%u) record rejected internal " + "use only", + privatetype); + continue; + } + + if (rdata.type == dns_rdatatype_nsec3param) { + /* + * Ignore attempts to add NSEC3PARAM records + * with any flags other than OPTOUT. + */ + if ((rdata.data[1] & ~DNS_NSEC3FLAG_OPTOUT) != + 0) + { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to add NSEC3PARAM " + "record with non OPTOUT " + "flag"); + continue; + } + } + + if ((options & DNS_ZONEOPT_CHECKWILDCARD) != 0 && + dns_name_internalwildcard(name)) + { + char namestr[DNS_NAME_FORMATSIZE]; + dns_name_format(name, namestr, sizeof(namestr)); + update_log(client, zone, LOGLEVEL_PROTOCOL, + "warning: ownername '%s' contains " + "a non-terminal wildcard", + namestr); + } + + if ((options & DNS_ZONEOPT_CHECKTTL) != 0) { + maxttl = dns_zone_getmaxttl(zone); + if (ttl > maxttl) { + ttl = maxttl; + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "reducing TTL to the " + "configured max-zone-ttl %d", + maxttl); + } + } + + if (isc_log_wouldlog(ns_lctx, LOGLEVEL_PROTOCOL)) { + char namestr[DNS_NAME_FORMATSIZE]; + char typestr[DNS_RDATATYPE_FORMATSIZE]; + char rdstr[2048]; + isc_buffer_t buf; + int len = 0; + const char *truncated = ""; + + dns_name_format(name, namestr, sizeof(namestr)); + dns_rdatatype_format(rdata.type, typestr, + sizeof(typestr)); + isc_buffer_init(&buf, rdstr, sizeof(rdstr)); + result = dns_rdata_totext(&rdata, NULL, &buf); + if (result == ISC_R_NOSPACE) { + len = (int)isc_buffer_usedlength(&buf); + truncated = " [TRUNCATED]"; + } else if (result != ISC_R_SUCCESS) { + snprintf(rdstr, sizeof(rdstr), + "[dns_" + "rdata_totext failed: %s]", + dns_result_totext(result)); + len = strlen(rdstr); + } else { + len = (int)isc_buffer_usedlength(&buf); + } + update_log(client, zone, LOGLEVEL_PROTOCOL, + "adding an RR at '%s' %s %.*s%s", + namestr, typestr, len, rdstr, + truncated); + } + + /* Prepare the affected RRset for the addition. */ + { + add_rr_prepare_ctx_t ctx; + ctx.db = db; + ctx.ver = ver; + ctx.diff = &diff; + ctx.name = name; + ctx.oldname = name; + ctx.update_rr = &rdata; + ctx.update_rr_ttl = ttl; + ctx.ignore_add = false; + dns_diff_init(mctx, &ctx.del_diff); + dns_diff_init(mctx, &ctx.add_diff); + CHECK(foreach_rr(db, ver, name, rdata.type, + covers, add_rr_prepare_action, + &ctx)); + + if (ctx.ignore_add) { + dns_diff_clear(&ctx.del_diff); + dns_diff_clear(&ctx.add_diff); + } else { + result = do_diff(&ctx.del_diff, db, ver, + &diff); + if (result == ISC_R_SUCCESS) { + result = do_diff(&ctx.add_diff, + db, ver, + &diff); + } + if (result != ISC_R_SUCCESS) { + dns_diff_clear(&ctx.del_diff); + dns_diff_clear(&ctx.add_diff); + goto failure; + } + CHECK(update_one_rr(db, ver, &diff, + DNS_DIFFOP_ADD, + name, ttl, &rdata)); + } + } + } else if (update_class == dns_rdataclass_any) { + if (rdata.type == dns_rdatatype_any) { + if (isc_log_wouldlog(ns_lctx, + LOGLEVEL_PROTOCOL)) + { + char namestr[DNS_NAME_FORMATSIZE]; + dns_name_format(name, namestr, + sizeof(namestr)); + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "delete all rrsets from " + "name '%s'", + namestr); + } + if (dns_name_equal(name, zonename)) { + CHECK(delete_if(type_not_soa_nor_ns_p, + db, ver, name, + dns_rdatatype_any, 0, + &rdata, &diff)); + } else { + CHECK(delete_if(type_not_dnssec, db, + ver, name, + dns_rdatatype_any, 0, + &rdata, &diff)); + } + } else if (dns_name_equal(name, zonename) && + (rdata.type == dns_rdatatype_soa || + rdata.type == dns_rdatatype_ns)) + { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "attempt to delete all SOA " + "or NS records ignored"); + continue; + } else { + if (isc_log_wouldlog(ns_lctx, + LOGLEVEL_PROTOCOL)) + { + char namestr[DNS_NAME_FORMATSIZE]; + char typestr[DNS_RDATATYPE_FORMATSIZE]; + dns_name_format(name, namestr, + sizeof(namestr)); + dns_rdatatype_format(rdata.type, + typestr, + sizeof(typestr)); + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "deleting rrset at '%s' %s", + namestr, typestr); + } + CHECK(delete_if(true_p, db, ver, name, + rdata.type, covers, &rdata, + &diff)); + } + } else if (update_class == dns_rdataclass_none) { + char namestr[DNS_NAME_FORMATSIZE]; + char typestr[DNS_RDATATYPE_FORMATSIZE]; + + /* + * The (name == zonename) condition appears in + * RFC2136 3.4.2.4 but is missing from the pseudocode. + */ + if (dns_name_equal(name, zonename)) { + if (rdata.type == dns_rdatatype_soa) { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to delete SOA " + "ignored"); + continue; + } + if (rdata.type == dns_rdatatype_ns) { + int count; + CHECK(rr_count(db, ver, name, + dns_rdatatype_ns, 0, + &count)); + if (count == 1) { + update_log(client, zone, + LOGLEVEL_PROTOCOL, + "attempt to " + "delete last " + "NS ignored"); + continue; + } + } + } + dns_name_format(name, namestr, sizeof(namestr)); + dns_rdatatype_format(rdata.type, typestr, + sizeof(typestr)); + update_log(client, zone, LOGLEVEL_PROTOCOL, + "deleting an RR at %s %s", namestr, typestr); + CHECK(delete_if(rr_equal_p, db, ver, name, rdata.type, + covers, &rdata, &diff)); + } + } + if (result != ISC_R_NOMORE) { + FAIL(result); + } + + /* + * Check that any changes to DNSKEY/NSEC3PARAM records make sense. + * If they don't then back out all changes to DNSKEY/NSEC3PARAM + * records. + */ + if (!ISC_LIST_EMPTY(diff.tuples)) { + CHECK(check_dnssec(client, zone, db, ver, &diff)); + } + + if (!ISC_LIST_EMPTY(diff.tuples)) { + unsigned int errors = 0; + CHECK(dns_zone_nscheck(zone, db, ver, &errors)); + if (errors != 0) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "update rejected: post update name server " + "sanity check failed"); + result = DNS_R_REFUSED; + goto failure; + } + } + if (!ISC_LIST_EMPTY(diff.tuples)) { + result = dns_zone_cdscheck(zone, db, ver); + if (result == DNS_R_BADCDS || result == DNS_R_BADCDNSKEY) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "update rejected: bad %s RRset", + result == DNS_R_BADCDS ? "CDS" : "CDNSKEY"); + result = DNS_R_REFUSED; + goto failure; + } + if (result != ISC_R_SUCCESS) { + goto failure; + } + } + + /* + * If any changes were made, increment the SOA serial number, + * update RRSIGs and NSECs (if zone is secure), and write the update + * to the journal. + */ + if (!ISC_LIST_EMPTY(diff.tuples)) { + char *journalfile; + dns_journal_t *journal; + bool has_dnskey; + + /* + * Increment the SOA serial, but only if it was not + * changed as a result of an update operation. + */ + if (!soa_serial_changed) { + CHECK(update_soa_serial( + db, ver, &diff, mctx, + dns_zone_getserialupdatemethod(zone))); + } + + CHECK(check_mx(client, zone, db, ver, &diff)); + + CHECK(remove_orphaned_ds(db, ver, &diff)); + + CHECK(rrset_exists(db, ver, zonename, dns_rdatatype_dnskey, 0, + &has_dnskey)); + +#define ALLOW_SECURE_TO_INSECURE(zone) \ + ((dns_zone_getoptions(zone) & DNS_ZONEOPT_SECURETOINSECURE) != 0) + + CHECK(rrset_exists(db, oldver, zonename, dns_rdatatype_dnskey, + 0, &had_dnskey)); + if (!ALLOW_SECURE_TO_INSECURE(zone)) { + if (had_dnskey && !has_dnskey) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "update rejected: all DNSKEY " + "records removed and " + "'dnssec-secure-to-insecure' " + "not set"); + result = DNS_R_REFUSED; + goto failure; + } + } + + CHECK(rollback_private(db, privatetype, ver, &diff)); + + CHECK(add_signing_records(db, privatetype, ver, &diff)); + + CHECK(add_nsec3param_records(client, zone, db, ver, &diff)); + + if (had_dnskey && !has_dnskey) { + /* + * We are transitioning from secure to insecure. + * Cause all NSEC3 chains to be deleted. When the + * the last signature for the DNSKEY records are + * remove any NSEC chain present will also be removed. + */ + CHECK(dns_nsec3param_deletechains(db, ver, zone, true, + &diff)); + } else if (has_dnskey && isdnssec(db, ver, privatetype)) { + dns_update_log_t log; + uint32_t interval = + dns_zone_getsigvalidityinterval(zone); + + log.func = update_log_cb; + log.arg = client; + result = dns_update_signatures(&log, zone, db, oldver, + ver, &diff, interval); + + if (result != ISC_R_SUCCESS) { + update_log(client, zone, ISC_LOG_ERROR, + "RRSIG/NSEC/NSEC3 update failed: %s", + isc_result_totext(result)); + goto failure; + } + } + + maxrecords = dns_zone_getmaxrecords(zone); + if (maxrecords != 0U) { + result = dns_db_getsize(db, ver, &records, NULL); + if (result == ISC_R_SUCCESS && records > maxrecords) { + update_log(client, zone, ISC_LOG_ERROR, + "records in zone (%" PRIu64 ") " + "exceeds max-records (%u)", + records, maxrecords); + result = DNS_R_TOOMANYRECORDS; + goto failure; + } + } + + journalfile = dns_zone_getjournal(zone); + if (journalfile != NULL) { + update_log(client, zone, LOGLEVEL_DEBUG, + "writing journal %s", journalfile); + + journal = NULL; + result = dns_journal_open(mctx, journalfile, + DNS_JOURNAL_CREATE, &journal); + if (result != ISC_R_SUCCESS) { + FAILS(result, "journal open failed"); + } + + result = dns_journal_write_transaction(journal, &diff); + if (result != ISC_R_SUCCESS) { + dns_journal_destroy(&journal); + FAILS(result, "journal write failed"); + } + + dns_journal_destroy(&journal); + } + + /* + * XXXRTH Just a note that this committing code will have + * to change to handle databases that need two-phase + * commit, but this isn't a priority. + */ + update_log(client, zone, LOGLEVEL_DEBUG, + "committing update transaction"); + + dns_db_closeversion(db, &ver, true); + + /* + * Mark the zone as dirty so that it will be written to disk. + */ + dns_zone_markdirty(zone); + + /* + * Notify slaves of the change we just made. + */ + dns_zone_notify(zone); + + /* + * Cause the zone to be signed with the key that we + * have just added or have the corresponding signatures + * deleted. + * + * Note: we are already committed to this course of action. + */ + for (tuple = ISC_LIST_HEAD(diff.tuples); tuple != NULL; + tuple = ISC_LIST_NEXT(tuple, link)) + { + isc_region_t r; + dns_secalg_t algorithm; + uint16_t keyid; + + if (tuple->rdata.type != dns_rdatatype_dnskey) { + continue; + } + + dns_rdata_tostruct(&tuple->rdata, &dnskey, NULL); + if ((dnskey.flags & + (DNS_KEYFLAG_OWNERMASK | DNS_KEYTYPE_NOAUTH)) != + DNS_KEYOWNER_ZONE) + { + continue; + } + + dns_rdata_toregion(&tuple->rdata, &r); + algorithm = dnskey.algorithm; + keyid = dst_region_computeid(&r); + + result = dns_zone_signwithkey( + zone, algorithm, keyid, + (tuple->op == DNS_DIFFOP_DEL)); + if (result != ISC_R_SUCCESS) { + update_log(client, zone, ISC_LOG_ERROR, + "dns_zone_signwithkey failed: %s", + dns_result_totext(result)); + } + } + + /* + * Cause the zone to add/delete NSEC3 chains for the + * deferred NSEC3PARAM changes. + * + * Note: we are already committed to this course of action. + */ + for (tuple = ISC_LIST_HEAD(diff.tuples); tuple != NULL; + tuple = ISC_LIST_NEXT(tuple, link)) + { + unsigned char buf[DNS_NSEC3PARAM_BUFFERSIZE]; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_nsec3param_t nsec3param; + + if (tuple->rdata.type != privatetype || + tuple->op != DNS_DIFFOP_ADD) + { + continue; + } + + if (!dns_nsec3param_fromprivate(&tuple->rdata, &rdata, + buf, sizeof(buf))) + { + continue; + } + dns_rdata_tostruct(&rdata, &nsec3param, NULL); + if (nsec3param.flags == 0) { + continue; + } + + result = dns_zone_addnsec3chain(zone, &nsec3param); + if (result != ISC_R_SUCCESS) { + update_log(client, zone, ISC_LOG_ERROR, + "dns_zone_addnsec3chain failed: %s", + dns_result_totext(result)); + } + } + } else { + update_log(client, zone, LOGLEVEL_DEBUG, "redundant request"); + dns_db_closeversion(db, &ver, true); + } + result = ISC_R_SUCCESS; + goto common; + +failure: + /* + * The reason for failure should have been logged at this point. + */ + if (ver != NULL) { + update_log(client, zone, LOGLEVEL_DEBUG, "rolling back"); + dns_db_closeversion(db, &ver, false); + } + +common: + dns_diff_clear(&temp); + dns_diff_clear(&diff); + + if (oldver != NULL) { + dns_db_closeversion(db, &oldver, false); + } + + if (db != NULL) { + dns_db_detach(&db); + } + + if (ssutable != NULL) { + dns_ssutable_detach(&ssutable); + } + + isc_task_detach(&task); + uev->result = result; + if (zone != NULL) { + INSIST(uev->zone == zone); /* we use this later */ + } + uev->ev_type = DNS_EVENT_UPDATEDONE; + uev->ev_action = updatedone_action; + + isc_task_send(client->task, &event); + + INSIST(ver == NULL); + INSIST(event == NULL); +} + +static void +updatedone_action(isc_task_t *task, isc_event_t *event) { + update_event_t *uev = (update_event_t *)event; + ns_client_t *client = (ns_client_t *)event->ev_arg; + + UNUSED(task); + + REQUIRE(event->ev_type == DNS_EVENT_UPDATEDONE); + REQUIRE(task == client->task); + REQUIRE(client->updatehandle == client->handle); + + INSIST(client->nupdates > 0); + switch (uev->result) { + case ISC_R_SUCCESS: + inc_stats(client, uev->zone, ns_statscounter_updatedone); + break; + case DNS_R_REFUSED: + inc_stats(client, uev->zone, ns_statscounter_updaterej); + break; + default: + inc_stats(client, uev->zone, ns_statscounter_updatefail); + break; + } + if (uev->zone != NULL) { + dns_zone_detach(&uev->zone); + } + + client->nupdates--; + + respond(client, uev->result); + + isc_quota_detach(&(isc_quota_t *){ &client->manager->sctx->updquota }); + isc_event_free(&event); + isc_nmhandle_detach(&client->updatehandle); +} + +/*% + * Update forwarding support. + */ +static void +forward_fail(isc_task_t *task, isc_event_t *event) { + ns_client_t *client = (ns_client_t *)event->ev_arg; + + UNUSED(task); + + INSIST(client->nupdates > 0); + client->nupdates--; + respond(client, DNS_R_SERVFAIL); + + isc_quota_detach(&(isc_quota_t *){ &client->manager->sctx->updquota }); + isc_event_free(&event); + isc_nmhandle_detach(&client->updatehandle); +} + +static void +forward_callback(void *arg, isc_result_t result, dns_message_t *answer) { + update_event_t *uev = arg; + ns_client_t *client = uev->ev_arg; + dns_zone_t *zone = uev->zone; + + if (result != ISC_R_SUCCESS) { + INSIST(answer == NULL); + uev->ev_type = DNS_EVENT_UPDATEDONE; + uev->ev_action = forward_fail; + inc_stats(client, zone, ns_statscounter_updatefwdfail); + } else { + uev->ev_type = DNS_EVENT_UPDATEDONE; + uev->ev_action = forward_done; + uev->answer = answer; + inc_stats(client, zone, ns_statscounter_updaterespfwd); + } + + isc_task_send(client->task, ISC_EVENT_PTR(&uev)); + dns_zone_detach(&zone); +} + +static void +forward_done(isc_task_t *task, isc_event_t *event) { + update_event_t *uev = (update_event_t *)event; + ns_client_t *client = (ns_client_t *)event->ev_arg; + + UNUSED(task); + + INSIST(client->nupdates > 0); + client->nupdates--; + ns_client_sendraw(client, uev->answer); + dns_message_detach(&uev->answer); + + isc_quota_detach(&(isc_quota_t *){ &client->manager->sctx->updquota }); + isc_event_free(&event); + isc_nmhandle_detach(&client->reqhandle); + isc_nmhandle_detach(&client->updatehandle); +} + +static void +forward_action(isc_task_t *task, isc_event_t *event) { + update_event_t *uev = (update_event_t *)event; + dns_zone_t *zone = uev->zone; + ns_client_t *client = (ns_client_t *)event->ev_arg; + isc_result_t result; + + result = dns_zone_forwardupdate(zone, client->message, forward_callback, + event); + if (result != ISC_R_SUCCESS) { + uev->ev_type = DNS_EVENT_UPDATEDONE; + uev->ev_action = forward_fail; + isc_task_send(client->task, &event); + inc_stats(client, zone, ns_statscounter_updatefwdfail); + dns_zone_detach(&zone); + } else { + inc_stats(client, zone, ns_statscounter_updatereqfwd); + } + + isc_task_detach(&task); +} + +static isc_result_t +send_forward_event(ns_client_t *client, dns_zone_t *zone) { + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + isc_result_t result = ISC_R_SUCCESS; + update_event_t *event = NULL; + isc_task_t *zonetask = NULL; + + result = checkupdateacl(client, dns_zone_getforwardacl(zone), + "update forwarding", dns_zone_getorigin(zone), + true, false); + if (result != ISC_R_SUCCESS) { + return (result); + } + + result = isc_quota_attach(&client->manager->sctx->updquota, + &(isc_quota_t *){ NULL }); + if (result != ISC_R_SUCCESS) { + update_log(client, zone, LOGLEVEL_PROTOCOL, + "update failed: too many DNS UPDATEs queued (%s)", + isc_result_totext(result)); + ns_stats_increment(client->manager->sctx->nsstats, + ns_statscounter_updatequota); + return (DNS_R_DROP); + } + + event = (update_event_t *)isc_event_allocate( + client->mctx, client, DNS_EVENT_UPDATE, forward_action, NULL, + sizeof(*event)); + event->zone = zone; + event->result = ISC_R_SUCCESS; + + INSIST(client->nupdates == 0); + client->nupdates++; + event->ev_arg = client; + + dns_name_format(dns_zone_getorigin(zone), namebuf, sizeof(namebuf)); + dns_rdataclass_format(dns_zone_getclass(zone), classbuf, + sizeof(classbuf)); + + ns_client_log(client, NS_LOGCATEGORY_UPDATE, NS_LOGMODULE_UPDATE, + LOGLEVEL_PROTOCOL, "forwarding update for zone '%s/%s'", + namebuf, classbuf); + + dns_zone_gettask(zone, &zonetask); + isc_nmhandle_attach(client->handle, &client->updatehandle); + isc_task_send(zonetask, ISC_EVENT_PTR(&event)); + + if (event != NULL) { + isc_event_free(ISC_EVENT_PTR(&event)); + } + return (result); +} diff --git a/lib/ns/version.c b/lib/ns/version.c new file mode 100644 index 0000000..0efa262 --- /dev/null +++ b/lib/ns/version.c @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/*! \file */ + +#include <ns/version.h> + +const char ns_version[] = VERSION; diff --git a/lib/ns/win32/DLLMain.c b/lib/ns/win32/DLLMain.c new file mode 100644 index 0000000..62c6e53 --- /dev/null +++ b/lib/ns/win32/DLLMain.c @@ -0,0 +1,49 @@ +/* + * 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 <signal.h> +#include <windows.h> + +/* + * Called when we enter the DLL + */ +__declspec(dllexport) BOOL WINAPI + DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { + switch (fdwReason) { + /* + * The DLL is loading due to process + * initialization or a call to LoadLibrary. + */ + case DLL_PROCESS_ATTACH: + break; + + /* The attached process creates a new thread. */ + case DLL_THREAD_ATTACH: + break; + + /* The thread of the attached process terminates. */ + case DLL_THREAD_DETACH: + break; + + /* + * The DLL is unloading from a process due to + * process termination or a call to FreeLibrary. + */ + case DLL_PROCESS_DETACH: + break; + + default: + break; + } + return (TRUE); +} diff --git a/lib/ns/win32/libns.def b/lib/ns/win32/libns.def new file mode 100644 index 0000000..50edf86 --- /dev/null +++ b/lib/ns/win32/libns.def @@ -0,0 +1,107 @@ +LIBRARY libns + +; Exported Functions +EXPORTS + +ns__client_put_cb +ns__client_request +ns__client_reset_cb +ns__client_setup +ns__interfacemgr_getif +ns__interfacemgr_nextif +ns__query_sfcache +ns__query_start +ns__client_tcpconn +ns_client_aclmsg +ns_client_addopt +ns_client_checkacl +ns_client_checkaclsilent +ns_client_drop +ns_client_dumprecursing +ns_client_error +ns_client_findversion +ns_client_getdestaddr +ns_client_getnamebuf +ns_client_getsockaddr +ns_client_keepname +ns_client_killoldestquery +ns_client_log +ns_client_logv +ns_client_newdbversion +ns_client_newname +ns_client_newnamebuf +ns_client_newrdataset +ns_client_putrdataset +ns_client_qnamereplace +ns_client_recursing +ns_client_releasename +ns_client_send +ns_client_sendraw +ns_client_settimeout +ns_client_shuttingdown +ns_client_sourceip +ns_clientmgr_create +ns_clientmgr_destroy +ns_clientmgr_shutdown +ns_hook_add +ns_hooktable_create +ns_hooktable_free +ns_hooktable_init +ns_interface_attach +ns_interface_detach +ns_interface_shutdown +ns_interfacemgr_attach +ns_interfacemgr_create +ns_interfacemgr_detach +ns_interfacemgr_dumprecursing +ns_interfacemgr_getaclenv +ns_interfacemgr_getserver +ns_interfacemgr_islistening +ns_interfacemgr_listeningon +ns_interfacemgr_scan +ns_interfacemgr_setbacklog +ns_interfacemgr_setlistenon4 +ns_interfacemgr_setlistenon6 +ns_interfacemgr_shutdown +ns_lib_init +ns_lib_shutdown +ns_listenelt_create +ns_listenelt_destroy +ns_listenlist_attach +ns_listenlist_create +ns_listenlist_default +ns_listenlist_detach +ns_log_init +ns_log_setcontext +ns_notify_start +ns_plugin_check +ns_plugin_expandpath +ns_plugin_register +ns_plugins_create +ns_plugins_free +ns_query_cancel +ns_query_done +ns_query_free +ns_query_init +ns_query_recurse +ns_query_start +ns_server_attach +ns_server_create +ns_server_detach +ns_server_getoption +ns_server_setoption +ns_server_setserverid +ns_sortlist_addrorder1 +ns_sortlist_addrorder2 +ns_sortlist_byaddrsetup +ns_sortlist_setup +ns_stats_attach +ns_stats_create +ns_stats_decrement +ns_stats_detach +ns_stats_get +ns_stats_get_counter +ns_stats_increment +ns_stats_update_if_greater +ns_update_start +ns_xfr_start diff --git a/lib/ns/win32/libns.vcxproj.filters b/lib/ns/win32/libns.vcxproj.filters new file mode 100644 index 0000000..2931412 --- /dev/null +++ b/lib/ns/win32/libns.vcxproj.filters @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="Resource Files"> + <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier> + <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions> + </Filter> + <Filter Include="Header Files"> + <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier> + <Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions> + </Filter> + <Filter Include="Source Files"> + <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier> + <Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions> + </Filter> + </ItemGroup> + <ItemGroup> + <None Include="libns.def" /> + </ItemGroup> + <ItemGroup> + <ClCompile Include="..\client.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\interfacemgr.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\hooks.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\lib.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\listenlist.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\log.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\notify.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\query.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\server.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\sortlist.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\stats.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\update.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\xfrout.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="DLLMain.c"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="version.c"> + <Filter>Source Files</Filter> + </ClCompile> + </ItemGroup> + <ItemGroup> + <ClInclude Include="..\include\ns\client.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\interfacemgr.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\hooks.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\lib.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\listenlist.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\log.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\notify.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\query.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\server.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\sortlist.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\stats.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\types.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\update.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\version.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\include\ns\xfrout.h"> + <Filter>Header Files</Filter> + </ClInclude> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/lib/ns/win32/libns.vcxproj.in b/lib/ns/win32/libns.vcxproj.in new file mode 100644 index 0000000..0249982 --- /dev/null +++ b/lib/ns/win32/libns.vcxproj.in @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" ToolsVersion="@TOOLS_VERSION@" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="Debug|@PLATFORM@"> + <Configuration>Debug</Configuration> + <Platform>@PLATFORM@</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|@PLATFORM@"> + <Configuration>Release</Configuration> + <Platform>@PLATFORM@</Platform> + </ProjectConfiguration> + </ItemGroup> + <PropertyGroup Label="Globals"> + <ProjectGuid>{82ACD33C-E75F-45B8-BB6D-42643A10D7EE}</ProjectGuid> + <Keyword>Win32Proj</Keyword> + <RootNamespace>libns</RootNamespace> + @WINDOWS_TARGET_PLATFORM_VERSION@ + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|@PLATFORM@'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <CharacterSet>MultiByte</CharacterSet> + @PLATFORM_TOOLSET@ + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|@PLATFORM@'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <CharacterSet>MultiByte</CharacterSet> + @PLATFORM_TOOLSET@ + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> + <ImportGroup Label="ExtensionSettings"> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|@PLATFORM@'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|@PLATFORM@'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <PropertyGroup Label="UserMacros" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|@PLATFORM@'"> + <LinkIncremental>true</LinkIncremental> + <OutDir>..\..\..\Build\$(Configuration)\</OutDir> + <IntDir>.\$(Configuration)\</IntDir> + <IntDirSharingDetected>None</IntDirSharingDetected> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|@PLATFORM@'"> + <LinkIncremental>false</LinkIncremental> + <OutDir>..\..\..\Build\$(Configuration)\</OutDir> + <IntDir>.\$(Configuration)\</IntDir> + <IntDirSharingDetected>None</IntDirSharingDetected> + </PropertyGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|@PLATFORM@'"> + <ClCompile> + <PrecompiledHeader> + </PrecompiledHeader> + <WarningLevel>Level4</WarningLevel> + <TreatWarningAsError>false</TreatWarningAsError> + <Optimization>Disabled</Optimization> + <PreprocessorDefinitions>WIN32;@USE_GSSAPI@_DEBUG;_USRDLL;LIBNS_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ForcedIncludeFiles>..\..\..\config.h</ForcedIncludeFiles> + <AdditionalIncludeDirectories>.\;..\..\..\;include;..\include;..\..\isc\win32;..\..\isc\win32\include;..\..\isc\include;..\..\..\lib\dns\include;@LIBXML2_INC@@OPENSSL_INC@@GSSAPI_INC@@GEOIP_INC@%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <FunctionLevelLinking>true</FunctionLevelLinking> + <PrecompiledHeaderOutputFile>.\$(Configuration)\$(TargetName).pch</PrecompiledHeaderOutputFile> + <AssemblerListingLocation>.\$(Configuration)\</AssemblerListingLocation> + <ObjectFileName>.\$(Configuration)\</ObjectFileName> + <ProgramDataBaseFileName>$(OutDir)$(TargetName).pdb</ProgramDataBaseFileName> + <BrowseInformation>true</BrowseInformation> + <CompileAs>CompileAsC</CompileAs> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>true</GenerateDebugInformation> + <OutputFile>..\..\..\Build\$(Configuration)\$(TargetName)$(TargetExt)</OutputFile> + <AdditionalLibraryDirectories>..\..\isc\win32\$(Configuration);..\..\..\lib\dns\win32\$(Configuration);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <AdditionalDependencies>@OPENSSL_LIBCRYPTO@@OPENSSL_LIBSSL@libisc.lib;libdns.lib;@LIBXML2_LIB@@GSSAPI_LIB@@GEOIP_LIB@ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies> + <ModuleDefinitionFile>$(ProjectName).def</ModuleDefinitionFile> + <ImportLibrary>.\$(Configuration)\$(ProjectName).lib</ImportLibrary> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|@PLATFORM@'"> + <ClCompile> + <WarningLevel>Level1</WarningLevel> + <TreatWarningAsError>true</TreatWarningAsError> + <PrecompiledHeader> + </PrecompiledHeader> + <Optimization>MaxSpeed</Optimization> + <FunctionLevelLinking>true</FunctionLevelLinking> + <IntrinsicFunctions>@INTRINSIC@</IntrinsicFunctions> + <PreprocessorDefinitions>WIN32;@USE_GSSAPI@NDEBUG;_USRDLL;LIBNS_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ForcedIncludeFiles>..\..\..\config.h</ForcedIncludeFiles> + <AdditionalIncludeDirectories>.\;..\..\..\;include;..\include;..\..\isc\win32;..\..\isc\win32\include;..\..\isc\include;..\..\..\lib\dns\include;@LIBXML2_INC@@OPENSSL_INC@@GSSAPI_INC@@GEOIP_INC@%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <InlineFunctionExpansion>OnlyExplicitInline</InlineFunctionExpansion> + <StringPooling>true</StringPooling> + <PrecompiledHeaderOutputFile>.\$(Configuration)\$(TargetName).pch</PrecompiledHeaderOutputFile> + <AssemblerListingLocation>.\$(Configuration)\</AssemblerListingLocation> + <ObjectFileName>.\$(Configuration)\</ObjectFileName> + <ProgramDataBaseFileName>$(OutDir)$(TargetName).pdb</ProgramDataBaseFileName> + <WholeProgramOptimization>false</WholeProgramOptimization> + <CompileAs>CompileAsC</CompileAs> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>false</GenerateDebugInformation> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + <OutputFile>..\..\..\Build\$(Configuration)\$(TargetName)$(TargetExt)</OutputFile> + <AdditionalLibraryDirectories>..\..\isc\win32\$(Configuration);..\..\..\lib\dns\win32\$(Configuration);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <AdditionalDependencies>@OPENSSL_LIBCRYPTO@@OPENSSL_LIBSSL@libisc.lib;libdns.lib;@LIBXML2_LIB@@GSSAPI_LIB@@GEOIP_LIB@ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies> + <ModuleDefinitionFile>$(ProjectName).def</ModuleDefinitionFile> + <ImportLibrary>.\$(Configuration)\$(ProjectName).lib</ImportLibrary> + <LinkTimeCodeGeneration>Default</LinkTimeCodeGeneration> + </Link> + </ItemDefinitionGroup> + <ItemGroup> + <None Include="libns.def" /> + </ItemGroup> + <ItemGroup> + <ClCompile Include="..\client.c" /> + <ClCompile Include="..\interfacemgr.c" /> + <ClCompile Include="..\hooks.c" /> + <ClCompile Include="..\lib.c" /> + <ClCompile Include="..\listenlist.c" /> + <ClCompile Include="..\log.c" /> + <ClCompile Include="..\notify.c" /> + <ClCompile Include="..\query.c" /> + <ClCompile Include="..\server.c" /> + <ClCompile Include="..\sortlist.c" /> + <ClCompile Include="..\stats.c" /> + <ClCompile Include="..\update.c" /> + <ClCompile Include="..\xfrout.c" /> + <ClCompile Include="DLLMain.c" /> + <ClCompile Include="version.c" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="..\include\ns\client.h" /> + <ClInclude Include="..\include\ns\interfacemgr.h" /> + <ClInclude Include="..\include\ns\hooks.h" /> + <ClInclude Include="..\include\ns\lib.h" /> + <ClInclude Include="..\include\ns\listenlist.h" /> + <ClInclude Include="..\include\ns\log.h" /> + <ClInclude Include="..\include\ns\notify.h" /> + <ClInclude Include="..\include\ns\query.h" /> + <ClInclude Include="..\include\ns\server.h" /> + <ClInclude Include="..\include\ns\sortlist.h" /> + <ClInclude Include="..\include\ns\stats.h" /> + <ClInclude Include="..\include\ns\types.h" /> + <ClInclude Include="..\include\ns\update.h" /> + <ClInclude Include="..\include\ns\version.h" /> + <ClInclude Include="..\include\ns\xfrout.h" /> + </ItemGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> + <ImportGroup Label="ExtensionTargets"> + </ImportGroup> +</Project> diff --git a/lib/ns/win32/libns.vcxproj.user b/lib/ns/win32/libns.vcxproj.user new file mode 100644 index 0000000..ace9a86 --- /dev/null +++ b/lib/ns/win32/libns.vcxproj.user @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +</Project>
\ No newline at end of file diff --git a/lib/ns/win32/version.c b/lib/ns/win32/version.c new file mode 100644 index 0000000..ac7a537 --- /dev/null +++ b/lib/ns/win32/version.c @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/*! \file */ + +#include <versions.h> + +#include <ns/version.h> + +LIBNS_EXTERNAL_DATA const char ns_version[] = VERSION; +LIBNS_EXTERNAL_DATA const char ns_major[] = MAJOR; +LIBNS_EXTERNAL_DATA const char ns_mapapi[] = MAPAPI; diff --git a/lib/ns/xfrout.c b/lib/ns/xfrout.c new file mode 100644 index 0000000..f0c52f2 --- /dev/null +++ b/lib/ns/xfrout.c @@ -0,0 +1,1813 @@ +/* + * 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 <inttypes.h> +#include <stdbool.h> + +#include <isc/formatcheck.h> +#include <isc/mem.h> +#include <isc/netmgr.h> +#include <isc/print.h> +#include <isc/stats.h> +#include <isc/util.h> + +#include <dns/db.h> +#include <dns/dbiterator.h> +#include <dns/dlz.h> +#include <dns/fixedname.h> +#include <dns/journal.h> +#include <dns/message.h> +#include <dns/peer.h> +#include <dns/rdataclass.h> +#include <dns/rdatalist.h> +#include <dns/rdataset.h> +#include <dns/rdatasetiter.h> +#include <dns/result.h> +#include <dns/rriterator.h> +#include <dns/soa.h> +#include <dns/stats.h> +#include <dns/tsig.h> +#include <dns/view.h> +#include <dns/zone.h> +#include <dns/zt.h> + +#include <ns/client.h> +#include <ns/log.h> +#include <ns/server.h> +#include <ns/stats.h> +#include <ns/xfrout.h> + +/*! \file + * \brief + * Outgoing AXFR and IXFR. + */ + +/* + * TODO: + * - IXFR over UDP + */ + +#define XFROUT_COMMON_LOGARGS \ + ns_lctx, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT + +#define XFROUT_PROTOCOL_LOGARGS XFROUT_COMMON_LOGARGS, ISC_LOG_INFO + +#define XFROUT_DEBUG_LOGARGS(n) XFROUT_COMMON_LOGARGS, ISC_LOG_DEBUG(n) + +#define XFROUT_RR_LOGARGS XFROUT_COMMON_LOGARGS, XFROUT_RR_LOGLEVEL + +#define XFROUT_RR_LOGLEVEL ISC_LOG_DEBUG(8) + +/*% + * Fail unconditionally and log as a client error. + * The test against ISC_R_SUCCESS is there to keep the Solaris compiler + * from complaining about "end-of-loop code not reached". + */ +#define FAILC(code, msg) \ + do { \ + result = (code); \ + ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, \ + NS_LOGMODULE_XFER_OUT, ISC_LOG_INFO, \ + "bad zone transfer request: %s (%s)", msg, \ + isc_result_totext(code)); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +#define FAILQ(code, msg, question, rdclass) \ + do { \ + char _buf1[DNS_NAME_FORMATSIZE]; \ + char _buf2[DNS_RDATACLASS_FORMATSIZE]; \ + result = (code); \ + dns_name_format(question, _buf1, sizeof(_buf1)); \ + dns_rdataclass_format(rdclass, _buf2, sizeof(_buf2)); \ + ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, \ + NS_LOGMODULE_XFER_OUT, ISC_LOG_INFO, \ + "bad zone transfer request: '%s/%s': %s (%s)", \ + _buf1, _buf2, msg, isc_result_totext(code)); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +#define CHECK(op) \ + do { \ + result = (op); \ + if (result != ISC_R_SUCCESS) \ + goto failure; \ + } while (0) + +/**************************************************************************/ + +static void +inc_stats(ns_client_t *client, dns_zone_t *zone, isc_statscounter_t counter) { + ns_stats_increment(client->sctx->nsstats, counter); + if (zone != NULL) { + isc_stats_t *zonestats = dns_zone_getrequeststats(zone); + if (zonestats != NULL) { + isc_stats_increment(zonestats, counter); + } + } +} + +/**************************************************************************/ + +/*% Log an RR (for debugging) */ + +static void +log_rr(dns_name_t *name, dns_rdata_t *rdata, uint32_t ttl) { + isc_result_t result; + isc_buffer_t buf; + char mem[2000]; + dns_rdatalist_t rdl; + dns_rdataset_t rds; + dns_rdata_t rd = DNS_RDATA_INIT; + + dns_rdatalist_init(&rdl); + rdl.type = rdata->type; + rdl.rdclass = rdata->rdclass; + rdl.ttl = ttl; + if (rdata->type == dns_rdatatype_sig || + rdata->type == dns_rdatatype_rrsig) + { + rdl.covers = dns_rdata_covers(rdata); + } else { + rdl.covers = dns_rdatatype_none; + } + dns_rdataset_init(&rds); + dns_rdata_init(&rd); + dns_rdata_clone(rdata, &rd); + ISC_LIST_APPEND(rdl.rdata, &rd, link); + RUNTIME_CHECK(dns_rdatalist_tordataset(&rdl, &rds) == ISC_R_SUCCESS); + + isc_buffer_init(&buf, mem, sizeof(mem)); + result = dns_rdataset_totext(&rds, name, false, false, &buf); + + /* + * We could use xfrout_log(), but that would produce + * very long lines with a repetitive prefix. + */ + if (result == ISC_R_SUCCESS) { + /* + * Get rid of final newline. + */ + INSIST(buf.used >= 1 && + ((char *)buf.base)[buf.used - 1] == '\n'); + buf.used--; + + isc_log_write(XFROUT_RR_LOGARGS, "%.*s", + (int)isc_buffer_usedlength(&buf), + (char *)isc_buffer_base(&buf)); + } else { + isc_log_write(XFROUT_RR_LOGARGS, "<RR too large to print>"); + } +} + +/**************************************************************************/ +/* + * An 'rrstream_t' is a polymorphic iterator that returns + * a stream of resource records. There are multiple implementations, + * e.g. for generating AXFR and IXFR records streams. + */ + +typedef struct rrstream_methods rrstream_methods_t; + +typedef struct rrstream { + isc_mem_t *mctx; + rrstream_methods_t *methods; +} rrstream_t; + +struct rrstream_methods { + isc_result_t (*first)(rrstream_t *); + isc_result_t (*next)(rrstream_t *); + void (*current)(rrstream_t *, dns_name_t **, uint32_t *, + dns_rdata_t **); + void (*pause)(rrstream_t *); + void (*destroy)(rrstream_t **); +}; + +static void +rrstream_noop_pause(rrstream_t *rs) { + UNUSED(rs); +} + +/**************************************************************************/ +/* + * An 'ixfr_rrstream_t' is an 'rrstream_t' that returns + * an IXFR-like RR stream from a journal file. + * + * The SOA at the beginning of each sequence of additions + * or deletions are included in the stream, but the extra + * SOAs at the beginning and end of the entire transfer are + * not included. + */ + +typedef struct ixfr_rrstream { + rrstream_t common; + dns_journal_t *journal; +} ixfr_rrstream_t; + +/* Forward declarations. */ +static void +ixfr_rrstream_destroy(rrstream_t **sp); + +static rrstream_methods_t ixfr_rrstream_methods; + +/* + * Returns: anything dns_journal_open() or dns_journal_iter_init() + * may return. + */ + +static isc_result_t +ixfr_rrstream_create(isc_mem_t *mctx, const char *journal_filename, + uint32_t begin_serial, uint32_t end_serial, size_t *sizep, + rrstream_t **sp) { + isc_result_t result; + ixfr_rrstream_t *s = NULL; + + INSIST(sp != NULL && *sp == NULL); + + s = isc_mem_get(mctx, sizeof(*s)); + s->common.mctx = NULL; + isc_mem_attach(mctx, &s->common.mctx); + s->common.methods = &ixfr_rrstream_methods; + s->journal = NULL; + + CHECK(dns_journal_open(mctx, journal_filename, DNS_JOURNAL_READ, + &s->journal)); + CHECK(dns_journal_iter_init(s->journal, begin_serial, end_serial, + sizep)); + + *sp = (rrstream_t *)s; + return (ISC_R_SUCCESS); + +failure: + ixfr_rrstream_destroy((rrstream_t **)(void *)&s); + return (result); +} + +static isc_result_t +ixfr_rrstream_first(rrstream_t *rs) { + ixfr_rrstream_t *s = (ixfr_rrstream_t *)rs; + return (dns_journal_first_rr(s->journal)); +} + +static isc_result_t +ixfr_rrstream_next(rrstream_t *rs) { + ixfr_rrstream_t *s = (ixfr_rrstream_t *)rs; + return (dns_journal_next_rr(s->journal)); +} + +static void +ixfr_rrstream_current(rrstream_t *rs, dns_name_t **name, uint32_t *ttl, + dns_rdata_t **rdata) { + ixfr_rrstream_t *s = (ixfr_rrstream_t *)rs; + dns_journal_current_rr(s->journal, name, ttl, rdata); +} + +static void +ixfr_rrstream_destroy(rrstream_t **rsp) { + ixfr_rrstream_t *s = (ixfr_rrstream_t *)*rsp; + if (s->journal != NULL) { + dns_journal_destroy(&s->journal); + } + isc_mem_putanddetach(&s->common.mctx, s, sizeof(*s)); +} + +static rrstream_methods_t ixfr_rrstream_methods = { + ixfr_rrstream_first, ixfr_rrstream_next, ixfr_rrstream_current, + rrstream_noop_pause, ixfr_rrstream_destroy +}; + +/**************************************************************************/ +/* + * An 'axfr_rrstream_t' is an 'rrstream_t' that returns + * an AXFR-like RR stream from a database. + * + * The SOAs at the beginning and end of the transfer are + * not included in the stream. + */ + +typedef struct axfr_rrstream { + rrstream_t common; + dns_rriterator_t it; + bool it_valid; +} axfr_rrstream_t; + +/* + * Forward declarations. + */ +static void +axfr_rrstream_destroy(rrstream_t **rsp); + +static rrstream_methods_t axfr_rrstream_methods; + +static isc_result_t +axfr_rrstream_create(isc_mem_t *mctx, dns_db_t *db, dns_dbversion_t *ver, + rrstream_t **sp) { + axfr_rrstream_t *s; + isc_result_t result; + + INSIST(sp != NULL && *sp == NULL); + + s = isc_mem_get(mctx, sizeof(*s)); + s->common.mctx = NULL; + isc_mem_attach(mctx, &s->common.mctx); + s->common.methods = &axfr_rrstream_methods; + s->it_valid = false; + + CHECK(dns_rriterator_init(&s->it, db, ver, 0)); + s->it_valid = true; + + *sp = (rrstream_t *)s; + return (ISC_R_SUCCESS); + +failure: + axfr_rrstream_destroy((rrstream_t **)(void *)&s); + return (result); +} + +static isc_result_t +axfr_rrstream_first(rrstream_t *rs) { + axfr_rrstream_t *s = (axfr_rrstream_t *)rs; + isc_result_t result; + result = dns_rriterator_first(&s->it); + if (result != ISC_R_SUCCESS) { + return (result); + } + /* Skip SOA records. */ + for (;;) { + dns_name_t *name_dummy = NULL; + uint32_t ttl_dummy; + dns_rdata_t *rdata = NULL; + dns_rriterator_current(&s->it, &name_dummy, &ttl_dummy, NULL, + &rdata); + if (rdata->type != dns_rdatatype_soa) { + break; + } + result = dns_rriterator_next(&s->it); + if (result != ISC_R_SUCCESS) { + break; + } + } + return (result); +} + +static isc_result_t +axfr_rrstream_next(rrstream_t *rs) { + axfr_rrstream_t *s = (axfr_rrstream_t *)rs; + isc_result_t result; + + /* Skip SOA records. */ + for (;;) { + dns_name_t *name_dummy = NULL; + uint32_t ttl_dummy; + dns_rdata_t *rdata = NULL; + result = dns_rriterator_next(&s->it); + if (result != ISC_R_SUCCESS) { + break; + } + dns_rriterator_current(&s->it, &name_dummy, &ttl_dummy, NULL, + &rdata); + if (rdata->type != dns_rdatatype_soa) { + break; + } + } + return (result); +} + +static void +axfr_rrstream_current(rrstream_t *rs, dns_name_t **name, uint32_t *ttl, + dns_rdata_t **rdata) { + axfr_rrstream_t *s = (axfr_rrstream_t *)rs; + dns_rriterator_current(&s->it, name, ttl, NULL, rdata); +} + +static void +axfr_rrstream_pause(rrstream_t *rs) { + axfr_rrstream_t *s = (axfr_rrstream_t *)rs; + dns_rriterator_pause(&s->it); +} + +static void +axfr_rrstream_destroy(rrstream_t **rsp) { + axfr_rrstream_t *s = (axfr_rrstream_t *)*rsp; + if (s->it_valid) { + dns_rriterator_destroy(&s->it); + } + isc_mem_putanddetach(&s->common.mctx, s, sizeof(*s)); +} + +static rrstream_methods_t axfr_rrstream_methods = { + axfr_rrstream_first, axfr_rrstream_next, axfr_rrstream_current, + axfr_rrstream_pause, axfr_rrstream_destroy +}; + +/**************************************************************************/ +/* + * An 'soa_rrstream_t' is a degenerate 'rrstream_t' that returns + * a single SOA record. + */ + +typedef struct soa_rrstream { + rrstream_t common; + dns_difftuple_t *soa_tuple; +} soa_rrstream_t; + +/* + * Forward declarations. + */ +static void +soa_rrstream_destroy(rrstream_t **rsp); + +static rrstream_methods_t soa_rrstream_methods; + +static isc_result_t +soa_rrstream_create(isc_mem_t *mctx, dns_db_t *db, dns_dbversion_t *ver, + rrstream_t **sp) { + soa_rrstream_t *s; + isc_result_t result; + + INSIST(sp != NULL && *sp == NULL); + + s = isc_mem_get(mctx, sizeof(*s)); + s->common.mctx = NULL; + isc_mem_attach(mctx, &s->common.mctx); + s->common.methods = &soa_rrstream_methods; + s->soa_tuple = NULL; + + CHECK(dns_db_createsoatuple(db, ver, mctx, DNS_DIFFOP_EXISTS, + &s->soa_tuple)); + + *sp = (rrstream_t *)s; + return (ISC_R_SUCCESS); + +failure: + soa_rrstream_destroy((rrstream_t **)(void *)&s); + return (result); +} + +static isc_result_t +soa_rrstream_first(rrstream_t *rs) { + UNUSED(rs); + return (ISC_R_SUCCESS); +} + +static isc_result_t +soa_rrstream_next(rrstream_t *rs) { + UNUSED(rs); + return (ISC_R_NOMORE); +} + +static void +soa_rrstream_current(rrstream_t *rs, dns_name_t **name, uint32_t *ttl, + dns_rdata_t **rdata) { + soa_rrstream_t *s = (soa_rrstream_t *)rs; + *name = &s->soa_tuple->name; + *ttl = s->soa_tuple->ttl; + *rdata = &s->soa_tuple->rdata; +} + +static void +soa_rrstream_destroy(rrstream_t **rsp) { + soa_rrstream_t *s = (soa_rrstream_t *)*rsp; + if (s->soa_tuple != NULL) { + dns_difftuple_free(&s->soa_tuple); + } + isc_mem_putanddetach(&s->common.mctx, s, sizeof(*s)); +} + +static rrstream_methods_t soa_rrstream_methods = { + soa_rrstream_first, soa_rrstream_next, soa_rrstream_current, + rrstream_noop_pause, soa_rrstream_destroy +}; + +/**************************************************************************/ +/* + * A 'compound_rrstream_t' objects owns a soa_rrstream + * and another rrstream, the "data stream". It returns + * a concatenated stream consisting of the soa_rrstream, then + * the data stream, then the soa_rrstream again. + * + * The component streams are owned by the compound_rrstream_t + * and are destroyed with it. + */ + +typedef struct compound_rrstream { + rrstream_t common; + rrstream_t *components[3]; + int state; + isc_result_t result; +} compound_rrstream_t; + +/* + * Forward declarations. + */ +static void +compound_rrstream_destroy(rrstream_t **rsp); + +static isc_result_t +compound_rrstream_next(rrstream_t *rs); + +static rrstream_methods_t compound_rrstream_methods; + +/* + * Requires: + * soa_stream != NULL && *soa_stream != NULL + * data_stream != NULL && *data_stream != NULL + * sp != NULL && *sp == NULL + * + * Ensures: + * *soa_stream == NULL + * *data_stream == NULL + * *sp points to a valid compound_rrstream_t + * The soa and data streams will be destroyed + * when the compound_rrstream_t is destroyed. + */ +static isc_result_t +compound_rrstream_create(isc_mem_t *mctx, rrstream_t **soa_stream, + rrstream_t **data_stream, rrstream_t **sp) { + compound_rrstream_t *s; + + INSIST(sp != NULL && *sp == NULL); + + s = isc_mem_get(mctx, sizeof(*s)); + s->common.mctx = NULL; + isc_mem_attach(mctx, &s->common.mctx); + s->common.methods = &compound_rrstream_methods; + s->components[0] = *soa_stream; + s->components[1] = *data_stream; + s->components[2] = *soa_stream; + s->state = -1; + s->result = ISC_R_FAILURE; + + *data_stream = NULL; + *soa_stream = NULL; + *sp = (rrstream_t *)s; + return (ISC_R_SUCCESS); +} + +static isc_result_t +compound_rrstream_first(rrstream_t *rs) { + compound_rrstream_t *s = (compound_rrstream_t *)rs; + s->state = 0; + do { + rrstream_t *curstream = s->components[s->state]; + s->result = curstream->methods->first(curstream); + } while (s->result == ISC_R_NOMORE && s->state < 2); + return (s->result); +} + +static isc_result_t +compound_rrstream_next(rrstream_t *rs) { + compound_rrstream_t *s = (compound_rrstream_t *)rs; + rrstream_t *curstream = s->components[s->state]; + s->result = curstream->methods->next(curstream); + while (s->result == ISC_R_NOMORE) { + /* + * Make sure locks held by the current stream + * are released before we switch streams. + */ + curstream->methods->pause(curstream); + if (s->state == 2) { + return (ISC_R_NOMORE); + } + s->state++; + curstream = s->components[s->state]; + s->result = curstream->methods->first(curstream); + } + return (s->result); +} + +static void +compound_rrstream_current(rrstream_t *rs, dns_name_t **name, uint32_t *ttl, + dns_rdata_t **rdata) { + compound_rrstream_t *s = (compound_rrstream_t *)rs; + rrstream_t *curstream; + INSIST(0 <= s->state && s->state < 3); + INSIST(s->result == ISC_R_SUCCESS); + curstream = s->components[s->state]; + curstream->methods->current(curstream, name, ttl, rdata); +} + +static void +compound_rrstream_pause(rrstream_t *rs) { + compound_rrstream_t *s = (compound_rrstream_t *)rs; + rrstream_t *curstream; + INSIST(0 <= s->state && s->state < 3); + curstream = s->components[s->state]; + curstream->methods->pause(curstream); +} + +static void +compound_rrstream_destroy(rrstream_t **rsp) { + compound_rrstream_t *s = (compound_rrstream_t *)*rsp; + s->components[0]->methods->destroy(&s->components[0]); + s->components[1]->methods->destroy(&s->components[1]); + s->components[2] = NULL; /* Copy of components[0]. */ + isc_mem_putanddetach(&s->common.mctx, s, sizeof(*s)); +} + +static rrstream_methods_t compound_rrstream_methods = { + compound_rrstream_first, compound_rrstream_next, + compound_rrstream_current, compound_rrstream_pause, + compound_rrstream_destroy +}; + +/**************************************************************************/ + +/*% + * Structure holding outgoing transfer statistics + */ +struct xfr_stats { + uint64_t nmsg; /*%< Number of messages sent */ + uint64_t nrecs; /*%< Number of records sent */ + uint64_t nbytes; /*%< Number of bytes sent */ + isc_time_t start; /*%< Start time of the transfer */ + isc_time_t end; /*%< End time of the transfer */ +}; + +/*% + * An 'xfrout_ctx_t' contains the state of an outgoing AXFR or IXFR + * in progress. + */ +typedef struct { + isc_mem_t *mctx; + ns_client_t *client; + unsigned int id; /* ID of request */ + dns_name_t *qname; /* Question name of request */ + dns_rdatatype_t qtype; /* dns_rdatatype_{a,i}xfr */ + dns_rdataclass_t qclass; + dns_zone_t *zone; /* (necessary for stats) */ + dns_db_t *db; + dns_dbversion_t *ver; + isc_quota_t *quota; + rrstream_t *stream; /* The XFR RR stream */ + bool question_added; /* QUESTION section sent? */ + bool end_of_stream; /* EOS has been reached */ + isc_buffer_t buf; /* Buffer for message owner + * names and rdatas */ + isc_buffer_t txbuf; /* Transmit message buffer */ + size_t cbytes; /* Length of current message */ + void *txmem; + unsigned int txmemlen; + dns_tsigkey_t *tsigkey; /* Key used to create TSIG */ + isc_buffer_t *lasttsig; /* the last TSIG */ + bool verified_tsig; /* verified request MAC */ + bool many_answers; + int sends; /* Send in progress */ + bool shuttingdown; + bool poll; + const char *mnemonic; /* Style of transfer */ + uint32_t end_serial; /* Serial number after XFR is done */ + struct xfr_stats stats; /*%< Transfer statistics */ + + /* Timeouts */ + uint64_t maxtime; /*%< Maximum XFR timeout (in ms) */ + isc_nm_timer_t *maxtime_timer; + + uint64_t idletime; /*%< XFR idle timeout (in ms) */ +} xfrout_ctx_t; + +static void +xfrout_ctx_create(isc_mem_t *mctx, ns_client_t *client, unsigned int id, + dns_name_t *qname, dns_rdatatype_t qtype, + dns_rdataclass_t qclass, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, isc_quota_t *quota, rrstream_t *stream, + dns_tsigkey_t *tsigkey, isc_buffer_t *lasttsig, + bool verified_tsig, unsigned int maxtime, + unsigned int idletime, bool many_answers, + xfrout_ctx_t **xfrp); + +static void +sendstream(xfrout_ctx_t *xfr); + +static void +xfrout_senddone(isc_nmhandle_t *handle, isc_result_t result, void *arg); + +static void +xfrout_fail(xfrout_ctx_t *xfr, isc_result_t result, const char *msg); + +static void +xfrout_maybe_destroy(xfrout_ctx_t *xfr); + +static void +xfrout_ctx_destroy(xfrout_ctx_t **xfrp); + +static void +xfrout_client_timeout(void *arg, isc_result_t result); + +static void +xfrout_log1(ns_client_t *client, dns_name_t *zonename, dns_rdataclass_t rdclass, + int level, const char *fmt, ...) ISC_FORMAT_PRINTF(5, 6); + +static void +xfrout_log(xfrout_ctx_t *xfr, int level, const char *fmt, ...) + ISC_FORMAT_PRINTF(3, 4); + +/**************************************************************************/ + +void +ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { + isc_result_t result; + dns_name_t *question_name; + dns_rdataset_t *question_rdataset; + dns_zone_t *zone = NULL, *raw = NULL, *mayberaw; + dns_db_t *db = NULL; + dns_dbversion_t *ver = NULL; + dns_rdataclass_t question_class; + rrstream_t *soa_stream = NULL; + rrstream_t *data_stream = NULL; + rrstream_t *stream = NULL; + dns_difftuple_t *current_soa_tuple = NULL; + dns_name_t *soa_name; + dns_rdataset_t *soa_rdataset; + dns_rdata_t soa_rdata = DNS_RDATA_INIT; + bool have_soa = false; + const char *mnemonic = NULL; + isc_mem_t *mctx = client->mctx; + dns_message_t *request = client->message; + xfrout_ctx_t *xfr = NULL; + isc_quota_t *quota = NULL; + dns_transfer_format_t format = client->view->transfer_format; + isc_netaddr_t na; + dns_peer_t *peer = NULL; + isc_buffer_t *tsigbuf = NULL; + char *journalfile; + char msg[NS_CLIENT_ACLMSGSIZE("zone transfer")]; + char keyname[DNS_NAME_FORMATSIZE]; + bool is_poll = false; + bool is_dlz = false; + bool is_ixfr = false; + bool useviewacl = false; + uint32_t begin_serial = 0, current_serial; + + switch (reqtype) { + case dns_rdatatype_axfr: + mnemonic = "AXFR"; + break; + case dns_rdatatype_ixfr: + mnemonic = "IXFR"; + break; + default: + UNREACHABLE(); + } + + ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, + ISC_LOG_DEBUG(6), "%s request", mnemonic); + /* + * Apply quota. + */ + result = isc_quota_attach(&client->sctx->xfroutquota, "a); + if (result != ISC_R_SUCCESS) { + isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, + "%s request denied: %s", mnemonic, + isc_result_totext(result)); + goto failure; + } + + /* + * Interpret the question section. + */ + result = dns_message_firstname(request, DNS_SECTION_QUESTION); + INSIST(result == ISC_R_SUCCESS); + + /* + * The question section must contain exactly one question, and + * it must be for AXFR/IXFR as appropriate. + */ + question_name = NULL; + dns_message_currentname(request, DNS_SECTION_QUESTION, &question_name); + question_rdataset = ISC_LIST_HEAD(question_name->list); + question_class = question_rdataset->rdclass; + INSIST(question_rdataset->type == reqtype); + if (ISC_LIST_NEXT(question_rdataset, link) != NULL) { + FAILC(DNS_R_FORMERR, "multiple questions"); + } + result = dns_message_nextname(request, DNS_SECTION_QUESTION); + if (result != ISC_R_NOMORE) { + FAILC(DNS_R_FORMERR, "multiple questions"); + } + + result = dns_zt_find(client->view->zonetable, question_name, 0, NULL, + &zone); + + if (result != ISC_R_SUCCESS || dns_zone_gettype(zone) == dns_zone_dlz) { + /* + * The normal zone table does not have a match, or this is + * marked in the zone table as a DLZ zone. Check the DLZ + * databases for a match. + */ + if (!ISC_LIST_EMPTY(client->view->dlz_searched)) { + result = dns_dlzallowzonexfr(client->view, + question_name, + &client->peeraddr, &db); + if (result == ISC_R_DEFAULT) { + useviewacl = true; + result = ISC_R_SUCCESS; + } + if (result == ISC_R_NOPERM) { + char _buf1[DNS_NAME_FORMATSIZE]; + char _buf2[DNS_RDATACLASS_FORMATSIZE]; + + result = DNS_R_REFUSED; + dns_name_format(question_name, _buf1, + sizeof(_buf1)); + dns_rdataclass_format(question_class, _buf2, + sizeof(_buf2)); + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_XFER_OUT, + ISC_LOG_ERROR, + "zone transfer '%s/%s' denied", + _buf1, _buf2); + goto failure; + } + if (result != ISC_R_SUCCESS) { + FAILQ(DNS_R_NOTAUTH, "non-authoritative zone", + question_name, question_class); + } + is_dlz = true; + } else { + /* + * not DLZ and not in normal zone table, we are + * not authoritative + */ + FAILQ(DNS_R_NOTAUTH, "non-authoritative zone", + question_name, question_class); + } + } else { + /* zone table has a match */ + switch (dns_zone_gettype(zone)) { + /* + * Master, slave, and mirror zones are OK for transfer. + */ + case dns_zone_primary: + case dns_zone_secondary: + case dns_zone_mirror: + case dns_zone_dlz: + break; + default: + FAILQ(DNS_R_NOTAUTH, "non-authoritative zone", + question_name, question_class); + } + CHECK(dns_zone_getdb(zone, &db)); + dns_db_currentversion(db, &ver); + } + + xfrout_log1(client, question_name, question_class, ISC_LOG_DEBUG(6), + "%s question section OK", mnemonic); + + /* + * Check the authority section. Look for a SOA record with + * the same name and class as the question. + */ + for (result = dns_message_firstname(request, DNS_SECTION_AUTHORITY); + result == ISC_R_SUCCESS; + result = dns_message_nextname(request, DNS_SECTION_AUTHORITY)) + { + soa_name = NULL; + dns_message_currentname(request, DNS_SECTION_AUTHORITY, + &soa_name); + + /* + * Ignore data whose owner name is not the zone apex. + */ + if (!dns_name_equal(soa_name, question_name)) { + continue; + } + + for (soa_rdataset = ISC_LIST_HEAD(soa_name->list); + soa_rdataset != NULL; + soa_rdataset = ISC_LIST_NEXT(soa_rdataset, link)) + { + /* + * Ignore non-SOA data. + */ + if (soa_rdataset->type != dns_rdatatype_soa) { + continue; + } + if (soa_rdataset->rdclass != question_class) { + continue; + } + + CHECK(dns_rdataset_first(soa_rdataset)); + dns_rdataset_current(soa_rdataset, &soa_rdata); + result = dns_rdataset_next(soa_rdataset); + if (result == ISC_R_SUCCESS) { + FAILC(DNS_R_FORMERR, "IXFR authority section " + "has multiple SOAs"); + } + have_soa = true; + goto got_soa; + } + } +got_soa: + if (result != ISC_R_NOMORE) { + CHECK(result); + } + + xfrout_log1(client, question_name, question_class, ISC_LOG_DEBUG(6), + "%s authority section OK", mnemonic); + + /* + * If not a DLZ zone or we are falling back to the view's transfer + * ACL, decide whether to allow this transfer. + */ + if (!is_dlz || useviewacl) { + dns_acl_t *acl; + + ns_client_aclmsg("zone transfer", question_name, reqtype, + client->view->rdclass, msg, sizeof(msg)); + if (useviewacl) { + acl = client->view->transferacl; + } else { + acl = dns_zone_getxfracl(zone); + } + CHECK(ns_client_checkacl(client, NULL, msg, acl, true, + ISC_LOG_ERROR)); + } + + /* + * AXFR over UDP is not possible. + */ + if (reqtype == dns_rdatatype_axfr && + (client->attributes & NS_CLIENTATTR_TCP) == 0) + { + FAILC(DNS_R_FORMERR, "attempted AXFR over UDP"); + } + + /* + * Look up the requesting server in the peer table. + */ + isc_netaddr_fromsockaddr(&na, &client->peeraddr); + (void)dns_peerlist_peerbyaddr(client->view->peers, &na, &peer); + + /* + * Decide on the transfer format (one-answer or many-answers). + */ + if (peer != NULL) { + (void)dns_peer_gettransferformat(peer, &format); + } + + /* + * Get a dynamically allocated copy of the current SOA. + */ + if (is_dlz) { + dns_db_currentversion(db, &ver); + } + + CHECK(dns_db_createsoatuple(db, ver, mctx, DNS_DIFFOP_EXISTS, + ¤t_soa_tuple)); + + current_serial = dns_soa_getserial(¤t_soa_tuple->rdata); + if (reqtype == dns_rdatatype_ixfr) { + size_t jsize; + uint64_t dbsize; + + /* + * Outgoing IXFR may have been disabled for this peer + * or globally. + */ + if ((client->attributes & NS_CLIENTATTR_TCP) != 0) { + bool provide_ixfr; + + provide_ixfr = client->view->provideixfr; + if (peer != NULL) { + (void)dns_peer_getprovideixfr(peer, + &provide_ixfr); + } + if (provide_ixfr == false) { + goto axfr_fallback; + } + } + + if (!have_soa) { + FAILC(DNS_R_FORMERR, "IXFR request missing SOA"); + } + + begin_serial = dns_soa_getserial(&soa_rdata); + + /* + * RFC1995 says "If an IXFR query with the same or + * newer version number than that of the server + * is received, it is replied to with a single SOA + * record of the server's current version, just as + * in AXFR". The claim about AXFR is incorrect, + * but other than that, we do as the RFC says. + * + * Sending a single SOA record is also how we refuse + * IXFR over UDP (currently, we always do). + */ + if (DNS_SERIAL_GE(begin_serial, current_serial) || + (client->attributes & NS_CLIENTATTR_TCP) == 0) + { + CHECK(soa_rrstream_create(mctx, db, ver, &stream)); + is_poll = true; + goto have_stream; + } + + /* + * Outgoing IXFR may have been disabled for this peer + * or globally. + */ + if ((client->attributes & NS_CLIENTATTR_TCP) != 0) { + bool provide_ixfr; + + provide_ixfr = client->view->provideixfr; + if (peer != NULL) { + (void)dns_peer_getprovideixfr(peer, + &provide_ixfr); + } + if (!provide_ixfr) { + xfrout_log1(client, question_name, + question_class, ISC_LOG_DEBUG(4), + "IXFR delta response disabled due " + "to 'provide-ixfr no;' being set"); + mnemonic = "AXFR-style IXFR"; + goto axfr_fallback; + } + } + + journalfile = is_dlz ? NULL : dns_zone_getjournal(zone); + if (journalfile != NULL) { + result = ixfr_rrstream_create( + mctx, journalfile, begin_serial, current_serial, + &jsize, &data_stream); + } else { + result = ISC_R_NOTFOUND; + } + if (result == ISC_R_NOTFOUND || result == ISC_R_RANGE) { + xfrout_log1(client, question_name, question_class, + ISC_LOG_INFO, + "IXFR version not in journal, " + "falling back to AXFR"); + mnemonic = "AXFR-style IXFR"; + goto axfr_fallback; + } + CHECK(result); + + result = dns_db_getsize(db, ver, NULL, &dbsize); + if (result == ISC_R_SUCCESS) { + uint32_t ratio = dns_zone_getixfrratio(zone); + if (ratio != 0 && ((100 * jsize) / dbsize) > ratio) { + data_stream->methods->destroy(&data_stream); + data_stream = NULL; + xfrout_log1(client, question_name, + question_class, ISC_LOG_INFO, + "IXFR delta size (%zu bytes) " + "exceeds the maximum ratio to " + "database size " + "(%" PRIu64 " bytes), " + "falling back to AXFR", + jsize, dbsize); + mnemonic = "AXFR-style IXFR"; + goto axfr_fallback; + } else { + xfrout_log1(client, question_name, + question_class, ISC_LOG_DEBUG(4), + "IXFR delta size (%zu bytes); " + "database size " + "(%" PRIu64 " bytes)", + jsize, dbsize); + } + } + is_ixfr = true; + } else { + axfr_fallback: + CHECK(axfr_rrstream_create(mctx, db, ver, &data_stream)); + } + + /* + * Bracket the data stream with SOAs. + */ + CHECK(soa_rrstream_create(mctx, db, ver, &soa_stream)); + CHECK(compound_rrstream_create(mctx, &soa_stream, &data_stream, + &stream)); + soa_stream = NULL; + data_stream = NULL; + +have_stream: + CHECK(dns_message_getquerytsig(request, mctx, &tsigbuf)); + /* + * Create the xfrout context object. This transfers the ownership + * of "stream", "db", "ver", and "quota" to the xfrout context object. + */ + + if (is_dlz) { + xfrout_ctx_create(mctx, client, request->id, question_name, + reqtype, question_class, zone, db, ver, quota, + stream, dns_message_gettsigkey(request), + tsigbuf, request->verified_sig, 3600, 3600, + (format == dns_many_answers) ? true : false, + &xfr); + } else { + xfrout_ctx_create( + mctx, client, request->id, question_name, reqtype, + question_class, zone, db, ver, quota, stream, + dns_message_gettsigkey(request), tsigbuf, + request->verified_sig, dns_zone_getmaxxfrout(zone), + dns_zone_getidleout(zone), + (format == dns_many_answers) ? true : false, &xfr); + } + + xfr->end_serial = current_serial; + xfr->mnemonic = mnemonic; + stream = NULL; + quota = NULL; + + CHECK(xfr->stream->methods->first(xfr->stream)); + + if (xfr->tsigkey != NULL) { + dns_name_format(&xfr->tsigkey->name, keyname, sizeof(keyname)); + } else { + keyname[0] = '\0'; + } + xfr->poll = is_poll; + if (is_poll) { + xfr->mnemonic = "IXFR poll response"; + xfrout_log1(client, question_name, question_class, + ISC_LOG_DEBUG(1), "IXFR poll up to date%s%s", + (xfr->tsigkey != NULL) ? ": TSIG " : "", keyname); + } else if (is_ixfr) { + xfrout_log1(client, question_name, question_class, ISC_LOG_INFO, + "%s started%s%s (serial %u -> %u)", mnemonic, + (xfr->tsigkey != NULL) ? ": TSIG " : "", keyname, + begin_serial, current_serial); + } else { + xfrout_log1(client, question_name, question_class, ISC_LOG_INFO, + "%s started%s%s (serial %u)", mnemonic, + (xfr->tsigkey != NULL) ? ": TSIG " : "", keyname, + current_serial); + } + + if (zone != NULL) { + dns_zone_getraw(zone, &raw); + mayberaw = (raw != NULL) ? raw : zone; + if ((client->attributes & NS_CLIENTATTR_WANTEXPIRE) != 0 && + (dns_zone_gettype(mayberaw) == dns_zone_secondary || + dns_zone_gettype(mayberaw) == dns_zone_mirror)) + { + isc_time_t expiretime; + uint32_t secs; + dns_zone_getexpiretime(zone, &expiretime); + secs = isc_time_seconds(&expiretime); + if (secs >= client->now && result == ISC_R_SUCCESS) { + client->attributes |= NS_CLIENTATTR_HAVEEXPIRE; + client->expire = secs - client->now; + } + } + if (raw != NULL) { + dns_zone_detach(&raw); + } + } + + /* Start the timers */ + if (xfr->maxtime > 0) { + xfrout_log(xfr, ISC_LOG_DEBUG(1), + "starting maxtime timer %" PRIu64 " ms", + xfr->maxtime); + isc_nm_timer_start(xfr->maxtime_timer, xfr->maxtime); + } + + /* + * Hand the context over to sendstream(). Set xfr to NULL; + * sendstream() is responsible for either passing the + * context on to a later event handler or destroying it. + */ + sendstream(xfr); + xfr = NULL; + + result = ISC_R_SUCCESS; + +failure: + if (result == DNS_R_REFUSED) { + inc_stats(client, zone, ns_statscounter_xfrrej); + } + if (quota != NULL) { + isc_quota_detach("a); + } + if (current_soa_tuple != NULL) { + dns_difftuple_free(¤t_soa_tuple); + } + if (stream != NULL) { + stream->methods->destroy(&stream); + } + if (soa_stream != NULL) { + soa_stream->methods->destroy(&soa_stream); + } + if (data_stream != NULL) { + data_stream->methods->destroy(&data_stream); + } + if (ver != NULL) { + dns_db_closeversion(db, &ver, false); + } + if (db != NULL) { + dns_db_detach(&db); + } + if (zone != NULL) { + dns_zone_detach(&zone); + } + /* XXX kludge */ + if (xfr != NULL) { + xfrout_fail(xfr, result, "setting up zone transfer"); + } else if (result != ISC_R_SUCCESS) { + ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, + NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(3), + "zone transfer setup failed"); + ns_client_error(client, result); + isc_nmhandle_detach(&client->reqhandle); + } +} + +static void +xfrout_ctx_create(isc_mem_t *mctx, ns_client_t *client, unsigned int id, + dns_name_t *qname, dns_rdatatype_t qtype, + dns_rdataclass_t qclass, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, isc_quota_t *quota, rrstream_t *stream, + dns_tsigkey_t *tsigkey, isc_buffer_t *lasttsig, + bool verified_tsig, unsigned int maxtime, + unsigned int idletime, bool many_answers, + xfrout_ctx_t **xfrp) { + xfrout_ctx_t *xfr = NULL; + unsigned int len = NS_CLIENT_TCP_BUFFER_SIZE; + void *mem = NULL; + + REQUIRE(xfrp != NULL && *xfrp == NULL); + + xfr = isc_mem_get(mctx, sizeof(*xfr)); + *xfr = (xfrout_ctx_t){ + .client = client, + .id = id, + .qname = qname, + .qtype = qtype, + .qclass = qclass, + .maxtime = maxtime * 1000, /* in milliseconds */ + .idletime = idletime * 1000, /* In milliseconds */ + .tsigkey = tsigkey, + .lasttsig = lasttsig, + .verified_tsig = verified_tsig, + .many_answers = many_answers, + }; + + isc_mem_attach(mctx, &xfr->mctx); + + if (zone != NULL) { /* zone will be NULL if it's DLZ */ + dns_zone_attach(zone, &xfr->zone); + } + dns_db_attach(db, &xfr->db); + dns_db_attachversion(db, ver, &xfr->ver); + + isc_time_now(&xfr->stats.start); + + isc_nm_timer_create(xfr->client->handle, xfrout_client_timeout, xfr, + &xfr->maxtime_timer); + + /* + * Allocate a temporary buffer for the uncompressed response + * message data. The buffer size must be 65535 bytes + * (NS_CLIENT_TCP_BUFFER_SIZE): small enough that compressed + * data will fit in a single TCP message, and big enough to + * hold a maximum-sized RR. + * + * Note that although 65535-byte RRs are allowed in principle, they + * cannot be zone-transferred (at least not if uncompressible), + * because the message and RR headers would push the size of the + * TCP message over the 65536 byte limit. + */ + mem = isc_mem_get(mctx, len); + isc_buffer_init(&xfr->buf, mem, len); + + /* + * Allocate another temporary buffer for the compressed + * response message. + */ + mem = isc_mem_get(mctx, len); + isc_buffer_init(&xfr->txbuf, (char *)mem, len); + xfr->txmem = mem; + xfr->txmemlen = len; + + /* + * These MUST be after the last "goto failure;" / CHECK to + * prevent a double free by the caller. + */ + xfr->quota = quota; + xfr->stream = stream; + + *xfrp = xfr; +} + +/* + * Arrange to send as much as we can of "stream" without blocking. + * + * Requires: + * The stream iterator is initialized and points at an RR, + * or possibly at the end of the stream (that is, the + * _first method of the iterator has been called). + */ +static void +sendstream(xfrout_ctx_t *xfr) { + dns_message_t *tcpmsg = NULL; + dns_message_t *msg = NULL; /* Client message if UDP, tcpmsg if TCP */ + isc_result_t result; + dns_rdataset_t *qrdataset; + dns_name_t *msgname = NULL; + dns_rdata_t *msgrdata = NULL; + dns_rdatalist_t *msgrdl = NULL; + dns_rdataset_t *msgrds = NULL; + dns_compress_t cctx; + bool cleanup_cctx = false; + bool is_tcp; + int n_rrs; + + isc_buffer_clear(&xfr->buf); + isc_buffer_clear(&xfr->txbuf); + + is_tcp = ((xfr->client->attributes & NS_CLIENTATTR_TCP) != 0); + if (!is_tcp) { + /* + * In the UDP case, we put the response data directly into + * the client message. + */ + msg = xfr->client->message; + CHECK(dns_message_reply(msg, true)); + } else { + /* + * TCP. Build a response dns_message_t, temporarily storing + * the raw, uncompressed owner names and RR data contiguously + * in xfr->buf. We know that if the uncompressed data fits + * in xfr->buf, the compressed data will surely fit in a TCP + * message. + */ + + dns_message_create(xfr->mctx, DNS_MESSAGE_INTENTRENDER, + &tcpmsg); + msg = tcpmsg; + + msg->id = xfr->id; + msg->rcode = dns_rcode_noerror; + msg->flags = DNS_MESSAGEFLAG_QR | DNS_MESSAGEFLAG_AA; + if ((xfr->client->attributes & NS_CLIENTATTR_RA) != 0) { + msg->flags |= DNS_MESSAGEFLAG_RA; + } + CHECK(dns_message_settsigkey(msg, xfr->tsigkey)); + CHECK(dns_message_setquerytsig(msg, xfr->lasttsig)); + if (xfr->lasttsig != NULL) { + isc_buffer_free(&xfr->lasttsig); + } + msg->verified_sig = xfr->verified_tsig; + + /* + * Add a EDNS option to the message? + */ + if ((xfr->client->attributes & NS_CLIENTATTR_WANTOPT) != 0) { + dns_rdataset_t *opt = NULL; + + CHECK(ns_client_addopt(xfr->client, msg, &opt)); + CHECK(dns_message_setopt(msg, opt)); + /* + * Add to first message only. + */ + xfr->client->attributes &= ~NS_CLIENTATTR_WANTNSID; + xfr->client->attributes &= ~NS_CLIENTATTR_HAVEEXPIRE; + } + + /* + * Account for reserved space. + */ + if (xfr->tsigkey != NULL) { + INSIST(msg->reserved != 0U); + } + isc_buffer_add(&xfr->buf, msg->reserved); + + /* + * Include a question section in the first message only. + * BIND 8.2.1 will not recognize an IXFR if it does not + * have a question section. + */ + if (!xfr->question_added) { + dns_name_t *qname = NULL; + isc_region_t r; + + /* + * Reserve space for the 12-byte message header + * and 4 bytes of question. + */ + isc_buffer_add(&xfr->buf, 12 + 4); + + qrdataset = NULL; + result = dns_message_gettemprdataset(msg, &qrdataset); + if (result != ISC_R_SUCCESS) { + goto failure; + } + dns_rdataset_makequestion(qrdataset, + xfr->client->message->rdclass, + xfr->qtype); + + result = dns_message_gettempname(msg, &qname); + if (result != ISC_R_SUCCESS) { + goto failure; + } + isc_buffer_availableregion(&xfr->buf, &r); + INSIST(r.length >= xfr->qname->length); + r.length = xfr->qname->length; + isc_buffer_putmem(&xfr->buf, xfr->qname->ndata, + xfr->qname->length); + dns_name_fromregion(qname, &r); + ISC_LIST_INIT(qname->list); + ISC_LIST_APPEND(qname->list, qrdataset, link); + + dns_message_addname(msg, qname, DNS_SECTION_QUESTION); + xfr->question_added = true; + } else { + /* + * Reserve space for the 12-byte message header + */ + isc_buffer_add(&xfr->buf, 12); + msg->tcp_continuation = 1; + } + } + + /* + * Try to fit in as many RRs as possible, unless "one-answer" + * format has been requested. + */ + for (n_rrs = 0;; n_rrs++) { + dns_name_t *name = NULL; + uint32_t ttl; + dns_rdata_t *rdata = NULL; + + unsigned int size; + isc_region_t r; + + msgname = NULL; + msgrdata = NULL; + msgrdl = NULL; + msgrds = NULL; + + xfr->stream->methods->current(xfr->stream, &name, &ttl, &rdata); + size = name->length + 10 + rdata->length; + isc_buffer_availableregion(&xfr->buf, &r); + if (size >= r.length) { + /* + * RR would not fit. If there are other RRs in the + * buffer, send them now and leave this RR to the + * next message. If this RR overflows the buffer + * all by itself, fail. + * + * In theory some RRs might fit in a TCP message + * when compressed even if they do not fit when + * uncompressed, but surely we don't want + * to send such monstrosities to an unsuspecting + * slave. + */ + if (n_rrs == 0) { + xfrout_log(xfr, ISC_LOG_WARNING, + "RR too large for zone transfer " + "(%d bytes)", + size); + /* XXX DNS_R_RRTOOLARGE? */ + result = ISC_R_NOSPACE; + goto failure; + } + break; + } + + if (isc_log_wouldlog(ns_lctx, XFROUT_RR_LOGLEVEL)) { + log_rr(name, rdata, ttl); /* XXX */ + } + + result = dns_message_gettempname(msg, &msgname); + if (result != ISC_R_SUCCESS) { + goto failure; + } + isc_buffer_availableregion(&xfr->buf, &r); + INSIST(r.length >= name->length); + r.length = name->length; + isc_buffer_putmem(&xfr->buf, name->ndata, name->length); + dns_name_fromregion(msgname, &r); + + /* Reserve space for RR header. */ + isc_buffer_add(&xfr->buf, 10); + + result = dns_message_gettemprdata(msg, &msgrdata); + if (result != ISC_R_SUCCESS) { + goto failure; + } + isc_buffer_availableregion(&xfr->buf, &r); + r.length = rdata->length; + isc_buffer_putmem(&xfr->buf, rdata->data, rdata->length); + dns_rdata_init(msgrdata); + dns_rdata_fromregion(msgrdata, rdata->rdclass, rdata->type, &r); + + result = dns_message_gettemprdatalist(msg, &msgrdl); + if (result != ISC_R_SUCCESS) { + goto failure; + } + msgrdl->type = rdata->type; + msgrdl->rdclass = rdata->rdclass; + msgrdl->ttl = ttl; + if (rdata->type == dns_rdatatype_sig || + rdata->type == dns_rdatatype_rrsig) + { + msgrdl->covers = dns_rdata_covers(rdata); + } else { + msgrdl->covers = dns_rdatatype_none; + } + ISC_LIST_APPEND(msgrdl->rdata, msgrdata, link); + + result = dns_message_gettemprdataset(msg, &msgrds); + if (result != ISC_R_SUCCESS) { + goto failure; + } + result = dns_rdatalist_tordataset(msgrdl, msgrds); + INSIST(result == ISC_R_SUCCESS); + + ISC_LIST_APPEND(msgname->list, msgrds, link); + + dns_message_addname(msg, msgname, DNS_SECTION_ANSWER); + msgname = NULL; + + xfr->stats.nrecs++; + + result = xfr->stream->methods->next(xfr->stream); + if (result == ISC_R_NOMORE) { + xfr->end_of_stream = true; + break; + } + CHECK(result); + + if (!xfr->many_answers) { + break; + } + /* + * At this stage, at least 1 RR has been rendered into + * the message. Check if we want to clamp this message + * here (TCP only). + */ + if ((isc_buffer_usedlength(&xfr->buf) >= + xfr->client->sctx->transfer_tcp_message_size) && + is_tcp) + { + break; + } + } + + if (is_tcp) { + isc_region_t used; + CHECK(dns_compress_init(&cctx, -1, xfr->mctx)); + dns_compress_setsensitive(&cctx, true); + cleanup_cctx = true; + CHECK(dns_message_renderbegin(msg, &cctx, &xfr->txbuf)); + CHECK(dns_message_rendersection(msg, DNS_SECTION_QUESTION, 0)); + CHECK(dns_message_rendersection(msg, DNS_SECTION_ANSWER, 0)); + CHECK(dns_message_renderend(msg)); + dns_compress_invalidate(&cctx); + cleanup_cctx = false; + + isc_buffer_usedregion(&xfr->txbuf, &used); + + xfrout_log(xfr, ISC_LOG_DEBUG(8), + "sending TCP message of %d bytes", used.length); + + isc_nmhandle_attach(xfr->client->handle, + &xfr->client->sendhandle); + if (xfr->idletime > 0) { + isc_nmhandle_setwritetimeout(xfr->client->sendhandle, + xfr->idletime); + } + isc_nm_send(xfr->client->sendhandle, &used, xfrout_senddone, + xfr); + xfr->sends++; + xfr->cbytes = used.length; + } else { + xfrout_log(xfr, ISC_LOG_DEBUG(8), "sending IXFR UDP response"); + ns_client_send(xfr->client); + xfr->stream->methods->pause(xfr->stream); + isc_nmhandle_detach(&xfr->client->reqhandle); + xfrout_ctx_destroy(&xfr); + return; + } + + /* Advance lasttsig to be the last TSIG generated */ + CHECK(dns_message_getquerytsig(msg, xfr->mctx, &xfr->lasttsig)); + +failure: + if (msgname != NULL) { + if (msgrds != NULL) { + if (dns_rdataset_isassociated(msgrds)) { + dns_rdataset_disassociate(msgrds); + } + dns_message_puttemprdataset(msg, &msgrds); + } + if (msgrdl != NULL) { + ISC_LIST_UNLINK(msgrdl->rdata, msgrdata, link); + dns_message_puttemprdatalist(msg, &msgrdl); + } + if (msgrdata != NULL) { + dns_message_puttemprdata(msg, &msgrdata); + } + dns_message_puttempname(msg, &msgname); + } + + if (tcpmsg != NULL) { + dns_message_detach(&tcpmsg); + } + + if (cleanup_cctx) { + dns_compress_invalidate(&cctx); + } + /* + * Make sure to release any locks held by database + * iterators before returning from the event handler. + */ + xfr->stream->methods->pause(xfr->stream); + + if (result == ISC_R_SUCCESS) { + return; + } + + if (xfr->client->sendhandle != NULL) { + isc_nmhandle_detach(&xfr->client->sendhandle); + } + + xfrout_fail(xfr, result, "sending zone data"); +} + +static void +xfrout_ctx_destroy(xfrout_ctx_t **xfrp) { + xfrout_ctx_t *xfr = *xfrp; + *xfrp = NULL; + + INSIST(xfr->sends == 0); + + isc_nm_timer_stop(xfr->maxtime_timer); + isc_nm_timer_detach(&xfr->maxtime_timer); + + if (xfr->stream != NULL) { + xfr->stream->methods->destroy(&xfr->stream); + } + if (xfr->buf.base != NULL) { + isc_mem_put(xfr->mctx, xfr->buf.base, xfr->buf.length); + } + if (xfr->txmem != NULL) { + isc_mem_put(xfr->mctx, xfr->txmem, xfr->txmemlen); + } + if (xfr->lasttsig != NULL) { + isc_buffer_free(&xfr->lasttsig); + } + if (xfr->quota != NULL) { + isc_quota_detach(&xfr->quota); + } + if (xfr->ver != NULL) { + dns_db_closeversion(xfr->db, &xfr->ver, false); + } + if (xfr->zone != NULL) { + dns_zone_detach(&xfr->zone); + } + if (xfr->db != NULL) { + dns_db_detach(&xfr->db); + } + + isc_mem_putanddetach(&xfr->mctx, xfr, sizeof(*xfr)); +} + +static void +xfrout_senddone(isc_nmhandle_t *handle, isc_result_t result, void *arg) { + xfrout_ctx_t *xfr = (xfrout_ctx_t *)arg; + + REQUIRE((xfr->client->attributes & NS_CLIENTATTR_TCP) != 0); + + INSIST(handle == xfr->client->handle); + + xfr->sends--; + INSIST(xfr->sends == 0); + + isc_nmhandle_detach(&xfr->client->sendhandle); + + /* + * Update transfer statistics if sending succeeded, accounting for the + * two-byte TCP length prefix included in the number of bytes sent. + */ + if (result == ISC_R_SUCCESS) { + xfr->stats.nmsg++; + xfr->stats.nbytes += xfr->cbytes; + } + + if (xfr->shuttingdown) { + xfrout_maybe_destroy(xfr); + } else if (result != ISC_R_SUCCESS) { + xfrout_fail(xfr, result, "send"); + } else if (!xfr->end_of_stream) { + sendstream(xfr); + } else { + /* End of zone transfer stream. */ + uint64_t msecs, persec; + + inc_stats(xfr->client, xfr->zone, ns_statscounter_xfrdone); + isc_time_now(&xfr->stats.end); + msecs = isc_time_microdiff(&xfr->stats.end, &xfr->stats.start); + msecs /= 1000; + if (msecs == 0) { + msecs = 1; + } + persec = (xfr->stats.nbytes * 1000) / msecs; + xfrout_log(xfr, xfr->poll ? ISC_LOG_DEBUG(1) : ISC_LOG_INFO, + "%s ended: " + "%" PRIu64 " messages, %" PRIu64 " records, " + "%" PRIu64 " bytes, " + "%u.%03u secs (%u bytes/sec) (serial %u)", + xfr->mnemonic, xfr->stats.nmsg, xfr->stats.nrecs, + xfr->stats.nbytes, (unsigned int)(msecs / 1000), + (unsigned int)(msecs % 1000), (unsigned int)persec, + xfr->end_serial); + + /* + * We're done, unreference the handle and destroy the xfr + * context. + */ + isc_nmhandle_detach(&xfr->client->reqhandle); + xfrout_ctx_destroy(&xfr); + } +} + +static void +xfrout_fail(xfrout_ctx_t *xfr, isc_result_t result, const char *msg) { + xfr->shuttingdown = true; + xfrout_log(xfr, ISC_LOG_ERROR, "%s: %s", msg, + isc_result_totext(result)); + xfrout_maybe_destroy(xfr); +} + +static void +xfrout_maybe_destroy(xfrout_ctx_t *xfr) { + REQUIRE(xfr->shuttingdown); + + ns_client_drop(xfr->client, ISC_R_CANCELED); + isc_nmhandle_detach(&xfr->client->reqhandle); + xfrout_ctx_destroy(&xfr); +} + +static void +xfrout_client_timeout(void *arg, isc_result_t result) { + xfrout_ctx_t *xfr = (xfrout_ctx_t *)arg; + + xfr->shuttingdown = true; + xfrout_log(xfr, ISC_LOG_ERROR, "%s: %s", "aborted", + isc_result_totext(result)); +} + +/* + * Log outgoing zone transfer messages in a format like + * <client>: transfer of <zone>: <message> + */ + +static void +xfrout_logv(ns_client_t *client, dns_name_t *zonename, dns_rdataclass_t rdclass, + int level, const char *fmt, va_list ap) ISC_FORMAT_PRINTF(5, 0); + +static void +xfrout_logv(ns_client_t *client, dns_name_t *zonename, dns_rdataclass_t rdclass, + int level, const char *fmt, va_list ap) { + char msgbuf[2048]; + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + + dns_name_format(zonename, namebuf, sizeof(namebuf)); + dns_rdataclass_format(rdclass, classbuf, sizeof(classbuf)); + vsnprintf(msgbuf, sizeof(msgbuf), fmt, ap); + ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, + level, "transfer of '%s/%s': %s", namebuf, classbuf, + msgbuf); +} + +/* + * Logging function for use when a xfrout_ctx_t has not yet been created. + */ +static void +xfrout_log1(ns_client_t *client, dns_name_t *zonename, dns_rdataclass_t rdclass, + int level, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + xfrout_logv(client, zonename, rdclass, level, fmt, ap); + va_end(ap); +} + +/* + * Logging function for use when there is a xfrout_ctx_t. + */ +static void +xfrout_log(xfrout_ctx_t *xfr, int level, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + xfrout_logv(xfr->client, xfr->qname, xfr->qclass, level, fmt, ap); + va_end(ap); +} |