diff options
Diffstat (limited to '')
-rw-r--r-- | src/knot/nameserver/process_query.c | 978 |
1 files changed, 978 insertions, 0 deletions
diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c new file mode 100644 index 0000000..34590df --- /dev/null +++ b/src/knot/nameserver/process_query.c @@ -0,0 +1,978 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "libdnssec/tsig.h" +#include "knot/common/log.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/nameserver/process_query.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/chaos.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/ixfr.h" +#include "knot/nameserver/update.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/notify.h" +#include "knot/server/server.h" +#include "libknot/libknot.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" + +/*! \brief Accessor to query-specific data. */ +#define QUERY_DATA(ctx) ((knotd_qdata_t *)(ctx)->data) + +static knotd_query_type_t query_type(const knot_pkt_t *pkt) +{ + switch (knot_wire_get_opcode(pkt->wire)) { + case KNOT_OPCODE_QUERY: + switch (knot_pkt_qtype(pkt)) { + case 0 /* RESERVED */: return KNOTD_QUERY_TYPE_INVALID; + case KNOT_RRTYPE_AXFR: return KNOTD_QUERY_TYPE_AXFR; + case KNOT_RRTYPE_IXFR: return KNOTD_QUERY_TYPE_IXFR; + default: return KNOTD_QUERY_TYPE_NORMAL; + } + case KNOT_OPCODE_NOTIFY: return KNOTD_QUERY_TYPE_NOTIFY; + case KNOT_OPCODE_UPDATE: return KNOTD_QUERY_TYPE_UPDATE; + default: return KNOTD_QUERY_TYPE_INVALID; + } +} + +/*! \brief Reinitialize query data structure. */ +static void query_data_init(knot_layer_t *ctx, knotd_qdata_params_t *params, + knotd_qdata_extra_t *extra) +{ + /* Initialize persistent data. */ + knotd_qdata_t *data = QUERY_DATA(ctx); + memset(data, 0, sizeof(*data)); + data->mm = ctx->mm; + data->params = params; + data->extra = extra; + data->rcode_ede = KNOT_EDNS_EDE_NONE; + + /* Initialize lists. */ + memset(extra, 0, sizeof(*extra)); + init_list(&extra->wildcards); + init_list(&extra->rrsigs); +} + +static int process_query_begin(knot_layer_t *ctx, void *params) +{ + /* Initialize context. */ + assert(ctx); + ctx->data = mm_alloc(ctx->mm, sizeof(knotd_qdata_t)); + knotd_qdata_extra_t *extra = mm_alloc(ctx->mm, sizeof(*extra)); + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_reset(knot_layer_t *ctx) +{ + assert(ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Remember persistent parameters. */ + knotd_qdata_params_t *params = qdata->params; + knotd_qdata_extra_t *extra = qdata->extra; + + /* Free allocated data. */ + knot_rrset_clear(&qdata->opt_rr, qdata->mm); + ptrlist_free(&extra->wildcards, qdata->mm); + nsec_clear_rrsigs(qdata); + if (extra->ext_cleanup != NULL) { + extra->ext_cleanup(qdata); + } + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_finish(knot_layer_t *ctx) +{ + process_query_reset(ctx); + mm_free(ctx->mm, ctx->data); + ctx->data = NULL; + + return KNOT_STATE_NOOP; +} + +static int process_query_in(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Check if at least header is parsed. */ + if (pkt->parsed < KNOT_WIRE_HEADER_SIZE) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Accept only queries. */ + if (knot_wire_get_qr(pkt->wire)) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Store for processing. */ + qdata->query = pkt; + qdata->type = query_type(pkt); + + /* Declare having response. */ + return KNOT_STATE_PRODUCE; +} + +/*! + * \brief Create a response for a given query in the INTERNET class. + */ +static int query_internet(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + switch (data->type) { + case KNOTD_QUERY_TYPE_NORMAL: return internet_process_query(pkt, data); + case KNOTD_QUERY_TYPE_NOTIFY: return notify_process_query(pkt, data); + case KNOTD_QUERY_TYPE_AXFR: return axfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_IXFR: return ixfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_UPDATE: return update_process_query(pkt, data); + default: + /* Nothing else is supported. */ + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } +} + +/*! + * \brief Create a response for a given query in the CHAOS class. + */ +static int query_chaos(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + /* Nothing except normal queries is supported. */ + if (data->type != KNOTD_QUERY_TYPE_NORMAL) { + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } + + data->rcode = knot_chaos_answer(pkt); + if (data->rcode != KNOT_RCODE_NOERROR) { + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_DONE; +} + +/*! \brief Find zone for given question. */ +static zone_t *answer_zone_find(const knot_pkt_t *query, knot_zonedb_t *zonedb) +{ + uint16_t qtype = knot_pkt_qtype(query); + uint16_t qclass = knot_pkt_qclass(query); + const knot_dname_t *qname = knot_pkt_qname(query); + zone_t *zone = NULL; + + // search for zone only for IN and ANY classes + if (qclass != KNOT_CLASS_IN && qclass != KNOT_CLASS_ANY) { + return NULL; + } + + /* In case of DS query, we strip the leftmost label when searching for + * the zone (but use whole qname in search for the record), as the DS + * records are only present in a parent zone. + */ + if (qtype == KNOT_RRTYPE_DS) { + const knot_dname_t *parent = knot_wire_next_label(qname, NULL); + zone = knot_zonedb_find_suffix(zonedb, parent); + /* If zone does not exist, search for its parent zone, + this will later result to NODATA answer. */ + /*! \note This is not 100% right, it may lead to DS name for example + * when following a CNAME chain, that should also be answered + * from the parent zone (if it exists). + */ + } + + if (zone == NULL) { + if (query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + zone = knot_zonedb_find_suffix(zonedb, qname); + } else { + // Direct match required. + zone = knot_zonedb_find(zonedb, qname); + } + } + + return zone; +} + +static int answer_edns_reserve(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Reserve size in the response. */ + return knot_pkt_reserve(resp, knot_edns_wire_size(&qdata->opt_rr)); +} + +static int answer_edns_init(const knot_pkt_t *query, knot_pkt_t *resp, + knotd_qdata_t *qdata) +{ + if (!knot_pkt_has_edns(query)) { + return KNOT_EOK; + } + + /* Initialize OPT record. */ + uint16_t max_payload; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + max_payload = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + max_payload = conf()->cache.srv_udp_max_payload_ipv6; + break; + case AF_UNIX: + max_payload = MIN(conf()->cache.srv_udp_max_payload_ipv4, + conf()->cache.srv_udp_max_payload_ipv6); + break; + default: + return KNOT_ERROR; + } + int ret = knot_edns_init(&qdata->opt_rr, max_payload, 0, + KNOT_EDNS_VERSION, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check supported version. */ + if (knot_edns_get_version(query->opt_rr) != KNOT_EDNS_VERSION) { + qdata->rcode = KNOT_RCODE_BADVERS; + } + + /* Set DO bit if set (DNSSEC requested). */ + if (knot_pkt_has_dnssec(query)) { + knot_edns_set_do(&qdata->opt_rr); + } + + /* Append NSID if requested and available. */ + if (knot_pkt_edns_option(query, KNOT_EDNS_OPTION_NSID) != NULL) { + size_t nsid_len = conf()->cache.srv_nsid_len; + const uint8_t *nsid_data = conf()->cache.srv_nsid_data; + + if (nsid_len > 0) { + ret = knot_edns_add_option(&qdata->opt_rr, + KNOT_EDNS_OPTION_NSID, + nsid_len, nsid_data, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Initialize EDNS Client Subnet if configured and present in query. */ + if (conf()->cache.srv_ecs) { + uint8_t *ecs_opt = knot_pkt_edns_option(query, KNOT_EDNS_OPTION_CLIENT_SUBNET); + if (ecs_opt != NULL) { + qdata->ecs = mm_alloc(qdata->mm, sizeof(knot_edns_client_subnet_t)); + if (qdata->ecs == NULL) { + return KNOT_ENOMEM; + } + const uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_parse(qdata->ecs, ecs_data, ecs_len); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + return ret; + } + qdata->ecs->scope_len = 0; + + /* Reserve space for the option in the answer. */ + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, + ecs_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } else { + qdata->ecs = NULL; + } + + return answer_edns_reserve(resp, qdata); +} + +static int answer_edns_put(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Add ECS if present. */ + int ret = KNOT_EOK; + if (qdata->ecs != NULL) { + uint8_t *ecs_opt = knot_edns_get_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, NULL); + if (ecs_opt != NULL) { + uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_write(ecs_data, ecs_len, qdata->ecs); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + size_t opt_wire_size = knot_edns_wire_size(&qdata->opt_rr); + + /* Add EDE. Pragmatic: only if space in pkt. */ + if (qdata->rcode_ede != KNOT_EDNS_EDE_NONE && + knot_pkt_reserve(resp, KNOT_EDNS_EDE_MIN_LENGTH) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, KNOT_EDNS_EDE_MIN_LENGTH); + assert(ret == KNOT_EOK); + + uint16_t ede_code = (uint16_t)qdata->rcode_ede; + assert((int)ede_code == qdata->rcode_ede); + ede_code = htobe16(ede_code); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EDE, + sizeof(ede_code), (uint8_t *)&ede_code, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + + /* Add EXPIRE if space and not catalog zone, which cannot expire. */ + if (knot_pkt_edns_option(qdata->query, KNOT_EDNS_OPTION_EXPIRE) != NULL && + qdata->extra->contents != NULL && !qdata->extra->zone->is_catalog_flag) { + int64_t timer = qdata->extra->zone->timers.next_expire == 0 + ? zone_soa_expire(qdata->extra->zone) + : qdata->extra->zone->timers.next_expire - time(NULL); + timer = MAX(timer, 0); + uint32_t timer_be; + knot_wire_write_u32((uint8_t *)&timer_be, (uint32_t)timer); + + uint16_t expire_size = KNOT_EDNS_OPTION_HDRLEN + sizeof(timer_be); + if (knot_pkt_reserve(resp, expire_size) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, expire_size); + assert(ret == KNOT_EOK); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EXPIRE, + sizeof(timer_be), (uint8_t *)&timer_be, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Align the response if QUIC with EDNS. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) { + int pad_len = knot_pkt_default_padding_size(resp, &qdata->opt_rr); + if (pad_len > -1) { + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_PADDING, + pad_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Reclaim reserved size. */ + ret = knot_pkt_reclaim(resp, opt_wire_size); + if (ret != KNOT_EOK) { + return ret; + } + + uint8_t *wire_end = resp->wire + resp->size; + + /* Write to packet. */ + assert(resp->current == KNOT_ADDITIONAL); + ret = knot_pkt_put(resp, KNOT_COMPR_HINT_NONE, &qdata->opt_rr, 0); + if (ret == KNOT_EOK) { + /* Save position of the OPT RR. */ + qdata->extra->opt_rr_pos = wire_end; + } + + return ret; +} + +/*! \brief Initialize response, sizes and find zone from which we're going to answer. */ +static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_layer_t *ctx) +{ + knotd_qdata_t *qdata = QUERY_DATA(ctx); + server_t *server = qdata->params->server; + + /* Initialize response. */ + int ret = knot_pkt_init_response(resp, query); + if (ret != KNOT_EOK) { + return ret; + } + knot_wire_clear_cd(resp->wire); + + /* Setup EDNS. */ + ret = answer_edns_init(query, resp, qdata); + if (ret != KNOT_EOK || qdata->rcode != 0) { + return KNOT_ERROR; + } + + /* Update maximal answer size. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + resp->max_size = KNOT_WIRE_MIN_PKTSIZE; + if (knot_pkt_has_edns(query)) { + uint16_t server_size; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + server_size = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + server_size = conf()->cache.srv_udp_max_payload_ipv6; + break; + default: + return KNOT_ERROR; + } + uint16_t client_size = knot_edns_get_payload(query->opt_rr); + uint16_t transfer = MIN(client_size, server_size); + resp->max_size = MAX(resp->max_size, transfer); + } + } else { + resp->max_size = KNOT_WIRE_MAX_PKTSIZE; + } + + /* All supported OPCODEs require a question. */ + const knot_dname_t *qname = knot_pkt_qname(query); + if (qname == NULL) { + switch (knot_wire_get_opcode(query->wire)) { + case KNOT_OPCODE_QUERY: + case KNOT_OPCODE_NOTIFY: + case KNOT_OPCODE_UPDATE: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_NOTIMPL; + } + return KNOT_ENOTSUP; + } + + /* Find zone for QNAME. */ + qdata->extra->zone = answer_zone_find(query, server->zone_db); + if (qdata->extra->zone != NULL && qdata->extra->contents == NULL) { + qdata->extra->contents = qdata->extra->zone->contents; + } + + /* Allow normal queries to catalog only if not UDP and if allowed by ACL. */ + if (qdata->extra->zone != NULL && qdata->extra->zone->is_catalog_flag && + query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP || + !process_query_acl_check(conf(), ACL_ACTION_TRANSFER, qdata)) { + qdata->extra->zone = NULL; + qdata->extra->contents = NULL; + } + } + + return KNOT_EOK; +} + +static void set_rcode_to_packet(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + uint8_t ext_rcode = KNOT_EDNS_RCODE_HI(qdata->rcode); + + if (ext_rcode != 0) { + /* No OPT RR and Ext RCODE results in SERVFAIL. */ + if (qdata->extra->opt_rr_pos == NULL) { + knot_wire_set_rcode(pkt->wire, KNOT_RCODE_SERVFAIL); + return; + } + + knot_edns_set_ext_rcode_wire(qdata->extra->opt_rr_pos, ext_rcode); + } + + knot_wire_set_rcode(pkt->wire, KNOT_EDNS_RCODE_LO(qdata->rcode)); +} + +static int process_query_err(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(ctx && pkt); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Initialize response from query packet. */ + knot_pkt_t *query = qdata->query; + (void)knot_pkt_init_response(pkt, query); + knot_wire_clear_cd(pkt->wire); + + /* Set TC bit if required. */ + if (qdata->err_truncated) { + knot_wire_set_aa(pkt->wire); + knot_wire_set_tc(pkt->wire); + } + + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_reserve(pkt, qdata) != KNOT_EOK || + answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + } + + /* Set final RCODE to packet. */ + if (qdata->rcode == KNOT_RCODE_NOERROR && !qdata->err_truncated) { + /* Default RCODE is SERVFAIL if not otherwise specified. */ + qdata->rcode = KNOT_RCODE_SERVFAIL; + } + set_rcode_to_packet(pkt, qdata); + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + set_rcode_to_packet(pkt, qdata); + } + + return KNOT_STATE_DONE; +} + +#define PROCESS_BEGIN(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_BEGIN]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + goto finish; \ + } \ + } \ + } + +#define PROCESS_END(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_END]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + next_state = process_query_err(ctx, pkt); \ + } \ + } \ + } + +static int process_query_out(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + + rcu_read_lock(); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + struct query_plan *plan = conf()->query_plan; + struct query_plan *zone_plan = NULL; + struct query_step *step; + + int next_state = KNOT_STATE_PRODUCE; + + /* Check parse state. */ + knot_pkt_t *query = qdata->query; + if (query->parsed < query->size) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Preprocessing. */ + if (prepare_answer(query, pkt, ctx) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + + if (qdata->extra->zone != NULL && qdata->extra->zone->query_plan != NULL) { + zone_plan = qdata->extra->zone->query_plan; + } + + /* Before query processing code. */ + PROCESS_BEGIN(plan, step, next_state, qdata); + PROCESS_BEGIN(zone_plan, step, next_state, qdata); + + /* Answer based on qclass. */ + if (next_state == KNOT_STATE_PRODUCE) { + switch (knot_pkt_qclass(pkt)) { + case KNOT_CLASS_CH: + next_state = query_chaos(pkt, ctx); + break; + case KNOT_CLASS_ANY: + case KNOT_CLASS_IN: + next_state = query_internet(pkt, ctx); + break; + default: + qdata->rcode = KNOT_RCODE_REFUSED; + next_state = KNOT_STATE_FAIL; + break; + } + } + + /* Postprocessing. */ + if (next_state == KNOT_STATE_DONE || next_state == KNOT_STATE_PRODUCE) { + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + } + +finish: + switch (next_state) { + case KNOT_STATE_NOOP: + break; + case KNOT_STATE_FAIL: + /* Error processing. */ + next_state = process_query_err(ctx, pkt); + break; + case KNOT_STATE_FINAL: + /* Just skipped postprocessing. */ + next_state = KNOT_STATE_DONE; + break; + default: + set_rcode_to_packet(pkt, qdata); + } + + /* After query processing code. */ + PROCESS_END(plan, step, next_state, qdata); + PROCESS_END(zone_plan, step, next_state, qdata); + + rcu_read_unlock(); + + return next_state; +} + +bool process_query_acl_check(conf_t *conf, acl_action_t action, + knotd_qdata_t *qdata) +{ + const knot_dname_t *zone_name = qdata->extra->zone->name; + knot_pkt_t *query = qdata->query; + const struct sockaddr_storage *query_source = knotd_qdata_remote_addr(qdata); + knot_tsig_key_t tsig = { 0 }; + + /* Skip if already checked and valid. */ + if (qdata->sign.tsig_key.name != NULL) { + return true; + } + + /* Authenticate with NOKEY if the packet isn't signed. */ + if (query->tsig_rr) { + tsig.name = query->tsig_rr->owner; + tsig.algorithm = knot_tsig_rdata_alg(query->tsig_rr); + } + + /* Log ACL details. */ + char addr_str[SOCKADDR_STRLEN]; + if (sockaddr_tostr(addr_str, sizeof(addr_str), query_source) <= 0) { + addr_str[0] = '\0'; + } + knot_dname_txt_storage_t key_name; + if (knot_dname_to_str(key_name, tsig.name, sizeof(key_name)) == NULL) { + key_name[0] = '\0'; + } + const knot_lookup_t *act = knot_lookup_by_id((knot_lookup_t *)acl_actions, action); + + bool automatic = false; + bool allowed = false; + + if (action != ACL_ACTION_UPDATE) { + // ACL_ACTION_QUERY is used for SOA/refresh query. + assert(action == ACL_ACTION_QUERY || action == ACL_ACTION_NOTIFY || + action == ACL_ACTION_TRANSFER); + const yp_name_t *item = (action == ACL_ACTION_NOTIFY) ? C_MASTER : C_NOTIFY; + conf_val_t rmts = conf_zone_get(conf, item, zone_name); + allowed = rmt_allowed(conf, &rmts, query_source, &tsig); + automatic = allowed; + } + if (!allowed) { + conf_val_t acl = conf_zone_get(conf, C_ACL, zone_name); + allowed = acl_allowed(conf, &acl, action, query_source, &tsig, zone_name, query); + } + + log_zone_debug(zone_name, + "ACL, %s, action %s, remote %s, key %s%s%s%s", + allowed ? "allowed" : "denied", + (act != NULL) ? act->name : "query", + addr_str, + (key_name[0] != '\0') ? "'" : "", + (key_name[0] != '\0') ? key_name : "none", + (key_name[0] != '\0') ? "'" : "", + automatic ? ", automatic" : ""); + + /* Check if authorized. */ + if (!allowed) { + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + return false; + } + + /* Remember used TSIG key. */ + qdata->sign.tsig_key = tsig; + + return true; +} + +int process_query_verify(knotd_qdata_t *qdata) +{ + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* NOKEY => no verification. */ + if (query->tsig_rr == NULL) { + return KNOT_EOK; + } + + /* Keep digest for signing response. */ + /*! \note This memory will be rewritten for multi-pkt answers. */ + ctx->tsig_digest = (uint8_t *)knot_tsig_rdata_mac(query->tsig_rr); + ctx->tsig_digestlen = knot_tsig_rdata_mac_length(query->tsig_rr); + + /* Checking query. */ + int ret = knot_tsig_server_check(query->tsig_rr, query->wire, + query->size, &ctx->tsig_key); + + /* Evaluate TSIG check results. */ + switch(ret) { + case KNOT_EOK: + qdata->rcode = KNOT_RCODE_NOERROR; + break; + case KNOT_TSIG_EBADKEY: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + break; + case KNOT_TSIG_EBADSIG: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADSIG; + break; + case KNOT_TSIG_EBADTIME: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADTIME; + ctx->tsig_time_signed = knot_tsig_rdata_time_signed(query->tsig_rr); + break; + case KNOT_EMALF: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_SERVFAIL; + break; + } + + /* Log possible error. */ + if (qdata->rcode == KNOT_RCODE_SERVFAIL) { + log_zone_error(qdata->extra->zone->name, + "TSIG, verification failed (%s)", knot_strerror(ret)); + } else if (qdata->rcode != KNOT_RCODE_NOERROR) { + const knot_lookup_t *item = NULL; + if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + item = knot_lookup_by_id(knot_tsig_rcode_names, qdata->rcode_tsig); + if (item == NULL) { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode_tsig); + } + } else { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode); + } + + char *key_name = knot_dname_to_str_alloc(ctx->tsig_key.name); + log_zone_debug(qdata->extra->zone->name, + "TSIG, key '%s', verification failed '%s'", + (key_name != NULL) ? key_name : "", + (item != NULL) ? item->name : ""); + free(key_name); + } + + return ret; +} + +int process_query_sign_response(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt->size == 0) { + // Nothing to sign. + return KNOT_EOK; + } + + int ret = KNOT_EOK; + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* KEY provided and verified TSIG or BADTIME allows signing. */ + if (ctx->tsig_key.name != NULL && knot_tsig_can_sign(qdata->rcode_tsig)) { + /* Sign query response. */ + size_t new_digest_len = dnssec_tsig_algorithm_size(ctx->tsig_key.algorithm); + if (ctx->pkt_count == 0) { + ret = knot_tsig_sign(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, qdata->rcode_tsig, + ctx->tsig_time_signed); + } else { + ret = knot_tsig_sign_next(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, + pkt->wire, pkt->size); + } + if (ret != KNOT_EOK) { + goto fail; /* Failed to sign. */ + } else { + ++ctx->pkt_count; + } + } else { + /* Copy TSIG from query and set RCODE. */ + if (query->tsig_rr && qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + ret = knot_tsig_add(pkt->wire, &pkt->size, pkt->max_size, + qdata->rcode_tsig, query->tsig_rr); + if (ret != KNOT_EOK) { + goto fail; /* Whatever it is, it's server fail. */ + } + } + } + + return KNOT_EOK; + + /* Server failure in signing. */ +fail: + qdata->rcode = KNOT_RCODE_SERVFAIL; + qdata->rcode_tsig = KNOT_RCODE_NOERROR; /* Don't sign again. */ + return ret; +} + +/*! \brief Synthesize RRSIG for given parameters, store in 'qdata' for later use */ +static int put_rrsig(const knot_dname_t *sig_owner, uint16_t type, + const knot_rrset_t *rrsigs, knot_rrinfo_t *rrinfo, + uint32_t ttl_limit, knotd_qdata_t *qdata) +{ + knot_rdataset_t synth_rrs; + knot_rdataset_init(&synth_rrs); + assert(type != KNOT_RRTYPE_ANY); + int ret = knot_synth_rrsig(type, &rrsigs->rrs, &synth_rrs, qdata->mm); + if (ret == KNOT_ENOENT) { + // No signature + return KNOT_EOK; + } + if (ret != KNOT_EOK) { + return ret; + } + + /* Create rrsig info structure. */ + struct rrsig_info *info = mm_alloc(qdata->mm, sizeof(struct rrsig_info)); + if (info == NULL) { + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + + /* Store RRSIG into info structure. */ + knot_dname_t *owner_copy = knot_dname_copy(sig_owner, qdata->mm); + if (owner_copy == NULL) { + mm_free(qdata->mm, info); + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + uint32_t orig_ttl = knot_rrsig_original_ttl(synth_rrs.rdata); + knot_rrset_init(&info->synth_rrsig, owner_copy, rrsigs->type, + rrsigs->rclass, MIN(orig_ttl, ttl_limit)); + /* Store filtered signature. */ + info->synth_rrsig.rrs = synth_rrs; + + info->rrinfo = rrinfo; + add_tail(&qdata->extra->rrsigs, &info->n); + + return KNOT_EOK; +} + +int process_query_put_rr(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const knot_rrset_t *rr, const knot_rrset_t *rrsigs, + uint16_t compr_hint, uint32_t flags) +{ + if (rr->rrs.count < 1) { + return KNOT_EMALF; + } + + /* Wildcard expansion applies only for answers. */ + bool expand = false; + if (pkt->current == KNOT_ANSWER) { + /* Expand if RR is wildcard. TRICK: if the asterix node is queried directly, we behave like if wildcard would be expanded. It's the same. */ + expand = knot_dname_is_wildcard(rr->owner); + } + + int ret = KNOT_EOK; + + /* If we already have compressed name on the wire and compression hint, + * we can just insert RRSet and fake synthesis by using compression + * hint. */ + knot_rrset_t to_add; + if (compr_hint == KNOT_COMPR_HINT_NONE && expand) { + knot_dname_t *qname_cpy = knot_dname_copy(qdata->name, &pkt->mm); + if (qname_cpy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(&to_add, qname_cpy, rr->type, rr->rclass, rr->ttl); + ret = knot_rdataset_copy(&to_add.rrs, &rr->rrs, &pkt->mm); + if (ret != KNOT_EOK) { + knot_dname_free(qname_cpy, &pkt->mm); + return ret; + } + to_add.additional = rr->additional; + flags |= KNOT_PF_FREE; + } else { + to_add = *rr; + } + + uint16_t rotate = conf()->cache.srv_ans_rotate ? knot_wire_get_id(qdata->query->wire) : 0; + uint16_t prev_count = pkt->rrset_count; + ret = knot_pkt_put_rotate(pkt, compr_hint, &to_add, rotate, flags); + if (ret != KNOT_EOK && (flags & KNOT_PF_FREE)) { + knot_rrset_clear(&to_add, &pkt->mm); + return ret; + } + + uint32_t rrsig_ttl_limit = UINT32_MAX; + if ((flags & KNOT_PF_SOAMINTTL) && to_add.type == KNOT_RRTYPE_SOA) { + rrsig_ttl_limit = knot_soa_minimum(to_add.rrs.rdata); + } + + const bool inserted = (prev_count != pkt->rrset_count); + if (inserted && + !knot_rrset_empty(rrsigs) && rr->type != KNOT_RRTYPE_RRSIG) { + // Get rrinfo of just inserted RR. + knot_rrinfo_t *rrinfo = &pkt->rr_info[pkt->rrset_count - 1]; + ret = put_rrsig(rr->owner, rr->type, rrsigs, rrinfo, rrsig_ttl_limit, qdata); + } + + return ret; +} + +/*! \brief Module implementation. */ +const knot_layer_api_t *process_query_layer(void) +{ + static const knot_layer_api_t api = { + .begin = &process_query_begin, + .reset = &process_query_reset, + .finish = &process_query_finish, + .consume = &process_query_in, + .produce = &process_query_out, + }; + return &api; +} |