From 6b2ea00621c984a88cb6e76ddd3da80590b6b25f Mon Sep 17 00:00:00 2001 From: treysis Date: Wed, 3 Mar 2021 11:33:55 +0100 Subject: [PATCH] Add filter-a plugin for IPv6-dominant environments (cherry picked from commit 78f6cd57e1cc166823415438fe2d19a324cf7a67) --- bin/plugins/Makefile.am | 3 + bin/plugins/filter-a.c | 906 +++++++++++++++++++++++++++++++++++++++ bin/plugins/filter-a.rst | 95 ++++ doc/man/Makefile.am | 3 + doc/man/conf.py | 1 + doc/man/filter-a.8in | 106 +++++ doc/man/filter-a.rst | 13 + util/copyrights | 3 + 8 files changed, 1130 insertions(+) create mode 100644 bin/plugins/filter-a.c create mode 100644 bin/plugins/filter-a.rst create mode 100644 doc/man/filter-a.8in create mode 100644 doc/man/filter-a.rst diff --git a/bin/plugins/Makefile.am b/bin/plugins/Makefile.am index c29b2615db..611dd1b1e0 100644 --- a/bin/plugins/Makefile.am +++ b/bin/plugins/Makefile.am @@ -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 index 0000000000..4361e975f5 --- /dev/null +++ b/bin/plugins/filter-a.c @@ -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 +#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 + +#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, ¶m_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, ¶m_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, ¶m_obj)); + + CHECK(check_syntax(param_obj, cfg, mctx, lctx, actx)); + +cleanup: + if (param_obj != NULL) { + cfg_obj_destroy(parser, ¶m_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 index 0000000000..58c4c4ccd0 --- /dev/null +++ b/bin/plugins/filter-a.rst @@ -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. diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am index a62ca2c4cc..19c776ad09 100644 --- a/doc/man/Makefile.am +++ b/doc/man/Makefile.am @@ -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 \ diff --git a/doc/man/conf.py b/doc/man/conf.py index 7ce9065209..659c7d16ec 100644 --- a/doc/man/conf.py +++ b/doc/man/conf.py @@ -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 index 0000000000..6e38500e72 --- /dev/null +++ b/doc/man/filter-a.8in @@ -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 index 0000000000..31da2f075b --- /dev/null +++ b/doc/man/filter-a.rst @@ -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 diff --git a/util/copyrights b/util/copyrights index aef7cbfb8c..439c141f01 100644 --- a/util/copyrights +++ b/util/copyrights @@ -191,6 +191,8 @@ ./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 @@ -1243,6 +1245,7 @@ ./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