From 67797dee6f2f81349be25609324ff7e71ee7bab2 Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Tue, 18 Aug 2020 15:45:55 +0200 Subject: [PATCH] catalog: implemented generating from conf --- doc/man/knot.conf.5in | 20 ++ doc/reference.rst | 18 ++ scripts/timerdb-info.py | 16 +- src/knot/conf/schema.c | 3 + src/knot/conf/schema.h | 3 + src/knot/conf/tools.c | 7 + src/knot/events/handlers/load.c | 40 +++- src/knot/zone/catalog.c | 206 +++++++++++++++++- src/knot/zone/catalog.h | 42 ++++ src/knot/zone/timers.c | 9 +- src/knot/zone/timers.h | 3 +- src/knot/zone/zone.c | 3 + src/knot/zone/zone.h | 4 + src/knot/zone/zonedb-load.c | 91 ++++++++ src/libdnssec/binary.c | 44 ++++ src/libdnssec/binary.h | 23 ++ src/libdnssec/error.c | 2 + src/libdnssec/error.h | 2 + .../tests/zone/catalog_generate/test.py | 94 ++++++++ tests-extra/tools/dnstest/server.py | 17 +- 20 files changed, 623 insertions(+), 24 deletions(-) create mode 100644 tests-extra/tests/zone/catalog_generate/test.py diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in index 67414b85bc..9e4182b23a 100644 --- a/doc/man/knot.conf.5in +++ b/doc/man/knot.conf.5in @@ -1439,6 +1439,7 @@ zone: refresh\-max\-interval: TIME catalog\-role: none | interpret catalog\-template: template_id + catalog\-zone: DNAME module: STR/STR ... .ft P .fi @@ -1747,6 +1748,11 @@ Trigger zone catalog feature. Possible values: .IP \(bu 2 \fBinterpret\fP – A catalog zone which is loaded from a zone file or XFR, and member zones shall be configured based on its contents. +.IP \(bu 2 +\fBgenerate\fP – A catalog zone whose contents are generated according to +assigned member zones. +.IP \(bu 2 +\fBmember\fP – A member zone that is assigned to one generated catalog zone. .UNINDENT .sp \fIDefault:\fP none @@ -1762,6 +1768,20 @@ This option must be set if and only if \fI\%catalog\-role\fP is \fIinterpret\fP\ .UNINDENT .sp \fIDefault:\fP not set +.SS catalog\-zone +.sp +Assign this member zone to specified generated catalog zone. +.sp +\fBNOTE:\fP +.INDENT 0.0 +.INDENT 3.5 +This option must be set if and only if \fI\%catalog\-role\fP is \fImember\fP\&. +.sp +The referenced catalog zone must exist and have \fI\%catalog\-role\fP set to \fIgenerate\fP\&. +.UNINDENT +.UNINDENT +.sp +\fIDefault:\fP not set .SS module .sp An ordered list of references to query modules in the form of \fImodule_name\fP or diff --git a/doc/reference.rst b/doc/reference.rst index 9927feed19..b4e9f6ba94 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -1579,6 +1579,7 @@ Definition of zones served by the server. refresh-max-interval: TIME catalog-role: none | interpret catalog-template: template_id + catalog-zone: DNAME module: STR/STR ... .. _zone_domain: @@ -1914,6 +1915,9 @@ Trigger zone catalog feature. Possible values: - ``none`` – Not a catalog zone. - ``interpret`` – A catalog zone which is loaded from a zone file or XFR, and member zones shall be configured based on its contents. +- ``generate`` – A catalog zone whose contents are generated according to + assigned member zones. +- ``member`` – A member zone that is assigned to one generated catalog zone. *Default:* none @@ -1929,6 +1933,20 @@ For the catalog-member zones, the specified configuration template will be appli *Default:* not set +.. _zone_catalog-zone: + +catalog-zone +------------ + +Assign this member zone to specified generated catalog zone. + +.. NOTE:: + This option must be set if and only if :ref:`zone_catalog-role` is *member*. + + The referenced catalog zone must exist and have :ref:`zone_catalog-role` set to *generate*. + +*Default:* not set + .. _zone_module: module diff --git a/scripts/timerdb-info.py b/scripts/timerdb-info.py index 6a7cd36109..c96fe417e5 100755 --- a/scripts/timerdb-info.py +++ b/scripts/timerdb-info.py @@ -32,15 +32,17 @@ class TimerDBInfo: 0x02: ("legacy_expire", cls.format_timestamp), 0x03: ("legacy_flush", cls.format_timestamp), # knot >= 2.4 - 0x80: ("soa_expire", cls.format_seconds), - 0x81: ("last_flush", cls.format_timestamp), - 0x82: ("last_refresh", cls.format_timestamp), - 0x83: ("next_refresh", cls.format_timestamp), + 0x80: ("soa_expire", cls.format_seconds), + 0x81: ("last_flush", cls.format_timestamp), + 0x82: ("last_refresh", cls.format_timestamp), + 0x83: ("next_refresh", cls.format_timestamp), # knot >= 2.6 - 0x84: ("last_resalt", cls.format_timestamp), - 0x85: ("next_ds_check", 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), + 0x86: ("next_ds_push", cls.format_timestamp), + # knot >= 3.1 + 0x87: ("catalog_member", cls.format_timestamp), } if id in timers: return (timers[id][0], timers[id][1](value)) diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 4aa2dae1b6..a3eb18aea4 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -143,6 +143,8 @@ static const knot_lookup_t journal_modes[] = { static const knot_lookup_t catalog_roles[] = { { CATALOG_ROLE_NONE, "none" }, { CATALOG_ROLE_INTERPRET, "interpret" }, + { CATALOG_ROLE_GENERATE, "generate" }, + { CATALOG_ROLE_MEMBER, "member" }, { 0, NULL } }; @@ -370,6 +372,7 @@ static const yp_item_t desc_policy[] = { { C_REFRESH_MAX_INTERVAL,YP_TINT, YP_VINT = { 2, UINT32_MAX, UINT32_MAX, YP_STIME } }, \ { C_CATALOG_ROLE, YP_TOPT, YP_VOPT = { catalog_roles, CATALOG_ROLE_NONE }, FLAGS }, \ { C_CATALOG_TPL, YP_TREF, YP_VREF = { C_TPL }, FLAGS, { check_ref } }, \ + { C_CATALOG_ZONE, YP_TDNAME,YP_VNONE, FLAGS | CONF_IO_FRLD_ZONES }, \ { C_MODULE, YP_TDATA, YP_VDATA = { 0, NULL, mod_id_to_bin, mod_id_to_txt }, \ YP_FMULTI | FLAGS, { check_modref } }, \ { C_COMMENT, YP_TSTR, YP_VNONE }, \ diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index bf0911110c..931ad12016 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -35,6 +35,7 @@ #define C_CATALOG_DB_MAX_SIZE "\x13""catalog-db-max-size" #define C_CATALOG_ROLE "\x0C""catalog-role" #define C_CATALOG_TPL "\x10""catalog-template" +#define C_CATALOG_ZONE "\x0C""catalog-zone" #define C_CDS_CDNSKEY "\x13""cds-cdnskey-publish" #define C_CHK_INTERVAL "\x0E""check-interval" #define C_COMMENT "\x07""comment" @@ -194,6 +195,8 @@ enum { enum { CATALOG_ROLE_NONE = 0, CATALOG_ROLE_INTERPRET = 1, + CATALOG_ROLE_GENERATE = 2, + CATALOG_ROLE_MEMBER = 3, }; extern const knot_lookup_t acl_actions[]; diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index 5a5f25a8a7..51573ecc40 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -646,11 +646,18 @@ int check_zone( C_CATALOG_ROLE, yp_dname(args->id)); conf_val_t catalog_tpl = conf_zone_get_txn(args->extra->conf, args->extra->txn, C_CATALOG_TPL, yp_dname(args->id)); + conf_val_t catalog_zone = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_CATALOG_ZONE, yp_dname(args->id)); if ((bool)(conf_opt(&catalog_role) == CATALOG_ROLE_INTERPRET) != (bool)(catalog_tpl.code == KNOT_EOK)) { args->err_str = "'catalog-role' must correspond to configured 'catalog-template'"; return KNOT_EINVAL; } + if ((bool)(conf_opt(&catalog_role) == CATALOG_ROLE_MEMBER) != + (bool)(catalog_zone.code == KNOT_EOK)) { + args->err_str = "'catalog-role' must correspond to configured 'catalog-zone'"; + return KNOT_EINVAL; + } return KNOT_EOK; } diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c index 0cdd870575..6b2c59614a 100644 --- a/src/knot/events/handlers/load.c +++ b/src/knot/events/handlers/load.c @@ -66,7 +66,9 @@ int event_load(conf_t *conf, zone_t *zone) int ret = KNOT_EOK; // If configured, load journal contents. - if (load_from == JOURNAL_CONTENT_ALL && !old_contents_exist && zf_from != ZONEFILE_LOAD_WHOLE) { + 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); if (ret != KNOT_EOK && ret != KNOT_ENOENT) { goto cleanup; @@ -77,7 +79,7 @@ int event_load(conf_t *conf, zone_t *zone) } // If configured, attempt to load zonefile. - if (zf_from != ZONEFILE_LOAD_NONE) { + 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); @@ -135,9 +137,18 @@ int event_load(conf_t *conf, zone_t *zone) } } } + 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); // 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 && + if ((load_from == JOURNAL_CONTENT_ALL || zone->cat_members != NULL) && !zone_in_journal_exists && zf_conts != NULL) { ret = zone_in_journal_store(conf, zone, zf_conts); if (ret != KNOT_EOK) { @@ -147,13 +158,21 @@ int event_load(conf_t *conf, zone_t *zone) } val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); - bool dnssec_enable = conf_bool(&val), zu_from_zf_conts = false; - bool do_diff = (zf_from == ZONEFILE_LOAD_DIFF || zf_from == ZONEFILE_LOAD_DIFSE); + 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); // Create zone_update structure according to current state. if (old_contents_exist) { - if (zf_conts == NULL) { + 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; @@ -168,7 +187,7 @@ int event_load(conf_t *conf, zone_t *zone) ret = zone_update_from_differences(&up, zone, NULL, zf_conts, UPDATE_INCREMENTAL, ignore_dnssec); } } else { - if (journal_conts != NULL && zf_from != ZONEFILE_LOAD_WHOLE) { + 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); @@ -249,7 +268,8 @@ int event_load(conf_t *conf, zone_t *zone) } // If the change is only automatically incremented SOA serial, make it no change. - if (zf_from == ZONEFILE_LOAD_DIFSE && (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && + if ((zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL) && + (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && changeset_differs_just_serial(&up.change)) { changeset_t *cpy = changeset_clone(&up.change); if (cpy == NULL) { @@ -282,6 +302,10 @@ int event_load(conf_t *conf, zone_t *zone) log_zone_info(zone->name, "loaded, serial %s -> %u%s, %zu bytes", old_serial_str, middle_serial, new_serial_str, zone->contents->size); + if (zone->cat_members != NULL) { + catalog_update_clear(zone->cat_members); + } + // Schedule depedent events. const knot_rdataset_t *soa = zone_soa(zone); zone->timers.soa_expire = knot_soa_expire(soa->rdata); diff --git a/src/knot/zone/catalog.c b/src/knot/zone/catalog.c index c9010a9053..f725f0c72b 100644 --- a/src/knot/zone/catalog.c +++ b/src/knot/zone/catalog.c @@ -22,15 +22,65 @@ #include <string.h> #include <urcu.h> +#include "contrib/string.h" +#include "contrib/wire_ctx.h" +#include "libdnssec/binary.h" + #include "knot/common/log.h" #include "knot/conf/conf.h" -#include "knot/zone/contents.h" +#include "knot/updates/zone-update.h" #define CATALOG_VERSION "1.0" #define CATALOG_ZONE_VERSION "2" // must be just one char long +#define CATALOG_ZONES_LABEL "zones" +#define CATALOG_SOA_REFRESH 3600 +#define CATALOG_SOA_RETRY 600 +#define CATALOG_SOA_EXPIRE (INT32_MAX - 1) const MDB_val catalog_iter_prefix = { 1, "" }; +knot_dname_t *catalog_member_owner(const knot_dname_t *member, + const knot_dname_t *catzone, + time_t member_time) +{ + dnssec_binary_t membbin = { knot_dname_size(member), (void *)member }; + uint64_t u64time = htobe64(member_time); + dnssec_binary_t timebin = { sizeof(uint64_t), (void *)&u64time }; + dnssec_binary_t rawhash; + + int ret = dnssec_binary_hash(DNSSEC_BIN_HASH_MD5, &rawhash, 2, &membbin, &timebin); + if (ret != KNOT_EOK) { + return NULL; + } + + char *hexhash = bin_to_hex(rawhash.data, rawhash.size); + dnssec_binary_free(&rawhash); + + long hexlen = strlen(hexhash); + assert(hexlen == 32); + long zoneslen = strlen(CATALOG_ZONES_LABEL); + assert(hexlen <= KNOT_DNAME_MAXLABELLEN && zoneslen <= KNOT_DNAME_MAXLABELLEN); + size_t catzlen = knot_dname_size(catzone); + + size_t outlen = 1 + hexlen + 1 + zoneslen + catzlen; + knot_dname_t *out; + if (outlen > KNOT_DNAME_MAXLEN || (out = malloc(outlen)) == NULL) { + free(hexhash); + return NULL; + } + + wire_ctx_t wire = wire_ctx_init(out, outlen); + wire_ctx_write_u8(&wire, hexlen); + wire_ctx_write(&wire, hexhash, hexlen); + wire_ctx_write_u8(&wire, zoneslen); + wire_ctx_write(&wire, CATALOG_ZONES_LABEL, zoneslen); + wire_ctx_write(&wire, catzone, catzlen); + assert(wire.error == KNOT_EOK); + + free(hexhash); + return out; +} + static bool check_zone_version(const zone_contents_t *zone) { size_t zone_size = knot_dname_size(zone->apex->owner); @@ -380,13 +430,28 @@ int catalog_update_init(catalog_update_t *u) return KNOT_ENOMEM; } pthread_mutex_init(&u->mutex, 0); + u->error = KNOT_EOK; return KNOT_EOK; } +catalog_update_t *catalog_update_new() +{ + catalog_update_t *u = calloc(1, sizeof(*u)); + if (u != NULL) { + int ret = catalog_update_init(u); + if (ret != KNOT_EOK) { + free(u); + u = NULL; + } + } + return u; +} + static int freecb(trie_val_t *tval, void *unused) { + catalog_upd_val_t *val = *tval; (void)unused; - free(*(void **)tval); + free(val); return 0; } @@ -396,6 +461,7 @@ void catalog_update_clear(catalog_update_t *u) trie_clear(u->add); trie_apply(u->rem, freecb, NULL); trie_clear(u->rem); + u->error = KNOT_EOK; } void catalog_update_deinit(catalog_update_t *u) @@ -405,6 +471,14 @@ void catalog_update_deinit(catalog_update_t *u) trie_free(u->rem); } +void catalog_update_free(catalog_update_t *u) +{ + if (u != NULL) { + catalog_update_deinit(u); + free(u); + } +} + int catalog_update_add(catalog_update_t *u, const knot_dname_t *member, const knot_dname_t *owner, const knot_dname_t *catzone, bool remove) @@ -524,6 +598,134 @@ int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone, return ret; } +static void set_rdata(knot_rrset_t *rrset, uint8_t *data, uint16_t len) +{ + knot_rdata_init(rrset->rrs.rdata, len, data); + rrset->rrs.size = knot_rdata_size(len); +} + +struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone, + uint32_t soa_serial) +{ + if (u->error != KNOT_EOK) { + return NULL; + } + zone_contents_t *c = zone_contents_new(catzone, true); + if (c == NULL) { + return c; + } + + // temporary storage to avoid allocations + uint8_t tmp[512]; + knot_rdata_t *rdata = (knot_rdata_t *)tmp; + knot_dname_t *tmpname = tmp + 256; + wire_ctx_t wire; + zone_node_t *unused = NULL; + knot_rrset_t rrset = { 0 }; + char invalid[9] = "\x07""invalid"; + char version[8] = "\x07""version"; + uint8_t version2[2] = "\x01" CATALOG_ZONE_VERSION; + + // set catalog zone's SOA + rrset.owner = (knot_dname_t *)catzone; + rrset.type = KNOT_RRTYPE_SOA; + rrset.rclass = KNOT_CLASS_IN; + rrset.ttl = 0; + rrset.rrs.count = 1; + rrset.rrs.rdata = rdata; + wire = wire_ctx_init(rdata->data, 250); + wire_ctx_write(&wire, invalid, sizeof(invalid)); + wire_ctx_write(&wire, invalid, sizeof(invalid)); + wire_ctx_write_u32(&wire, soa_serial); + wire_ctx_write_u32(&wire, CATALOG_SOA_REFRESH); + wire_ctx_write_u32(&wire, CATALOG_SOA_RETRY); + wire_ctx_write_u32(&wire, CATALOG_SOA_EXPIRE); + wire_ctx_write_u32(&wire, 0); + rdata->len = wire_ctx_offset(&wire); + rrset.rrs.size = knot_rdata_size(rdata->len); + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // set catalog zone's NS + unused = NULL; + rrset.type = KNOT_RRTYPE_NS; + set_rdata(&rrset, (uint8_t *)invalid, sizeof(invalid)); + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // set catalog zone's version TXT + unused = NULL; + rrset.type = KNOT_RRTYPE_TXT; + set_rdata(&rrset, version2, 2); + size_t catz_len = knot_dname_size(catzone); + if (catz_len + sizeof(version) > KNOT_DNAME_MAXLEN) { + goto fail; + } + memcpy(tmpname, version, sizeof(version)); + memcpy(tmpname + sizeof(version), catzone, catz_len); + rrset.owner = tmpname; + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // insert member zone PTR records + rrset.type = KNOT_RRTYPE_PTR; + catalog_it_t *it = catalog_it_begin(u, false); + while (!catalog_it_finished(it)) { + catalog_upd_val_t *val = catalog_it_val(it); + rrset.owner = val->owner; + set_rdata(&rrset, val->member, knot_dname_size(val->member)); + unused = NULL; + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + catalog_it_next(it); + } + catalog_it_free(it); + + return c; + +fail: + zone_contents_deep_free(c); + return NULL; +} + +int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu) +{ + knot_rrset_t ptr = { 0 }; + ptr.type = KNOT_RRTYPE_PTR; + ptr.rclass = KNOT_CLASS_IN; + ptr.ttl = 0; + uint8_t tmp[KNOT_DNAME_MAXLEN + sizeof(knot_rdata_t)]; + ptr.rrs.count = 1; + ptr.rrs.rdata = (knot_rdata_t *)tmp; + + int ret = u->error; + catalog_it_t *it = catalog_it_begin(u, true); + while (!catalog_it_finished(it) && ret == KNOT_EOK) { + catalog_upd_val_t *val = catalog_it_val(it); + ptr.owner = val->owner; + set_rdata(&ptr, val->member, knot_dname_size(val->member)); + ret = zone_update_remove(zu, &ptr); + catalog_it_next(it); + } + catalog_it_free(it); + + it = catalog_it_begin(u, false); + while (!catalog_it_finished(it) && ret == KNOT_EOK) { + catalog_upd_val_t *val = catalog_it_val(it); + ptr.owner = val->owner; + set_rdata(&ptr, val->member, knot_dname_size(val->member)); + ret = zone_update_add(zu, &ptr); + catalog_it_next(it); + } + catalog_it_free(it); + + return ret; +} + typedef struct { const knot_dname_t *zone; catalog_update_t *u; diff --git a/src/knot/zone/catalog.h b/src/knot/zone/catalog.h index 003be20574..eb0477aaa4 100644 --- a/src/knot/zone/catalog.h +++ b/src/knot/zone/catalog.h @@ -42,6 +42,7 @@ typedef enum { typedef struct { trie_t *rem; // tree of catalog_upd_val_t, that gonna be removed from catalog trie_t *add; // tree of catalog_upd_val_t, that gonna be added to catalog + int error; // error occured during generating of upd pthread_mutex_t mutex; // lock for accessing this struct } catalog_update_t; @@ -54,6 +55,21 @@ typedef struct { extern const MDB_val catalog_iter_prefix; +/*! + * \brief Generate owner name for catalog PTR record. + * + * \param member Name of the member zone respective to the PTR record. + * \param catzone Catalog zone name to contain the PTR. + * \param member_time Timestamp of member zone addition. + * + * \return Owner name or NULL on error (e.g. ENOMEM, too long result...). + * + * \note Don't forget to free the return value later. + */ +knot_dname_t *catalog_member_owner(const knot_dname_t *member, + const knot_dname_t *catzone, + time_t member_time); + /*! * \brief Initialize catalog structure. * @@ -215,6 +231,7 @@ int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to, * \return KNOT_EOK, KNOT_ENOMEM */ int catalog_update_init(catalog_update_t *u); +catalog_update_t *catalog_update_new(void); /*! * \brief Clear contents of catalog update structure. @@ -229,6 +246,7 @@ void catalog_update_clear(catalog_update_t *u); * \param u Catalog update structure. */ void catalog_update_deinit(catalog_update_t *u); +void catalog_update_free(catalog_update_t *u); /*! * \brief Add a new record to catalog update structure. @@ -272,6 +290,30 @@ struct zone_contents; int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone, bool remove, bool check_ver, catalog_t *check); +/*! + * \brief Generate catalog zone contents from (full) catalog update. + * + * \param u Catalog update to read. + * \param catzone Catalog zone name. + * \param soa_serial SOA serial of the generated zone. + * + * \return Catalog zone contents, or NULL if ENOMEM. + */ +struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone, + uint32_t soa_serial); + +struct zone_update; + +/*! + * \brief Incrementally update catalog zone from catalog update. + * + * \param u Catalog update to read. + * \param zu Zone update to be updated. + * + * \return KNOT_E* + */ +int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu); + /*! * \brief Add to catalog update removals of all member zones of a single catalog zone. * diff --git a/src/knot/zone/timers.c b/src/knot/zone/timers.c index 42d1cc105f..ed82061313 100644 --- a/src/knot/zone/timers.c +++ b/src/knot/zone/timers.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* 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 @@ -56,6 +56,7 @@ enum timer_id { TIMER_LAST_RESALT, TIMER_NEXT_DS_CHECK, TIMER_NEXT_DS_PUSH, + TIMER_CATALOG_MEMBER, }; #define TIMER_SIZE (sizeof(uint8_t) + sizeof(uint64_t)) @@ -86,6 +87,7 @@ static int deserialize_timers(zone_timers_t *timers_ptr, 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; + case TIMER_CATALOG_MEMBER:timers.catalog_member = value; break; default: break; // ignore } } @@ -104,14 +106,15 @@ 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("BLBLBLBLBLBLBL", + MDB_val v = knot_lmdb_make_key("BLBLBLBLBLBLBLBL", 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); + TIMER_NEXT_DS_PUSH, (uint64_t)timers->next_ds_push, + TIMER_CATALOG_MEMBER,(uint64_t)timers->catalog_member); 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 62ab9f4f4b..f6c5286f83 100644 --- a/src/knot/zone/timers.h +++ b/src/knot/zone/timers.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* 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 @@ -33,6 +33,7 @@ struct zone_timers { 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. + time_t catalog_member; //!< This catalog member zone created. }; typedef struct zone_timers zone_timers_t; diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c index 5e608f28af..b4cfeb0965 100644 --- a/src/knot/zone/zone.c +++ b/src/knot/zone/zone.c @@ -219,6 +219,9 @@ void zone_free(zone_t **zone_ptr) /* Control update. */ zone_control_clear(zone); + free(zone->catalog_gen); + catalog_update_free(zone->cat_members); + /* Free preferred master. */ pthread_mutex_destroy(&zone->preferred_lock); free(zone->preferred_master); diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h index db2e889047..7f55c20e75 100644 --- a/src/knot/zone/zone.h +++ b/src/knot/zone/zone.h @@ -94,6 +94,10 @@ typedef struct zone catalog_t *catalog; catalog_update_t *catalog_upd; + /*! \brief Catalog-generate feature. */ + knot_dname_t *catalog_gen; + catalog_update_t *cat_members; + /*! \brief Preferred master lock. Also used for flags access. */ pthread_mutex_t preferred_lock; /*! \brief Preferred master for remote operation. */ diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c index c5160678d6..d648e519fe 100644 --- a/src/knot/zone/zonedb-load.c +++ b/src/knot/zone/zonedb-load.c @@ -88,6 +88,65 @@ static void time_set_default(time_t *time, time_t value) } } +static void catalogs_generate(knot_zonedb_t *db_new, knot_zonedb_t *db_old) +{ + // general comment: catz->contents!=NULL means incremental update of catalog + + if (db_old != NULL) { + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = knot_zonedb_iter_val(it); + knot_dname_t *cg = zone->catalog_gen; + if (cg != NULL && knot_zonedb_find(db_new, zone->name) == NULL) { + zone_t *catz = knot_zonedb_find(db_new, cg); + if (catz != NULL && catz->contents != NULL) { + assert(catz->cat_members != NULL); // if this failed to allocate, catz wasn't added to zonedb + knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member); + if (owner == NULL) { + catz->cat_members->error = KNOT_ENOENT; + knot_zonedb_iter_next(it); + continue; + } + int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, true); + free(owner); + if (ret != KNOT_EOK) { + catz->cat_members->error = ret; + } + } + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + } + + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_new); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = knot_zonedb_iter_val(it); + knot_dname_t *cg = zone->catalog_gen; + zone_t *catz = cg != NULL ? knot_zonedb_find(db_new, cg) : NULL; + if (cg != NULL && catz == NULL) { + log_zone_warning(zone->name, "member zone belongs to non-existing catalog zone"); + continue; + } + if (cg != NULL && (catz->contents == NULL || knot_zonedb_find(db_old, zone->name) == NULL)) { + assert(catz->cat_members != NULL); + knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member); + if (owner == NULL) { + catz->cat_members->error = KNOT_ENOENT; + knot_zonedb_iter_next(it); + continue; + } + int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, false); + free(owner); + if (ret != KNOT_EOK) { + catz->cat_members->error = ret; + } + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); +} + /*! * \brief Set default timers for new zones or invalidate if not valid. */ @@ -146,6 +205,12 @@ static zone_t *create_zone_reload(conf_t *conf, const knot_dname_t *name, zone_control_clear(old_zone); } + zone->cat_members = old_zone->cat_members; + old_zone->cat_members = NULL; + + zone->catalog_gen = old_zone->catalog_gen; + old_zone->catalog_gen = NULL; + return zone; } @@ -167,6 +232,30 @@ static zone_t *create_zone_new(conf_t *conf, const knot_dname_t *name, timers_sanitize(conf, zone); + conf_val_t role = conf_zone_get(conf, C_CATALOG_ROLE, name); + if (conf_opt(&role) == CATALOG_ROLE_MEMBER) { + conf_val_t catz = conf_zone_get(conf, C_CATALOG_ZONE, name); + assert(catz.code == KNOT_EOK); // conf consistency checked in conf/tools.c + zone->catalog_gen = knot_dname_copy(conf_dname(&catz), NULL); + if (zone->timers.catalog_member == 0) { + zone->timers.catalog_member = time(NULL); + } + if (zone->catalog_gen == NULL) { + log_zone_error(zone->name, "failed to initialize catalog member zone (%s)", + knot_strerror(KNOT_ENOMEM)); + zone_free(&zone); + return NULL; + } + } else if (conf_opt(&role) == CATALOG_ROLE_GENERATE) { + zone->cat_members = catalog_update_new(); + if (zone->cat_members == NULL) { + log_zone_error(zone->name, "failed to initialize catalog zone (%s)", + knot_strerror(KNOT_ENOMEM)); + zone_free(&zone); + return NULL; + } + } + if (zone_expired(zone)) { // expired => force bootstrap, no load attempt log_zone_info(zone->name, "zone will be bootstrapped"); @@ -552,6 +641,8 @@ void zonedb_reload(conf_t *conf, server_t *server) return; } + catalogs_generate(db_new, server->zone_db); + /* Switch the databases. */ knot_zonedb_t **db_current = &server->zone_db; knot_zonedb_t *db_old = rcu_xchg_pointer(db_current, db_new); diff --git a/src/libdnssec/binary.c b/src/libdnssec/binary.c index 59153f39e5..c556f9f670 100644 --- a/src/libdnssec/binary.c +++ b/src/libdnssec/binary.c @@ -15,6 +15,7 @@ */ #include <assert.h> +#include <stdarg.h> #include <string.h> #include "contrib/base64.h" @@ -161,3 +162,46 @@ int dnssec_binary_to_base64(const dnssec_binary_t *binary, return DNSSEC_EOK; } + +_public_ +int dnssec_binary_hash(dnssec_bin_hash_t alg, dnssec_binary_t *out, size_t nbin, ...) +{ + if (alg == DNSSEC_BIN_HASH_INVALID || !out || nbin == 0) { + return DNSSEC_EINVAL; + } + + gnutls_digest_algorithm_t gnutls_alg = GNUTLS_DIG_UNKNOWN; + switch (alg) { + case DNSSEC_BIN_HASH_INVALID: break; + case DNSSEC_BIN_HASH_MD5: gnutls_alg = GNUTLS_DIG_MD5; break; + case DNSSEC_BIN_HASH_SHA1: gnutls_alg = GNUTLS_DIG_SHA1; break; + case DNSSEC_BIN_HASH_SHA256: gnutls_alg = GNUTLS_DIG_SHA256; break; + case DNSSEC_BIN_HASH_SHA384: gnutls_alg = GNUTLS_DIG_SHA384; break; + } + + _cleanup_hash_ gnutls_hash_hd_t digest = NULL; + int r = gnutls_hash_init(&digest, gnutls_alg); + if (r < 0) { + return DNSSEC_HASH_ERROR; + } + + va_list arg; + va_start(arg, nbin); + for (size_t i = 0; i < nbin; i++) { + dnssec_binary_t *bin = va_arg(arg, dnssec_binary_t *); + + r = gnutls_hash(digest, bin->data, bin->size); + if (r != 0) { + return DNSSEC_HASH_ERROR; + } + } + va_end(arg); + + out->size = gnutls_hash_get_len(gnutls_alg); + out->data = malloc(out->size); + if (out->data == NULL) { + return DNSSEC_ENOMEM; + } + gnutls_hash_output(digest, out->data); + return DNSSEC_EOK; +} diff --git a/src/libdnssec/binary.h b/src/libdnssec/binary.h index 8ff4174e0f..bef75e91ea 100644 --- a/src/libdnssec/binary.h +++ b/src/libdnssec/binary.h @@ -113,4 +113,27 @@ int dnssec_binary_from_base64(const dnssec_binary_t *base64, */ int dnssec_binary_to_base64(const dnssec_binary_t *binary, dnssec_binary_t *base64); + +typedef enum dnssec_bin_hash { + DNSSEC_BIN_HASH_INVALID = 0, + DNSSEC_BIN_HASH_MD5 = 1, + DNSSEC_BIN_HASH_SHA1 = 2, + DNSSEC_BIN_HASH_SHA256 = 3, + DNSSEC_BIN_HASH_SHA384 = 4, +} dnssec_bin_hash_t; + +/*! + * \brief Perform MD5/SHA/... checksum of any data. + * + * \param alg Hash algorithm to use. + * \param out Output: resulting hash in the form of binary. + * \param nbin Number of input binaries. + * \param ... Any number of (dnssec_binary_t *) input data. + * + * \note Don't forget to dnssec_binary_free() the output. + * + * \return DNSSEC_E* + */ +int dnssec_binary_hash(dnssec_bin_hash_t alg, dnssec_binary_t *out, size_t nbin, ...); + /*! @} */ diff --git a/src/libdnssec/error.c b/src/libdnssec/error.c index a0f5e05f4e..e52184a03e 100644 --- a/src/libdnssec/error.c +++ b/src/libdnssec/error.c @@ -68,6 +68,8 @@ static const error_message_t ERROR_MESSAGES[] = { { DNSSEC_P11_TOO_MANY_MODULES, "too many PKCS #11 modules loaded" }, { DNSSEC_P11_TOKEN_NOT_AVAILABLE, "PKCS #11 token not available" }, + { DNSSEC_HASH_ERROR, "hashing error" }, + { 0 } }; diff --git a/src/libdnssec/error.h b/src/libdnssec/error.h index f998fcba8c..9e1b2a6be1 100644 --- a/src/libdnssec/error.h +++ b/src/libdnssec/error.h @@ -80,6 +80,8 @@ enum dnssec_error { DNSSEC_P11_TOO_MANY_MODULES, DNSSEC_P11_TOKEN_NOT_AVAILABLE, + DNSSEC_HASH_ERROR, + DNSSEC_ERROR_MAX = -1001 }; diff --git a/tests-extra/tests/zone/catalog_generate/test.py b/tests-extra/tests/zone/catalog_generate/test.py new file mode 100644 index 0000000000..dcd84a9ff9 --- /dev/null +++ b/tests-extra/tests/zone/catalog_generate/test.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +'''Test of Catalog zone generation.''' + +from dnstest.test import Test +from dnstest.utils import set_err, detail_log +import random + +t = Test() + +master = t.server("knot") +slave = t.server("knot") + +catz = t.zone("example.") +zone = t.zone("example.com.") + +t.link(catz, master, slave) +t.link(zone, master) + +for z in zone: + master.zones[z.name].catalog_gen_link(master.zones[catz[0].name]) + +slave.zones[catz[0].name].catalog = True +slave.dnssec(catz[0]).enable = True +slave.dnssec(catz[0]).single_type_signing = True + +t.start() + +# testcatse 1: initial catalog zone with 1 member +slave.zones_wait(zone) + +# testcase 2: adding member zones online/offline +add_online = random.choice([True, False]) + +zone_add = t.zone("flags.") + t.zone("records.") +t.link(zone_add, master) +for z in zone_add: + master.zones[z.name].catalog_gen_link(master.zones[catz[0].name]) + +master.gen_confile() + +if add_online: + master.reload() +else: + master.stop() + t.sleep(1) + master.start() + +slave.zones_wait(zone + zone_add) + +# testcase 3: removing member zone online/offline +rem_online = random.choice([True, False]) + +serial_bef_rem = slave.zone_wait(catz, udp=False, tsig=True) +master.zones.pop("example.com.") +master.gen_confile() + +if rem_online: + master.reload() +else: + master.stop() + t.sleep(1) + master.start() + +slave.zone_wait(catz, serial_bef_rem, udp=False, tsig=True) +resp = slave.dig("example.com.", "SOA") +resp.check(rcode="REFUSED") + +#testcase 4: remove/add same member zone while slave offline, with purge +resp0 = slave.dig("records.", "DNSKEY") +resp0.check_count(1, "DNSKEY") +dnskey0 = resp0.resp.answer[0].to_rdataset() +slave.stop() + +temp_rem = master.zones.pop("records.") +master.gen_confile() +master.reload() +t.sleep(7) +master.ctl("-f zone-purge +orphan records.") +master.zones["records."] = temp_rem +master.gen_confile() +master.reload() + +slave.start() +t.sleep(3) +slave.ctl("zone-refresh") +t.sleep(7) +resp1 = slave.dig("records.", "DNSKEY") +resp1.check_count(1, "DNSKEY") +dnskey1 = resp1.resp.answer[0].to_rdataset() +if dnskey0 == dnskey1: + set_err("ZONE NOT PURGED") + +t.end() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 1eb58055f3..4d62ec81ac 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -80,6 +80,7 @@ class Zone(object): self.modules = [] self.dnssec = ZoneDnssec() self.catalog = None + self.catz = None @property def name(self): @@ -96,6 +97,10 @@ class Zone(object): def clear_modules(self): self.modules.clear() + def catalog_gen_link(self, catz): + self.catz = catz + catz.catz = catz + def disable_master(self, new_zone_file): self.zfile.remove() self.zfile = new_zone_file @@ -647,7 +652,7 @@ class Server(object): count += 1 return count - def zone_wait(self, zone, serial=None, equal=False, greater=True): + def zone_wait(self, zone, serial=None, equal=False, greater=True, udp=True, tsig=None): '''Try to get SOA record. With an optional serial number and given relation (equal or/and greater).''' @@ -659,8 +664,8 @@ class Server(object): for t in range(60): try: - resp = self.dig(zone.name, "SOA", udp=True, tries=1, - timeout=2, log_no_sep=True) + resp = self.dig(zone.name, "SOA", udp=udp, tries=1, + timeout=2, log_no_sep=True, tsig=tsig) except: pass else: @@ -1472,6 +1477,12 @@ class Knot(Server): elif z.ixfr: s.item_str("zonefile-load", "difference") + if z.catz == z: + s.item_str("catalog-role", "generate") + elif z.catz is not None: + s.item_str("catalog-role", "member") + s.item_str("catalog-zone", z.catz.name) + if z.dnssec.enable: s.item_str("dnssec-signing", "on") s.item_str("dnssec-policy", z.dnssec.shared_policy_with or z.name) -- GitLab