From 2dbb39d29a0e708d88ab1b3385afd8286d692aef Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Wed, 4 Sep 2019 15:15:28 +0000 Subject: [PATCH] dnssec: implemented pushing updated DS to parent --- Knot.files | 3 +- doc/man/knot.conf.5in | 23 ++ doc/reference.rst | 19 ++ scripts/timerdb-info.py | 6 +- src/knot/Makefile.inc | 3 +- src/knot/conf/schema.c | 2 + src/knot/conf/schema.h | 1 + src/knot/conf/tools.c | 10 + src/knot/dnssec/key-events.c | 8 +- src/knot/dnssec/zone-events.h | 2 +- src/knot/dnssec/zone-sign.c | 5 + src/knot/events/events.c | 5 +- src/knot/events/events.h | 3 +- src/knot/events/handlers.h | 6 +- src/knot/events/handlers/dnssec.c | 6 +- .../{parent_ds_query.c => ds_check.c} | 8 +- src/knot/events/handlers/ds_push.c | 244 ++++++++++++++++++ src/knot/events/replan.c | 11 +- src/knot/modules/onlinesign/onlinesign.c | 4 +- src/knot/zone/timers.c | 31 ++- src/knot/zone/timers.h | 5 +- .../tests/dnssec/ds_push/data/com.zone | 6 + .../dnssec/ds_push/data/example.com.zone | 9 + tests-extra/tests/dnssec/ds_push/test.py | 160 ++++++++++++ tests-extra/tools/dnstest/server.py | 5 +- tests/knot/test_zone_timers.c | 26 +- 26 files changed, 556 insertions(+), 55 deletions(-) rename src/knot/events/handlers/{parent_ds_query.c => ds_check.c} (88%) create mode 100644 src/knot/events/handlers/ds_push.c create mode 100644 tests-extra/tests/dnssec/ds_push/data/com.zone create mode 100644 tests-extra/tests/dnssec/ds_push/data/example.com.zone create mode 100644 tests-extra/tests/dnssec/ds_push/test.py diff --git a/Knot.files b/Knot.files index e766ce60fa..d865ad6786 100644 --- a/Knot.files +++ b/Knot.files @@ -128,13 +128,14 @@ src/knot/events/events.c src/knot/events/events.h src/knot/events/handlers.h src/knot/events/handlers/dnssec.c +src/knot/events/handlers/ds_check.c +src/knot/events/handlers/ds_push.c src/knot/events/handlers/expire.c src/knot/events/handlers/flush.c src/knot/events/handlers/freeze_thaw.c src/knot/events/handlers/load.c src/knot/events/handlers/notify.c src/knot/events/handlers/nsec3resalt.c -src/knot/events/handlers/parent_ds_query.c src/knot/events/handlers/refresh.c src/knot/events/handlers/update.c src/knot/events/replan.c diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in index abaed61297..e4f79f6946 100644 --- a/doc/man/knot.conf.5in +++ b/doc/man/knot.conf.5in @@ -706,6 +706,7 @@ policy: nsec3\-salt\-lifetime: TIME signing\-threads: INT ksk\-submission: submission_id + ds\-push: remote_id cds\-cdnskey\-publish: none | delete\-dnssec | rollover | always | double\-ds offline\-ksk: BOOL .ft P @@ -929,6 +930,28 @@ A reference to \fI\%submission\fP section holding parameters of KSK submittion checks. .sp \fIDefault:\fP not set +.SS ds\-push +.sp +A \fI\%reference\fP to parent\(aqs DNS server. Whenever the CDS record in +this zone is changed, corresponding DS record is sent as an update (DDNS) to +parent DNS server. +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +For proper operation ref:\fIcds\-cdnskey\-publish<policy_cds\-cdnskey\-publish>\fP +must not be disabled. +.UNINDENT +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +This feature does not work with Onlinesign module. +.UNINDENT +.UNINDENT +.sp +\fIDefault:\fP not set .SS signing\-threads .sp When signing zone or update, use this number of threads for parallel signing. diff --git a/doc/reference.rst b/doc/reference.rst index 64e22889b2..d45222642e 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -770,6 +770,7 @@ DNSSEC policy configuration. nsec3-salt-lifetime: TIME signing-threads: INT ksk-submission: submission_id + ds-push: remote_id cds-cdnskey-publish: none | delete-dnssec | rollover | always | double-ds offline-ksk: BOOL @@ -1034,6 +1035,24 @@ KSK submittion checks. *Default:* not set +.. _policy_ds-push: + +ds-push +------- + +A :ref:`reference<remote_id>` to parent's DNS server. Whenever the CDS record in +this zone is changed, corresponding DS record is sent as an update (DDNS) to +parent DNS server. + +.. NOTE:: + For proper operation ref:`cds-cdnskey-publish<policy_cds-cdnskey-publish>` + must not be disabled. + +.. NOTE:: + This feature does not work with :ref:`Onlinesign<mod-onlinesign>` module. + +*Default:* not set + .. _policy_signing-threads: signing-threads diff --git a/scripts/timerdb-info.py b/scripts/timerdb-info.py index 26eb079c85..6a7cd36109 100755 --- a/scripts/timerdb-info.py +++ b/scripts/timerdb-info.py @@ -37,8 +37,10 @@ class TimerDBInfo: 0x82: ("last_refresh", cls.format_timestamp), 0x83: ("next_refresh", cls.format_timestamp), # knot >= 2.6 - 0x84: ("last_resalt", cls.format_timestamp), - 0x85: ("next_parent_ds_q", cls.format_timestamp), + 0x84: ("last_resalt", cls.format_timestamp), + 0x85: ("next_ds_check", cls.format_timestamp), + # knot >= 2.8 + 0x86: ("next_ds_push", cls.format_timestamp), } if id in timers: return (timers[id][0], timers[id][1](value)) diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc index f9a5f4d2db..ea2cbb497f 100644 --- a/src/knot/Makefile.inc +++ b/src/knot/Makefile.inc @@ -66,6 +66,8 @@ libknotd_la_SOURCES = \ knot/events/events.h \ knot/events/handlers.h \ knot/events/handlers/dnssec.c \ + knot/events/handlers/ds_check.c \ + knot/events/handlers/ds_push.c \ knot/events/handlers/expire.c \ knot/events/handlers/flush.c \ knot/events/handlers/freeze_thaw.c \ @@ -74,7 +76,6 @@ libknotd_la_SOURCES = \ knot/events/handlers/nsec3resalt.c \ knot/events/handlers/refresh.c \ knot/events/handlers/update.c \ - knot/events/handlers/parent_ds_query.c \ knot/events/replan.c \ knot/events/replan.h \ knot/nameserver/axfr.c \ diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index cc081ebf52..97ba8c5864 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -291,6 +291,8 @@ static const yp_item_t desc_policy[] = { CONF_IO_FRLD_ZONES }, { C_KSK_SBM, YP_TREF, YP_VREF = { C_SBM }, CONF_IO_FRLD_ZONES, { check_ref } }, + { C_DS_PUSH, YP_TREF, YP_VREF = { C_RMT }, YP_FMULTI | CONF_IO_FRLD_ZONES, + { check_ref } }, { C_SIGNING_THREADS, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } }, { C_CDS_CDNSKEY, YP_TOPT, YP_VOPT = { cds_cdnskey, CDS_CDNSKEY_ROLLOVER } }, { C_OFFLINE_KSK, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index befbf2bd9e..742072b25e 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -41,6 +41,7 @@ #define C_DNSSEC_POLICY "\x0D""dnssec-policy" #define C_DNSSEC_SIGNING "\x0E""dnssec-signing" #define C_DOMAIN "\x06""domain" +#define C_DS_PUSH "\x07""ds-push" #define C_ECS "\x12""edns-client-subnet" #define C_FILE "\x04""file" #define C_GLOBAL_MODULE "\x0D""global-module" diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index 1e3d3c4a43..88c58483c3 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -400,6 +400,16 @@ int check_policy( } } + conf_val_t cds_cdnskey = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_CDS_CDNSKEY, args->id, args->id_len); + conf_val_t ds_push = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_DS_PUSH, args->id, args->id_len); + + if (conf_val_count(&ds_push) > 0 && conf_opt(&cds_cdnskey) == CDS_CDNSKEY_NONE) { + args->err_str = "DS push requires enabled CDS/CDNSKEY publication"; + return KNOT_EINVAL; + } + return KNOT_EOK; } diff --git a/src/knot/dnssec/key-events.c b/src/knot/dnssec/key-events.c index 658fa77033..3172efbb93 100644 --- a/src/knot/dnssec/key-events.c +++ b/src/knot/dnssec/key-events.c @@ -594,7 +594,7 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, ret = generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false); } if (ret == KNOT_EOK) { - reschedule->plan_ds_query = true; + reschedule->plan_ds_check = true; plan_ds_keytag = dnssec_key_get_keytag(ctx->zone->keys[0].key); } } @@ -685,7 +685,7 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, case SUBMIT: ret = submit_key(ctx, next.key); if (ret == KNOT_EOK) { - reschedule->plan_ds_query = true; + reschedule->plan_ds_check = true; plan_ds_keytag = dnssec_key_get_keytag(next.key->key); } break; @@ -716,7 +716,7 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, if (ret == KNOT_EOK && next.ready_key >= 0) { // just to make sure DS check is scheduled - reschedule->plan_ds_query = true; + reschedule->plan_ds_check = true; plan_ds_keytag = next.ready_key; } @@ -728,7 +728,7 @@ int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, ret = kdnssec_ctx_commit(ctx); } - if (ret == KNOT_EOK && reschedule->plan_ds_query) { + if (ret == KNOT_EOK && reschedule->plan_ds_check) { char param[32]; (void)snprintf(param, sizeof(param), "KEY_SUBMISSION=%hu", plan_ds_keytag); log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, ctx->zone->dname, param, diff --git a/src/knot/dnssec/zone-events.h b/src/knot/dnssec/zone-events.h index 7039d7b29d..292bdb1adf 100644 --- a/src/knot/dnssec/zone-events.h +++ b/src/knot/dnssec/zone-events.h @@ -48,7 +48,7 @@ typedef struct { knot_time_t next_nsec3resalt; knot_time_t last_nsec3resalt; bool keys_changed; - bool plan_ds_query; + bool plan_ds_check; } zone_sign_reschedule_t; /*! diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c index aa0f4140c2..85a8ff0bcf 100644 --- a/src/knot/dnssec/zone-sign.c +++ b/src/knot/dnssec/zone-sign.c @@ -816,6 +816,11 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update, if (!knot_rrset_empty(&add_r.cds)) { ret = changeset_add_addition(&ch, &add_r.cds, CHANGESET_CHECK); + if (node_rrtype_exists(ch.add->apex, KNOT_RRTYPE_CDS)) { + // there is indeed a change to CDS + update->zone->timers.next_ds_push = time(NULL); + zone_events_schedule_now(update->zone, ZONE_EVENT_DS_PUSH); + } CHECK_RET; } diff --git a/src/knot/events/events.c b/src/knot/events/events.c index f62843a642..6082c042af 100644 --- a/src/knot/events/events.c +++ b/src/knot/events/events.c @@ -48,7 +48,8 @@ static const event_info_t EVENT_INFO[] = { { ZONE_EVENT_UFREEZE, event_ufreeze, "update freeze" }, { ZONE_EVENT_UTHAW, event_uthaw, "update thaw" }, { ZONE_EVENT_NSEC3RESALT, event_nsec3resalt, "NSEC3 resalt" }, - { ZONE_EVENT_PARENT_DS_Q, event_parent_ds_q, "parent DS query" }, + { ZONE_EVENT_DS_CHECK, event_ds_check, "DS check" }, + { ZONE_EVENT_DS_PUSH, event_ds_push, "DS push" }, { 0 } }; @@ -79,7 +80,7 @@ bool ufreeze_applies(zone_event_type_t type) case ZONE_EVENT_FLUSH: case ZONE_EVENT_DNSSEC: case ZONE_EVENT_NSEC3RESALT: - case ZONE_EVENT_PARENT_DS_Q: + case ZONE_EVENT_DS_CHECK: return true; default: return false; diff --git a/src/knot/events/events.h b/src/knot/events/events.h index e14daae0ad..53612f731f 100644 --- a/src/knot/events/events.h +++ b/src/knot/events/events.h @@ -40,7 +40,8 @@ typedef enum zone_event_type { ZONE_EVENT_UFREEZE, ZONE_EVENT_UTHAW, ZONE_EVENT_NSEC3RESALT, - ZONE_EVENT_PARENT_DS_Q, + ZONE_EVENT_DS_CHECK, + ZONE_EVENT_DS_PUSH, // terminator ZONE_EVENT_COUNT, } zone_event_type_t; diff --git a/src/knot/events/handlers.h b/src/knot/events/handlers.h index ca50ccbcac..b000ba22f6 100644 --- a/src/knot/events/handlers.h +++ b/src/knot/events/handlers.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2019 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 @@ -44,4 +44,6 @@ int event_uthaw(conf_t *conf, zone_t *zone); /*! \brief Recreates salt for NSEC3 hashing. */ int event_nsec3resalt(conf_t *conf, zone_t *zone); /*! \brief When CDS/CDNSKEY published, look for matching DS */ -int event_parent_ds_q(conf_t *conf, zone_t *zone); +int event_ds_check(conf_t *conf, zone_t *zone); +/*! \brief After change of CDS/CDNSKEY, push the new DS to parent zone as DDNS. */ +int event_ds_push(conf_t *conf, zone_t *zone); diff --git a/src/knot/events/handlers/dnssec.c b/src/knot/events/handlers/dnssec.c index 32d3b7d3d2..50af00a6e2 100644 --- a/src/knot/events/handlers/dnssec.c +++ b/src/knot/events/handlers/dnssec.c @@ -50,8 +50,8 @@ void event_dnssec_reschedule(conf_t *conf, zone_t *zone, log_dnssec_next(zone->name, (time_t)refresh_at); - if (refresh->plan_ds_query) { - zone->timers.next_parent_ds_q = now; + if (refresh->plan_ds_check) { + zone->timers.next_ds_check = now; } if (refresh->last_nsec3resalt) { @@ -60,7 +60,7 @@ void event_dnssec_reschedule(conf_t *conf, zone_t *zone, zone_events_schedule_at(zone, ZONE_EVENT_DNSSEC, refresh_at ? (time_t)refresh_at : ignore, - ZONE_EVENT_PARENT_DS_Q, refresh->plan_ds_query ? now : ignore, + ZONE_EVENT_DS_CHECK, refresh->plan_ds_check ? now : ignore, ZONE_EVENT_NSEC3RESALT, refresh->next_nsec3resalt ? refresh->next_nsec3resalt : ignore, ZONE_EVENT_NOTIFY, zone_changed ? now : ignore ); diff --git a/src/knot/events/handlers/parent_ds_query.c b/src/knot/events/handlers/ds_check.c similarity index 88% rename from src/knot/events/handlers/parent_ds_query.c rename to src/knot/events/handlers/ds_check.c index 2056a654a6..96006f4d0f 100644 --- a/src/knot/events/handlers/parent_ds_query.c +++ b/src/knot/events/handlers/ds_check.c @@ -17,7 +17,7 @@ #include "knot/dnssec/ds_query.h" #include "knot/zone/zone.h" -int event_parent_ds_q(conf_t *conf, zone_t *zone) +int event_ds_check(conf_t *conf, zone_t *zone) { kdnssec_ctx_t ctx = { 0 }; @@ -35,12 +35,12 @@ int event_parent_ds_q(conf_t *conf, zone_t *zone) ret = knot_parent_ds_query(&ctx, &keyset, conf->cache.srv_tcp_reply_timeout * 1000); - zone->timers.next_parent_ds_q = 0; + zone->timers.next_ds_check = 0; if (ret != KNOT_EOK) { if (ctx.policy->ksk_sbm_check_interval > 0) { time_t next_check = time(NULL) + ctx.policy->ksk_sbm_check_interval; - zone->timers.next_parent_ds_q = next_check; - zone_events_schedule_at(zone, ZONE_EVENT_PARENT_DS_Q, next_check); + zone->timers.next_ds_check = next_check; + zone_events_schedule_at(zone, ZONE_EVENT_DS_CHECK, next_check); } } else { zone_events_schedule_now(zone, ZONE_EVENT_DNSSEC); diff --git a/src/knot/events/handlers/ds_push.c b/src/knot/events/handlers/ds_push.c new file mode 100644 index 0000000000..603dd8abb3 --- /dev/null +++ b/src/knot/events/handlers/ds_push.c @@ -0,0 +1,244 @@ +/* Copyright (C) 2019 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; + knot_dname_t *parent_soa; + knot_rrset_t del_old_ds; + knot_rrset_t new_ds; + const struct sockaddr *remote; + struct query_edns_data edns; +}; + +#define DS_PUSH_RETRY 600 + +#define DS_PUSH_LOG(priority, zone, remote, fmt, ...) \ + ns_log(priority, zone, LOG_OPERATION_DS_PUSH, LOG_DIRECTION_OUT, remote, \ + 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) +{ + const knot_dname_t *query_name = knot_wire_next_label(data->zone, NULL); + + int ret = knot_pkt_put_question(pkt, query_name, 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. + free(data->parent_soa); + return KNOT_STATE_DONE; + } + + const knot_rrset_t *parent_soa = sect_soa(pkt, KNOT_ANSWER); + if (parent_soa == NULL) { + parent_soa = sect_soa(pkt, KNOT_AUTHORITY); + } + if (parent_soa == NULL) { + DS_PUSH_LOG(LOG_WARNING, data->zone, data->remote, + "malformed message"); + return KNOT_STATE_FAIL; + } + + data->parent_soa = knot_dname_copy(parent_soa->owner, NULL); + + return KNOT_STATE_RESET; +} + +static int ds_push_reset(knot_layer_t *layer) +{ + (void)layer; + return KNOT_STATE_PRODUCE; +} + +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, +}; + +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; + + struct ds_push_data data = { + .zone = zone->name, + .new_ds = zone_cds, + .remote = (struct sockaddr *)&parent->addr, + }; + + knot_rrset_init(&data.del_old_ds, zone->name, KNOT_RRTYPE_DS, KNOT_CLASS_ANY, 0); + query_edns_data_init(&data.edns, conf, zone->name, parent->addr.ss_family); + + 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_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr *dst = (struct sockaddr *)&parent->addr; + const struct sockaddr *src = (struct sockaddr *)&parent->via; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &parent->key, 0); + if (req == NULL) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int 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, "success"); + } else if (knot_pkt_ext_rcode(req->resp) == 0) { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + "failed (%s)", knot_strerror(ret)); + } else { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(req->resp)); + } + + 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_reply_timeout * 1000; + + conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->name); + conf_val_t ds_push = conf_id_get(conf, C_POLICY, C_DS_PUSH, &policy_id); + while (ds_push.code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &ds_push); + 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, &ds_push, i); + ret = send_ds_push(conf, zone, &parent, timeout); + if (ret == KNOT_EOK) { + 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_val_next(&ds_push); + } + + return KNOT_EOK; +} diff --git a/src/knot/events/replan.c b/src/knot/events/replan.c index 9df2e2d107..206bd3aad7 100644 --- a/src/knot/events/replan.c +++ b/src/knot/events/replan.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2019 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 @@ -130,10 +130,14 @@ void replan_from_timers(conf_t *conf, zone_t *zone) } } - time_t ds = zone->timers.next_parent_ds_q; + time_t ds = zone->timers.next_ds_check; if (ds == 0) { ds = TIME_IGNORE; } + time_t ds_push = zone->timers.next_ds_push; + if (ds_push == 0) { + ds_push = TIME_IGNORE; + } zone_events_schedule_at(zone, ZONE_EVENT_REFRESH, refresh, @@ -141,7 +145,8 @@ void replan_from_timers(conf_t *conf, zone_t *zone) ZONE_EVENT_EXPIRE, expire, ZONE_EVENT_FLUSH, flush, ZONE_EVENT_NSEC3RESALT, resalt, - ZONE_EVENT_PARENT_DS_Q, ds); + ZONE_EVENT_DS_CHECK, ds, + ZONE_EVENT_DS_PUSH, ds_push); } void replan_load_new(zone_t *zone) diff --git a/src/knot/modules/onlinesign/onlinesign.c b/src/knot/modules/onlinesign/onlinesign.c index 72538a66ea..6c55ece808 100644 --- a/src/knot/modules/onlinesign/onlinesign.c +++ b/src/knot/modules/onlinesign/onlinesign.c @@ -518,7 +518,7 @@ static knotd_in_state_t pre_routine(knotd_in_state_t state, knot_pkt_t *pkt, ret = knot_dnssec_key_rollover(mod->dnssec, KEY_ROLL_ALLOW_KSK_ROLL | KEY_ROLL_ALLOW_ZSK_ROLL, &resch); } if (ret == KNOT_EOK) { - if (resch.plan_ds_query && mod->dnssec->policy->ksk_sbm_check_interval > 0) { + if (resch.plan_ds_check && mod->dnssec->policy->ksk_sbm_check_interval > 0) { ctx->event_parent_ds_q = mod->dnssec->now + mod->dnssec->policy->ksk_sbm_check_interval; } else { ctx->event_parent_ds_q = 0; @@ -646,7 +646,7 @@ static int online_sign_ctx_new(online_sign_ctx_t **ctx_ptr, knotd_mod_t *mod) return ret; } - if (resch.plan_ds_query) { + if (resch.plan_ds_check) { ctx->event_parent_ds_q = time(NULL); } ctx->event_rollover = resch.next_rollover; diff --git a/src/knot/zone/timers.c b/src/knot/zone/timers.c index fff69fdd1b..886039a9e9 100644 --- a/src/knot/zone/timers.c +++ b/src/knot/zone/timers.c @@ -54,7 +54,8 @@ enum timer_id { TIMER_LAST_REFRESH, TIMER_NEXT_REFRESH, TIMER_LAST_RESALT, - TIMER_NEXT_PARENT_DS_Q + TIMER_NEXT_DS_CHECK, + TIMER_NEXT_DS_PUSH, }; #define TIMER_SIZE (sizeof(uint8_t) + sizeof(uint64_t)) @@ -78,12 +79,13 @@ static int deserialize_timers(zone_timers_t *timers_ptr, uint8_t id = wire_ctx_read_u8(&wire); uint64_t value = wire_ctx_read_u64(&wire); switch (id) { - case TIMER_SOA_EXPIRE: timers.soa_expire = value; break; - case TIMER_LAST_FLUSH: timers.last_flush = value; break; - case TIMER_LAST_REFRESH: timers.last_refresh = value; break; - case TIMER_NEXT_REFRESH: timers.next_refresh = value; break; - case TIMER_LAST_RESALT: timers.last_resalt = value; break; - case TIMER_NEXT_PARENT_DS_Q: timers.next_parent_ds_q = value; break; + case TIMER_SOA_EXPIRE: timers.soa_expire = value; break; + case TIMER_LAST_FLUSH: timers.last_flush = value; break; + case TIMER_LAST_REFRESH: timers.last_refresh = value; break; + case TIMER_NEXT_REFRESH: timers.next_refresh = value; break; + case TIMER_LAST_RESALT: timers.last_resalt = value; break; + case TIMER_NEXT_DS_CHECK: timers.next_ds_check = value; break; + case TIMER_NEXT_DS_PUSH: timers.next_ds_push = value; break; default: break; // ignore } } @@ -102,13 +104,14 @@ static void txn_write_timers(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const zone_timers_t *timers) { MDB_val k = { knot_dname_size(zone), (void *)zone }; - MDB_val v = knot_lmdb_make_key("BLBLBLBLBLBL", - TIMER_SOA_EXPIRE, (uint64_t)timers->soa_expire, - TIMER_LAST_FLUSH, (uint64_t)timers->last_flush, - TIMER_LAST_REFRESH, (uint64_t)timers->last_refresh, - TIMER_NEXT_REFRESH, (uint64_t)timers->next_refresh, - TIMER_LAST_RESALT, (uint64_t)timers->last_resalt, - TIMER_NEXT_PARENT_DS_Q, (uint64_t)timers->next_parent_ds_q); + MDB_val v = knot_lmdb_make_key("BLBLBLBLBLBLBL", + TIMER_SOA_EXPIRE, (uint64_t)timers->soa_expire, + TIMER_LAST_FLUSH, (uint64_t)timers->last_flush, + TIMER_LAST_REFRESH, (uint64_t)timers->last_refresh, + TIMER_NEXT_REFRESH, (uint64_t)timers->next_refresh, + TIMER_LAST_RESALT, (uint64_t)timers->last_resalt, + TIMER_NEXT_DS_CHECK, (uint64_t)timers->next_ds_check, + TIMER_NEXT_DS_PUSH, (uint64_t)timers->next_ds_push); knot_lmdb_insert(txn, &k, &v); free(v.mv_data); } diff --git a/src/knot/zone/timers.h b/src/knot/zone/timers.h index 5fd5b003ce..62ab9f4f4b 100644 --- a/src/knot/zone/timers.h +++ b/src/knot/zone/timers.h @@ -30,8 +30,9 @@ struct zone_timers { time_t last_flush; //!< Last zone file synchronization. time_t last_refresh; //!< Last successful zone refresh attempt. time_t next_refresh; //!< Next zone refresh attempt. - time_t last_resalt; //!< Last NSEC3 resalt - time_t next_parent_ds_q; //!< Next parent ds query + time_t last_resalt; //!< Last NSEC3 resalt. + time_t next_ds_check; //!< Next parent DS check. + time_t next_ds_push; //!< Next DDNS to parent zone with updated DS record. }; typedef struct zone_timers zone_timers_t; diff --git a/tests-extra/tests/dnssec/ds_push/data/com.zone b/tests-extra/tests/dnssec/ds_push/data/com.zone new file mode 100644 index 0000000000..34859ad30c --- /dev/null +++ b/tests-extra/tests/dnssec/ds_push/data/com.zone @@ -0,0 +1,6 @@ +$ORIGIN com. +$TTL 1200 + +@ SOA ns admin 20110100 7 7 16 600 +ns AAAA ::0 + diff --git a/tests-extra/tests/dnssec/ds_push/data/example.com.zone b/tests-extra/tests/dnssec/ds_push/data/example.com.zone new file mode 100644 index 0000000000..ecd27e06e9 --- /dev/null +++ b/tests-extra/tests/dnssec/ds_push/data/example.com.zone @@ -0,0 +1,9 @@ +example.com. 3 SOA dns1.example.com. hostmaster.example.com. 2010111227 21600 3600 604800 3 +example.com. 0 NS dns1.example.com. +example.com. 2 MX 10 mail.example.com. +dns1.example.com. 4 A 192.0.2.1 +dns1.example.com. 3 AAAA 2001:db8::1 +foo.example.com. 5 A 192.0.2.4 +mail.example.com. 3 A 192.0.2.3 +mail.example.com. 1 AAAA 2001:db8::3 + diff --git a/tests-extra/tests/dnssec/ds_push/test.py b/tests-extra/tests/dnssec/ds_push/test.py new file mode 100644 index 0000000000..62f81c9357 --- /dev/null +++ b/tests-extra/tests/dnssec/ds_push/test.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +""" +Check of automatic KSK rollover with DS push. +""" + +import collections +import os +import shutil +import datetime +import subprocess +from subprocess import check_call + +from dnstest.utils import * +from dnstest.keys import Keymgr +from dnstest.test import Test + +def pregenerate_key(server, zone, alg): + class a_class_with_name: + def __init__(self, name): + self.name = name + + server.gen_key(a_class_with_name("notexisting.zone."), ksk=True, alg=alg, + addtopolicy=zone[0].name) + +# check zone if keys are present and used for signing +def check_zone(server, zone, dnskeys, dnskey_rrsigs, cdnskeys, soa_rrsigs, msg): + qdnskeys = server.dig("example.com", "DNSKEY", bufsize=4096) + found_dnskeys = qdnskeys.count("DNSKEY") + + qdnskeyrrsig = server.dig("example.com", "DNSKEY", dnssec=True, bufsize=4096) + found_rrsigs = qdnskeyrrsig.count("RRSIG") + + qcdnskey = server.dig("example.com", "CDNSKEY", bufsize=4096) + found_cdnskeys = qcdnskey.count("CDNSKEY") + + qsoa = server.dig("example.com", "SOA", dnssec=True, bufsize=4096) + found_soa_rrsigs = qsoa.count("RRSIG") + + check_log("DNSKEYs: %d (expected %d)" % (found_dnskeys, dnskeys)); + check_log("RRSIGs: %d (expected %d)" % (found_soa_rrsigs, soa_rrsigs)); + check_log("DNSKEY-RRSIGs: %d (expected %d)" % (found_rrsigs, dnskey_rrsigs)); + check_log("CDNSKEYs: %d (expected %d)" % (found_cdnskeys, cdnskeys)); + + if found_dnskeys != dnskeys: + set_err("BAD DNSKEY COUNT: " + msg) + detail_log("!DNSKEYs not published and activated as expected: " + msg) + + if found_soa_rrsigs != soa_rrsigs: + set_err("BAD RRSIG COUNT: " + msg) + detail_log("!RRSIGs not published and activated as expected: " + msg) + + if found_rrsigs != dnskey_rrsigs: + set_err("BAD DNSKEY RRSIG COUNT: " + msg) + detail_log("!RRSIGs not published and activated as expected: " + msg) + + if found_cdnskeys != cdnskeys: + set_err("BAD CDNSKEY COUNT: " + msg) + detail_log("!CDNSKEYs not published and activated as expected: " + msg) + + detail_log(SEP) + + # Valgrind delay breaks the timing! + if not server.valgrind: + server.zone_backup(zone, flush=True) + server.zone_verify(zone) + +def wait_for_rrsig_count(t, server, rrtype, rrsig_count, timeout): + rtime = 0 + while True: + qdnskeyrrsig = server.dig("example.com", rrtype, dnssec=True, bufsize=4096) + found_rrsigs = qdnskeyrrsig.count("RRSIG") + if found_rrsigs == rrsig_count: + break + rtime = rtime + 1 + t.sleep(1) + if rtime > timeout: + break + +def wait_for_dnskey_count(t, server, dnskey_count, timeout): + rtime = 0 + while True: + qdnskeyrrsig = server.dig("example.com", "DNSKEY", dnssec=True, bufsize=4096) + found_dnskeys = qdnskeyrrsig.count("DNSKEY") + if found_dnskeys == dnskey_count: + break + rtime = rtime + 1 + t.sleep(1) + if rtime > timeout: + break + +def watch_ksk_rollover(t, server, zone, before_keys, after_keys, total_keys, desc, set_ksk_lifetime): + check_zone(server, zone, before_keys, 1, 1, 1, desc + ": initial keys") + orig_ksk_lifetime = server.dnssec(zone).ksk_lifetime + + server.dnssec(zone).ksk_lifetime = set_ksk_lifetime if set_ksk_lifetime > 0 else orig_ksk_lifetime + server.gen_confile() + server.reload() + + wait_for_dnskey_count(t, server, total_keys, 20) + + t.sleep(3) + check_zone(server, zone, total_keys, 1, 1, 1, desc + ": published new") + + server.dnssec(zone).ksk_lifetime = orig_ksk_lifetime + server.gen_confile() + server.reload() + + wait_for_rrsig_count(t, server, "DNSKEY", 2, 20) + check_zone(server, zone, total_keys, 2, 1, 1 if before_keys > 1 else 2, desc + ": both keys active") + + wait_for_rrsig_count(t, server, "DNSKEY", 1, 20) + check_zone(server, zone, total_keys, 1, 1, 1, desc + ": old key retired") + + wait_for_dnskey_count(t, server, after_keys, 20) + check_zone(server, zone, after_keys, 1, 1, 1, desc + ": old key removed") + +t = Test(tsig=False) + +parent = t.server("knot") +parent_zone = t.zone("com.", storage=".") +t.link(parent_zone, parent, ddns=True) + +parent.dnssec(parent_zone).enable = True + +child = t.server("knot") +child_zone = t.zone("example.com.", storage=".") +t.link(child_zone, child) + +child.zonefile_sync = 24 * 60 * 60 + +child.dnssec(child_zone).enable = True +child.dnssec(child_zone).manual = False +child.dnssec(child_zone).alg = "ECDSAP256SHA256" +child.dnssec(child_zone).dnskey_ttl = 2 +child.dnssec(child_zone).zsk_lifetime = 99999 +child.dnssec(child_zone).ksk_lifetime = 300 # this can be possibly left also infinity +child.dnssec(child_zone).propagation_delay = 11 +child.dnssec(child_zone).ksk_sbm_check = [ parent ] +child.dnssec(child_zone).ksk_sbm_check_interval = 2 +child.dnssec(child_zone).ds_push = parent +child.dnssec(child_zone).ksk_shared = True +child.dnssec(child_zone).cds_publish = "always" + +# parameters +ZONE = "example.com." + +#t.start() +t.generate_conf() +parent.start() +t.sleep(2) +child.start() +child.zone_wait(child_zone) + +t.sleep(5) + +pregenerate_key(child, child_zone, "ECDSAP256SHA256") +watch_ksk_rollover(t, child, child_zone, 2, 2, 3, "KSK rollover", 27) + +t.end() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 079db0b22c..b54489ed20 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -58,6 +58,7 @@ class ZoneDnssec(object): self.nsec3_salt_len = None self.ksk_sbm_check = [] self.ksk_sbm_check_interval = None + self.ds_push = None self.ksk_shared = None self.cds_publish = None self.offline_ksk = None @@ -1116,7 +1117,7 @@ class Knot(Server): if slave.tsig: s.item_str("key", slave.tsig.name) servers.add(slave.name) - for parent in z.dnssec.ksk_sbm_check: + for parent in z.dnssec.ksk_sbm_check + [ z.dnssec.ds_push ] if z.dnssec.ds_push else z.dnssec.ksk_sbm_check: if parent.name not in servers: if not have_remote: s.begin("remote") @@ -1224,6 +1225,8 @@ class Knot(Server): self._str(s, "nsec3-salt-length", z.dnssec.nsec3_salt_len) if len(z.dnssec.ksk_sbm_check) > 0: s.item("ksk-submission", z.name) + if z.dnssec.ds_push: + self._str(s, "ds-push", z.dnssec.ds_push.name) self._bool(s, "ksk-shared", z.dnssec.ksk_shared) self._str(s, "cds-cdnskey-publish", z.dnssec.cds_publish) self._str(s, "offline-ksk", z.dnssec.offline_ksk) diff --git a/tests/knot/test_zone_timers.c b/tests/knot/test_zone_timers.c index 849081be3a..f364155b89 100644 --- a/tests/knot/test_zone_timers.c +++ b/tests/knot/test_zone_timers.c @@ -31,9 +31,21 @@ static const zone_timers_t MOCK_TIMERS = { .next_refresh = 1474559960, .last_flush = 1, .last_resalt = 2, - .next_parent_ds_q = 0, + .next_ds_check = 1474559961, + .next_ds_push = 1474559962, }; +static bool timers_eq(const zone_timers_t *a, const zone_timers_t *b) +{ + return a->soa_expire == b->soa_expire && + a->last_refresh == b->last_refresh && + a->next_refresh == b->next_refresh && + a->last_flush == b->last_flush && + a->last_resalt == b->last_resalt && + a->next_ds_check == b->next_ds_check && + a->next_ds_push == b->next_ds_push; +} + static bool keep_all(const knot_dname_t *zone, void *data) { return true; @@ -44,14 +56,6 @@ static bool remove_all(const knot_dname_t *zone, void *data) return false; } -static bool timers_eq(const zone_timers_t *a, const zone_timers_t *b) -{ - return a->soa_expire == b->soa_expire && - a->last_refresh == b->last_refresh && - a->next_refresh == b->next_refresh && - a->last_flush == b->last_flush; -} - int main(int argc, char *argv[]) { plan_lazy(); @@ -83,9 +87,7 @@ int main(int argc, char *argv[]) memset(&timers, 0, sizeof(timers)); ret = zone_timers_read(db, zone, &timers); ok(ret == KNOT_EOK, "zone_timers_read()"); - ok(timers_eq(&timers, &MOCK_TIMERS), "timers unmalformed (%u == %u, %ld == %ld etc.)", - timers.soa_expire, MOCK_TIMERS.soa_expire, (long)timers.last_refresh, - (long)MOCK_TIMERS.last_refresh); + ok(timers_eq(&timers, &MOCK_TIMERS), "inconsistent timers"); // Sweep none ret = zone_timers_sweep(db, keep_all, NULL); -- GitLab