]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add filter-a plugin for IPv6-dominant environments
authortreysis <treysis@gmx.net>
Wed, 3 Mar 2021 10:33:55 +0000 (11:33 +0100)
committerMatthijs Mekking <matthijs@isc.org>
Fri, 19 Mar 2021 07:06:55 +0000 (08:06 +0100)
(cherry picked from commit 78f6cd57e1cc166823415438fe2d19a324cf7a67)

bin/plugins/Makefile.am
bin/plugins/filter-a.c [new file with mode: 0644]
bin/plugins/filter-a.rst [new file with mode: 0644]
doc/man/Makefile.am
doc/man/conf.py
doc/man/filter-a.8in [new file with mode: 0644]
doc/man/filter-a.rst [new file with mode: 0644]
util/copyrights

index c29b2615db9a068ea3e876673b24c15c34686c6d..611dd1b1e047b390a4e442b454a5ca7f71ecbb9e 100644 (file)
@@ -7,6 +7,9 @@ AM_CPPFLAGS +=                  \
        $(LIBISCCFG_CFLAGS)
 
 pkglib_LTLIBRARIES = filter-aaaa.la
+pkglib_LTLIBRARIES += filter-a.la
 
 filter_aaaa_la_SOURCES = filter-aaaa.c
+filter_a_la_SOURCES = filter-a.c
 filter_aaaa_la_LDFLAGS = -avoid-version -module -shared -export-dynamic
+filter_a_la_LDFLAGS = -avoid-version -module -shared -export-dynamic
diff --git a/bin/plugins/filter-a.c b/bin/plugins/filter-a.c
new file mode 100644 (file)
index 0000000..4361e97
--- /dev/null
@@ -0,0 +1,906 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * 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 */
+
+/* aliases for the exported symbols */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <isc/buffer.h>
+#include <isc/hash.h>
+#include <isc/ht.h>
+#include <isc/lib.h>
+#include <isc/log.h>
+#include <isc/mem.h>
+#include <isc/netaddr.h>
+#include <isc/result.h>
+#include <isc/types.h>
+#include <isc/util.h>
+
+#include <dns/acl.h>
+#include <dns/db.h>
+#include <dns/enumtype.h>
+#include <dns/log.h>
+#include <dns/message.h>
+#include <dns/rdataset.h>
+#include <dns/result.h>
+#include <dns/types.h>
+#include <dns/view.h>
+
+#include <isccfg/aclconf.h>
+#include <isccfg/cfg.h>
+#include <isccfg/grammar.h>
+
+#include <ns/client.h>
+#include <ns/hooks.h>
+#include <ns/log.h>
+#include <ns/query.h>
+#include <ns/types.h>
+
+#define CHECK(op)                              \
+       do {                                   \
+               result = (op);                 \
+               if (result != ISC_R_SUCCESS) { \
+                       goto cleanup;          \
+               }                              \
+       } while (0)
+
+/*
+ * Possible values for the settings of filter-a-on-v6 and
+ * filter-a-on-v4: "no" is NONE, "yes" is FILTER, "break-dnssec"
+ * is BREAK_DNSSEC.
+ */
+typedef enum { NONE = 0, FILTER = 1, BREAK_DNSSEC = 2 } filter_a_t;
+
+/*
+ * Persistent data for use by this module. This will be associated
+ * with client object address in the hash table, and will remain
+ * accessible until the client object is detached.
+ */
+typedef struct filter_data {
+       filter_a_t mode;
+       uint32_t flags;
+} filter_data_t;
+
+typedef struct filter_instance {
+       ns_plugin_t *module;
+       isc_mem_t *mctx;
+
+       /*
+        * Memory pool for use with persistent data.
+        */
+       isc_mempool_t *datapool;
+       isc_mutex_t plock;
+
+       /*
+        * Hash table associating a client object with its persistent data.
+        */
+       isc_ht_t *ht;
+       isc_mutex_t hlock;
+
+       /*
+        * Values configured when the module is loaded.
+        */
+       filter_a_t v4_a;
+       filter_a_t v6_a;
+       dns_acl_t *a_acl;
+} filter_instance_t;
+
+/*
+ * Per-client flags set by this module
+ */
+#define FILTER_A_RECURSING 0x0001 /* Recursing for AAAA */
+#define FILTER_A_FILTERED  0x0002 /* A was removed from answer */
+
+/*
+ * Client attribute tests.
+ */
+#define WANTDNSSEC(c)  (((c)->attributes & NS_CLIENTATTR_WANTDNSSEC) != 0)
+#define RECURSIONOK(c) (((c)->query.attributes & NS_QUERYATTR_RECURSIONOK) != 0)
+
+/*
+ * Forward declarations of functions referenced in install_hooks().
+ */
+static ns_hookresult_t
+filter_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp);
+static ns_hookresult_t
+filter_respond_begin(void *arg, void *cbdata, isc_result_t *resp);
+static ns_hookresult_t
+filter_respond_any_found(void *arg, void *cbdata, isc_result_t *resp);
+static ns_hookresult_t
+filter_prep_response_begin(void *arg, void *cbdata, isc_result_t *resp);
+static ns_hookresult_t
+filter_query_done_send(void *arg, void *cbdata, isc_result_t *resp);
+static ns_hookresult_t
+filter_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp);
+
+/*%
+ * Register the functions to be called at each hook point in 'hooktable', using
+ * memory context 'mctx' for allocating copies of stack-allocated structures
+ * passed to ns_hook_add().  Make sure 'inst' will be passed as the 'cbdata'
+ * argument to every callback.
+ */
+static void
+install_hooks(ns_hooktable_t *hooktable, isc_mem_t *mctx,
+             filter_instance_t *inst) {
+       const ns_hook_t filter_init = {
+               .action = filter_qctx_initialize,
+               .action_data = inst,
+       };
+
+       const ns_hook_t filter_respbegin = {
+               .action = filter_respond_begin,
+               .action_data = inst,
+       };
+
+       const ns_hook_t filter_respanyfound = {
+               .action = filter_respond_any_found,
+               .action_data = inst,
+       };
+
+       const ns_hook_t filter_prepresp = {
+               .action = filter_prep_response_begin,
+               .action_data = inst,
+       };
+
+       const ns_hook_t filter_donesend = {
+               .action = filter_query_done_send,
+               .action_data = inst,
+       };
+
+       const ns_hook_t filter_destroy = {
+               .action = filter_qctx_destroy,
+               .action_data = inst,
+       };
+
+       ns_hook_add(hooktable, mctx, NS_QUERY_QCTX_INITIALIZED, &filter_init);
+       ns_hook_add(hooktable, mctx, NS_QUERY_RESPOND_BEGIN, &filter_respbegin);
+       ns_hook_add(hooktable, mctx, NS_QUERY_RESPOND_ANY_FOUND,
+                   &filter_respanyfound);
+       ns_hook_add(hooktable, mctx, NS_QUERY_PREP_RESPONSE_BEGIN,
+                   &filter_prepresp);
+       ns_hook_add(hooktable, mctx, NS_QUERY_DONE_SEND, &filter_donesend);
+       ns_hook_add(hooktable, mctx, NS_QUERY_QCTX_DESTROYED, &filter_destroy);
+}
+
+/**
+** Support for parsing of parameters and configuration of the module.
+**/
+
+/*
+ * Support for parsing of parameters.
+ */
+static const char *filter_a_enums[] = { "break-dnssec", NULL };
+
+static isc_result_t
+parse_filter_a(cfg_parser_t *pctx, const cfg_type_t *type, cfg_obj_t **ret) {
+       return (cfg_parse_enum_or_other(pctx, type, &cfg_type_boolean, ret));
+}
+
+static void
+doc_filter_a(cfg_printer_t *pctx, const cfg_type_t *type) {
+       cfg_doc_enum_or_other(pctx, type, &cfg_type_boolean);
+}
+
+static cfg_type_t cfg_type_filter_a = {
+       "filter_a",   parse_filter_a,  cfg_print_ustring,
+       doc_filter_a, &cfg_rep_string, filter_a_enums,
+};
+
+static cfg_clausedef_t param_clauses[] = {
+       { "filter-a", &cfg_type_bracketed_aml, 0 },
+       { "filter-a-on-v6", &cfg_type_filter_a, 0 },
+       { "filter-a-on-v4", &cfg_type_filter_a, 0 },
+};
+
+static cfg_clausedef_t *param_clausesets[] = { param_clauses, NULL };
+
+static cfg_type_t cfg_type_parameters = { "filter-a-params", cfg_parse_mapbody,
+                                         cfg_print_mapbody, cfg_doc_mapbody,
+                                         &cfg_rep_map,      param_clausesets };
+
+static isc_result_t
+parse_filter_a_on(const cfg_obj_t *param_obj, const char *param_name,
+                 filter_a_t *dstp) {
+       const cfg_obj_t *obj = NULL;
+       isc_result_t result;
+
+       result = cfg_map_get(param_obj, param_name, &obj);
+       if (result != ISC_R_SUCCESS) {
+               return (ISC_R_SUCCESS);
+       }
+
+       if (cfg_obj_isboolean(obj)) {
+               if (cfg_obj_asboolean(obj)) {
+                       *dstp = FILTER;
+               } else {
+                       *dstp = NONE;
+               }
+       } else if (strcasecmp(cfg_obj_asstring(obj), "break-dnssec") == 0) {
+               *dstp = BREAK_DNSSEC;
+       } else {
+               result = ISC_R_UNEXPECTED;
+       }
+
+       return (result);
+}
+
+static isc_result_t
+check_syntax(cfg_obj_t *fmap, const void *cfg, isc_mem_t *mctx, isc_log_t *lctx,
+            void *actx) {
+       isc_result_t result = ISC_R_SUCCESS;
+       const cfg_obj_t *aclobj = NULL;
+       dns_acl_t *acl = NULL;
+       filter_a_t f4 = NONE, f6 = NONE;
+
+       cfg_map_get(fmap, "filter-a", &aclobj);
+       if (aclobj == NULL) {
+               return (result);
+       }
+
+       CHECK(cfg_acl_fromconfig(aclobj, (const cfg_obj_t *)cfg, lctx,
+                                (cfg_aclconfctx_t *)actx, mctx, 0, &acl));
+
+       CHECK(parse_filter_a_on(fmap, "filter-a-on-v6", &f6));
+       CHECK(parse_filter_a_on(fmap, "filter-a-on-v4", &f4));
+
+       if ((f4 != NONE || f6 != NONE) && dns_acl_isnone(acl)) {
+               cfg_obj_log(aclobj, lctx, ISC_LOG_WARNING,
+                           "\"filter-a\" is 'none;' but "
+                           "either filter-a-on-v6 or filter-a-on-v4 "
+                           "is enabled");
+               result = ISC_R_FAILURE;
+       } else if (f4 == NONE && f6 == NONE && !dns_acl_isnone(acl)) {
+               cfg_obj_log(aclobj, lctx, ISC_LOG_WARNING,
+                           "\"filter-a\" is set but "
+                           "neither filter-a-on-v6 or filter-a-on-v4 "
+                           "is enabled");
+               result = ISC_R_FAILURE;
+       }
+
+cleanup:
+       if (acl != NULL) {
+               dns_acl_detach(&acl);
+       }
+
+       return (result);
+}
+
+static isc_result_t
+parse_parameters(filter_instance_t *inst, 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 = ISC_R_SUCCESS;
+       cfg_parser_t *parser = NULL;
+       cfg_obj_t *param_obj = NULL;
+       const cfg_obj_t *obj = NULL;
+       isc_buffer_t b;
+
+       CHECK(cfg_parser_create(mctx, lctx, &parser));
+
+       isc_buffer_constinit(&b, parameters, strlen(parameters));
+       isc_buffer_add(&b, strlen(parameters));
+       CHECK(cfg_parse_buffer(parser, &b, cfg_file, cfg_line,
+                              &cfg_type_parameters, 0, &param_obj));
+
+       CHECK(check_syntax(param_obj, cfg, mctx, lctx, actx));
+
+       CHECK(parse_filter_a_on(param_obj, "filter-a-on-v6", &inst->v6_a));
+       CHECK(parse_filter_a_on(param_obj, "filter-a-on-v4", &inst->v4_a));
+
+       result = cfg_map_get(param_obj, "filter-a", &obj);
+       if (result == ISC_R_SUCCESS) {
+               CHECK(cfg_acl_fromconfig(obj, (const cfg_obj_t *)cfg, lctx,
+                                        (cfg_aclconfctx_t *)actx, mctx, 0,
+                                        &inst->a_acl));
+       } else {
+               CHECK(dns_acl_any(mctx, &inst->a_acl));
+       }
+
+cleanup:
+       if (param_obj != NULL) {
+               cfg_obj_destroy(parser, &param_obj);
+       }
+       if (parser != NULL) {
+               cfg_parser_destroy(&parser);
+       }
+       return (result);
+}
+
+/**
+** Mandatory plugin API functions:
+**
+** - plugin_destroy
+** - plugin_register
+** - plugin_version
+** - plugin_check
+**/
+
+/*
+ * Called by ns_plugin_register() to initialize the plugin and
+ * register hook functions into the view hook table.
+ */
+isc_result_t
+plugin_register(const char *parameters, const void *cfg, const char *cfg_file,
+               unsigned long cfg_line, isc_mem_t *mctx, isc_log_t *lctx,
+               void *actx, ns_hooktable_t *hooktable, void **instp) {
+       filter_instance_t *inst = NULL;
+       isc_result_t result;
+
+       isc_log_write(lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS,
+                     ISC_LOG_INFO,
+                     "registering 'filter-a' "
+                     "module from %s:%lu, %s parameters",
+                     cfg_file, cfg_line, parameters != NULL ? "with" : "no");
+
+       inst = isc_mem_get(mctx, sizeof(*inst));
+       memset(inst, 0, sizeof(*inst));
+       isc_mem_attach(mctx, &inst->mctx);
+
+       if (parameters != NULL) {
+               CHECK(parse_parameters(inst, parameters, cfg, cfg_file,
+                                      cfg_line, mctx, lctx, actx));
+       }
+
+       isc_mempool_create(mctx, sizeof(filter_data_t), &inst->datapool);
+       CHECK(isc_ht_init(&inst->ht, mctx, 16));
+       isc_mutex_init(&inst->hlock);
+
+       /*
+        * Fill the mempool with 1K filter_a state objects at
+        * a time; ideally after a single allocation, the mempool will
+        * have enough to handle all the simultaneous queries the system
+        * requires and it won't be necessary to allocate more.
+        *
+        * We don't set any limit on the number of free state objects
+        * so that they'll always be returned to the pool and not
+        * freed until the pool is destroyed on shutdown.
+        */
+       isc_mempool_setfillcount(inst->datapool, 1024);
+       isc_mempool_setfreemax(inst->datapool, UINT_MAX);
+       isc_mutex_init(&inst->plock);
+       isc_mempool_associatelock(inst->datapool, &inst->plock);
+
+       /*
+        * Set hook points in the view's hooktable.
+        */
+       install_hooks(hooktable, mctx, inst);
+
+       *instp = inst;
+
+cleanup:
+       if (result != ISC_R_SUCCESS && inst != NULL) {
+               plugin_destroy((void **)&inst);
+       }
+
+       return (result);
+}
+
+isc_result_t
+plugin_check(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 = ISC_R_SUCCESS;
+       cfg_parser_t *parser = NULL;
+       cfg_obj_t *param_obj = NULL;
+       isc_buffer_t b;
+
+       CHECK(cfg_parser_create(mctx, lctx, &parser));
+
+       isc_buffer_constinit(&b, parameters, strlen(parameters));
+       isc_buffer_add(&b, strlen(parameters));
+       CHECK(cfg_parse_buffer(parser, &b, cfg_file, cfg_line,
+                              &cfg_type_parameters, 0, &param_obj));
+
+       CHECK(check_syntax(param_obj, cfg, mctx, lctx, actx));
+
+cleanup:
+       if (param_obj != NULL) {
+               cfg_obj_destroy(parser, &param_obj);
+       }
+       if (parser != NULL) {
+               cfg_parser_destroy(&parser);
+       }
+       return (result);
+}
+
+/*
+ * Called by ns_plugins_free(); frees memory allocated by
+ * the module when it was registered.
+ */
+void
+plugin_destroy(void **instp) {
+       filter_instance_t *inst = (filter_instance_t *)*instp;
+
+       if (inst->ht != NULL) {
+               isc_ht_destroy(&inst->ht);
+               isc_mutex_destroy(&inst->hlock);
+       }
+       if (inst->datapool != NULL) {
+               isc_mempool_destroy(&inst->datapool);
+               isc_mutex_destroy(&inst->plock);
+       }
+       if (inst->a_acl != NULL) {
+               dns_acl_detach(&inst->a_acl);
+       }
+
+       isc_mem_putanddetach(&inst->mctx, inst, sizeof(*inst));
+       *instp = NULL;
+
+       return;
+}
+
+/*
+ * Returns plugin API version for compatibility checks.
+ */
+int
+plugin_version(void) {
+       return (NS_PLUGIN_VERSION);
+}
+
+/**
+** "filter-a" feature implementation begins here.
+**/
+
+/*%
+ * Structure describing the filtering to be applied by process_section().
+ */
+typedef struct section_filter {
+       query_ctx_t *qctx;
+       filter_a_t mode;
+       dns_section_t section;
+       const dns_name_t *name;
+       dns_rdatatype_t type;
+       bool only_if_aaaa_exists;
+} section_filter_t;
+
+/*
+ * Check whether this is an IPv4 client.
+ */
+static bool
+is_v4_client(ns_client_t *client) {
+       if (isc_sockaddr_pf(&client->peeraddr) == AF_INET) {
+               return (true);
+       }
+       if (isc_sockaddr_pf(&client->peeraddr) == AF_INET6 &&
+           IN6_IS_ADDR_V4MAPPED(&client->peeraddr.type.sin6.sin6_addr))
+       {
+               return (true);
+       }
+       return (false);
+}
+
+/*
+ * Check whether this is an IPv6 client.
+ */
+static bool
+is_v6_client(ns_client_t *client) {
+       if (isc_sockaddr_pf(&client->peeraddr) == AF_INET6 &&
+           !IN6_IS_ADDR_V4MAPPED(&client->peeraddr.type.sin6.sin6_addr))
+       {
+               return (true);
+       }
+       return (false);
+}
+
+static filter_data_t *
+client_state_get(const query_ctx_t *qctx, filter_instance_t *inst) {
+       filter_data_t *client_state = NULL;
+       isc_result_t result;
+
+       LOCK(&inst->hlock);
+       result = isc_ht_find(inst->ht, (const unsigned char *)&qctx->client,
+                            sizeof(qctx->client), (void **)&client_state);
+       UNLOCK(&inst->hlock);
+
+       return (result == ISC_R_SUCCESS ? client_state : NULL);
+}
+
+static void
+client_state_create(const query_ctx_t *qctx, filter_instance_t *inst) {
+       filter_data_t *client_state;
+       isc_result_t result;
+
+       client_state = isc_mempool_get(inst->datapool);
+       if (client_state == NULL) {
+               return;
+       }
+
+       client_state->mode = NONE;
+       client_state->flags = 0;
+
+       LOCK(&inst->hlock);
+       result = isc_ht_add(inst->ht, (const unsigned char *)&qctx->client,
+                           sizeof(qctx->client), client_state);
+       UNLOCK(&inst->hlock);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+}
+
+static void
+client_state_destroy(const query_ctx_t *qctx, filter_instance_t *inst) {
+       filter_data_t *client_state = client_state_get(qctx, inst);
+       isc_result_t result;
+
+       if (client_state == NULL) {
+               return;
+       }
+
+       LOCK(&inst->hlock);
+       result = isc_ht_delete(inst->ht, (const unsigned char *)&qctx->client,
+                              sizeof(qctx->client));
+       UNLOCK(&inst->hlock);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
+       isc_mempool_put(inst->datapool, client_state);
+}
+
+/*%
+ * Mark 'rdataset' and 'sigrdataset' as rendered, gracefully handling NULL
+ * pointers and non-associated rdatasets.
+ */
+static void
+mark_as_rendered(dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset) {
+       if (rdataset != NULL && dns_rdataset_isassociated(rdataset)) {
+               rdataset->attributes |= DNS_RDATASETATTR_RENDERED;
+       }
+       if (sigrdataset != NULL && dns_rdataset_isassociated(sigrdataset)) {
+               sigrdataset->attributes |= DNS_RDATASETATTR_RENDERED;
+       }
+}
+
+/*%
+ * Check whether an RRset of given 'type' is present at given 'name'.  If
+ * it is found and either it is not signed or the combination of query
+ * flags and configured processing 'mode' allows it, mark the RRset and its
+ * associated signatures as already rendered to prevent them from appearing
+ * in the response message stored in 'qctx'.  If 'only_if_aaaa_exists' is
+ * true, an RRset of type AAAA must also exist at 'name' in order for the
+ * above processing to happen.
+ */
+static bool
+process_name(query_ctx_t *qctx, filter_a_t mode, const dns_name_t *name,
+            dns_rdatatype_t type, bool only_if_aaaa_exists) {
+       dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
+       isc_result_t result;
+       bool modified = false;
+
+       if (only_if_aaaa_exists) {
+               CHECK(dns_message_findtype(name, dns_rdatatype_aaaa, 0, NULL));
+       }
+
+       (void)dns_message_findtype(name, type, 0, &rdataset);
+       (void)dns_message_findtype(name, dns_rdatatype_rrsig, type,
+                                  &sigrdataset);
+
+       if (rdataset != NULL &&
+           (sigrdataset == NULL || !WANTDNSSEC(qctx->client) ||
+            mode == BREAK_DNSSEC))
+       {
+               /*
+                * An RRset of given 'type' was found at 'name' and at least
+                * one of the following is true:
+                *
+                *   - the RRset is not signed,
+                *   - the client did not set the DO bit in its request,
+                *   - configuration allows us to tamper with signed responses.
+                *
+                * This means it is okay to filter out this RRset and its
+                * signatures, if any, from the response.
+                */
+               mark_as_rendered(rdataset, sigrdataset);
+               modified = true;
+       }
+
+cleanup:
+       return (modified);
+}
+
+/*%
+ * Apply the requested section filter, i.e. prevent (when possible, as
+ * determined by process_name()) RRsets of given 'type' from being rendered
+ * in the given 'section' of the response message stored in 'qctx'.  Clear
+ * the AD bit if the answer and/or authority section was modified.  If
+ * 'name' is NULL, all names in the given 'section' are processed;
+ * otherwise, only 'name' is.  'only_if_aaaa_exists' is passed through to
+ * process_name().
+ */
+static void
+process_section(const section_filter_t *filter) {
+       query_ctx_t *qctx = filter->qctx;
+       filter_a_t mode = filter->mode;
+       dns_section_t section = filter->section;
+       const dns_name_t *name = filter->name;
+       dns_rdatatype_t type = filter->type;
+       bool only_if_aaaa_exists = filter->only_if_aaaa_exists;
+
+       dns_message_t *message = qctx->client->message;
+       isc_result_t result;
+
+       for (result = dns_message_firstname(message, section);
+            result == ISC_R_SUCCESS;
+            result = dns_message_nextname(message, section))
+       {
+               dns_name_t *cur = NULL;
+               dns_message_currentname(message, section, &cur);
+               if (name != NULL && !dns_name_equal(name, cur)) {
+                       /*
+                        * We only want to process 'name' and this is not it.
+                        */
+                       continue;
+               }
+
+               if (!process_name(qctx, mode, cur, type, only_if_aaaa_exists)) {
+                       /*
+                        * Response was not modified, do not touch the AD bit.
+                        */
+                       continue;
+               }
+
+               if (section == DNS_SECTION_ANSWER ||
+                   section == DNS_SECTION_AUTHORITY) {
+                       message->flags &= ~DNS_MESSAGEFLAG_AD;
+               }
+       }
+}
+
+/*
+ * Initialize filter state, fetching it from a memory pool and storing it
+ * in a hash table keyed according to the client object; this enables us to
+ * retrieve persistent data related to a client query for as long as the
+ * object persists.
+ */
+static ns_hookresult_t
+filter_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+       filter_data_t *client_state;
+
+       *resp = ISC_R_UNSET;
+
+       client_state = client_state_get(qctx, inst);
+       if (client_state == NULL) {
+               client_state_create(qctx, inst);
+       }
+
+       return (NS_HOOK_CONTINUE);
+}
+
+/*
+ * Determine whether this client should have A filtered or not, based on
+ * the client address family and the settings of filter-a-on-v6 and
+ * filter-a-on-v4.
+ */
+static ns_hookresult_t
+filter_prep_response_begin(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+       filter_data_t *client_state = client_state_get(qctx, inst);
+       isc_result_t result;
+
+       *resp = ISC_R_UNSET;
+
+       if (client_state == NULL) {
+               return (NS_HOOK_CONTINUE);
+       }
+
+       if (inst->v4_a != NONE || inst->v6_a != NONE) {
+               result = ns_client_checkaclsilent(qctx->client, NULL,
+                                                 inst->a_acl, true);
+               if (result == ISC_R_SUCCESS && inst->v4_a != NONE &&
+                   is_v4_client(qctx->client)) {
+                       client_state->mode = inst->v4_a;
+               } else if (result == ISC_R_SUCCESS && inst->v6_a != NONE &&
+                          is_v6_client(qctx->client))
+               {
+                       client_state->mode = inst->v6_a;
+               }
+       }
+
+       return (NS_HOOK_CONTINUE);
+}
+
+/*
+ * Hide A rrsets if there is a matching AAAA. Trigger recursion if
+ * necessary to find out whether an AAAA exists.
+ *
+ * (This version is for processing answers to explicit A queries; ANY
+ * queries are handled in filter_respond_any_found().)
+ */
+static ns_hookresult_t
+filter_respond_begin(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+       filter_data_t *client_state = client_state_get(qctx, inst);
+       isc_result_t result = ISC_R_UNSET;
+
+       *resp = ISC_R_UNSET;
+
+       if (client_state == NULL) {
+               return (NS_HOOK_CONTINUE);
+       }
+
+       if (client_state->mode != BREAK_DNSSEC &&
+           (client_state->mode != FILTER ||
+            (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL &&
+             dns_rdataset_isassociated(qctx->sigrdataset))))
+       {
+               return (NS_HOOK_CONTINUE);
+       }
+
+       if (qctx->qtype == dns_rdatatype_a) {
+               dns_rdataset_t *trdataset;
+               trdataset = ns_client_newrdataset(qctx->client);
+               result = dns_db_findrdataset(
+                       qctx->db, qctx->node, qctx->version, dns_rdatatype_aaaa,
+                       0, qctx->client->now, trdataset, NULL);
+               if (dns_rdataset_isassociated(trdataset)) {
+                       dns_rdataset_disassociate(trdataset);
+               }
+               ns_client_putrdataset(qctx->client, &trdataset);
+
+               /*
+                * We found an A. If we also found an AAAA, then the A
+                * must not be rendered.
+                *
+                * If the AAAA is not in our cache, then any result other than
+                * DNS_R_DELEGATION or ISC_R_NOTFOUND means there is no AAAAA,
+                * and so AAAAs are okay.
+                *
+                * We assume there is no AAAA if we can't recurse for this
+                * client. That might be the wrong answer, but what else
+                * can we do?  Besides, the fact that we have the A and
+                * are using this mechanism in the first place suggests
+                * that we care more about AAAAs than As, and would have
+                * cached an AAAA if it existed.
+                */
+               if (result == ISC_R_SUCCESS) {
+                       mark_as_rendered(qctx->rdataset, qctx->sigrdataset);
+                       qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD;
+                       client_state->flags |= FILTER_A_FILTERED;
+               } else if (!qctx->authoritative && RECURSIONOK(qctx->client) &&
+                          (result == DNS_R_DELEGATION ||
+                           result == ISC_R_NOTFOUND))
+               {
+                       /*
+                        * This is an ugly kludge to recurse
+                        * for the AAAA and discard the result.???
+                        *
+                        * Continue to add the A now.
+                        * We'll make a note to not render it
+                        * if the recursion for the AAAA succeeds.
+                        */
+                       result = ns_query_recurse(qctx->client,
+                                                 dns_rdatatype_aaaa,
+                                                 qctx->client->query.qname,
+                                                 NULL, NULL, qctx->resuming);
+                       if (result == ISC_R_SUCCESS) {
+                               client_state->flags |= FILTER_A_RECURSING;
+                               qctx->client->query.attributes |=
+                                       NS_QUERYATTR_RECURSING;
+                       }
+               }
+       } else if (qctx->qtype == dns_rdatatype_aaaa &&
+                  (client_state->flags & FILTER_A_RECURSING) != 0)
+       {
+               const section_filter_t filter_answer = {
+                       .qctx = qctx,
+                       .mode = client_state->mode,
+                       .section = DNS_SECTION_ANSWER,
+                       .name = qctx->fname,
+                       .type = dns_rdatatype_a,
+               };
+               process_section(&filter_answer);
+
+               client_state->flags &= ~FILTER_A_RECURSING;
+
+               result = ns_query_done(qctx);
+
+               *resp = result;
+
+               return (NS_HOOK_RETURN);
+       }
+
+       *resp = result;
+       return (NS_HOOK_CONTINUE);
+}
+
+/*
+ * When answering an ANY query, remove A if AAAA is present.
+ */
+static ns_hookresult_t
+filter_respond_any_found(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+       filter_data_t *client_state = client_state_get(qctx, inst);
+
+       *resp = ISC_R_UNSET;
+
+       if (client_state != NULL && client_state->mode != NONE) {
+               /*
+                * If we are authoritative, require an AAAA record to be
+                * present before filtering out A records; otherwise,
+                * just assume an AAAA record exists even if it was not in the
+                * cache (and therefore is not in the response message),
+                * thus proceeding with filtering out A records.
+                */
+               const section_filter_t filter_answer = {
+                       .qctx = qctx,
+                       .mode = client_state->mode,
+                       .section = DNS_SECTION_ANSWER,
+                       .name = qctx->tname,
+                       .type = dns_rdatatype_a,
+                       .only_if_aaaa_exists = qctx->authoritative,
+               };
+               process_section(&filter_answer);
+       }
+
+       return (NS_HOOK_CONTINUE);
+}
+
+/*
+ * Hide A rrsets in the additional section if there is a matching AAAA, and
+ * hide NS in the authority section if A was filtered in the answer
+ * section.
+ */
+static ns_hookresult_t
+filter_query_done_send(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+       filter_data_t *client_state = client_state_get(qctx, inst);
+
+       *resp = ISC_R_UNSET;
+
+       if (client_state != NULL && client_state->mode != NONE) {
+               const section_filter_t filter_additional = {
+                       .qctx = qctx,
+                       .mode = client_state->mode,
+                       .section = DNS_SECTION_ADDITIONAL,
+                       .type = dns_rdatatype_a,
+                       .only_if_aaaa_exists = true,
+               };
+               process_section(&filter_additional);
+
+               if ((client_state->flags & FILTER_A_FILTERED) != 0) {
+                       const section_filter_t filter_authority = {
+                               .qctx = qctx,
+                               .mode = client_state->mode,
+                               .section = DNS_SECTION_AUTHORITY,
+                               .type = dns_rdatatype_ns,
+                       };
+                       process_section(&filter_authority);
+               }
+       }
+
+       return (NS_HOOK_CONTINUE);
+}
+
+/*
+ * If the client is being detached, then we can delete our persistent data
+ * from hash table and return it to the memory pool.
+ */
+static ns_hookresult_t
+filter_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp) {
+       query_ctx_t *qctx = (query_ctx_t *)arg;
+       filter_instance_t *inst = (filter_instance_t *)cbdata;
+
+       *resp = ISC_R_UNSET;
+
+       if (!qctx->detach_client) {
+               return (NS_HOOK_CONTINUE);
+       }
+
+       client_state_destroy(qctx, inst);
+
+       return (NS_HOOK_CONTINUE);
+}
diff --git a/bin/plugins/filter-a.rst b/bin/plugins/filter-a.rst
new file mode 100644 (file)
index 0000000..58c4c4c
--- /dev/null
@@ -0,0 +1,95 @@
+.. 
+   Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+   
+   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.
+
+..
+   Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+   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 http://mozilla.org/MPL/2.0/.
+
+   See the COPYRIGHT file distributed with this work for additional
+   information regarding copyright ownership.
+
+
+.. highlight: console
+
+.. _man_filter-a:
+
+filter-a.so - filter A in DNS responses when AAAA is present
+---------------------------------------------------------------
+
+Synopsis
+~~~~~~~~
+
+:program:`plugin query` "filter-a.so" [{ parameters }];
+
+Description
+~~~~~~~~~~~
+
+``filter-a.so`` is a query plugin module for ``named``, enabling
+``named`` to omit some IPv4 addresses when responding to clients.
+
+For example:
+
+::
+
+   plugin query "/usr/local/lib/filter-a.so" {
+           filter-a-on-v6 yes;
+           filter-a-on-v4 yes;
+           filter-a { 192.0.2.1; 2001:db8:2::1; };
+   };
+
+This module is intended to aid transition from IPv4 to IPv6 by
+withholding IPv4 addresses from DNS clients which are not connected to
+the IPv4 Internet, when the name being looked up has an IPv6 address
+available. Use of this module is not recommended unless absolutely
+necessary.
+
+Note: This mechanism can erroneously cause other servers not to give
+A records to their clients. If a recursing server with both IPv6 and
+IPv4 network connections queries an authoritative server using this
+mechanism via IPv6, it is denied A records even if its client is
+using IPv4.
+
+Options
+~~~~~~~
+
+``filter-a``
+   This option specifies a list of client addresses for which A filtering is to
+   be applied. The default is ``any``.
+
+``filter-a-on-v6``
+   If set to ``yes``, this option indicates that the DNS client is at an IPv6 address, in
+   ``filter-a``. If the response does not include DNSSEC
+   signatures, then all A records are deleted from the response. This
+   filtering applies to all responses, not only authoritative
+   ones.
+
+   If set to ``break-dnssec``, then A records are deleted even when
+   DNSSEC is enabled. As suggested by the name, this causes the response
+   to fail to verify, because the DNSSEC protocol is designed to detect
+   deletions.
+
+   This mechanism can erroneously cause other servers not to give A
+   records to their clients. If a recursing server with both IPv6 and IPv4
+   network connections queries an authoritative server using this
+   mechanism via IPv6, it is denied A records even if its client is
+   using IPv4.
+
+``filter-a-on-v4``
+   This option is identical to ``filter-a-on-v6``, except that it filters A responses
+   to queries from IPv4 clients instead of IPv6 clients. To filter all
+   responses, set both options to ``yes``.
+
+See Also
+~~~~~~~~
+
+BIND 9 Administrator Reference Manual.
index a62ca2c4cc973c77f592082fd72edf1df6c15f88..19c776ad09fac31d28dc0784dd282b22fbfa0b8e 100644 (file)
@@ -17,6 +17,7 @@ MANPAGES_RST =                                \
        dnssec-verify.rst               \
        dnstap-read.rst                 \
        filter-aaaa.rst                 \
+       filter-a.rst                    \
        host.rst                        \
        index.rst                       \
        mdig.rst                        \
@@ -62,6 +63,7 @@ MANPAGES_RST =                                \
        ../../bin/pkcs11/pkcs11-list.rst \
        ../../bin/pkcs11/pkcs11-tokens.rst \
        ../../bin/plugins/filter-aaaa.rst \
+       ../../bin/plugins/filter-a.rst \
        ../../bin/rndc/rndc.conf.rst \
        ../../bin/rndc/rndc.rst \
        ../../bin/tools/arpaname.rst \
@@ -96,6 +98,7 @@ man_MANS =                            \
        dnssec-signzone.1               \
        dnssec-verify.1                 \
        filter-aaaa.8                   \
+       filter-a.8                      \
        named-checkconf.1               \
        named-checkzone.1               \
        named-journalprint.1            \
index 7ce906520981d5c1e042bd4c3a2c487c274a6805..659c7d16ec75b8d00fd9171fa3d17778fa209a26 100644 (file)
@@ -74,6 +74,7 @@ man_pages = [
     ('dnssec-verify', 'dnssec-verify', 'DNSSEC zone verification tool', author, 1),
     ('dnstap-read', 'dnstap-read', 'print dnstap data in human-readable form', author, 1),
     ('filter-aaaa', 'filter-aaaa', 'filter AAAA in DNS responses when A is present', author, 8),
+    ('filter-a', 'filter-a', 'filter A in DNS responses when AAAA is present', author, 8),
     ('host', 'host', 'DNS lookup utility', author, 1),
     ('mdig', 'mdig', 'DNS pipelined lookup utility', author, 1),
     ('named-checkconf', 'named-checkconf', 'named configuration file syntax checking tool', author, 1),
diff --git a/doc/man/filter-a.8in b/doc/man/filter-a.8in
new file mode 100644 (file)
index 0000000..6e38500
--- /dev/null
@@ -0,0 +1,106 @@
+.\" Man page generated from reStructuredText.
+.
+.TH "FILTER-A" "8" "@RELEASE_DATE@" "@PACKAGE_VERSION@" "BIND 9"
+.SH NAME
+filter-a \- filter A in DNS responses when AAAA is present
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.SH SYNOPSIS
+.sp
+\fBplugin query\fP "filter\-a.so" [{ parameters }];
+.SH DESCRIPTION
+.sp
+\fBfilter\-a.so\fP is a query plugin module for \fBnamed\fP, enabling
+\fBnamed\fP to omit some IPv4 addresses when responding to clients.
+.sp
+For example:
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.nf
+.ft C
+plugin query "/usr/local/lib/filter\-a.so" {
+        filter\-a\-on\-v6 yes;
+        filter\-a\-on\-v4 yes;
+        filter\-a { 192.0.2.1; 2001:db8:2::1; };
+};
+.ft P
+.fi
+.UNINDENT
+.UNINDENT
+.sp
+This module is intended to aid transition from IPv4 to IPv6 by
+withholding IPv4 addresses from DNS clients which are not connected to
+the IPv4 Internet, when the name being looked up has an IPv6 address
+available. Use of this module is not recommended unless absolutely
+necessary.
+.sp
+Note: This mechanism can erroneously cause other servers not to give
+A records to their clients. If a recursing server with both IPv6 and
+IPv4 network connections queries an authoritative server using this
+mechanism via IPv6, it is denied A records even if its client is
+using IPv4.
+.SH OPTIONS
+.INDENT 0.0
+.TP
+.B \fBfilter\-a\fP
+This option specifies a list of client addresses for which A filtering is to
+be applied. The default is \fBany\fP\&.
+.TP
+.B \fBfilter\-a\-on\-v6\fP
+If set to \fByes\fP, this option indicates that the DNS client is at an IPv6 address, in
+\fBfilter\-a\fP\&. If the response does not include DNSSEC
+signatures, then all A records are deleted from the response. This
+filtering applies to all responses, not only authoritative
+ones.
+.sp
+If set to \fBbreak\-dnssec\fP, then A records are deleted even when
+DNSSEC is enabled. As suggested by the name, this causes the response
+to fail to verify, because the DNSSEC protocol is designed to detect
+deletions.
+.sp
+This mechanism can erroneously cause other servers not to give A
+records to their clients. If a recursing server with both IPv6 and IPv4
+network connections queries an authoritative server using this
+mechanism via IPv6, it is denied A records even if its client is
+using IPv4.
+.TP
+.B \fBfilter\-a\-on\-v4\fP
+This option is identical to \fBfilter\-a\-on\-v6\fP, except that it filters A responses
+to queries from IPv4 clients instead of IPv6 clients. To filter all
+responses, set both options to \fByes\fP\&.
+.UNINDENT
+.SH SEE ALSO
+.sp
+BIND 9 Administrator Reference Manual.
+.SH AUTHOR
+Internet Systems Consortium
+.SH COPYRIGHT
+2021, Internet Systems Consortium
+.\" Generated by docutils manpage writer.
+.
diff --git a/doc/man/filter-a.rst b/doc/man/filter-a.rst
new file mode 100644 (file)
index 0000000..31da2f0
--- /dev/null
@@ -0,0 +1,13 @@
+.. 
+   Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+   
+   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.
+
+:orphan:
+
+.. include:: ../../bin/plugins/filter-a.rst
\ No newline at end of file
index aef7cbfb8c06c7aa911d096d5c01399a0392fa65..439c141f01d94deb9b5b309ec5da317b470fa0bf 100644 (file)
 ./bin/pkcs11/win32/pk11tokens.vcxproj.filters.in       X       2014,2015,2018,2019,2020
 ./bin/pkcs11/win32/pk11tokens.vcxproj.in       X       2014,2015,2016,2017,2018,2019,2020
 ./bin/pkcs11/win32/pk11tokens.vcxproj.user     X       2014,2018,2019,2020,2021
+./bin/plugins/filter-a.c                       C       2018,2019,2020,2021
+./bin/plugins/filter-a.rst                     RST     2020,2021
 ./bin/plugins/filter-aaaa.c                    C       2018,2019,2020,2021
 ./bin/plugins/filter-aaaa.rst                  RST     2020,2021
 ./bin/rndc/rndc.c                              C       2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021
 ./doc/man/dnssec-signzone.rst                  RST     2020,2021
 ./doc/man/dnssec-verify.rst                    RST     2020,2021
 ./doc/man/dnstap-read.rst                      RST     2020,2021
+./doc/man/filter-a.rst                         RST     2020,2021
 ./doc/man/filter-aaaa.rst                      RST     2020,2021
 ./doc/man/host.rst                             RST     2020,2021
 ./doc/man/index.rst                            RST     2020,2021