From: Libor Peltan Date: Thu, 11 Sep 2025 14:54:42 +0000 (+0200) Subject: zone: implemented including records from subzone(s) X-Git-Tag: v3.5.0~12^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6408e6493e6363fd357f1afceb8fa5edd6d32ac3;p=thirdparty%2Fknot-dns.git zone: implemented including records from subzone(s) --- diff --git a/doc/reference.rst b/doc/reference.rst index 608b29bd34..6e934e1d80 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -2696,6 +2696,7 @@ Definition of zones served by the server. serial-policy: increment | unixtime | dateserial serial-modulo: INT/INT | +INT | -INT | INT/INT+INT | INT/INT-INT reverse-generate: DNAME ... + include-from: DNAME ... refresh-min-interval: TIME refresh-max-interval: TIME retry-min-interval: TIME @@ -3295,6 +3296,21 @@ Current limitations: *Default:* none +.. _zone_include-from: + +include-from +------------ + +A list of subzones that should be flattened into this zone. The flattening deletes +all delegation-related records (including NS, SOA, ...) from both zones and copies +all other records from the subzone to this zone. + +This feature works analogously to :ref:`zone_reverse-generate` in the way that subzones' +records are being imported while loading this zone's zone file, and that it implies +:ref:`zone_zonefile-load`: *difference-no-serial* and :ref:`zone_journal-content`: *all*. + +*Default:* none + .. _zone_refresh-min-interval: refresh-min-interval diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 5e80ab4375..bb3cf2e499 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -496,6 +496,7 @@ static const yp_item_t desc_external[] = { { C_DS_PUSH, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI | CONF_REF_EMPTY | FLAGS, \ { check_ref } }, \ { C_REVERSE_GEN, YP_TDNAME,YP_VNONE, YP_FMULTI | FLAGS | CONF_IO_FRLD_ZONES }, \ + { C_INCLUDE_FROM, YP_TDNAME,YP_VNONE, YP_FMULTI | FLAGS | CONF_IO_FDIFF_ZONES, { check_include_from } }, \ { C_SERIAL_POLICY, YP_TOPT, YP_VOPT = { serial_policies, SERIAL_POLICY_INCREMENT } }, \ { C_SERIAL_MODULO, YP_TSTR, YP_VSTR = { "0/1" }, YP_FNONE, { check_modulo_shift } }, \ { C_ZONEMD_GENERATE, YP_TOPT, YP_VOPT = { zone_digest, ZONE_DIGEST_NONE }, FLAGS }, \ diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index 3de02cfe6e..7b48c3a2e9 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -67,6 +67,7 @@ #define C_GLOBAL_MODULE "\x0D""global-module" #define C_ID "\x02""id" #define C_IDENT "\x08""identity" +#define C_INCLUDE_FROM "\x0C""include-from" #define C_INCL "\x07""include" #define C_IXFR_BENEVOLENT "\x0F""ixfr-benevolent" #define C_IXFR_BY_ONE "\x0B""ixfr-by-one" diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index bbffbc9a14..b937cd45b8 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -1025,6 +1025,18 @@ static conf_val_t conf_get_wrap( } } +int check_include_from( + knotd_conf_check_args_t *args) +{ + if (knot_dname_in_bailiwick(args->data, args->id) < 0 || + knot_dname_is_equal(args->data, args->id)) { + args->err_str = "not a subzone"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + #define CHECK_ZONE_INTERVALS(low_item, high_item) { \ conf_val_t high = conf_get_wrap(args, high_item); \ if (high.code == KNOT_EOK) { \ @@ -1177,6 +1189,15 @@ static int check_zone_or_tpl( } } + conf_val_t inc_from = conf_get_wrap(args, C_INCLUDE_FROM); + if (inc_from.code == KNOT_EOK) { + conf_val_t rev_from = conf_get_wrap(args, C_REVERSE_GEN); + if (rev_from.code == KNOT_EOK) { + args->err_str = "include-from not compatible with reverse-from"; + return KNOT_EINVAL; + } + } + return KNOT_EOK; } diff --git a/src/knot/conf/tools.h b/src/knot/conf/tools.h index 10a5cb4fd5..f48ed5bbc5 100644 --- a/src/knot/conf/tools.h +++ b/src/knot/conf/tools.h @@ -151,6 +151,10 @@ int check_template( knotd_conf_check_args_t *args ); +int check_include_from( + knotd_conf_check_args_t *args +); + int check_zone( knotd_conf_check_args_t *args ); diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c index a6bf4e8668..3073a7af22 100644 --- a/src/knot/events/handlers/load.c +++ b/src/knot/events/handlers/load.c @@ -58,6 +58,7 @@ int event_load(conf_t *conf, zone_t *zone) // Note: zone->reverse_from!=NULL almost works, but we need to check if configured even when failed. if (conf_zone_get(conf, C_REVERSE_GEN, zone->name).code == KNOT_EOK || + conf_zone_get(conf, C_INCLUDE_FROM, zone->name).code == KNOT_EOK || zone->cat_members != NULL) { // This should be equivalent to setting catalog-role:generate. zf_from = ZONEFILE_LOAD_DIFSE; load_from = JOURNAL_CONTENT_ALL; diff --git a/src/knot/zone/reverse.c b/src/knot/zone/reverse.c index 98734e7578..a629a9bdef 100644 --- a/src/knot/zone/reverse.c +++ b/src/knot/zone/reverse.c @@ -105,8 +105,63 @@ static int reverse_from_node(zone_node_t *node, void *data) return ret; } +static bool flatten_apex_nocopy(uint16_t type) +{ + return type == KNOT_RRTYPE_SOA || + type == KNOT_RRTYPE_NS || + type == KNOT_RRTYPE_DNSKEY || + type == KNOT_RRTYPE_NSEC3PARAM || + type == KNOT_RRTYPE_CDNSKEY || + type == KNOT_RRTYPE_CDS; +} + +static bool flatten_apex_delete(uint16_t type) +{ + return type == KNOT_RRTYPE_NS || + type == KNOT_RRTYPE_DS; +} + +static int flatten_from_node(zone_node_t *node, void *data) +{ + rev_ctx_t *ctx = data; + + bool apex = node_rrtype_exists(node, KNOT_RRTYPE_SOA); + + int ret = KNOT_EOK; + + zone_node_t *target_node = NULL; + + for (int i = 0; i < node->rrset_count && ret == KNOT_EOK; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + if (apex && flatten_apex_nocopy(rrset.type)) { + continue; + } + + assert(ctx->rev_upd == NULL); // not implemented with update in mind + + ret = zone_contents_add_rr(ctx->rev_conts, &rrset, &target_node); + } + + if (apex && target_node == NULL) { + target_node = (zone_node_t *)zone_contents_find_node(ctx->rev_conts, node->owner); + } + + // TODO delete whole subtree from rev_conts BEFORE adding records from included zone? + for (int i = 0; apex && target_node != NULL && i < target_node->rrset_count && ret == KNOT_EOK; ) { + knot_rrset_t rrset = node_rrset_at(target_node, i); + if (flatten_apex_delete(rrset.type)) { + ret = zone_contents_remove_rr(ctx->rev_conts, &rrset, &target_node); + } else { + i++; // NOTE otherwise we jump to next RRSet by deleting the current one + } + } + + return ret; +} + int zone_reverse(zone_contents_t *from, zone_contents_t *to_conts, - zone_update_t *to_upd, bool to_upd_rem) + zone_update_t *to_upd, bool to_upd_rem, + zone_include_method_t method) { const knot_dname_t *to_name; if (to_upd != NULL) { @@ -123,7 +178,16 @@ int zone_reverse(zone_contents_t *from, zone_contents_t *to_conts, .ipv6 = (knot_dname_in_bailiwick(to_name, reverse6postfix) >= 0) }; - return zone_contents_apply(from, reverse_from_node, &ctx); + switch (method) { + case ZONE_INCLUDE_REVERSE: + return zone_contents_apply(from, reverse_from_node, &ctx); + case ZONE_INCLUDE_FLATTEN: + assert(to_upd == NULL && to_conts != NULL); // flattening from changeset is problematic since SOA is no present in changeset's zone_contents + return zone_contents_apply(from, flatten_from_node, &ctx); + default: + assert(0); + return KNOT_ERROR; + } } int zones_reverse(list_t *zones, zone_contents_t *to_conts, const knot_dname_t **fail_fwd) @@ -132,12 +196,11 @@ int zones_reverse(list_t *zones, zone_contents_t *to_conts, const knot_dname_t * zone_include_t *n; WALK_LIST(n, *zones) { zone_t *z = n->include; - assert(n->method == ZONE_INCLUDE_REVERSE); rcu_read_lock(); if (z->contents == NULL) { ret = KNOT_ETRYAGAIN; } else { - ret = zone_reverse(z->contents, to_conts, NULL, false); + ret = zone_reverse(z->contents, to_conts, NULL, false, n->method); } rcu_read_unlock(); if (ret != KNOT_EOK) { diff --git a/src/knot/zone/reverse.h b/src/knot/zone/reverse.h index 3c27d07fb5..eac1848878 100644 --- a/src/knot/zone/reverse.h +++ b/src/knot/zone/reverse.h @@ -14,17 +14,19 @@ * \param to_conts Out/optional: resulting reverse zone. * \param to_upd Out/optional: resulting update of reverse zone. * \param to_upd_rem Trigger removal from reverse zone. + * \param method Including mode. * * \return KNOT_E* */ int zone_reverse(zone_contents_t *from, zone_contents_t *to_conts, - zone_update_t *to_upd, bool to_upd_rem); + zone_update_t *to_upd, bool to_upd_rem, + zone_include_method_t method); inline static int changeset_reverse(changeset_t *from, zone_update_t *to) { - int ret = zone_reverse(from->remove, NULL, to, true); + int ret = zone_reverse(from->remove, NULL, to, true, ZONE_INCLUDE_REVERSE); if (ret == KNOT_EOK) { - ret = zone_reverse(from->add, NULL, to, false); + ret = zone_reverse(from->add, NULL, to, false, ZONE_INCLUDE_REVERSE); } return ret; } diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h index 894a7abc3e..155fe510dd 100644 --- a/src/knot/zone/zone.h +++ b/src/knot/zone/zone.h @@ -139,6 +139,7 @@ typedef struct zone typedef enum { ZONE_INCLUDE_REVERSE, + ZONE_INCLUDE_FLATTEN, } zone_include_method_t; typedef struct { diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c index eee25c1ea3..526b40a046 100644 --- a/src/knot/zone/zonedb-load.c +++ b/src/knot/zone/zonedb-load.c @@ -395,7 +395,12 @@ static void reg_reverse(conf_t *conf, knot_zonedb_t *db_new, zone_t *zone) } zone_includes_clear(zone); + zone_include_method_t method = ZONE_INCLUDE_REVERSE; conf_val_t val = conf_zone_get(conf, C_REVERSE_GEN, zone->name); + if (val.code != KNOT_EOK) { + method = ZONE_INCLUDE_FLATTEN; + val = conf_zone_get(conf, C_INCLUDE_FROM, zone->name); + } while (val.code == KNOT_EOK) { const knot_dname_t *forw_name = conf_dname(&val); zone_t *forw = knot_zonedb_find(db_new, forw_name); @@ -405,7 +410,7 @@ static void reg_reverse(conf_t *conf, knot_zonedb_t *db_new, zone_t *zone) log_zone_warning(zone->name, "zone to reverse %s does not exist", forw_str); } else { - (void)zone_includes_add(zone, forw, ZONE_INCLUDE_REVERSE); + (void)zone_includes_add(zone, forw, method); zone_local_notify_subscribe(forw, zone); } conf_val_next(&val); diff --git a/tests-extra/tests/zone/include_from/data/com.cz.zone b/tests-extra/tests/zone/include_from/data/com.cz.zone new file mode 100644 index 0000000000..f19f407f32 --- /dev/null +++ b/tests-extra/tests/zone/include_from/data/com.cz.zone @@ -0,0 +1,8 @@ +$ORIGIN com.cz. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111201 10800 3600 1209600 7200 + NS dns1 + TXT "auth-txt" + CDS 57855 5 1 B6DCD485719ADCA18E5F3D48A2331627FDD3636B +dns1 A 192.0.2.1 diff --git a/tests-extra/tests/zone/include_from/data/cz.zone b/tests-extra/tests/zone/include_from/data/cz.zone new file mode 100644 index 0000000000..99b1a07710 --- /dev/null +++ b/tests-extra/tests/zone/include_from/data/cz.zone @@ -0,0 +1,11 @@ +$ORIGIN cz. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111201 10800 3600 1209600 7200 + NS dns1 +dns1 A 192.0.2.1 +com NS dns1 +com DS 57855 5 1 B6DCD485719ADCA18E5F3D48A2331627FDD3636B +com TXT "nonauth-txt" +org NS dns1.org +dns1.org A 192.0.2.2 diff --git a/tests-extra/tests/zone/include_from/data/net.cz.zone b/tests-extra/tests/zone/include_from/data/net.cz.zone new file mode 100644 index 0000000000..a12453c512 --- /dev/null +++ b/tests-extra/tests/zone/include_from/data/net.cz.zone @@ -0,0 +1,6 @@ +$ORIGIN net.cz. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111201 10800 3600 1209600 7200 + NS dns1 +dns1 A 192.0.2.1 diff --git a/tests-extra/tests/zone/include_from/data/org.cz.zone b/tests-extra/tests/zone/include_from/data/org.cz.zone new file mode 100644 index 0000000000..c29aa5b051 --- /dev/null +++ b/tests-extra/tests/zone/include_from/data/org.cz.zone @@ -0,0 +1,6 @@ +$ORIGIN org.cz. +$TTL 3600 + +@ SOA dns1 hostmaster 2010111201 10800 3600 1209600 7200 + NS dns1 +dns1 A 192.0.2.1 diff --git a/tests-extra/tests/zone/include_from/test.py b/tests-extra/tests/zone/include_from/test.py new file mode 100644 index 0000000000..22f9fbb614 --- /dev/null +++ b/tests-extra/tests/zone/include_from/test.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +""" +Test of flattening subzones. +""" + +from dnstest.utils import * +from dnstest.test import Test +import random + +t = Test() + +master = t.server("knot") # only providing the subzones +flattener = t.server("knot") +slave = t.server("knot") # only slaving the flattened zone + +parent = t.zone("cz.", storage=".") +childs = t.zone("com.cz.", storage=".") + t.zone("net.cz.", storage=".") + t.zone("org.cz.", storage=".") + +t.link(childs, master, flattener) +t.link(parent, flattener, slave) + +flattener.zones[parent[0].name].include_from = childs + +flattener.dnssec(parent).enable = random.choice([False, True]) + +t.start() +serial = slave.zone_wait(parent) + +for z in childs: + for ty in [ "SOA", "NS", "DS", "CDS" ]: + r = slave.dig(z.name, ty) + r.check(rcode="NOERROR") + r.check_count(0, ty) + r = slave.dig("dns1." + z.name, "A") + r.check(rcode="NOERROR", rdata="192.0.2.1") + +r = slave.dig("dns1.org.cz", "A") +r.check(rcode="NOERROR", rdata="192.0.2.2") + +r = slave.dig("com.cz.", "TXT") +r.check(rcode="NOERROR", rdata="auth-txt") +r.check(rcode="NOERROR", rdata="nonauth-txt") +r.check_count(2, "TXT") + +up = master.update(childs[0]) +up.add("dns1", 3600, "AAAA", "1::2") +up.send("NOERROR") + +serial = slave.zone_wait(parent, serial) +r = slave.dig("dns1.com.cz.", "AAAA") +r.check(rcode="NOERROR", rdata="1::2") + +flattener.zones[parent[0].name].zfile.append_rndTXT("txt.cz.", rdata="added-txt") +if random.choice([False, True]): + flattener.ctl("zone-reload " + parent[0].name) +else: + up = master.update(childs[1]) + up.add("anything", 3600, "TXT", "dontcare") + up.send("NOERROR") + +serial = slave.zone_wait(parent, serial) +r = slave.dig("txt.cz.", "TXT") +r.check(rcode="NOERROR", rdata="added-txt") + +invalid_conf = random.choice(["include_self", "include_parent", "also_reverse"]) +if invalid_conf == "include_self": + flattener.zones[parent[0].name].include_from = parent +elif invalid_conf == "include_parent": + flattener.zones[childs[0].name].include_from = parent +else: + flattener.zones[parent[0].name].reverse_from = childs +flattener.gen_confile() +try: + flattener.reload() + set_err("INVALID CONF ACCEPTED: " + invalid_conf) +except: + pass + +t.end() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 30a78211f0..02b0b475c9 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -102,6 +102,7 @@ class Zone(object): self.journal_content = journal_content # journal contents self.modules = [] self.reverse_from = None + self.include_from = None self.external = None self.dnssec = ZoneDnssec() self.catalog_role = ZoneCatalogRole.NONE @@ -1969,6 +1970,8 @@ class Knot(Server): if z.reverse_from: s.item("reverse-generate", "[ " + ", ".join([ x.name for x in z.reverse_from ]) + " ]") + if z.include_from: + s.item("include-from", "[ " + ", ".join([ x.name for x in z.include_from ]) + " ]") self._str(s, "refresh-min-interval", z.refresh_min) self._str(s, "refresh-max-interval", z.refresh_max)