From 3b9b6d0b8e7f798023c9d109c490449d528fde80 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 17:59:48 +0200 Subject: Adding upstream version 1:9.18.19. Signed-off-by: Daniel Baumann --- lib/ns/client.c | 3093 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3093 insertions(+) create mode 100644 lib/ns/client.c (limited to 'lib/ns/client.c') diff --git a/lib/ns/client.c b/lib/ns/client.c new file mode 100644 index 0000000..5ed64fd --- /dev/null +++ b/lib/ns/client.c @@ -0,0 +1,3093 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/*** + *** 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 */ + +atomic_uint_fast64_t ns_client_requests = 0; + +static void +clientmgr_attach(ns_clientmgr_t *source, ns_clientmgr_t **targetp); +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); + +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 +client_extendederror_reset(ns_client_t *client) { + if (client->ede == NULL) { + return; + } + isc_mem_put(client->mctx, client->ede->value, client->ede->length); + isc_mem_put(client->mctx, client->ede, sizeof(dns_ednsopt_t)); + client->ede = NULL; +} + +void +ns_client_extendederror(ns_client_t *client, uint16_t code, const char *text) { + unsigned char ede[DNS_EDE_EXTRATEXT_LEN + 2]; + isc_buffer_t buf; + uint16_t len = sizeof(uint16_t); + + REQUIRE(NS_CLIENT_VALID(client)); + + if (client->ede != NULL) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), + "already have ede, ignoring %u %s", code, + text == NULL ? "(null)" : text); + return; + } + + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, + ISC_LOG_DEBUG(1), "set ede: info-code %u extra-text %s", + code, text == NULL ? "(null)" : text); + + isc_buffer_init(&buf, ede, sizeof(ede)); + isc_buffer_putuint16(&buf, code); + if (text != NULL && strlen(text) > 0) { + if (strlen(text) < DNS_EDE_EXTRATEXT_LEN) { + isc_buffer_putstr(&buf, text); + len += (uint16_t)(strlen(text)); + } else { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, + NS_LOGMODULE_CLIENT, ISC_LOG_WARNING, + "ede extra-text too long, ignoring"); + } + } + + client->ede = isc_mem_get(client->mctx, sizeof(dns_ednsopt_t)); + client->ede->code = DNS_OPT_EDE; + client->ede->length = len; + client->ede->value = isc_mem_get(client->mctx, len); + memmove(client->ede->value, ede, len); +}; + +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_extendederror_reset(client); + client->signer = NULL; + client->udpsize = 512; + client->extflags = 0; + client->ednsversion = -1; + client->additionaldepth = 0; + 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->manager->send_mctx, + NS_CLIENT_TCP_BUFFER_SIZE); + client->tcpbuf_size = 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_result_t result; + isc_region_t r; + dns_ttl_t min_ttl = 0; + + REQUIRE(client->sendhandle == NULL); + + if (isc_buffer_base(buffer) == client->tcpbuf) { + size_t used = isc_buffer_usedlength(buffer); + client->tcpbuf = isc_mem_reget(client->manager->send_mctx, + client->tcpbuf, + client->tcpbuf_size, used); + client->tcpbuf_size = used; + r.base = client->tcpbuf; + r.length = used; + } else { + isc_buffer_usedregion(buffer, &r); + } + isc_nmhandle_attach(client->handle, &client->sendhandle); + + if (isc_nm_is_http_handle(client->handle)) { + result = dns_message_response_minttl(client->message, &min_ttl); + if (result == ISC_R_SUCCESS) { + isc_nm_set_maxage(client->handle, min_ttl); + } + } + 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 = NULL; + isc_buffer_t buffer; + isc_region_t r; + isc_region_t *mr = NULL; + + 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->manager->send_mctx, client->tcpbuf, + client->tcpbuf_size); + } + + ns_client_drop(client, result); +} + +void +ns_client_send(ns_client_t *client) { + isc_result_t result; + unsigned char *data = NULL; + 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 = NULL; +#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 = client->manager->aclenv; + + 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->manager->send_mctx, client->tcpbuf, + client->tcpbuf_size); + } + + 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 = NULL; + dns_resolver_t *resolver = NULL; + 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 = NULL; + + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(opt != NULL && *opt == NULL); + REQUIRE(message != NULL); + + env = client->manager->aclenv; + 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->usehostname) { + result = 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++; + } + + if (client->ede != NULL) { + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_EDE; + ednsopts[count].length = client->ede->length; + ednsopts[count].value = client->ede->value; + 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->manager->send_mctx, client->tcpbuf, + client->tcpbuf_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); + +#ifdef WANT_SINGLETRACE + isc_log_setforcelog(false); +#endif /* WANT_SINGLETRACE */ +} + +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; + + isc_mem_put(client->manager->send_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); + } + client_extendederror_reset(client); + + dns_message_detach(&client->message); + + if (client->manager != NULL) { + ns_clientmgr_detach(&client->manager); + } + + /* + * 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); + } + + 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 */ + static const char *ra_reasons[] = { + "ACLs not processed yet", + "no resolver in view", + "recursion not enabled for view", + "allow-recursion did not match", + "allow-query-cache did not match", + "allow-recursion-on did not match", + "allow-query-cache-on did not match", + }; + enum refusal_reasons { + INVALID, + NO_RESOLVER, + RECURSION_DISABLED, + ALLOW_RECURSION, + ALLOW_QUERY_CACHE, + ALLOW_RECURSION_ON, + ALLOW_QUERY_CACHE_ON + } ra_refusal_reason = INVALID; + + if (eresult != ISC_R_SUCCESS) { + return; + } + + client = isc_nmhandle_getdata(handle); + if (client == NULL) { + ns_interface_t *ifp = (ns_interface_t *)arg; + ns_clientmgr_t *clientmgr = + ns_interfacemgr_getclientmgr(ifp->mgr); + + INSIST(VALID_MANAGER(clientmgr)); + + client = isc_nmhandle_getextra(handle); + + result = ns__client_setup(client, 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; + + 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_nm_bad_request(handle); + return; + } +#endif /* if NS_CLIENT_DROPPORT */ + + env = client->manager->aclenv; + 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_nm_bad_request(handle); + 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. + */ + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(10), + "dropped request: invalid message header"); + isc_nm_bad_request(handle); + return; + } + +#ifdef WANT_SINGLETRACE + if (id == 0) { + isc_log_setforcelog(true); + } +#endif /* WANT_SINGLETRACE */ + + /* + * 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) { + ns_client_log(client, DNS_LOGCATEGORY_SECURITY, + NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(10), + "dropped request: unexpected response"); + isc_nm_bad_request(handle); + 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); + 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_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; + client->ede = NULL; + + /* + * 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); + return; + } + + /* + * Are returning NOTIMP to all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_EDNSNOTIMP) != 0) { + ns_client_error(client, DNS_R_NOTIMP); + return; + } + + /* + * Are returning REFUSED to all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_EDNSREFUSED) != 0) { + ns_client_error(client, DNS_R_REFUSED); + return; + } + + /* + * Are we dropping all EDNS queries? + */ + if ((client->sctx->options & NS_SERVER_DROPEDNS) != 0) { + ns_client_drop(client, ISC_R_SUCCESS); + return; + } + + result = process_opt(client, opt); + if (result != ISC_R_SUCCESS) { + 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); + return; + } + + if (notimp) { + client->message->rcode = dns_rcode_notimp; + } + + ns_client_send(client); + 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); + return; + } + + 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_extendederror(client, DNS_EDE_PROHIBITED, NULL); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_REFUSED); + 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 primary. + */ + if (!(client->message->tsigstatus == dns_tsigerror_badkey && + client->message->opcode == dns_opcode_update)) + { + ns_client_error(client, sigresult); + 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; + + /* must be initialized before ns_client_log uses it as index */ + if (client->view->resolver == NULL) { + ra_refusal_reason = NO_RESOLVER; + } else if (!client->view->recursion) { + ra_refusal_reason = RECURSION_DISABLED; + } else if (ns_client_checkaclsilent(client, NULL, + client->view->recursionacl, + true) != ISC_R_SUCCESS) + { + ra_refusal_reason = ALLOW_RECURSION; + } else if (ns_client_checkaclsilent(client, NULL, + client->view->cacheacl, + true) != ISC_R_SUCCESS) + { + ra_refusal_reason = ALLOW_QUERY_CACHE; + } else if (ns_client_checkaclsilent(client, &client->destaddr, + client->view->recursiononacl, + true) != ISC_R_SUCCESS) + { + ra_refusal_reason = ALLOW_RECURSION_ON; + } else if (ns_client_checkaclsilent(client, &client->destaddr, + client->view->cacheonacl, + true) != ISC_R_SUCCESS) + { + ra_refusal_reason = ALLOW_QUERY_CACHE_ON; + } else { + ra = true; + 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 (%s)", + ra_reasons[ra_refusal_reason]); + + /* + * 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_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); +} + +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. + */ + + if (new) { + REQUIRE(VALID_MANAGER(mgr)); + REQUIRE(client != NULL); + REQUIRE(mgr->tid == isc_nm_tid()); + + *client = (ns_client_t){ .magic = 0, .tid = mgr->tid }; + + isc_mem_attach(mgr->mctx, &client->mctx); + clientmgr_attach(mgr, &client->manager); + ns_server_attach(mgr->sctx, &client->sctx); + isc_task_attach(mgr->task, &client->task); + + dns_message_create(client->mctx, DNS_MESSAGE_INTENTPARSE, + &client->message); + + client->sendbuf = isc_mem_get(client->manager->send_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 { + REQUIRE(NS_CLIENT_VALID(client)); + REQUIRE(client->tid == isc_nm_tid()); + + 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; + int tid = client->tid; + + /* + * 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, + .tid = tid }; + } + + 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->manager->send_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) { + ns_clientmgr_detach(&client->manager); + } + 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; +} + +void +ns_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) { + MTRACE("clientmgr_destroy"); + + isc_refcount_destroy(&manager->references); + manager->magic = 0; + + dns_aclenv_detach(&manager->aclenv); + + isc_mutex_destroy(&manager->reclock); + + isc_task_detach(&manager->task); + ns_server_detach(&manager->sctx); + + isc_mem_detach(&manager->send_mctx); + + isc_mem_putanddetach(&manager->mctx, manager, sizeof(*manager)); +} + +isc_result_t +ns_clientmgr_create(ns_server_t *sctx, isc_taskmgr_t *taskmgr, + isc_timermgr_t *timermgr, dns_aclenv_t *aclenv, int tid, + ns_clientmgr_t **managerp) { + ns_clientmgr_t *manager = NULL; + isc_mem_t *mctx = NULL; + isc_result_t result; + + isc_mem_create(&mctx); + isc_mem_setname(mctx, "clientmgr"); + + manager = isc_mem_get(mctx, sizeof(*manager)); + *manager = (ns_clientmgr_t){ .magic = 0, .mctx = mctx }; + + isc_mutex_init(&manager->reclock); + + manager->taskmgr = taskmgr; + manager->timermgr = timermgr; + manager->tid = tid; + + dns_aclenv_attach(aclenv, &manager->aclenv); + + result = isc_task_create_bound(manager->taskmgr, 20, &manager->task, + manager->tid); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + isc_task_setname(manager->task, "clientmgr", NULL); + + isc_refcount_init(&manager->references, 1); + manager->sctx = NULL; + ns_server_attach(sctx, &manager->sctx); + + ISC_LIST_INIT(manager->recursing); + + /* + * We create specialised per-worker memory context specifically + * dedicated and tuned for allocating send buffers as it is a very + * common operation. Not doing so may result in excessive memory + * use in certain workloads. + * + * Please see this thread for more details: + * + * https://github.com/jemalloc/jemalloc/issues/2483 + * + * In particular, this information from the jemalloc developers is + * of the most interest: + * + * https://github.com/jemalloc/jemalloc/issues/2483#issuecomment-1639019699 + * https://github.com/jemalloc/jemalloc/issues/2483#issuecomment-1698173849 + * + * In essence, we use the following memory management strategy: + * + * 1. We use a per-worker memory arena for send buffers memory + * allocation to reduce lock contention (In reality, we create a + * per-client manager arena, but we have one client manager per + * worker). + * + * 2. The automatically created arenas settings remain unchanged + * and may be controlled by users (e.g. by setting the + * "MALLOC_CONF" variable). + * + * 3. We attune the arenas to not use dirty pages cache as the + * cache would have a poor reuse rate, and that is known to + * significantly contribute to excessive memory use. + * + * 4. There is no strict need for the dirty cache, as there is a + * per arena bin for each allocation size, so because we initially + * allocate strictly 64K per send buffer (enough for a DNS + * message), allocations would get directed to one bin (an "object + * pool" or a "slab") maintained within an arena. That is, there + * is an object pool already, specifically to optimise for the + * case of frequent allocations of objects of the given size. The + * object pool should suffice our needs, as we will end up + * recycling the objects from there without the need to back it by + * an additional layer of dirty pages cache. The dirty pages cache + * would have worked better in the case when there are more + * allocation bins involved due to a higher reuse rate (the case + * of a more "generic" memory management). + */ + isc_mem_create_arena(&manager->send_mctx); + isc_mem_setname(manager->send_mctx, "sendbufs"); + (void)isc_mem_arena_set_dirty_decay_ms(manager->send_mctx, 0); + /* + * Disable muzzy pages cache too, as versions < 5.2.0 have it + * enabled by default. The muzzy pages cache goes right below the + * dirty pages cache and backs it. + */ + (void)isc_mem_arena_set_muzzy_decay_ms(manager->send_mctx, 0); + + manager->magic = MANAGER_MAGIC; + + MTRACE("create"); + + *managerp = manager; + + return (ISC_R_SUCCESS); +} + +void +ns_clientmgr_shutdown(ns_clientmgr_t *manager) { + ns_client_t *client; + + REQUIRE(VALID_MANAGER(manager)); + + MTRACE("destroy"); + + LOCK(&manager->reclock); + for (client = ISC_LIST_HEAD(manager->recursing); client != NULL; + client = ISC_LIST_NEXT(client, rlink)) + { + ns_query_cancel(client); + } + UNLOCK(&manager->reclock); +} + +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 = client->manager->aclenv; + isc_netaddr_t tmpnetaddr; + int match; + isc_sockaddr_t local; + + if (acl == NULL) { + if (default_allow) { + goto allow; + } else { + goto deny; + } + } + + if (netaddr == NULL) { + isc_netaddr_fromsockaddr(&tmpnetaddr, &client->peeraddr); + netaddr = &tmpnetaddr; + } + + local = isc_nmhandle_localaddr(client->handle); + result = dns_acl_match_port_transport( + netaddr, isc_sockaddr_getport(&local), + isc_nm_socket_type(client->handle), + isc_nm_has_encryption(client->handle), 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_extendederror(client, DNS_EDE_PROHIBITED, NULL); + 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); +} -- cgit v1.2.3