]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
zone: implemented including records from subzone(s)
authorLibor Peltan <libor.peltan@nic.cz>
Thu, 11 Sep 2025 14:54:42 +0000 (16:54 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Fri, 12 Sep 2025 07:37:57 +0000 (09:37 +0200)
16 files changed:
doc/reference.rst
src/knot/conf/schema.c
src/knot/conf/schema.h
src/knot/conf/tools.c
src/knot/conf/tools.h
src/knot/events/handlers/load.c
src/knot/zone/reverse.c
src/knot/zone/reverse.h
src/knot/zone/zone.h
src/knot/zone/zonedb-load.c
tests-extra/tests/zone/include_from/data/com.cz.zone [new file with mode: 0644]
tests-extra/tests/zone/include_from/data/cz.zone [new file with mode: 0644]
tests-extra/tests/zone/include_from/data/net.cz.zone [new file with mode: 0644]
tests-extra/tests/zone/include_from/data/org.cz.zone [new file with mode: 0644]
tests-extra/tests/zone/include_from/test.py [new file with mode: 0644]
tests-extra/tools/dnstest/server.py

index 608b29bd34746a623b9f2ee25d4b0b7277a7a814..6e934e1d80edaae3f7b54c8ce38600ec30cdba55 100644 (file)
@@ -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
index 5e80ab437574b93d64e89d9153bbf7cfdb64821f..bb3cf2e499536f5c9bb7a65224959a2dd7ff219e 100644 (file)
@@ -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 }, \
index 3de02cfe6e37a96a27385c6ec9b2f8a58884bdff..7b48c3a2e904f254dd8ee0973e03252b6d3195d4 100644 (file)
@@ -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"
index bbffbc9a14bdf1fc65cb8da40687c192b586b29e..b937cd45b8a06e12dbc2dcdc8621e83652cd7b1f 100644 (file)
@@ -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;
 }
 
index 10a5cb4fd587b6e3627efb005684d69bcddca659..f48ed5bbc5b535fd43ada11dcd35b72562840a4e 100644 (file)
@@ -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
 );
index a6bf4e8668080324316b29f9572fdd497e9299be..3073a7af22b7772ed4d9ef2c635a370ac58b638e 100644 (file)
@@ -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;
index 98734e7578f6d9261c76344c99f74e4a0af6d476..a629a9bdeff6eaff42819fdaa31e2d8ffb83f1fa 100644 (file)
@@ -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) {
index 3c27d07fb565b072710b63466fa877c4faa95ff3..eac1848878521fbfa15aa26742a14feeb8320b68 100644 (file)
  * \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;
 }
index 894a7abc3e8bfbf1366fab227a2b22d53e4d9463..155fe510dd7a2e9d210f78cbd1c858b9066ec1c2 100644 (file)
@@ -139,6 +139,7 @@ typedef struct zone
 
 typedef enum {
        ZONE_INCLUDE_REVERSE,
+       ZONE_INCLUDE_FLATTEN,
 } zone_include_method_t;
 
 typedef struct {
index eee25c1ea3cb84923195399cc2d8f28e10c19d76..526b40a0468fbc66507af944e543156903719ce4 100644 (file)
@@ -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 (file)
index 0000000..f19f407
--- /dev/null
@@ -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 (file)
index 0000000..99b1a07
--- /dev/null
@@ -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 (file)
index 0000000..a12453c
--- /dev/null
@@ -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 (file)
index 0000000..c29aa5b
--- /dev/null
@@ -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 (file)
index 0000000..22f9fbb
--- /dev/null
@@ -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()
index 30a78211f0485c2b4111f4cc6e88dada782e03e7..02b0b475c94da0c7bdeb973f09d84eeb6dfec99a 100644 (file)
@@ -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)