diff options
Diffstat (limited to 'src/knot/events/handlers')
-rw-r--r-- | src/knot/events/handlers/backup.c | 71 | ||||
-rw-r--r-- | src/knot/events/handlers/dnssec.c | 116 | ||||
-rw-r--r-- | src/knot/events/handlers/ds_check.c | 49 | ||||
-rw-r--r-- | src/knot/events/handlers/ds_push.c | 277 | ||||
-rw-r--r-- | src/knot/events/handlers/expire.c | 46 | ||||
-rw-r--r-- | src/knot/events/handlers/flush.c | 33 | ||||
-rw-r--r-- | src/knot/events/handlers/freeze_thaw.c | 46 | ||||
-rw-r--r-- | src/knot/events/handlers/load.c | 406 | ||||
-rw-r--r-- | src/knot/events/handlers/notify.c | 212 | ||||
-rw-r--r-- | src/knot/events/handlers/refresh.c | 1391 | ||||
-rw-r--r-- | src/knot/events/handlers/update.c | 433 |
11 files changed, 3080 insertions, 0 deletions
diff --git a/src/knot/events/handlers/backup.c b/src/knot/events/handlers/backup.c new file mode 100644 index 0000000..a6b258c --- /dev/null +++ b/src/knot/events/handlers/backup.c @@ -0,0 +1,71 @@ +/* Copyright (C) 2020 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 <assert.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/handlers.h" +#include "knot/zone/backup.h" + +int event_backup(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_backup_ctx_t *ctx = zone->backup_ctx; + if (ctx == NULL) { + return KNOT_EINVAL; + } + + bool restore = ctx->restore_mode; + + if (!restore && ctx->failed) { + // No need to proceed with already faulty backup. + return KNOT_EOK; + } + + char *back_dir = strdup(ctx->backup_dir); + if (back_dir == NULL) { + return KNOT_ENOMEM; + } + + if (restore) { + // expire zone + zone_contents_t *expired = zone_switch_contents(zone, NULL); + synchronize_rcu(); + knot_sem_wait(&zone->cow_lock); + zone_contents_deep_free(expired); + knot_sem_post(&zone->cow_lock); + zone->zonefile.exists = false; + } + + int ret = zone_backup(conf, zone); + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "zone %s '%s'", + restore ? "restored from" : "backed up to", back_dir); + } else { + log_zone_warning(zone->name, "zone %s failed (%s)", + restore ? "restore" : "backup", knot_strerror(ret)); + } + + if (restore && ret == KNOT_EOK) { + zone_reset(conf, zone); + } + + free(back_dir); + return ret; +} diff --git a/src/knot/events/handlers/dnssec.c b/src/knot/events/handlers/dnssec.c new file mode 100644 index 0000000..8263b0d --- /dev/null +++ b/src/knot/events/handlers/dnssec.c @@ -0,0 +1,116 @@ +/* Copyright (C) 2022 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 <assert.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/zone-events.h" +#include "knot/updates/apply.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +static void log_dnssec_next(const knot_dname_t *zone, knot_time_t refresh_at) +{ + char time_str[64] = { 0 }; + struct tm time_gm = { 0 }; + time_t refresh = refresh_at; + localtime_r(&refresh, &time_gm); + strftime(time_str, sizeof(time_str), KNOT_LOG_TIME_FORMAT, &time_gm); + if (refresh_at == 0) { + log_zone_warning(zone, "DNSSEC, next signing not scheduled"); + } else { + log_zone_info(zone, "DNSSEC, next signing at %s", time_str); + } +} + +void event_dnssec_reschedule(conf_t *conf, zone_t *zone, + const zone_sign_reschedule_t *refresh, bool zone_changed) +{ + time_t now = time(NULL); + time_t ignore = -1; + knot_time_t refresh_at = refresh->next_sign; + + refresh_at = knot_time_min(refresh_at, refresh->next_rollover); + refresh_at = knot_time_min(refresh_at, refresh->next_nsec3resalt); + + log_dnssec_next(zone->name, (time_t)refresh_at); + + if (refresh->plan_ds_check) { + zone->timers.next_ds_check = now; + } + + zone_events_schedule_at(zone, + ZONE_EVENT_DNSSEC, refresh_at ? (time_t)refresh_at : ignore, + ZONE_EVENT_DS_CHECK, refresh->plan_ds_check ? now : ignore + ); + if (zone_changed) { + zone_schedule_notify(zone, 0); + } +} + +int event_dnssec(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_sign_reschedule_t resch = { 0 }; + zone_sign_roll_flags_t r_flags = KEY_ROLL_ALLOW_ALL; + int sign_flags = 0; + bool zone_changed = false; + + if (zone_get_flag(zone, ZONE_FORCE_RESIGN, true)) { + log_zone_info(zone->name, "DNSSEC, dropping previous " + "signatures, re-signing zone"); + sign_flags = ZONE_SIGN_DROP_SIGNATURES; + } else { + log_zone_info(zone->name, "DNSSEC, signing zone"); + sign_flags = 0; + } + + if (zone_get_flag(zone, ZONE_FORCE_KSK_ROLL, true)) { + r_flags |= KEY_ROLL_FORCE_KSK_ROLL; + } + if (zone_get_flag(zone, ZONE_FORCE_ZSK_ROLL, true)) { + r_flags |= KEY_ROLL_FORCE_ZSK_ROLL; + } + + zone_update_t up; + int ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_dnssec_zone_sign(&up, conf, sign_flags, r_flags, 0, &resch); + if (ret != KNOT_EOK) { + goto done; + } + + zone_changed = !zone_update_no_change(&up); + + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + goto done; + } + +done: + // Schedule dependent events + event_dnssec_reschedule(conf, zone, &resch, zone_changed); + + if (ret != KNOT_EOK) { + zone_update_clear(&up); + } + return ret; +} diff --git a/src/knot/events/handlers/ds_check.c b/src/knot/events/handlers/ds_check.c new file mode 100644 index 0000000..0138bed --- /dev/null +++ b/src/knot/events/handlers/ds_check.c @@ -0,0 +1,49 @@ +/* Copyright (C) 2022 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 "knot/dnssec/ds_query.h" +#include "knot/zone/zone.h" + +int event_ds_check(conf_t *conf, zone_t *zone) +{ + kdnssec_ctx_t ctx = { 0 }; + + int ret = kdnssec_ctx_init(conf, &ctx, zone->name, zone_kaspdb(zone), NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_parent_ds_query(conf, &ctx, conf->cache.srv_tcp_remote_io_timeout); + + zone->timers.next_ds_check = 0; + switch (ret) { + case KNOT_NO_READY_KEY: + break; + case KNOT_EOK: + zone_events_schedule_now(zone, ZONE_EVENT_DNSSEC); + break; + default: + if (ctx.policy->ksk_sbm_check_interval > 0) { + time_t next_check = time(NULL) + ctx.policy->ksk_sbm_check_interval; + zone->timers.next_ds_check = next_check; + zone_events_schedule_at(zone, ZONE_EVENT_DS_CHECK, next_check); + } + } + + kdnssec_ctx_deinit(&ctx); + + return KNOT_EOK; // allways ok, if failure it has been rescheduled +} diff --git a/src/knot/events/handlers/ds_push.c b/src/knot/events/handlers/ds_push.c new file mode 100644 index 0000000..11aef75 --- /dev/null +++ b/src/knot/events/handlers/ds_push.c @@ -0,0 +1,277 @@ +/* Copyright (C) 2022 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 <assert.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +struct ds_push_data { + const knot_dname_t *zone; + const knot_dname_t *parent_query; + knot_dname_t *parent_soa; + knot_rrset_t del_old_ds; + knot_rrset_t new_ds; + const struct sockaddr *remote; + query_edns_data_t edns; +}; + +#define DS_PUSH_RETRY 600 + +#define DS_PUSH_LOG(priority, zone, remote, reused, fmt, ...) \ + ns_log(priority, zone, LOG_OPERATION_DS_PUSH, LOG_DIRECTION_OUT, remote, \ + reused, fmt, ## __VA_ARGS__) + +static const knot_rdata_t remove_cds = { 5, { 0, 0, 0, 0, 0 } }; + +static int ds_push_begin(knot_layer_t *layer, void *params) +{ + layer->data = params; + + return KNOT_STATE_PRODUCE; +} + +static int parent_soa_produce(struct ds_push_data *data, knot_pkt_t *pkt) +{ + assert(data->parent_query[0] != '\0'); + data->parent_query = knot_wire_next_label(data->parent_query, NULL); + + int ret = knot_pkt_put_question(pkt, data->parent_query, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + ret = query_put_edns(pkt, &data->edns); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_CONSUME; +} + +static int ds_push_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_push_data *data = layer->data; + + query_init_pkt(pkt); + + if (data->parent_soa == NULL) { + return parent_soa_produce(data, pkt); + } + + knot_wire_set_opcode(pkt->wire, KNOT_OPCODE_UPDATE); + int ret = knot_pkt_put_question(pkt, data->parent_soa, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + knot_pkt_begin(pkt, KNOT_AUTHORITY); + + assert(data->del_old_ds.type == KNOT_RRTYPE_DS); + ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, &data->del_old_ds, 0); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + assert(data->new_ds.type == KNOT_RRTYPE_DS); + assert(!knot_rrset_empty(&data->new_ds)); + if (knot_rdata_cmp(data->new_ds.rrs.rdata, &remove_cds) != 0) { + // Otherwise only remove DS - it was a special "remove CDS". + ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, &data->new_ds, 0); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + query_put_edns(pkt, &data->edns); + + return KNOT_STATE_CONSUME; +} + +static const knot_rrset_t *sect_soa(const knot_pkt_t *pkt, knot_section_t sect) +{ + const knot_pktsection_t *s = knot_pkt_section(pkt, sect); + const knot_rrset_t *rr = s->count > 0 ? knot_pkt_rr(s, 0) : NULL; + if (rr == NULL || rr->type != KNOT_RRTYPE_SOA || rr->rrs.count != 1) { + return NULL; + } + return rr; +} + +static int ds_push_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_push_data *data = layer->data; + + if (data->parent_soa != NULL) { + // DS push has already been sent, just finish the action. + return KNOT_STATE_DONE; + } + + const knot_rrset_t *parent_soa = sect_soa(pkt, KNOT_ANSWER); + if (parent_soa != NULL) { + // parent SOA obtained, continue with DS push + data->parent_soa = knot_dname_copy(parent_soa->owner, NULL); + return KNOT_STATE_RESET; + } + + if (data->parent_query[0] == '\0') { + // query for parent SOA systematically fails + DS_PUSH_LOG(LOG_WARNING, data->zone, data->remote, + layer->flags & KNOT_REQUESTOR_REUSED, + "unable to query parent SOA"); + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_RESET; // cut off one more label and re-query +} + +static int ds_push_reset(knot_layer_t *layer) +{ + (void)layer; + return KNOT_STATE_PRODUCE; +} + +static int ds_push_finish(knot_layer_t *layer) +{ + struct ds_push_data *data = layer->data; + free(data->parent_soa); + data->parent_soa = NULL; + return layer->state; +} + +static const knot_layer_api_t DS_PUSH_API = { + .begin = ds_push_begin, + .produce = ds_push_produce, + .reset = ds_push_reset, + .consume = ds_push_consume, + .finish = ds_push_finish, +}; + +static int send_ds_push(conf_t *conf, zone_t *zone, + const conf_remote_t *parent, int timeout) +{ + knot_rrset_t zone_cds = node_rrset(zone->contents->apex, KNOT_RRTYPE_CDS); + if (knot_rrset_empty(&zone_cds)) { + return KNOT_EOK; // No CDS, do nothing. + } + zone_cds.type = KNOT_RRTYPE_DS; + zone_cds.ttl = node_rrset(zone->contents->apex, KNOT_RRTYPE_DNSKEY).ttl; + + struct ds_push_data data = { + .zone = zone->name, + .parent_query = zone->name, + .new_ds = zone_cds, + .remote = (struct sockaddr *)&parent->addr, + .edns = query_edns_data_init(conf, parent->addr.ss_family, 0) + }; + + knot_rrset_init(&data.del_old_ds, zone->name, KNOT_RRTYPE_DS, KNOT_CLASS_ANY, 0); + int ret = knot_rrset_add_rdata(&data.del_old_ds, NULL, 0, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &DS_PUSH_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (pkt == NULL) { + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &parent->addr; + const struct sockaddr_storage *src = &parent->via; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &parent->key, 0); + if (req == NULL) { + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + ret = knot_requestor_exec(&requestor, req, timeout); + + if (ret == KNOT_EOK && knot_pkt_ext_rcode(req->resp) == 0) { + DS_PUSH_LOG(LOG_INFO, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "success"); + } else if (knot_pkt_ext_rcode(req->resp) == 0) { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "failed (%s)", knot_strerror(ret)); + } else { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(req->resp)); + } + + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + return ret; +} + +int event_ds_push(conf_t *conf, zone_t *zone) +{ + assert(zone); + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + int timeout = conf->cache.srv_tcp_remote_io_timeout; + + conf_val_t ds_push = conf_zone_get(conf, C_DS_PUSH, zone->name); + if (ds_push.code != KNOT_EOK) { + conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->name); + conf_id_fix_default(&policy_id); + ds_push = conf_id_get(conf, C_POLICY, C_DS_PUSH, &policy_id); + } + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &ds_push, &iter); + while (iter.id->code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + int ret = KNOT_EOK; + for (int i = 0; i < addr_count; i++) { + conf_remote_t parent = conf_remote(conf, iter.id, i); + ret = send_ds_push(conf, zone, &parent, timeout); + if (ret == KNOT_EOK) { + zone->timers.next_ds_push = 0; + break; + } + } + + if (ret != KNOT_EOK) { + time_t next_push = time(NULL) + DS_PUSH_RETRY; + zone_events_schedule_at(zone, ZONE_EVENT_DS_PUSH, next_push); + zone->timers.next_ds_push = next_push; + } + + conf_mix_iter_next(&iter); + } + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/expire.c b/src/knot/events/handlers/expire.c new file mode 100644 index 0000000..d7deedd --- /dev/null +++ b/src/knot/events/handlers/expire.c @@ -0,0 +1,46 @@ +/* Copyright (C) 2022 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 <assert.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/contents.h" +#include "knot/zone/zone.h" + +int event_expire(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_contents_t *expired = zone_switch_contents(zone, NULL); + log_zone_info(zone->name, "zone expired"); + + synchronize_rcu(); + knot_sem_wait(&zone->cow_lock); + zone_contents_deep_free(expired); + knot_sem_post(&zone->cow_lock); + + zone->zonefile.exists = false; + + zone->timers.next_expire = time(NULL); + zone->timers.next_refresh = zone->timers.next_expire; + replan_from_timers(conf, zone); + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/flush.c b/src/knot/events/handlers/flush.c new file mode 100644 index 0000000..65663cb --- /dev/null +++ b/src/knot/events/handlers/flush.c @@ -0,0 +1,33 @@ +/* Copyright (C) 2021 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 <assert.h> +#include <time.h> + +#include "knot/conf/conf.h" +#include "knot/zone/zone.h" + +int event_flush(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + return zone_flush_journal(conf, zone, true); +} diff --git a/src/knot/events/handlers/freeze_thaw.c b/src/knot/events/handlers/freeze_thaw.c new file mode 100644 index 0000000..dfa867f --- /dev/null +++ b/src/knot/events/handlers/freeze_thaw.c @@ -0,0 +1,46 @@ +/* Copyright (C) 2021 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 "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/events.h" +#include "knot/zone/zone.h" + +int event_ufreeze(conf_t *conf, zone_t *zone) +{ + assert(zone); + + pthread_mutex_lock(&zone->events.mx); + zone->events.ufrozen = true; + pthread_mutex_unlock(&zone->events.mx); + + log_zone_info(zone->name, "zone updates frozen"); + + return KNOT_EOK; +} + +int event_uthaw(conf_t *conf, zone_t *zone) +{ + assert(zone); + + pthread_mutex_lock(&zone->events.mx); + zone->events.ufrozen = false; + pthread_mutex_unlock(&zone->events.mx); + + log_zone_info(zone->name, "zone updates unfrozen"); + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c new file mode 100644 index 0000000..13e3298 --- /dev/null +++ b/src/knot/events/handlers/load.c @@ -0,0 +1,406 @@ +/* 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 <assert.h> + +#include "knot/catalog/generate.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/zone-events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "knot/updates/acl.h" + +static bool dontcare_load_error(conf_t *conf, const zone_t *zone) +{ + return (zone->contents == NULL && zone_load_can_bootstrap(conf, zone->name)); +} + +static bool allowed_xfr(conf_t *conf, const zone_t *zone) +{ + conf_val_t acl = conf_zone_get(conf, C_ACL, zone->name); + while (acl.code == KNOT_EOK) { + conf_val_t action = conf_id_get(conf, C_ACL, C_ACTION, &acl); + while (action.code == KNOT_EOK) { + if (conf_opt(&action) == ACL_ACTION_TRANSFER) { + return true; + } + conf_val_next(&action); + } + conf_val_next(&acl); + } + + return false; +} + +int event_load(conf_t *conf, zone_t *zone) +{ + zone_update_t up = { 0 }; + zone_contents_t *journal_conts = NULL, *zf_conts = NULL; + bool old_contents_exist = (zone->contents != NULL), zone_in_journal_exists = false; + + conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, zone->name); + unsigned load_from = conf_opt(&val); + + val = conf_zone_get(conf, C_ZONEFILE_LOAD, zone->name); + unsigned zf_from = conf_opt(&val); + + int ret = KNOT_EOK; + + // If configured, load journal contents. + if (!old_contents_exist && + ((load_from == JOURNAL_CONTENT_ALL && zf_from != ZONEFILE_LOAD_WHOLE) || + zone->cat_members != NULL)) { + ret = zone_load_from_journal(conf, zone, &journal_conts); + switch (ret) { + case KNOT_EOK: + zone_in_journal_exists = true; + break; + case KNOT_ENOENT: + zone_in_journal_exists = false; + break; + default: + goto cleanup; + } + } else { + zone_in_journal_exists = zone_journal_has_zij(zone); + } + + // If configured, attempt to load zonefile. + if (zf_from != ZONEFILE_LOAD_NONE && zone->cat_members == NULL) { + struct timespec mtime; + char *filename = conf_zonefile(conf, zone->name); + ret = zonefile_exists(filename, &mtime); + if (ret == KNOT_EOK) { + conf_val_t semchecks = conf_zone_get(conf, C_SEM_CHECKS, zone->name); + semcheck_optional_t mode = conf_opt(&semchecks); + if (mode == SEMCHECK_DNSSEC_AUTO) { + conf_val_t validation = conf_zone_get(conf, C_DNSSEC_VALIDATION, zone->name); + if (conf_bool(&validation)) { + /* Disable duplicate DNSSEC checks, which are the + same as DNSSEC validation in zone update commit. */ + mode = SEMCHECK_DNSSEC_OFF; + } + } + + ret = zone_load_contents(conf, zone->name, &zf_conts, mode, false); + } + if (ret != KNOT_EOK) { + assert(!zf_conts); + if (dontcare_load_error(conf, zone)) { + log_zone_info(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } else { + log_zone_error(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } + free(filename); + goto load_end; + } + free(filename); + + // Save zonefile information. + zone->zonefile.serial = zone_contents_serial(zf_conts); + zone->zonefile.exists = (zf_conts != NULL); + zone->zonefile.mtime = mtime; + + // If configured and possible, fix the SOA serial of zonefile. + zone_contents_t *relevant = (zone->contents != NULL ? zone->contents : journal_conts); + if (zf_conts != NULL && zf_from == ZONEFILE_LOAD_DIFSE && relevant != NULL) { + uint32_t serial = zone_contents_serial(relevant); + conf_val_t policy = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + uint32_t set = serial_next(serial, conf_opt(&policy), 1); + zone_contents_set_soa_serial(zf_conts, set); + log_zone_info(zone->name, "zone file parsed, serial updated %u -> %u", + zone->zonefile.serial, set); + zone->zonefile.serial = set; + } else { + log_zone_info(zone->name, "zone file parsed, serial %u", + zone->zonefile.serial); + } + + // If configured and appliable to zonefile, load journal changes. + if (load_from != JOURNAL_CONTENT_NONE) { + ret = zone_load_journal(conf, zone, zf_conts); + if (ret != KNOT_EOK) { + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + log_zone_warning(zone->name, "failed to load journal (%s)", + knot_strerror(ret)); + } + } + } + if (zone->cat_members != NULL && !old_contents_exist) { + uint32_t serial = journal_conts == NULL ? 1 : zone_contents_serial(journal_conts); + serial = serial_next(serial, SERIAL_POLICY_UNIXTIME, 1); // unixtime hardcoded + zf_conts = catalog_update_to_zone(zone->cat_members, zone->name, serial); + if (zf_conts == NULL) { + ret = zone->cat_members->error == KNOT_EOK ? KNOT_ENOMEM : zone->cat_members->error; + goto cleanup; + } + } + + // If configured contents=all, but not present, store zonefile. + if ((load_from == JOURNAL_CONTENT_ALL || zone->cat_members != NULL) && + !zone_in_journal_exists && (zf_conts != NULL || old_contents_exist)) { + zone_contents_t *store_c = old_contents_exist ? zone->contents : zf_conts; + ret = zone_in_journal_store(conf, zone, store_c); + if (ret != KNOT_EOK) { + log_zone_warning(zone->name, "failed to write zone-in-journal (%s)", + knot_strerror(ret)); + } else { + zone_in_journal_exists = true; + } + } + + val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = (conf_bool(&val) && zone->cat_members == NULL), zu_from_zf_conts = false; + bool do_diff = (zf_from == ZONEFILE_LOAD_DIFF || zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL); + bool ignore_dnssec = (do_diff && dnssec_enable); + + val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + bool update_zonemd = (digest_alg != ZONE_DIGEST_NONE); + + // Create zone_update structure according to current state. + if (old_contents_exist) { + if (zone->cat_members != NULL) { + ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL); + if (ret == KNOT_EOK) { + ret = catalog_update_to_update(zone->cat_members, &up); + } + if (ret == KNOT_EOK) { + ret = zone_update_increment_soa(&up, conf); + } + } else if (zf_conts == NULL) { + // nothing to be re-loaded + ret = KNOT_EOK; + goto cleanup; + } else if (zf_from == ZONEFILE_LOAD_WHOLE) { + // throw old zone contents and load new from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + zu_from_zf_conts = true; + } else { + // compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, NULL, zf_conts, UPDATE_INCREMENTAL, + ignore_dnssec, update_zonemd); + } + } else { + if (journal_conts != NULL && (zf_from != ZONEFILE_LOAD_WHOLE || zone->cat_members != NULL)) { + if (zf_conts == NULL) { + // load zone-in-journal + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } else { + // load zone-in-journal, compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, journal_conts, zf_conts, + UPDATE_HYBRID, ignore_dnssec, update_zonemd); + if (ret == KNOT_ESEMCHECK || ret == KNOT_ERANGE) { + log_zone_warning(zone->name, + "zone file changed with SOA serial %s, " + "ignoring zone file and loading from journal", + (ret == KNOT_ESEMCHECK ? "unupdated" : "decreased")); + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } + } + } else { + if (zf_conts == NULL) { + // nothing to be loaded + ret = KNOT_ENOENT; + } else { + // load from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + if (zf_from == ZONEFILE_LOAD_WHOLE) { + zu_from_zf_conts = true; + } + } + } + } + +load_end: + if (ret != KNOT_EOK) { + switch (ret) { + case KNOT_ENOENT: + if (zone_load_can_bootstrap(conf, zone->name)) { + log_zone_info(zone->name, "zone will be bootstrapped"); + } else { + log_zone_info(zone->name, "zone not found"); + } + break; + case KNOT_ESEMCHECK: + log_zone_warning(zone->name, "zone file changed without SOA serial update"); + break; + case KNOT_ERANGE: + if (serial_compare(zone->zonefile.serial, zone_contents_serial(zone->contents)) == SERIAL_INCOMPARABLE) { + log_zone_warning(zone->name, "zone file changed with incomparable SOA serial"); + } else { + log_zone_warning(zone->name, "zone file changed with decreased SOA serial"); + } + break; + } + goto cleanup; + } + + bool zf_serial_updated = (zf_conts != NULL && zone_contents_serial(zf_conts) != zone_contents_serial(zone->contents)); + + // The contents are already part of zone_update. + zf_conts = NULL; + journal_conts = NULL; + + ret = zone_update_verify_digest(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + uint32_t middle_serial = zone_contents_serial(up.new_cont); + + if (do_diff && old_contents_exist && dnssec_enable && zf_serial_updated && + !zone_in_journal_exists) { + ret = zone_update_start_extra(&up, conf); + if (ret != KNOT_EOK) { + goto cleanup; + } + } + + // Sign zone using DNSSEC if configured. + zone_sign_reschedule_t dnssec_refresh = { 0 }; + if (dnssec_enable) { + ret = knot_dnssec_zone_sign(&up, conf, 0, KEY_ROLL_ALLOW_ALL, 0, &dnssec_refresh); + if (ret != KNOT_EOK) { + goto cleanup; + } + if (zu_from_zf_conts && (up.flags & UPDATE_HYBRID) && allowed_xfr(conf, zone)) { + log_zone_warning(zone->name, + "with automatic DNSSEC signing and outgoing transfers enabled, " + "'zonefile-load: difference' should be set to avoid malformed " + "IXFR after manual zone file update"); + } + } else if (update_zonemd) { + /* Don't update ZONEMD if no change and ZONEMD is up-to-date. + * If ZONEFILE_LOAD_DIFSE, the change is non-empty and ZONEMD + * is directly updated without its verification. */ + if (!zone_update_no_change(&up) || !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + if (zone_update_to(&up) == NULL || middle_serial == zone->zonefile.serial) { + ret = zone_update_increment_soa(&up, conf); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + goto cleanup; + } + } + } + + // If the change is only automatically incremented SOA serial, make it no change. + if ((zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL) && + (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && + changeset_differs_just_serial(&up.change, update_zonemd)) { + changeset_t *cpy = changeset_clone(&up.change); + if (cpy == NULL) { + ret = KNOT_ENOMEM; + goto cleanup; + } + ret = zone_update_apply_changeset_reverse(&up, cpy); + if (ret != KNOT_EOK) { + changeset_free(cpy); + goto cleanup; + } + + // If the original ZONEMD is outdated, use the reverted changeset again. + if (update_zonemd && !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + ret = zone_update_apply_changeset(&up, cpy); + changeset_free(cpy); + if (ret != KNOT_EOK) { + goto cleanup; + } + } else { + changeset_free(cpy); + // Revert automatic zone serial increment. + zone->zonefile.serial = zone_contents_serial(up.new_cont); + /* Reset possibly set the resigned flag. Note that dnssec + * reschedule isn't reverted, but shouldn't be a problem + * for non-empty zones as SOA, ZONEMD, and their RRSIGs + * are always updated with other changes in the zone. */ + zone->zonefile.resigned = false; + } + } + + uint32_t old_serial = 0, new_serial = zone_contents_serial(up.new_cont); + char old_serial_str[11] = "none", new_serial_str[15] = ""; + if (old_contents_exist) { + old_serial = zone_contents_serial(zone->contents); + (void)snprintf(old_serial_str, sizeof(old_serial_str), "%u", old_serial); + } + if (new_serial != middle_serial) { + (void)snprintf(new_serial_str, sizeof(new_serial_str), " -> %u", new_serial); + } + + // Commit zone_update back to zone (including journal update, rcu,...). + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + char expires_in[32] = ""; + if (zone->timers.next_expire > 0) { + (void)snprintf(expires_in, sizeof(expires_in), + ", expires in %u seconds", + (uint32_t)MAX(zone->timers.next_expire - time(NULL), 0)); + } + + log_zone_info(zone->name, "loaded, serial %s -> %u%s, %zu bytes%s", + old_serial_str, middle_serial, new_serial_str, zone->contents->size, expires_in); + + if (zone->cat_members != NULL) { + catalog_update_clear(zone->cat_members); + } + + // Schedule dependent events. + if (dnssec_enable) { + event_dnssec_reschedule(conf, zone, &dnssec_refresh, false); // false since we handle NOTIFY below + } + + replan_from_timers(conf, zone); + + if (!zone_timers_serial_notified(&zone->timers, new_serial)) { + zone_schedule_notify(zone, 0); + } + + return KNOT_EOK; + +cleanup: + // Try to bootstrap the zone if local error. + replan_from_timers(conf, zone); + + zone_update_clear(&up); + zone_contents_deep_free(zf_conts); + zone_contents_deep_free(journal_conts); + + return (dontcare_load_error(conf, zone) ? KNOT_EOK : ret); +} diff --git a/src/knot/events/handlers/notify.c b/src/knot/events/handlers/notify.c new file mode 100644 index 0000000..dc3965d --- /dev/null +++ b/src/knot/events/handlers/notify.c @@ -0,0 +1,212 @@ +/* Copyright (C) 2022 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 <assert.h> + +#include "contrib/openbsd/siphash.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +static notifailed_rmt_hash notifailed_hash(conf_val_t *rmt_id) +{ + SIPHASH_KEY zero_key = { 0, 0 }; + SIPHASH_CTX ctx; + SipHash24_Init(&ctx, &zero_key); + SipHash24_Update(&ctx, rmt_id->data, rmt_id->len); + return SipHash24_End(&ctx); +} + +/*! + * \brief NOTIFY message processing data. + */ +struct notify_data { + const knot_dname_t *zone; + const knot_rrset_t *soa; + const struct sockaddr *remote; + query_edns_data_t edns; +}; + +static int notify_begin(knot_layer_t *layer, void *params) +{ + layer->data = params; + + return KNOT_STATE_PRODUCE; +} + +static int notify_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct notify_data *data = layer->data; + + // mandatory: NOTIFY opcode, AA flag, SOA qtype + query_init_pkt(pkt); + knot_wire_set_opcode(pkt->wire, KNOT_OPCODE_NOTIFY); + knot_wire_set_aa(pkt->wire); + knot_pkt_put_question(pkt, data->zone, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + + // unsecure hint: new SOA + if (data->soa) { + knot_pkt_begin(pkt, KNOT_ANSWER); + knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, data->soa, 0); + } + + query_put_edns(pkt, &data->edns); + + return KNOT_STATE_CONSUME; +} + +static int notify_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + return KNOT_STATE_DONE; +} + +static const knot_layer_api_t NOTIFY_API = { + .begin = notify_begin, + .produce = notify_produce, + .consume = notify_consume, +}; + +#define NOTIFY_OUT_LOG(priority, zone, remote, reused, fmt, ...) \ + ns_log(priority, zone, LOG_OPERATION_NOTIFY, LOG_DIRECTION_OUT, remote, \ + (reused), fmt, ## __VA_ARGS__) + +static int send_notify(conf_t *conf, zone_t *zone, const knot_rrset_t *soa, + const conf_remote_t *slave, int timeout, bool retry) +{ + struct notify_data data = { + .zone = zone->name, + .soa = soa, + .remote = (struct sockaddr *)&slave->addr, + .edns = query_edns_data_init(conf, slave->addr.ss_family, 0) + }; + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &NOTIFY_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (!pkt) { + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &slave->addr; + const struct sockaddr_storage *src = &slave->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &slave->key, flags); + if (!req) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int ret = knot_requestor_exec(&requestor, req, timeout); + + const char *log_retry = retry ? "retry, " : ""; + + if (ret == KNOT_EOK && knot_pkt_ext_rcode(req->resp) == 0) { + NOTIFY_OUT_LOG(LOG_INFO, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sserial %u", log_retry, knot_soa_serial(soa->rrs.rdata)); + zone->timers.last_notified_serial = (knot_soa_serial(soa->rrs.rdata) | LAST_NOTIFIED_SERIAL_VALID); + } else if (knot_pkt_ext_rcode(req->resp) == 0) { + NOTIFY_OUT_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sfailed (%s)", log_retry, knot_strerror(ret)); + } else { + NOTIFY_OUT_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sserver responded with error '%s'", + log_retry, knot_pkt_ext_rcode_name(req->resp)); + } + + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + return ret; +} + +int event_notify(conf_t *conf, zone_t *zone) +{ + assert(zone); + + bool failed = false; + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + // NOTIFY content + int timeout = conf->cache.srv_tcp_remote_io_timeout; + knot_rrset_t soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA); + + // in case of re-try, NOTIFY only failed remotes + pthread_mutex_lock(&zone->preferred_lock); + bool retry = (zone->notifailed.size > 0); + + // send NOTIFY to each remote, use working address + conf_val_t notify = conf_zone_get(conf, C_NOTIFY, zone->name); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, ¬ify, &iter); + while (iter.id->code == KNOT_EOK) { + notifailed_rmt_hash rmt_hash = notifailed_hash(iter.id); + if (retry && notifailed_rmt_dynarray_bsearch(&zone->notifailed, &rmt_hash) == NULL) { + conf_mix_iter_next(&iter); + continue; + } + pthread_mutex_unlock(&zone->preferred_lock); + + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + int ret = KNOT_EOK; + + for (int i = 0; i < addr_count; i++) { + conf_remote_t slave = conf_remote(conf, iter.id, i); + ret = send_notify(conf, zone, &soa, &slave, timeout, retry); + if (ret == KNOT_EOK) { + break; + } + } + + pthread_mutex_lock(&zone->preferred_lock); + if (ret != KNOT_EOK) { + failed = true; + notifailed_rmt_dynarray_add(&zone->notifailed, &rmt_hash); + } else { + notifailed_rmt_dynarray_remove(&zone->notifailed, &rmt_hash); + } + + conf_mix_iter_next(&iter); + } + + if (failed) { + notifailed_rmt_dynarray_sort_dedup(&zone->notifailed); + + uint32_t retry_in = knot_soa_retry(soa.rrs.rdata); + conf_val_t val = conf_zone_get(conf, C_RETRY_MIN_INTERVAL, zone->name); + retry_in = MAX(retry_in, conf_int(&val)); + val = conf_zone_get(conf, C_RETRY_MAX_INTERVAL, zone->name); + retry_in = MIN(retry_in, conf_int(&val)); + + zone_events_schedule_at(zone, ZONE_EVENT_NOTIFY, time(NULL) + retry_in); + } + pthread_mutex_unlock(&zone->preferred_lock); + + return failed ? KNOT_ERROR : KNOT_EOK; +} diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c new file mode 100644 index 0000000..9125aac --- /dev/null +++ b/src/knot/events/handlers/refresh.c @@ -0,0 +1,1391 @@ +/* Copyright (C) 2022 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 <assert.h> +#include <stdint.h> + +#include "contrib/mempattern.h" +#include "libdnssec/random.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/zone-events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/nameserver/ixfr.h" +#include "knot/query/layer.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/updates/changesets.h" +#include "knot/zone/adjust.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "libknot/errcode.h" + +/*! + * \brief Refresh event processing. + * + * The following diagram represents refresh event processing. + * + * \verbatim + * O + * | + * +-----v-----+ + * | BEGIN | + * +---+---+---+ + * has SOA | | no SOA + * +-------------------+ +------------------------------+ + * | | + * +------v------+ outdated +--------------+ error +-------v------+ + * | SOA query +------------> IXFR query +-----------> AXFR query | + * +-----+---+---+ +------+-------+ +----+----+----+ + * error | | current | success success | | error + * | +-----+ +---------------+ | | + * | | | +--------------------------------------+ | + * | | | | +----------+ +--------------+ + * | | | | | | | + * | +--v-v-v--+ | +--v--v--+ + * | | DONE | | | FAIL | + * | +---------+ | +--------+ + * +----------------------------+ + * + * \endverbatim + */ + +#define REFRESH_LOG(priority, data, direction, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_REFRESH, direction, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +#define AXFRIN_LOG(priority, data, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_AXFR, LOG_DIRECTION_IN, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +#define IXFRIN_LOG(priority, data, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_IXFR, LOG_DIRECTION_IN, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +enum state { + REFRESH_STATE_INVALID = 0, + STATE_SOA_QUERY, + STATE_TRANSFER, +}; + +enum xfr_type { + XFR_TYPE_NOTIMP = -2, + XFR_TYPE_ERROR = -1, + XFR_TYPE_UNDETERMINED = 0, + XFR_TYPE_UPTODATE, + XFR_TYPE_AXFR, + XFR_TYPE_IXFR, +}; + +struct refresh_data { + knot_layer_t *layer; //!< Used for reading requestor flags. + + // transfer configuration, initialize appropriately: + + zone_t *zone; //!< Zone to eventually updated. + conf_t *conf; //!< Server configuration. + const struct sockaddr *remote; //!< Remote endpoint. + const knot_rrset_t *soa; //!< Local SOA (NULL for AXFR). + const size_t max_zone_size; //!< Maximal zone size. + bool use_edns; //!< Allow EDNS in SOA/AXFR/IXFR queries. + query_edns_data_t edns; //!< EDNS data to be used in queries. + zone_master_fallback_t *fallback; //!< Flags allowing zone_master_try() fallbacks. + bool fallback_axfr; //!< Flag allowing fallback to AXFR, + uint32_t expire_timer; //!< Result: expire timer from answer EDNS. + + // internal state, initialize with zeroes: + + int ret; //!< Error code. + enum state state; //!< Event processing state. + enum xfr_type xfr_type; //!< Transer type (mostly IXFR versus AXFR). + knot_rrset_t *initial_soa_copy; //!< Copy of the received initial SOA. + struct xfr_stats stats; //!< Transfer statistics. + struct timespec started; //!< When refresh started. + size_t change_size; //!< Size of added and removed RRs. + + struct { + zone_contents_t *zone; //!< AXFR result, new zone. + } axfr; + + struct { + struct ixfr_proc *proc; //!< IXFR processing context. + knot_rrset_t *final_soa; //!< SOA denoting end of transfer. + list_t changesets; //!< IXFR result, zone updates. + } ixfr; + + bool updated; // TODO: Can we fid a better way to check if zone was updated? + knot_mm_t *mm; // TODO: This used to be used in IXFR. Remove or reuse. +}; + +static const uint32_t EXPIRE_TIMER_INVALID = ~0U; + +static bool serial_is_current(uint32_t local_serial, uint32_t remote_serial) +{ + return (serial_compare(local_serial, remote_serial) & SERIAL_MASK_GEQ); +} + +static time_t bootstrap_next(uint8_t *count) +{ + // Let the increment gradually grow in a sensible way. + time_t increment = 5 * (*count) * (*count); + + if (increment < 7200) { // two hours + (*count)++; + } else { + increment = 7200; + } + + // Add a random delay to prevent burst refresh. + return increment + dnssec_random_uint16_t() % 30; +} + +static void limit_timer(conf_t *conf, const knot_dname_t *zone, uint32_t *timer, + const char *tm_name, const yp_name_t *low, const yp_name_t *upp) +{ + uint32_t tlow = 0; + if (low > 0) { + conf_val_t val1 = conf_zone_get(conf, low, zone); + tlow = conf_int(&val1); + } + conf_val_t val2 = conf_zone_get(conf, upp, zone); + uint32_t tupp = conf_int(&val2); + + const char *msg = "%s timer trimmed to '%s-%s-interval'"; + if (*timer < tlow) { + *timer = tlow; + log_zone_debug(zone, msg, tm_name, tm_name, "min"); + } else if (*timer > tupp) { + *timer = tupp; + log_zone_debug(zone, msg, tm_name, tm_name, "max"); + } +} + +/*! + * \brief Modify the expire timer wrt the received EDNS EXPIRE (RFC 7314, section 4) + * + * \param data The refresh data. + * \param pkt A received packet to parse. + * \param strictly_follow Strictly use EDNS EXPIRE as the expire timer value. + * (false == RFC 7314, section 4, second paragraph, + * true == third paragraph) + */ +static void consume_edns_expire(struct refresh_data *data, knot_pkt_t *pkt, bool strictly_follow) +{ + if (data->zone->is_catalog_flag) { + data->expire_timer = EXPIRE_TIMER_INVALID; + return; + } + + uint8_t *expire_opt = knot_pkt_edns_option(pkt, KNOT_EDNS_OPTION_EXPIRE); + if (expire_opt != NULL && knot_edns_opt_get_length(expire_opt) == sizeof(uint32_t)) { + uint32_t edns_expire = knot_wire_read_u32(knot_edns_opt_get_data(expire_opt)); + data->expire_timer = strictly_follow ? edns_expire : + MAX(edns_expire, data->zone->timers.next_expire - time(NULL)); + } +} + +static void finalize_timers(struct refresh_data *data) +{ + conf_t *conf = data->conf; + zone_t *zone = data->zone; + + // EDNS EXPIRE -- RFC 7314, section 4, fourth paragraph. + data->expire_timer = MIN(data->expire_timer, zone_soa_expire(data->zone)); + assert(data->expire_timer != EXPIRE_TIMER_INVALID); + + time_t now = time(NULL); + const knot_rdataset_t *soa = zone_soa(zone); + + uint32_t soa_refresh = knot_soa_refresh(soa->rdata); + limit_timer(conf, zone->name, &soa_refresh, "refresh", + C_REFRESH_MIN_INTERVAL, C_REFRESH_MAX_INTERVAL); + zone->timers.next_refresh = now + soa_refresh; + zone->timers.last_refresh_ok = true; + + if (zone->is_catalog_flag) { + // It's already zero in most cases. + zone->timers.next_expire = 0; + } else { + limit_timer(conf, zone->name, &data->expire_timer, "expire", + // Limit min if not received as EDNS Expire. + data->expire_timer == knot_soa_expire(soa->rdata) ? + C_EXPIRE_MIN_INTERVAL : 0, + C_EXPIRE_MAX_INTERVAL); + zone->timers.next_expire = now + data->expire_timer; + } +} + +static void fill_expires_in(char *expires_in, size_t size, const struct refresh_data *data) +{ + assert(!data->zone->is_catalog_flag || data->zone->timers.next_expire == 0); + if (data->zone->timers.next_expire > 0) { + (void)snprintf(expires_in, size, + ", expires in %u seconds", data->expire_timer); + } +} + +static void xfr_log_publish(const struct refresh_data *data, + const uint32_t old_serial, + const uint32_t new_serial, + const uint32_t master_serial, + bool has_master_serial, + bool axfr_bootstrap) +{ + struct timespec finished = time_now(); + double duration = time_diff_ms(&data->started, &finished) / 1000.0; + + char old_info[32] = "none"; + if (!axfr_bootstrap) { + (void)snprintf(old_info, sizeof(old_info), "%u", old_serial); + } + + char master_info[32] = ""; + if (has_master_serial) { + (void)snprintf(master_info, sizeof(master_info), + ", remote serial %u", master_serial); + } + + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "zone updated, %0.2f seconds, serial %s -> %u%s%s", + duration, old_info, new_serial, master_info, expires_in); +} + +static void xfr_log_read_ms(const knot_dname_t *zone, int ret) +{ + log_zone_error(zone, "failed reading master serial from KASP DB (%s)", knot_strerror(ret)); +} + +static int axfr_init(struct refresh_data *data) +{ + zone_contents_t *new_zone = zone_contents_new(data->zone->name, true); + if (new_zone == NULL) { + return KNOT_ENOMEM; + } + + data->axfr.zone = new_zone; + return KNOT_EOK; +} + +static void axfr_cleanup(struct refresh_data *data) +{ + zone_contents_deep_free(data->axfr.zone); + data->axfr.zone = NULL; +} + +static void axfr_slave_sign_serial(zone_contents_t *new_contents, zone_t *zone, + conf_t *conf, uint32_t *master_serial) +{ + // Update slave's serial to ensure it's growing and consistent with + // its serial policy. + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + unsigned serial_policy = conf_opt(&val); + + *master_serial = zone_contents_serial(new_contents); + + uint32_t new_serial, lastsigned_serial; + if (zone->contents != NULL) { + // Retransfer or AXFR-fallback - increment current serial. + new_serial = serial_next(zone_contents_serial(zone->contents), serial_policy, 1); + } else if (zone_get_lastsigned_serial(zone, &lastsigned_serial) == KNOT_EOK) { + // Bootstrap - increment stored serial. + new_serial = serial_next(lastsigned_serial, serial_policy, 1); + } else { + // Bootstrap - try to reuse master serial, considering policy. + new_serial = serial_next(*master_serial, serial_policy, 0); + } + zone_contents_set_soa_serial(new_contents, new_serial); +} + +static int axfr_finalize(struct refresh_data *data) +{ + zone_contents_t *new_zone = data->axfr.zone; + + conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name); + bool dnssec_enable = conf_bool(&val); + uint32_t old_serial = zone_contents_serial(data->zone->contents), master_serial = 0; + bool bootstrap = (data->zone->contents == NULL); + + if (dnssec_enable) { + axfr_slave_sign_serial(new_zone, data->zone, data->conf, &master_serial); + } + + zone_update_t up = { 0 }; + int ret = zone_update_from_contents(&up, data->zone, new_zone, UPDATE_FULL); + if (ret != KNOT_EOK) { + data->fallback->remote = false; + return ret; + } + // Seized by zone_update. Don't free the contents again in axfr_cleanup. + data->axfr.zone = NULL; + + ret = zone_update_semcheck(data->conf, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(data->conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + return ret; + } + + val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name); + unsigned digest_alg = conf_opt(&val); + + if (dnssec_enable) { + zone_sign_reschedule_t resch = { 0 }; + ret = knot_dnssec_zone_sign(&up, data->conf, ZONE_SIGN_KEEP_SERIAL, KEY_ROLL_ALLOW_ALL, 0, &resch); + event_dnssec_reschedule(data->conf, data->zone, &resch, true); + } else if (digest_alg != ZONE_DIGEST_NONE) { + assert(zone_update_to(&up) != NULL); + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback->remote = false; + return ret; + } + + ret = zone_update_commit(data->conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + AXFRIN_LOG(LOG_WARNING, data, + "failed to store changes (%s)", knot_strerror(ret)); + data->fallback->remote = false; + return ret; + } + + if (dnssec_enable) { + ret = zone_set_master_serial(data->zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_warning(data->zone->name, + "unable to save master serial, future transfers might be broken"); + } + } + + finalize_timers(data); + xfr_log_publish(data, old_serial, zone_contents_serial(new_zone), + master_serial, dnssec_enable, bootstrap); + + return KNOT_EOK; +} + +static int axfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data) +{ + assert(rr); + assert(data); + assert(data->axfr.zone); + + // zc is stateless structure which can be initialized for each rr + // the changes are stored only in data->axfr.zone (aka zc.z) + zcreator_t zc = { + .z = data->axfr.zone, + .master = false, + .ret = KNOT_EOK + }; + + if (rr->type == KNOT_RRTYPE_SOA && + node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) { + return KNOT_STATE_DONE; + } + + data->ret = zcreator_step(&zc, rr); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + data->change_size += knot_rrset_size(rr); + if (data->change_size > data->max_zone_size) { + AXFRIN_LOG(LOG_WARNING, data, + "zone size exceeded"); + data->ret = KNOT_EZONESIZE; + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_CONSUME; +} + +static int axfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data) +{ + assert(pkt); + assert(data); + + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + int ret = KNOT_STATE_CONSUME; + for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) { + ret = axfr_consume_rr(knot_pkt_rr(answer, i), data); + } + return ret; +} + +static int axfr_consume(knot_pkt_t *pkt, struct refresh_data *data, bool reuse_soa) +{ + assert(pkt); + assert(data); + + // Check RCODE + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + AXFRIN_LOG(LOG_WARNING, data, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + // Initialize with first packet + if (data->axfr.zone == NULL) { + data->ret = axfr_init(data); + if (data->ret != KNOT_EOK) { + AXFRIN_LOG(LOG_WARNING, data, + "failed to initialize (%s)", + knot_strerror(data->ret)); + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + + AXFRIN_LOG(LOG_INFO, data, "started"); + xfr_stats_begin(&data->stats); + data->change_size = 0; + } + + int next; + // Process saved SOA if fallback from IXFR + if (data->initial_soa_copy != NULL) { + next = reuse_soa ? axfr_consume_rr(data->initial_soa_copy, data) : + KNOT_STATE_CONSUME; + knot_rrset_free(data->initial_soa_copy, data->mm); + data->initial_soa_copy = NULL; + if (next != KNOT_STATE_CONSUME) { + return next; + } + } + + // Process answer packet + xfr_stats_add(&data->stats, pkt->size); + next = axfr_consume_packet(pkt, data); + + // Finalize + if (next == KNOT_STATE_DONE) { + xfr_stats_end(&data->stats); + } + + return next; +} + +/*! \brief Initialize IXFR-in processing context. */ +static int ixfr_init(struct refresh_data *data) +{ + struct ixfr_proc *proc = mm_alloc(data->mm, sizeof(*proc)); + if (proc == NULL) { + return KNOT_ENOMEM; + } + + memset(proc, 0, sizeof(struct ixfr_proc)); + proc->state = IXFR_START; + proc->mm = data->mm; + + data->ixfr.proc = proc; + data->ixfr.final_soa = NULL; + + init_list(&data->ixfr.changesets); + + return KNOT_EOK; +} + +/*! \brief Clean up data allocated by IXFR-in processing. */ +static void ixfr_cleanup(struct refresh_data *data) +{ + if (data->ixfr.proc == NULL) { + return; + } + + knot_rrset_free(data->ixfr.final_soa, data->mm); + data->ixfr.final_soa = NULL; + mm_free(data->mm, data->ixfr.proc); + data->ixfr.proc = NULL; + + changesets_free(&data->ixfr.changesets); +} + +static bool ixfr_serial_once(changeset_t *ch, int policy, uint32_t *master_serial, uint32_t *local_serial) +{ + uint32_t ch_from = changeset_from(ch), ch_to = changeset_to(ch); + + if (ch_from != *master_serial || (serial_compare(ch_from, ch_to) & SERIAL_MASK_GEQ)) { + return false; + } + + uint32_t new_from = *local_serial; + uint32_t new_to = serial_next(new_from, policy, 1); + knot_soa_serial_set(ch->soa_from->rrs.rdata, new_from); + knot_soa_serial_set(ch->soa_to->rrs.rdata, new_to); + + *master_serial = ch_to; + *local_serial = new_to; + + return true; +} + +static int ixfr_slave_sign_serial(list_t *changesets, zone_t *zone, + conf_t *conf, uint32_t *master_serial) +{ + uint32_t local_serial = zone_contents_serial(zone->contents), lastsigned; + + if (zone_get_lastsigned_serial(zone, &lastsigned) != KNOT_EOK || lastsigned != local_serial) { + // this is kind of assert + return KNOT_ERROR; + } + + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + unsigned serial_policy = conf_opt(&val); + + int ret = zone_get_master_serial(zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "failed to read master serial" + "from KASP DB (%s)", knot_strerror(ret)); + return ret; + } + changeset_t *chs; + WALK_LIST(chs, *changesets) { + if (!ixfr_serial_once(chs, serial_policy, master_serial, &local_serial)) { + return KNOT_EINVAL; + } + } + + return KNOT_EOK; +} + +static int ixfr_finalize(struct refresh_data *data) +{ + conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name); + bool dnssec_enable = conf_bool(&val); + uint32_t master_serial = 0, old_serial = zone_contents_serial(data->zone->contents); + + if (dnssec_enable) { + int ret = ixfr_slave_sign_serial(&data->ixfr.changesets, data->zone, data->conf, &master_serial); + if (ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed to adjust SOA serials from unsigned remote (%s)", + knot_strerror(ret)); + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + } + + zone_update_t up = { 0 }; + int ret = zone_update_init(&up, data->zone, UPDATE_INCREMENTAL | UPDATE_STRICT | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + + changeset_t *set; + WALK_LIST(set, data->ixfr.changesets) { + ret = zone_update_apply_changeset(&up, set); + if (ret != KNOT_EOK) { + uint32_t serial_from = knot_soa_serial(set->soa_from->rrs.rdata); + uint32_t serial_to = knot_soa_serial(set->soa_to->rrs.rdata); + zone_update_clear(&up); + IXFRIN_LOG(LOG_WARNING, data, + "serial %u -> %u, failed to apply changes to zone (%s)", + serial_from, serial_to, knot_strerror(ret)); + return ret; + } + } + + ret = zone_update_semcheck(data->conf, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(data->conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback_axfr = false; + return ret; + } + + val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name); + unsigned digest_alg = conf_opt(&val); + + if (dnssec_enable) { + ret = knot_dnssec_sign_update(&up, data->conf); + } else if (digest_alg != ZONE_DIGEST_NONE) { + assert(zone_update_to(&up) != NULL); + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + + ret = zone_update_commit(data->conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + IXFRIN_LOG(LOG_WARNING, data, + "failed to store changes (%s)", knot_strerror(ret)); + return ret; + } + + if (dnssec_enable && !EMPTY_LIST(data->ixfr.changesets)) { + ret = zone_set_master_serial(data->zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_warning(data->zone->name, + "unable to save master serial, future transfers might be broken"); + } + } + + finalize_timers(data); + xfr_log_publish(data, old_serial, zone_contents_serial(data->zone->contents), + master_serial, dnssec_enable, false); + + return KNOT_EOK; +} + +/*! \brief Stores starting SOA into changesets structure. */ +static int ixfr_solve_start(const knot_rrset_t *rr, struct refresh_data *data) +{ + assert(data->ixfr.final_soa == NULL); + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + // Store terminal SOA + data->ixfr.final_soa = knot_rrset_copy(rr, data->mm); + if (data->ixfr.final_soa == NULL) { + return KNOT_ENOMEM; + } + + // Initialize list for changes + init_list(&data->ixfr.changesets); + + return KNOT_EOK; +} + +/*! \brief Decides what to do with a starting SOA (deletions). */ +static int ixfr_solve_soa_del(const knot_rrset_t *rr, struct refresh_data *data) +{ + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + // Create new changeset. + changeset_t *change = changeset_new(data->zone->name); + if (change == NULL) { + return KNOT_ENOMEM; + } + + // Store SOA into changeset. + change->soa_from = knot_rrset_copy(rr, NULL); + if (change->soa_from == NULL) { + changeset_free(change); + return KNOT_ENOMEM; + } + + // Add changeset. + add_tail(&data->ixfr.changesets, &change->n); + + return KNOT_EOK; +} + +/*! \brief Stores ending SOA into changeset. */ +static int ixfr_solve_soa_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + change->soa_to = knot_rrset_copy(rr, NULL); + if (change->soa_to == NULL) { + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +/*! \brief Adds single RR into remove section of changeset. */ +static int ixfr_solve_del(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + return changeset_add_removal(change, rr, 0); +} + +/*! \brief Adds single RR into add section of changeset. */ +static int ixfr_solve_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + return changeset_add_addition(change, rr, 0); +} + +/*! \brief Decides what the next IXFR-in state should be. */ +static int ixfr_next_state(struct refresh_data *data, const knot_rrset_t *rr) +{ + const bool soa = (rr->type == KNOT_RRTYPE_SOA); + enum ixfr_state state = data->ixfr.proc->state; + + if ((state == IXFR_SOA_ADD || state == IXFR_ADD) && + knot_rrset_equal(rr, data->ixfr.final_soa, true)) { + return IXFR_DONE; + } + + switch (state) { + case IXFR_START: + // Final SOA already stored or transfer start. + return data->ixfr.final_soa ? IXFR_SOA_DEL : IXFR_START; + case IXFR_SOA_DEL: + // Empty delete section or start of delete section. + return soa ? IXFR_SOA_ADD : IXFR_DEL; + case IXFR_SOA_ADD: + // Empty add section or start of add section. + return soa ? IXFR_SOA_DEL : IXFR_ADD; + case IXFR_DEL: + // End of delete section or continue. + return soa ? IXFR_SOA_ADD : IXFR_DEL; + case IXFR_ADD: + // End of add section or continue. + return soa ? IXFR_SOA_DEL : IXFR_ADD; + default: + assert(0); + return IXFR_INVALID; + } +} + +/*! + * \brief Processes single RR according to current IXFR-in state. The states + * correspond with IXFR-in message structure, in the order they are + * mentioned in the code. + * + * \param rr RR to process. + * \param proc Processing context. + * + * \return KNOT_E* + */ +static int ixfr_step(const knot_rrset_t *rr, struct refresh_data *data) +{ + data->ixfr.proc->state = ixfr_next_state(data, rr); + changeset_t *change = TAIL(data->ixfr.changesets); + + switch (data->ixfr.proc->state) { + case IXFR_START: + return ixfr_solve_start(rr, data); + case IXFR_SOA_DEL: + return ixfr_solve_soa_del(rr, data); + case IXFR_DEL: + return ixfr_solve_del(rr, change, data->mm); + case IXFR_SOA_ADD: + return ixfr_solve_soa_add(rr, change, data->mm); + case IXFR_ADD: + return ixfr_solve_add(rr, change, data->mm); + case IXFR_DONE: + return KNOT_EOK; + default: + return KNOT_ERROR; + } +} + +static int ixfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data) +{ + if (knot_dname_in_bailiwick(rr->owner, data->zone->name) < 0) { + return KNOT_STATE_CONSUME; + } + + data->ret = ixfr_step(rr, data); + if (data->ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed (%s)", knot_strerror(data->ret)); + return KNOT_STATE_FAIL; + } + + data->change_size += knot_rrset_size(rr); + if (data->change_size / 2 > data->max_zone_size) { + IXFRIN_LOG(LOG_WARNING, data, + "transfer size exceeded"); + data->ret = KNOT_EZONESIZE; + return KNOT_STATE_FAIL; + } + + if (data->ixfr.proc->state == IXFR_DONE) { + return KNOT_STATE_DONE; + } + + return KNOT_STATE_CONSUME; +} + +/*! + * \brief Processes IXFR reply packet and fills in the changesets structure. + * + * \param pkt Packet containing the IXFR reply in wire format. + * \param adata Answer data, including processing context. + * + * \return KNOT_STATE_CONSUME, KNOT_STATE_DONE, KNOT_STATE_FAIL + */ +static int ixfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data) +{ + // Process RRs in the message. + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + int ret = KNOT_STATE_CONSUME; + for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) { + ret = ixfr_consume_rr(knot_pkt_rr(answer, i), data); + } + return ret; +} + +static enum xfr_type determine_xfr_type(const knot_pktsection_t *answer, + uint32_t zone_serial, const knot_rrset_t *initial_soa) +{ + if (answer->count < 1) { + return XFR_TYPE_NOTIMP; + } + + const knot_rrset_t *rr_one = knot_pkt_rr(answer, 0); + if (initial_soa != NULL) { + if (rr_one->type == KNOT_RRTYPE_SOA) { + return knot_rrset_equal(initial_soa, rr_one, true) ? + XFR_TYPE_AXFR : XFR_TYPE_IXFR; + } + return XFR_TYPE_AXFR; + } + + if (answer->count == 1) { + if (rr_one->type == KNOT_RRTYPE_SOA) { + return serial_is_current(zone_serial, knot_soa_serial(rr_one->rrs.rdata)) ? + XFR_TYPE_UPTODATE : XFR_TYPE_UNDETERMINED; + } + return XFR_TYPE_ERROR; + } + + const knot_rrset_t *rr_two = knot_pkt_rr(answer, 1); + if (answer->count == 2 && rr_one->type == KNOT_RRTYPE_SOA && + knot_rrset_equal(rr_one, rr_two, true)) { + return XFR_TYPE_AXFR; + } + + return (rr_one->type == KNOT_RRTYPE_SOA && rr_two->type != KNOT_RRTYPE_SOA) ? + XFR_TYPE_AXFR : XFR_TYPE_IXFR; +} + +static int ixfr_consume(knot_pkt_t *pkt, struct refresh_data *data) +{ + assert(pkt); + assert(data); + + // Check RCODE + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + IXFRIN_LOG(LOG_WARNING, data, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + // Initialize with first packet + if (data->ixfr.proc == NULL) { + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + + uint32_t master_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &master_serial); + if (data->ret != KNOT_EOK) { + xfr_log_read_ms(data->zone->name, data->ret); + data->fallback_axfr = false; + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + data->xfr_type = determine_xfr_type(answer, master_serial, + data->initial_soa_copy); + switch (data->xfr_type) { + case XFR_TYPE_ERROR: + IXFRIN_LOG(LOG_WARNING, data, + "malformed response SOA"); + data->ret = KNOT_EMALF; + data->xfr_type = XFR_TYPE_IXFR; // unrecognisable IXFR type is the same as failed IXFR + return KNOT_STATE_FAIL; + case XFR_TYPE_NOTIMP: + IXFRIN_LOG(LOG_WARNING, data, + "not supported by remote"); + data->ret = KNOT_ENOTSUP; + data->xfr_type = XFR_TYPE_IXFR; + return KNOT_STATE_FAIL; + case XFR_TYPE_UNDETERMINED: + // Store the SOA and check with next packet + data->initial_soa_copy = knot_rrset_copy(knot_pkt_rr(answer, 0), data->mm); + if (data->initial_soa_copy == NULL) { + data->ret = KNOT_ENOMEM; + return KNOT_STATE_FAIL; + } + xfr_stats_add(&data->stats, pkt->size); + return KNOT_STATE_CONSUME; + case XFR_TYPE_AXFR: + IXFRIN_LOG(LOG_INFO, data, + "receiving AXFR-style IXFR"); + return axfr_consume(pkt, data, true); + case XFR_TYPE_UPTODATE: + consume_edns_expire(data, pkt, false); + finalize_timers(data); + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + IXFRIN_LOG(LOG_INFO, data, + "zone is up-to-date%s", expires_in); + xfr_stats_begin(&data->stats); + xfr_stats_add(&data->stats, pkt->size); + xfr_stats_end(&data->stats); + return KNOT_STATE_DONE; + case XFR_TYPE_IXFR: + break; + default: + assert(0); + data->ret = KNOT_EPROCESSING; + return KNOT_STATE_FAIL; + } + + data->ret = ixfr_init(data); + if (data->ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed to initialize (%s)", knot_strerror(data->ret)); + data->fallback_axfr = false; + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + + IXFRIN_LOG(LOG_INFO, data, "started"); + xfr_stats_begin(&data->stats); + data->change_size = 0; + } + + int next; + // Process saved SOA if existing + if (data->initial_soa_copy != NULL) { + next = ixfr_consume_rr(data->initial_soa_copy, data); + knot_rrset_free(data->initial_soa_copy, data->mm); + data->initial_soa_copy = NULL; + if (next != KNOT_STATE_CONSUME) { + return next; + } + } + + // Process answer packet + xfr_stats_add(&data->stats, pkt->size); + next = ixfr_consume_packet(pkt, data); + + // Finalize + if (next == KNOT_STATE_DONE) { + xfr_stats_end(&data->stats); + } + + return next; +} + +static int soa_query_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + query_init_pkt(pkt); + + data->ret = knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN, + KNOT_RRTYPE_SOA); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + if (data->use_edns) { + data->ret = query_put_edns(pkt, &data->edns); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + return KNOT_STATE_CONSUME; +} + +static int soa_query_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_IN, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + const knot_rrset_t *rr = answer->count == 1 ? knot_pkt_rr(answer, 0) : NULL; + if (!rr || rr->type != KNOT_RRTYPE_SOA || rr->rrs.count != 1) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_IN, + "malformed message"); + conf_val_t val = conf_zone_get(data->conf, C_SEM_CHECKS, data->zone->name); + if (conf_opt(&val) == SEMCHECKS_SOFT) { + data->xfr_type = XFR_TYPE_AXFR; + data->state = STATE_TRANSFER; + return KNOT_STATE_RESET; + } else { + data->ret = KNOT_EMALF; + return KNOT_STATE_FAIL; + } + } + + uint32_t local_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &local_serial); + if (data->ret != KNOT_EOK) { + xfr_log_read_ms(data->zone->name, data->ret); + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + uint32_t remote_serial = knot_soa_serial(rr->rrs.rdata); + bool current = serial_is_current(local_serial, remote_serial); + bool master_uptodate = serial_is_current(remote_serial, local_serial); + + if (!current) { + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, zone is outdated", remote_serial); + data->state = STATE_TRANSFER; + return KNOT_STATE_RESET; // continue with transfer + } else if (master_uptodate) { + consume_edns_expire(data, pkt, false); + finalize_timers(data); + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, zone is up-to-date%s", + remote_serial, expires_in); + return KNOT_STATE_DONE; + } else { + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, remote is outdated", remote_serial); + return KNOT_STATE_FAIL; + } +} + +static int transfer_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + query_init_pkt(pkt); + + bool ixfr = (data->xfr_type == XFR_TYPE_IXFR); + + data->ret = knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN, + ixfr ? KNOT_RRTYPE_IXFR : KNOT_RRTYPE_AXFR); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + if (ixfr) { + assert(data->soa); + knot_rrset_t *sending_soa = knot_rrset_copy(data->soa, data->mm); + uint32_t master_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &master_serial); + if (data->ret != KNOT_EOK) { + data->fallback->remote = false; + xfr_log_read_ms(data->zone->name, data->ret); + } + if (sending_soa == NULL || data->ret != KNOT_EOK) { + knot_rrset_free(sending_soa, data->mm); + return KNOT_STATE_FAIL; + } + knot_soa_serial_set(sending_soa->rrs.rdata, master_serial); + knot_pkt_begin(pkt, KNOT_AUTHORITY); + knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, sending_soa, 0); + knot_rrset_free(sending_soa, data->mm); + } + + if (data->use_edns) { + data->ret = query_put_edns(pkt, &data->edns); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + return KNOT_STATE_CONSUME; +} + +static int transfer_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + consume_edns_expire(data, pkt, true); + if (data->expire_timer < 2) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_NONE, + "remote is expired, ignoring"); + return KNOT_STATE_IGNORE; + } + + data->fallback_axfr = (data->xfr_type == XFR_TYPE_IXFR); + + int next = (data->xfr_type == XFR_TYPE_AXFR) ? axfr_consume(pkt, data, false) : + ixfr_consume(pkt, data); + + // Transfer completed + if (next == KNOT_STATE_DONE) { + // Log transfer even if we still can fail + xfr_log_finished(data->zone->name, + data->xfr_type == XFR_TYPE_IXFR || + data->xfr_type == XFR_TYPE_UPTODATE ? + LOG_OPERATION_IXFR : LOG_OPERATION_AXFR, + LOG_DIRECTION_IN, data->remote, + layer->flags & KNOT_REQUESTOR_REUSED, + &data->stats); + + /* + * TODO: Move finialization into finish + * callback. And update requestor to allow reset from fallback + * as we need IXFR to AXFR failover. + */ + if (tsig_unsigned_count(layer->tsig) != 0) { + data->ret = KNOT_EMALF; + return KNOT_STATE_FAIL; + } + + // Finalize and publish the zone + switch (data->xfr_type) { + case XFR_TYPE_IXFR: + data->ret = ixfr_finalize(data); + break; + case XFR_TYPE_AXFR: + data->ret = axfr_finalize(data); + break; + default: + return next; + } + if (data->ret == KNOT_EOK) { + data->updated = true; + } else { + next = KNOT_STATE_FAIL; + } + } + + return next; +} + +static int refresh_begin(knot_layer_t *layer, void *_data) +{ + layer->data = _data; + struct refresh_data *data = _data; + data->layer = layer; + + if (data->soa) { + data->state = STATE_SOA_QUERY; + data->xfr_type = XFR_TYPE_IXFR; + data->initial_soa_copy = NULL; + } else { + data->state = STATE_TRANSFER; + data->xfr_type = XFR_TYPE_AXFR; + data->initial_soa_copy = NULL; + } + + data->started = time_now(); + + return KNOT_STATE_PRODUCE; +} + +static int refresh_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + switch (data->state) { + case STATE_SOA_QUERY: return soa_query_produce(layer, pkt); + case STATE_TRANSFER: return transfer_produce(layer, pkt); + default: + return KNOT_STATE_FAIL; + } +} + +static int refresh_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + data->fallback->address = false; // received something, other address not needed + + switch (data->state) { + case STATE_SOA_QUERY: return soa_query_consume(layer, pkt); + case STATE_TRANSFER: return transfer_consume(layer, pkt); + default: + return KNOT_STATE_FAIL; + } +} + +static int refresh_reset(knot_layer_t *layer) +{ + return KNOT_STATE_PRODUCE; +} + +static int refresh_finish(knot_layer_t *layer) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + // clean processing context + axfr_cleanup(data); + ixfr_cleanup(data); + + return KNOT_STATE_NOOP; +} + +static const knot_layer_api_t REFRESH_API = { + .begin = refresh_begin, + .produce = refresh_produce, + .consume = refresh_consume, + .reset = refresh_reset, + .finish = refresh_finish, +}; + +static size_t max_zone_size(conf_t *conf, const knot_dname_t *zone) +{ + conf_val_t val = conf_zone_get(conf, C_ZONE_MAX_SIZE, zone); + return conf_int(&val); +} + +typedef struct { + bool force_axfr; + bool send_notify; +} try_refresh_ctx_t; + +static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master, + void *ctx, zone_master_fallback_t *fallback) +{ + // TODO: Abstract interface to issue DNS queries. This is almost copy-pasted. + + assert(zone); + assert(master); + assert(ctx); + assert(fallback); + + try_refresh_ctx_t *trctx = ctx; + + knot_rrset_t soa = { 0 }; + if (zone->contents) { + soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA); + } + + struct refresh_data data = { + .zone = zone, + .conf = conf, + .remote = (struct sockaddr *)&master->addr, + .soa = zone->contents && !trctx->force_axfr ? &soa : NULL, + .max_zone_size = max_zone_size(conf, zone->name), + .use_edns = !master->no_edns, + .edns = query_edns_data_init(conf, master->addr.ss_family, + QUERY_EDNS_OPT_EXPIRE), + .expire_timer = EXPIRE_TIMER_INVALID, + .fallback = fallback, + .fallback_axfr = false, // will be set upon IXFR consume + }; + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &REFRESH_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (!pkt) { + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &master->addr; + const struct sockaddr_storage *src = &master->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &master->key, flags); + if (!req) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int timeout = conf->cache.srv_tcp_remote_io_timeout; + + int ret; + + // while loop runs 0x or 1x; IXFR to AXFR failover + while (ret = knot_requestor_exec(&requestor, req, timeout), + ret = (data.ret == KNOT_EOK ? ret : data.ret), + data.fallback_axfr && ret != KNOT_EOK) { + REFRESH_LOG(LOG_WARNING, &data, LOG_DIRECTION_IN, + "fallback to AXFR (%s)", knot_strerror(ret)); + ixfr_cleanup(&data); + data.ret = KNOT_EOK; + data.xfr_type = XFR_TYPE_AXFR; + data.fallback_axfr = false, + requestor.layer.state = KNOT_STATE_RESET; + requestor.layer.flags |= KNOT_REQUESTOR_CLOSE; + } + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + if (ret == KNOT_EOK) { + trctx->send_notify = data.updated && !master->block_notify_after_xfr; + trctx->force_axfr = false; + } + + return ret; +} + +int event_refresh(conf_t *conf, zone_t *zone) +{ + assert(zone); + + if (!zone_is_slave(conf, zone)) { + return KNOT_ENOTSUP; + } + + try_refresh_ctx_t trctx = { 0 }; + + // TODO: Flag on zone is ugly. Event specific parameters would be nice. + if (zone_get_flag(zone, ZONE_FORCE_AXFR, true)) { + trctx.force_axfr = true; + zone->zonefile.retransfer = true; + } + + int ret = zone_master_try(conf, zone, try_refresh, &trctx, "refresh"); + zone_clear_preferred_master(zone); + if (ret != KNOT_EOK) { + const knot_rdataset_t *soa = zone_soa(zone); + uint32_t next; + + if (soa) { + next = knot_soa_retry(soa->rdata); + } else { + next = bootstrap_next(&zone->zonefile.bootstrap_cnt); + } + + limit_timer(conf, zone->name, &next, "retry", + C_RETRY_MIN_INTERVAL, C_RETRY_MAX_INTERVAL); + zone->timers.next_refresh = time(NULL) + next; + zone->timers.last_refresh_ok = false; + + char time_str[64] = { 0 }; + struct tm time_gm = { 0 }; + localtime_r(&zone->timers.next_refresh, &time_gm); + strftime(time_str, sizeof(time_str), KNOT_LOG_TIME_FORMAT, &time_gm); + + log_zone_error(zone->name, "refresh, failed (%s), next retry at %s", + knot_strerror(ret), time_str); + } else { + zone->zonefile.bootstrap_cnt = 0; + } + + /* Reschedule events. */ + replan_from_timers(conf, zone); + if (trctx.send_notify) { + zone_schedule_notify(zone, 1); + } + + return ret; +} diff --git a/src/knot/events/handlers/update.c b/src/knot/events/handlers/update.c new file mode 100644 index 0000000..f337eb5 --- /dev/null +++ b/src/knot/events/handlers/update.c @@ -0,0 +1,433 @@ +/* Copyright (C) 2022 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 <assert.h> + +#include "knot/events/handlers.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/capture.h" +#include "knot/query/requestor.h" +#include "knot/updates/ddns.h" +#include "knot/zone/digest.h" +#include "knot/zone/zone.h" +#include "libdnssec/random.h" +#include "libknot/libknot.h" +#include "contrib/net.h" +#include "contrib/time.h" + +#define UPDATE_LOG(priority, qdata, fmt...) \ + ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_UPDATE, \ + LOG_DIRECTION_IN, (struct sockaddr *)knotd_qdata_remote_addr(qdata), \ + false, fmt) + +static void init_qdata_from_request(knotd_qdata_t *qdata, + zone_t *zone, + knot_request_t *req, + knotd_qdata_params_t *params, + knotd_qdata_extra_t *extra) +{ + memset(qdata, 0, sizeof(*qdata)); + qdata->params = params; + qdata->query = req->query; + qdata->sign = req->sign; + qdata->extra = extra; + memset(extra, 0, sizeof(*extra)); + qdata->extra->zone = zone; +} + +static int check_prereqs(knot_request_t *request, + const zone_t *zone, zone_update_t *update, + knotd_qdata_t *qdata) +{ + uint16_t rcode = KNOT_RCODE_NOERROR; + int ret = ddns_process_prereqs(request->query, update, &rcode); + if (ret != KNOT_EOK) { + UPDATE_LOG(LOG_WARNING, qdata, "prerequisites not met (%s)", + knot_strerror(ret)); + assert(rcode != KNOT_RCODE_NOERROR); + knot_wire_set_rcode(request->resp->wire, rcode); + return ret; + } + + return KNOT_EOK; +} + +static int process_single_update(knot_request_t *request, + const zone_t *zone, zone_update_t *update, + knotd_qdata_t *qdata) +{ + uint16_t rcode = KNOT_RCODE_NOERROR; + int ret = ddns_process_update(zone, request->query, update, &rcode); + if (ret != KNOT_EOK) { + UPDATE_LOG(LOG_WARNING, qdata, "failed to apply (%s)", + knot_strerror(ret)); + assert(rcode != KNOT_RCODE_NOERROR); + knot_wire_set_rcode(request->resp->wire, rcode); + return ret; + } + + return KNOT_EOK; +} + +static void set_rcodes(list_t *requests, const uint16_t rcode) +{ + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + if (knot_wire_get_rcode(req->resp->wire) == KNOT_RCODE_NOERROR) { + knot_wire_set_rcode(req->resp->wire, rcode); + } + } +} + +static int process_bulk(zone_t *zone, list_t *requests, zone_update_t *up) +{ + // Walk all the requests and process. + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + // Init qdata structure for logging (unique per-request). + knotd_qdata_params_t params = { + .remote = &req->remote + }; + knotd_qdata_t qdata; + knotd_qdata_extra_t extra; + init_qdata_from_request(&qdata, zone, req, ¶ms, &extra); + + int ret = check_prereqs(req, zone, up, &qdata); + if (ret != KNOT_EOK) { + // Skip updates with failed prereqs. + continue; + } + + ret = process_single_update(req, zone, up, &qdata); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int process_normal(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(requests); + + // Init zone update structure + zone_update_t up; + int ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Process all updates. + ret = process_bulk(zone, requests, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Sign update. + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = conf_bool(&val); + val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + if (dnssec_enable) { + ret = knot_dnssec_sign_update(&up, conf); + } else if (digest_alg != ZONE_DIGEST_NONE) { + if (zone_update_to(&up) == NULL) { + ret = zone_update_increment_soa(&up, conf); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(&up, digest_alg, false); + } + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Apply changes. + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + if (ret == KNOT_EZONESIZE) { + set_rcodes(requests, KNOT_RCODE_REFUSED); + } else { + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + } + return ret; + } + + return KNOT_EOK; +} + +static void process_requests(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(zone); + assert(requests); + + /* Keep original state. */ + struct timespec t_start = time_now(); + const uint32_t old_serial = zone_contents_serial(zone->contents); + + /* Process authenticated packet. */ + int ret = process_normal(conf, zone, requests); + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "DDNS, processing failed (%s)", + knot_strerror(ret)); + return; + } + + /* Evaluate response. */ + const uint32_t new_serial = zone_contents_serial(zone->contents); + if (new_serial == old_serial) { + log_zone_info(zone->name, "DDNS, finished, no changes to the zone were made"); + return; + } + + struct timespec t_end = time_now(); + log_zone_info(zone->name, "DDNS, finished, serial %u -> %u, " + "%.02f seconds", old_serial, new_serial, + time_diff_ms(&t_start, &t_end) / 1000.0); + + zone_schedule_notify(zone, 1); +} + +static int remote_forward(conf_t *conf, knot_request_t *request, conf_remote_t *remote) +{ + /* Copy request and assign new ID. */ + knot_pkt_t *query = knot_pkt_new(NULL, request->query->max_size, NULL); + int ret = knot_pkt_copy(query, request->query); + if (ret != KNOT_EOK) { + knot_pkt_free(query); + return ret; + } + knot_wire_set_id(query->wire, dnssec_random_uint16_t()); + knot_tsig_append(query->wire, &query->size, query->max_size, query->tsig_rr); + + /* Prepare packet capture layer. */ + const knot_layer_api_t *capture = query_capture_api(); + struct capture_param capture_param = { + .sink = request->resp + }; + + /* Create requestor instance. */ + knot_requestor_t re; + ret = knot_requestor_init(&re, capture, &capture_param, NULL); + if (ret != KNOT_EOK) { + knot_pkt_free(query); + return ret; + } + + /* Create a request. */ + const struct sockaddr_storage *dst = &remote->addr; + const struct sockaddr_storage *src = &remote->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(re.mm, dst, src, query, NULL, flags); + if (req == NULL) { + knot_requestor_clear(&re); + knot_pkt_free(query); + return KNOT_ENOMEM; + } + + /* Execute the request. */ + int timeout = conf->cache.srv_tcp_remote_io_timeout; + ret = knot_requestor_exec(&re, req, timeout); + + knot_request_free(req, re.mm); + knot_requestor_clear(&re); + + return ret; +} + +static void forward_request(conf_t *conf, zone_t *zone, knot_request_t *request) +{ + /* Read the ddns master or the first master. */ + conf_val_t remote = conf_zone_get(conf, C_DDNS_MASTER, zone->name); + if (remote.code != KNOT_EOK) { + remote = conf_zone_get(conf, C_MASTER, zone->name); + } + + /* Get the number of remote addresses. */ + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &remote); + size_t addr_count = conf_val_count(&addr); + assert(addr_count > 0); + + /* Try all remote addresses to forward the request to. */ + int ret = KNOT_EOK; + for (size_t i = 0; i < addr_count; i++) { + conf_remote_t master = conf_remote(conf, &remote, i); + + ret = remote_forward(conf, request, &master); + if (ret == KNOT_EOK) { + break; + } + } + + /* Restore message ID and TSIG. */ + knot_wire_set_id(request->resp->wire, knot_wire_get_id(request->query->wire)); + knot_tsig_append(request->resp->wire, &request->resp->size, + request->resp->max_size, request->resp->tsig_rr); + + /* Set RCODE if forwarding failed. */ + if (ret != KNOT_EOK) { + knot_wire_set_rcode(request->resp->wire, KNOT_RCODE_SERVFAIL); + log_zone_error(zone->name, "DDNS, failed to forward updates to the master (%s)", + knot_strerror(ret)); + } else { + log_zone_info(zone->name, "DDNS, updates forwarded to the master"); + } +} + +static void forward_requests(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(zone); + assert(requests); + + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + forward_request(conf, zone, req); + } +} + +static void send_update_response(conf_t *conf, zone_t *zone, knot_request_t *req) +{ + if (req->resp) { + if (!zone_is_slave(conf, zone)) { + // Sign the response with TSIG where applicable + knotd_qdata_t qdata; + knotd_qdata_extra_t extra; + init_qdata_from_request(&qdata, zone, req, NULL, &extra); + + (void)process_query_sign_response(req->resp, &qdata); + } + + if (net_is_stream(req->fd)) { + net_dns_tcp_send(req->fd, req->resp->wire, req->resp->size, + conf->cache.srv_tcp_remote_io_timeout, NULL); + } else { + net_dgram_send(req->fd, req->resp->wire, req->resp->size, + &req->remote); + } + } +} + +static void free_request(knot_request_t *req) +{ + close(req->fd); + knot_pkt_free(req->query); + knot_pkt_free(req->resp); + dnssec_binary_free(&req->sign.tsig_key.secret); + free(req); +} + +static void send_update_responses(conf_t *conf, zone_t *zone, list_t *updates) +{ + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, *updates) { + knot_request_t *req = node->d; + send_update_response(conf, zone, req); + free_request(req); + } + ptrlist_free(updates, NULL); +} + +static int init_update_responses(list_t *updates) +{ + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, *updates) { + knot_request_t *req = node->d; + req->resp = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (req->resp == NULL) { + return KNOT_ENOMEM; + } + + assert(req->query); + knot_pkt_init_response(req->resp, req->query); + } + + return KNOT_EOK; +} + +static size_t update_dequeue(zone_t *zone, list_t *updates) +{ + assert(zone); + assert(updates); + + pthread_mutex_lock(&zone->ddns_lock); + + if (EMPTY_LIST(zone->ddns_queue)) { + /* Lost race during reload. */ + pthread_mutex_unlock(&zone->ddns_lock); + return 0; + } + + *updates = zone->ddns_queue; + size_t update_count = zone->ddns_queue_size; + init_list(&zone->ddns_queue); + zone->ddns_queue_size = 0; + + pthread_mutex_unlock(&zone->ddns_lock); + + return update_count; +} + +int event_update(conf_t *conf, zone_t *zone) +{ + assert(zone); + + /* Get list of pending updates. */ + list_t updates; + size_t update_count = update_dequeue(zone, &updates); + if (update_count == 0) { + return KNOT_EOK; + } + + /* Init updates responses. */ + int ret = init_update_responses(&updates); + if (ret != KNOT_EOK) { + /* Send what responses we can. */ + set_rcodes(&updates, KNOT_RCODE_SERVFAIL); + send_update_responses(conf, zone, &updates); + return ret; + } + + /* Process update list - forward if zone has master, or execute. + RCODEs are set. */ + if (zone_is_slave(conf, zone)) { + log_zone_info(zone->name, + "DDNS, forwarding %zu updates", update_count); + forward_requests(conf, zone, &updates); + } else { + log_zone_info(zone->name, + "DDNS, processing %zu updates", update_count); + process_requests(conf, zone, &updates); + } + + /* Send responses. */ + send_update_responses(conf, zone, &updates); + + return KNOT_EOK; +} |