2018-08-10 19:32:12 -07:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*! \file */
|
|
|
|
|
|
|
|
#include <config.h>
|
|
|
|
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <inttypes.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#include <isc/buffer.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
#include <isc/hash.h>
|
2018-09-19 16:25:50 -07:00
|
|
|
#include <isc/ht.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
#include <isc/lib.h>
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <isc/log.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
#include <isc/mem.h>
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <isc/netaddr.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
#include <isc/result.h>
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <isc/types.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
#include <isc/util.h>
|
|
|
|
|
2018-08-12 23:06:00 -07:00
|
|
|
#include <isccfg/aclconf.h>
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <isccfg/cfg.h>
|
2018-08-12 23:06:00 -07:00
|
|
|
#include <isccfg/grammar.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
|
|
|
|
#include <ns/client.h>
|
|
|
|
#include <ns/hooks.h>
|
|
|
|
#include <ns/log.h>
|
|
|
|
#include <ns/query.h>
|
2018-09-14 12:32:36 -07:00
|
|
|
#include <ns/types.h>
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-14 12:32:36 -07:00
|
|
|
#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>
|
|
|
|
|
|
|
|
#define CHECK(op) \
|
|
|
|
do { \
|
|
|
|
result = (op); \
|
|
|
|
if (result != ISC_R_SUCCESS) { \
|
|
|
|
goto cleanup; \
|
|
|
|
} \
|
2018-08-12 23:06:00 -07:00
|
|
|
} while (0)
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
2018-09-19 16:25:50 -07:00
|
|
|
* Possible values for the settings of filter-aaaa-on-v4 and
|
|
|
|
* filter-aaaa-on-v6: "no" is NONE, "yes" is FILTER, "break-dnssec"
|
|
|
|
* is BREAK_DNSSEC.
|
|
|
|
*/
|
|
|
|
typedef enum {
|
|
|
|
NONE = 0,
|
|
|
|
FILTER = 1,
|
|
|
|
BREAK_DNSSEC = 2
|
|
|
|
} filter_aaaa_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.
|
2018-08-13 01:18:09 -07:00
|
|
|
*/
|
2018-09-19 16:25:50 -07:00
|
|
|
typedef struct filter_data {
|
|
|
|
filter_aaaa_t mode;
|
|
|
|
uint32_t flags;
|
|
|
|
} filter_data_t;
|
2018-08-13 01:18:09 -07:00
|
|
|
|
|
|
|
/*
|
2018-09-19 16:25:50 -07:00
|
|
|
* Memory pool for use with persistent data.
|
2018-08-13 01:18:09 -07:00
|
|
|
*/
|
|
|
|
static isc_mempool_t *datapool = NULL;
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
/*
|
|
|
|
* Hash table associating a client object with its persistent data.
|
|
|
|
*/
|
|
|
|
static isc_ht_t *client_ht = NULL;
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
/*
|
|
|
|
* Per-client flags set by this module
|
|
|
|
*/
|
|
|
|
#define FILTER_AAAA_RECURSING 0x0001 /* Recursing for A */
|
|
|
|
#define FILTER_AAAA_FILTERED 0x0002 /* AAAA was removed from answer */
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
|
|
|
* Client attribute tests.
|
|
|
|
*/
|
|
|
|
#define WANTDNSSEC(c) (((c)->attributes & NS_CLIENTATTR_WANTDNSSEC) != 0)
|
|
|
|
#define RECURSIONOK(c) (((c)->query.attributes & \
|
2018-08-10 19:32:12 -07:00
|
|
|
NS_QUERYATTR_RECURSIONOK) != 0)
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
|
|
|
* Hook registration structures: pointers to these structures will
|
|
|
|
* be added to a hook table when this module is registered.
|
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_init = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_qctx_initialize,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-13 01:18:09 -07:00
|
|
|
};
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_respond_begin(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_respbegin = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_respond_begin,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-13 01:18:09 -07:00
|
|
|
};
|
2018-08-10 19:32:12 -07:00
|
|
|
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_respond_any_found(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_respanyfound = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_respond_any_found,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-13 01:18:09 -07:00
|
|
|
};
|
2018-08-10 19:32:12 -07:00
|
|
|
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_prep_response_begin(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_prepresp = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_prep_response_begin,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-13 01:18:09 -07:00
|
|
|
};
|
2018-08-10 19:32:12 -07:00
|
|
|
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_query_done_send(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_donesend = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_query_done_send,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-10 19:32:12 -07:00
|
|
|
};
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp);
|
2018-09-19 23:38:23 -07:00
|
|
|
static const ns_hook_t filter_destroy = {
|
2018-08-13 21:08:08 -07:00
|
|
|
.action = filter_qctx_destroy,
|
2018-09-19 16:25:50 -07:00
|
|
|
.action_data = &client_ht,
|
2018-08-13 01:18:09 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
** Support for parsing of parameters and configuration of the module.
|
|
|
|
**/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Values configured when the module is loaded.
|
|
|
|
*/
|
|
|
|
static filter_aaaa_t v4_aaaa = NONE;
|
|
|
|
static filter_aaaa_t v6_aaaa = NONE;
|
2018-08-12 23:06:00 -07:00
|
|
|
static dns_acl_t *aaaa_acl = NULL;
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
|
|
|
* Support for parsing of parameters.
|
|
|
|
*/
|
2018-08-12 23:06:00 -07:00
|
|
|
static const char *filter_aaaa_enums[] = { "break-dnssec", NULL };
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-08-12 23:06:00 -07:00
|
|
|
static isc_result_t
|
|
|
|
parse_filter_aaaa(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));
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-08-12 23:06:00 -07:00
|
|
|
static void
|
|
|
|
doc_filter_aaaa(cfg_printer_t *pctx, const cfg_type_t *type) {
|
|
|
|
cfg_doc_enum_or_other(pctx, type, &cfg_type_boolean);
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-08-12 23:06:00 -07:00
|
|
|
static cfg_type_t cfg_type_filter_aaaa = {
|
|
|
|
"filter_aaaa", parse_filter_aaaa, cfg_print_ustring,
|
|
|
|
doc_filter_aaaa, &cfg_rep_string, filter_aaaa_enums,
|
|
|
|
};
|
|
|
|
|
|
|
|
static cfg_clausedef_t param_clauses[] = {
|
|
|
|
{ "filter-aaaa", &cfg_type_bracketed_aml, 0 },
|
|
|
|
{ "filter-aaaa-on-v4", &cfg_type_filter_aaaa, 0 },
|
|
|
|
{ "filter-aaaa-on-v6", &cfg_type_filter_aaaa, 0 },
|
|
|
|
};
|
|
|
|
|
|
|
|
static cfg_clausedef_t *param_clausesets[] = {
|
|
|
|
param_clauses,
|
|
|
|
NULL
|
|
|
|
};
|
|
|
|
|
|
|
|
static cfg_type_t cfg_type_parameters = {
|
|
|
|
"filter-aaaa-params", cfg_parse_mapbody, cfg_print_mapbody,
|
|
|
|
cfg_doc_mapbody, &cfg_rep_map, param_clausesets
|
|
|
|
};
|
|
|
|
|
|
|
|
static isc_result_t
|
|
|
|
parse_filter_aaaa_on(const cfg_obj_t *param_obj, const char *param_name,
|
2018-08-13 01:18:09 -07:00
|
|
|
filter_aaaa_t *dstp)
|
2018-08-12 23:06:00 -07:00
|
|
|
{
|
|
|
|
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)) {
|
2018-08-13 01:18:09 -07:00
|
|
|
*dstp = FILTER;
|
2018-08-12 23:06:00 -07:00
|
|
|
} else {
|
2018-08-13 01:18:09 -07:00
|
|
|
*dstp = NONE;
|
2018-08-12 23:06:00 -07:00
|
|
|
}
|
|
|
|
} else if (strcasecmp(cfg_obj_asstring(obj), "break-dnssec") == 0) {
|
2018-08-13 01:18:09 -07:00
|
|
|
*dstp = BREAK_DNSSEC;
|
2018-08-12 23:06:00 -07:00
|
|
|
} else {
|
|
|
|
result = ISC_R_UNEXPECTED;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (result);
|
|
|
|
}
|
|
|
|
|
|
|
|
static isc_result_t
|
|
|
|
parse_parameters(const char *parameters, const void *cfg,
|
|
|
|
void *actx, ns_hookctx_t *hctx)
|
|
|
|
{
|
|
|
|
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(hctx->mctx, hctx->lctx, &parser));
|
|
|
|
|
|
|
|
isc_buffer_constinit(&b, parameters, strlen(parameters));
|
|
|
|
isc_buffer_add(&b, strlen(parameters));
|
|
|
|
CHECK(cfg_parse_buffer(parser, &b, &cfg_type_parameters,
|
|
|
|
¶m_obj));
|
|
|
|
|
|
|
|
CHECK(parse_filter_aaaa_on(param_obj, "filter-aaaa-on-v4", &v4_aaaa));
|
|
|
|
CHECK(parse_filter_aaaa_on(param_obj, "filter-aaaa-on-v6", &v6_aaaa));
|
|
|
|
|
|
|
|
obj = NULL;
|
|
|
|
result = cfg_map_get(param_obj, "filter-aaaa", &obj);
|
|
|
|
if (result == ISC_R_SUCCESS) {
|
|
|
|
CHECK(cfg_acl_fromconfig(obj, (const cfg_obj_t *) cfg,
|
|
|
|
hctx->lctx,
|
|
|
|
(cfg_aclconfctx_t *) actx,
|
|
|
|
hctx->mctx, 0, &aaaa_acl));
|
|
|
|
} else {
|
|
|
|
CHECK(dns_acl_any(hctx->mctx, &aaaa_acl));
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup:
|
|
|
|
if (param_obj != NULL) {
|
|
|
|
cfg_obj_destroy(parser, ¶m_obj);
|
|
|
|
}
|
|
|
|
if (parser != NULL) {
|
|
|
|
cfg_parser_destroy(&parser);
|
|
|
|
}
|
|
|
|
return (result);
|
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/**
|
2018-09-14 12:32:36 -07:00
|
|
|
** Mandatory hook API functions:
|
|
|
|
**
|
|
|
|
** - hook_destroy
|
|
|
|
** - hook_register
|
|
|
|
** - hook_version
|
2018-08-13 01:18:09 -07:00
|
|
|
**/
|
|
|
|
|
2018-08-12 23:06:00 -07:00
|
|
|
/*
|
2018-08-13 01:18:09 -07:00
|
|
|
* Called by ns_hookmodule_load() to register hook functions into
|
|
|
|
* a hook table.
|
2018-08-12 23:06:00 -07:00
|
|
|
*/
|
2018-08-10 19:32:12 -07:00
|
|
|
isc_result_t
|
2018-09-19 16:25:50 -07:00
|
|
|
hook_register(const char *parameters,
|
2018-09-14 12:32:36 -07:00
|
|
|
const char *cfg_file, unsigned long cfg_line,
|
2018-08-13 01:18:09 -07:00
|
|
|
const void *cfg, void *actx,
|
|
|
|
ns_hookctx_t *hctx, ns_hooktable_t *hooktable, void **instp)
|
2018-08-10 19:32:12 -07:00
|
|
|
{
|
2018-08-13 01:18:09 -07:00
|
|
|
isc_result_t result;
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
UNUSED(instp);
|
|
|
|
|
2018-09-14 12:32:36 -07:00
|
|
|
isc_log_write(hctx->lctx, NS_LOGCATEGORY_GENERAL,
|
|
|
|
NS_LOGMODULE_HOOKS, ISC_LOG_INFO,
|
|
|
|
"loading 'filter-aaaa' "
|
|
|
|
"module from %s:%lu, %s parameters",
|
|
|
|
cfg_file, cfg_line, parameters != NULL ? "with" : "no");
|
2018-08-12 23:06:00 -07:00
|
|
|
|
2018-09-14 12:32:36 -07:00
|
|
|
if (parameters != NULL) {
|
2018-08-13 01:18:09 -07:00
|
|
|
CHECK(parse_parameters(parameters, cfg, actx, hctx));
|
2018-08-12 11:19:36 -07:00
|
|
|
}
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-19 23:38:23 -07:00
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_QCTX_INITIALIZED,
|
|
|
|
&filter_init);
|
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_RESPOND_BEGIN,
|
|
|
|
&filter_respbegin);
|
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_RESPOND_ANY_FOUND,
|
2018-08-10 19:32:12 -07:00
|
|
|
&filter_respanyfound);
|
2018-09-19 23:38:23 -07:00
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_PREP_RESPONSE_BEGIN,
|
|
|
|
&filter_prepresp);
|
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_DONE_SEND,
|
|
|
|
&filter_donesend);
|
|
|
|
ns_hook_add(hooktable, hctx->mctx, NS_QUERY_QCTX_DESTROYED,
|
|
|
|
&filter_destroy);
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
CHECK(isc_mempool_create(hctx->mctx, sizeof(filter_data_t),
|
2018-08-13 01:18:09 -07:00
|
|
|
&datapool));
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
CHECK(isc_ht_init(&client_ht, hctx->mctx, 16));
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
/*
|
2018-08-13 01:18:09 -07:00
|
|
|
* Fill the mempool with 1K filter_aaaa 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.
|
2018-08-10 19:32:12 -07:00
|
|
|
*/
|
2018-08-13 01:18:09 -07:00
|
|
|
isc_mempool_setfillcount(datapool, 1024);
|
|
|
|
isc_mempool_setfreemax(datapool, UINT_MAX);
|
|
|
|
|
|
|
|
cleanup:
|
|
|
|
if (result != ISC_R_SUCCESS) {
|
|
|
|
if (datapool != NULL) {
|
|
|
|
isc_mempool_destroy(&datapool);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (result);
|
2018-08-10 19:32:12 -07:00
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
2018-09-14 12:32:36 -07:00
|
|
|
* Called by ns_hookmodule_unload_all(); frees memory allocated by
|
2018-08-13 01:18:09 -07:00
|
|
|
* the module when it was registered.
|
|
|
|
*/
|
2018-08-10 19:32:12 -07:00
|
|
|
void
|
|
|
|
hook_destroy(void **instp) {
|
|
|
|
UNUSED(instp);
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
if (client_ht != NULL) {
|
|
|
|
isc_ht_destroy(&client_ht);
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
if (datapool != NULL) {
|
|
|
|
isc_mempool_destroy(&datapool);
|
|
|
|
}
|
2018-08-12 23:06:00 -07:00
|
|
|
if (aaaa_acl != NULL) {
|
|
|
|
dns_acl_detach(&aaaa_acl);
|
|
|
|
}
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
|
|
|
* Returns hook module API version for compatibility checks.
|
|
|
|
*/
|
2018-08-10 19:32:12 -07:00
|
|
|
int
|
2018-09-14 12:32:36 -07:00
|
|
|
hook_version(void) {
|
2018-08-10 19:32:12 -07:00
|
|
|
return (NS_HOOK_VERSION);
|
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/**
|
2018-09-14 12:32:36 -07:00
|
|
|
** "filter-aaaa" feature implementation begins here.
|
2018-08-13 01:18:09 -07:00
|
|
|
**/
|
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
/*%
|
|
|
|
* Structure describing the filtering to be applied by process_section().
|
|
|
|
*/
|
|
|
|
typedef struct section_filter {
|
|
|
|
query_ctx_t * qctx;
|
|
|
|
filter_aaaa_t mode;
|
|
|
|
dns_section_t section;
|
|
|
|
const dns_name_t * name;
|
|
|
|
dns_rdatatype_t type;
|
|
|
|
bool only_if_a_exists;
|
|
|
|
} section_filter_t;
|
|
|
|
|
2018-08-10 19:32:12 -07:00
|
|
|
/*
|
2018-09-14 12:32:36 -07:00
|
|
|
* Check whether this is an IPv4 client.
|
2018-08-10 19:32:12 -07:00
|
|
|
*/
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2018-09-14 12:32:36 -07:00
|
|
|
* Check whether this is an IPv6 client.
|
2018-08-10 19:32:12 -07:00
|
|
|
*/
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
static filter_data_t *
|
|
|
|
client_state_get(const query_ctx_t *qctx, isc_ht_t **htp) {
|
|
|
|
filter_data_t *client_state = NULL;
|
|
|
|
isc_result_t result;
|
|
|
|
|
|
|
|
result = isc_ht_find(*htp, (const unsigned char *)&qctx->client,
|
|
|
|
sizeof(qctx->client), (void **)&client_state);
|
|
|
|
|
|
|
|
return (result == ISC_R_SUCCESS ? client_state : NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
client_state_create(const query_ctx_t *qctx, isc_ht_t **htp) {
|
|
|
|
filter_data_t *client_state;
|
|
|
|
isc_result_t result;
|
|
|
|
|
|
|
|
client_state = isc_mempool_get(datapool);
|
|
|
|
if (client_state == NULL) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
client_state->mode = NONE;
|
|
|
|
client_state->flags = 0;
|
|
|
|
|
|
|
|
result = isc_ht_add(*htp, (const unsigned char *)&qctx->client,
|
|
|
|
sizeof(qctx->client), client_state);
|
|
|
|
RUNTIME_CHECK(result == ISC_R_SUCCESS);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
client_state_destroy(const query_ctx_t *qctx, isc_ht_t **htp) {
|
|
|
|
filter_data_t *client_state = client_state_get(qctx, htp);
|
|
|
|
isc_result_t result;
|
|
|
|
|
|
|
|
if (client_state == NULL) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = isc_ht_delete(*htp, (const unsigned char *)&qctx->client,
|
|
|
|
sizeof(qctx->client));
|
|
|
|
RUNTIME_CHECK(result == ISC_R_SUCCESS);
|
|
|
|
|
|
|
|
isc_mempool_put(datapool, client_state);
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
/*%
|
|
|
|
* 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_a_exists' is
|
|
|
|
* true, an RRset of type A must also exist at 'name' in order for the
|
|
|
|
* above processing to happen.
|
|
|
|
*/
|
|
|
|
static bool
|
|
|
|
process_name(query_ctx_t *qctx, filter_aaaa_t mode, const dns_name_t *name,
|
|
|
|
dns_rdatatype_t type, bool only_if_a_exists)
|
|
|
|
{
|
|
|
|
dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
|
|
|
|
isc_result_t result;
|
|
|
|
bool modified = false;
|
|
|
|
|
|
|
|
if (only_if_a_exists) {
|
|
|
|
CHECK(dns_message_findtype(name, dns_rdatatype_a, 0, NULL));
|
|
|
|
}
|
|
|
|
|
|
|
|
dns_message_findtype(name, type, 0, &rdataset);
|
|
|
|
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_a_exists' is passed through to
|
|
|
|
* process_name().
|
|
|
|
*/
|
|
|
|
static void
|
|
|
|
process_section(const section_filter_t *filter) {
|
|
|
|
query_ctx_t *qctx = filter->qctx;
|
|
|
|
filter_aaaa_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_a_exists = filter->only_if_a_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_a_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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
2018-09-19 16:25:50 -07:00
|
|
|
* Initialize filter state, fetching it from a memory pool and storing it
|
2018-09-20 15:07:33 +02:00
|
|
|
* 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.
|
2018-08-13 01:18:09 -07:00
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
|
|
|
filter_data_t *client_state;
|
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state = client_state_get(qctx, htp);
|
|
|
|
if (client_state == NULL) {
|
|
|
|
client_state_create(qctx, htp);
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2018-09-20 15:07:33 +02:00
|
|
|
* Determine whether this client should have AAAA filtered or not, based on
|
|
|
|
* the client address family and the settings of filter-aaaa-on-v4 and
|
|
|
|
* filter-aaaa-on-v6.
|
2018-08-10 19:32:12 -07:00
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_prep_response_begin(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
|
|
|
filter_data_t *client_state = client_state_get(qctx, htp);
|
2018-08-10 19:32:12 -07:00
|
|
|
isc_result_t result;
|
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
if (client_state == NULL) {
|
|
|
|
return (false);
|
|
|
|
}
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
if (v4_aaaa != NONE || v6_aaaa != NONE) {
|
2018-08-10 19:32:12 -07:00
|
|
|
result = ns_client_checkaclsilent(qctx->client, NULL,
|
2018-08-12 23:06:00 -07:00
|
|
|
aaaa_acl, true);
|
2018-08-10 19:32:12 -07:00
|
|
|
if (result == ISC_R_SUCCESS &&
|
2018-08-13 01:18:09 -07:00
|
|
|
v4_aaaa != NONE &&
|
2018-08-10 19:32:12 -07:00
|
|
|
is_v4_client(qctx->client))
|
|
|
|
{
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state->mode = v4_aaaa;
|
2018-08-10 19:32:12 -07:00
|
|
|
} else if (result == ISC_R_SUCCESS &&
|
2018-08-13 01:18:09 -07:00
|
|
|
v6_aaaa != NONE &&
|
2018-08-10 19:32:12 -07:00
|
|
|
is_v6_client(qctx->client))
|
|
|
|
{
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state->mode = v6_aaaa;
|
2018-08-10 19:32:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2018-08-13 01:18:09 -07:00
|
|
|
* Hide AAAA rrsets if there is a matching A. Trigger recursion if
|
|
|
|
* necessary to find out whether an A exists.
|
|
|
|
*
|
2018-09-20 15:07:33 +02:00
|
|
|
* (This version is for processing answers to explicit AAAA queries; ANY
|
|
|
|
* queries are handled in filter_respond_any_found().)
|
2018-08-10 19:32:12 -07:00
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_respond_begin(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
|
|
|
filter_data_t *client_state = client_state_get(qctx, htp);
|
2018-08-10 19:32:12 -07:00
|
|
|
isc_result_t result = ISC_R_UNSET;
|
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
if (client_state == NULL) {
|
|
|
|
return (false);
|
|
|
|
}
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
if (client_state->mode != BREAK_DNSSEC &&
|
|
|
|
(client_state->mode != FILTER ||
|
2018-08-10 19:32:12 -07:00
|
|
|
(WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL &&
|
|
|
|
dns_rdataset_isassociated(qctx->sigrdataset))))
|
|
|
|
{
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (qctx->qtype == dns_rdatatype_aaaa) {
|
|
|
|
dns_rdataset_t *trdataset;
|
|
|
|
trdataset = ns_client_newrdataset(qctx->client);
|
|
|
|
result = dns_db_findrdataset(qctx->db, qctx->node,
|
|
|
|
qctx->version,
|
|
|
|
dns_rdatatype_a, 0,
|
|
|
|
qctx->client->now,
|
|
|
|
trdataset, NULL);
|
|
|
|
if (dns_rdataset_isassociated(trdataset)) {
|
|
|
|
dns_rdataset_disassociate(trdataset);
|
|
|
|
}
|
|
|
|
ns_client_putrdataset(qctx->client, &trdataset);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We found an AAAA. If we also found an A, then the AAAA
|
|
|
|
* must not be rendered.
|
|
|
|
*
|
|
|
|
* If the A is not in our cache, then any result other than
|
|
|
|
* DNS_R_DELEGATION or ISC_R_NOTFOUND means there is no A,
|
|
|
|
* and so AAAAs are okay.
|
|
|
|
*
|
|
|
|
* We assume there is no A 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 AAAA and
|
|
|
|
* are using this mechanism in the first place suggests
|
|
|
|
* that we care more about As than AAAAs, and would have
|
|
|
|
* cached an A if it existed.
|
|
|
|
*/
|
|
|
|
if (result == ISC_R_SUCCESS) {
|
2018-09-20 15:07:33 +02:00
|
|
|
mark_as_rendered(qctx->rdataset, qctx->sigrdataset);
|
2018-09-14 12:32:36 -07:00
|
|
|
qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD;
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state->flags |= FILTER_AAAA_FILTERED;
|
2018-08-10 19:32:12 -07:00
|
|
|
} else if (!qctx->authoritative &&
|
|
|
|
RECURSIONOK(qctx->client) &&
|
|
|
|
(result == DNS_R_DELEGATION ||
|
|
|
|
result == ISC_R_NOTFOUND))
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* This is an ugly kludge to recurse
|
|
|
|
* for the A and discard the result.
|
|
|
|
*
|
|
|
|
* Continue to add the AAAA now.
|
|
|
|
* We'll make a note to not render it
|
|
|
|
* if the recursion for the A succeeds.
|
|
|
|
*/
|
|
|
|
result = ns_query_recurse(qctx->client,
|
|
|
|
dns_rdatatype_a,
|
|
|
|
qctx->client->query.qname,
|
|
|
|
NULL, NULL, qctx->resuming);
|
|
|
|
if (result == ISC_R_SUCCESS) {
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state->flags |= FILTER_AAAA_RECURSING;
|
2018-08-10 19:32:12 -07:00
|
|
|
qctx->client->query.attributes |=
|
|
|
|
NS_QUERYATTR_RECURSING;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (qctx->qtype == dns_rdatatype_a &&
|
2018-09-19 16:25:50 -07:00
|
|
|
(client_state->flags & FILTER_AAAA_RECURSING) != 0)
|
2018-08-10 19:32:12 -07:00
|
|
|
{
|
2018-09-20 15:07:33 +02:00
|
|
|
const section_filter_t filter_answer = {
|
|
|
|
.qctx = qctx,
|
|
|
|
.mode = client_state->mode,
|
|
|
|
.section = DNS_SECTION_ANSWER,
|
|
|
|
.name = qctx->fname,
|
|
|
|
.type = dns_rdatatype_aaaa,
|
|
|
|
};
|
|
|
|
process_section(&filter_answer);
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state->flags &= ~FILTER_AAAA_RECURSING;
|
2018-08-10 19:32:12 -07:00
|
|
|
|
|
|
|
result = ns_query_done(qctx);
|
|
|
|
|
|
|
|
*resp = result;
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
return (true);
|
2018-08-10 19:32:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
*resp = result;
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
/*
|
|
|
|
* When answering an ANY query, remove AAAA if A is present.
|
|
|
|
*/
|
2018-08-10 19:32:12 -07:00
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_respond_any_found(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
|
|
|
filter_data_t *client_state = client_state_get(qctx, htp);
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
if (client_state != NULL && client_state->mode != NONE) {
|
|
|
|
/*
|
|
|
|
* If we are authoritative, require an A record to be
|
|
|
|
* present before filtering out AAAA records; otherwise,
|
|
|
|
* just assume an A record exists even if it was not in the
|
|
|
|
* cache (and therefore is not in the response message),
|
|
|
|
* thus proceeding with filtering out AAAA records.
|
|
|
|
*/
|
|
|
|
const section_filter_t filter_answer = {
|
|
|
|
.qctx = qctx,
|
|
|
|
.mode = client_state->mode,
|
|
|
|
.section = DNS_SECTION_ANSWER,
|
|
|
|
.name = qctx->tname,
|
|
|
|
.type = dns_rdatatype_aaaa,
|
|
|
|
.only_if_a_exists = qctx->authoritative,
|
|
|
|
};
|
|
|
|
process_section(&filter_answer);
|
2018-08-10 19:32:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return (false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2018-09-20 15:07:33 +02:00
|
|
|
* Hide AAAA rrsets in the additional section if there is a matching A, and
|
|
|
|
* hide NS in the authority section if AAAA was filtered in the answer
|
2018-08-10 19:32:12 -07:00
|
|
|
* section.
|
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_query_done_send(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
|
|
|
filter_data_t *client_state = client_state_get(qctx, htp);
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
2018-08-10 19:32:12 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
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_aaaa,
|
|
|
|
.only_if_a_exists = true,
|
|
|
|
};
|
|
|
|
process_section(&filter_additional);
|
|
|
|
|
|
|
|
if ((client_state->flags & FILTER_AAAA_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);
|
2018-08-10 19:32:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (false);
|
|
|
|
}
|
2018-08-13 01:18:09 -07:00
|
|
|
|
|
|
|
/*
|
2018-09-20 15:07:33 +02:00
|
|
|
* If the client is being detached, then we can delete our persistent data
|
|
|
|
* from hash table and return it to the memory pool.
|
2018-08-13 01:18:09 -07:00
|
|
|
*/
|
|
|
|
static bool
|
2018-09-19 16:25:50 -07:00
|
|
|
filter_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp) {
|
|
|
|
query_ctx_t *qctx = (query_ctx_t *) arg;
|
|
|
|
isc_ht_t **htp = (isc_ht_t **) cbdata;
|
2018-08-13 01:18:09 -07:00
|
|
|
|
2018-09-20 15:07:33 +02:00
|
|
|
*resp = ISC_R_UNSET;
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
if (!qctx->detach_client) {
|
|
|
|
return (false);
|
2018-08-13 01:18:09 -07:00
|
|
|
}
|
|
|
|
|
2018-09-19 16:25:50 -07:00
|
|
|
client_state_destroy(qctx, htp);
|
|
|
|
|
2018-08-13 01:18:09 -07:00
|
|
|
return (false);
|
|
|
|
}
|