]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
reverse+include-from: allow at slave, forcing ixfr-from-axfr
authorLibor Peltan <libor.peltan@nic.cz>
Fri, 12 Sep 2025 13:01:09 +0000 (15:01 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Mon, 15 Sep 2025 12:45:25 +0000 (14:45 +0200)
doc/reference.rst
src/knot/events/handlers/load.c
src/knot/events/handlers/refresh.c
src/knot/updates/zone-update.c
src/knot/zone/reverse.c
src/knot/zone/reverse.h
src/knot/zone/zone.c
src/knot/zone/zone.h
tests-extra/tests/zone/include_from/test.py

index 399c9fb9882d84d73cd3219040f09e0ac3faf20b..23f8cb7dde9b731f429f6afb363a29c94fad2de0 100644 (file)
@@ -3380,6 +3380,10 @@ Current limitations:
 - Is slow for large zones (even when changing a little).
 - Recomputes all reverse records upon any change in any of the reversed zones.
 
+In case of secondary zone (i.e. :ref:`zone_master` is specified) this option implies
+:ref:`zone_ixfr-from-axfr`: *on* and :ref:`zone_journal-content`: *all*, otherwise
+:ref:`zone_zonefile-load`: *difference-no-serial* and :ref:`zone_journal-content`: *all*.
+
 *Default:* none
 
 .. _zone_include-from:
@@ -3391,8 +3395,8 @@ A list of subzones that should be flattened into this zone. The flattening delet
 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
+In case of secondary zone (i.e. :ref:`zone_master` is specified) this option implies
+:ref:`zone_ixfr-from-axfr`: *on* and :ref:`zone_journal-content`: *all*, otherwise
 :ref:`zone_zonefile-load`: *difference-no-serial* and :ref:`zone_journal-content`: *all*.
 
 *Default:* none
index 9352d06891af46ab99453af8d112656dd97ff436..3f5f7b39b1a4e15134576e819fffc8cd59e8a3a0 100644 (file)
@@ -64,10 +64,9 @@ int event_load(conf_t *conf, zone_t *zone)
        val = conf_zone_get(conf, C_ZONEFILE_LOAD, zone->name);
        unsigned zf_from = conf_opt(&val);
 
-       // 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.
+       bool includes_configured = zone_includes_configured(conf, zone);
+
+       if (includes_configured || zone->cat_members != NULL) { // The latter should be equivalent to setting catalog-role:generate.
                zf_from = ZONEFILE_LOAD_DIFSE;
                load_from = JOURNAL_CONTENT_ALL;
        }
@@ -116,7 +115,7 @@ int event_load(conf_t *conf, zone_t *zone)
        // Attempt to load changes from database. If fails, load full zone from there later.
        if (db_enabled && (old_contents_exist || journal_conts != NULL) &&
            zone->cat_members == NULL && EMPTY_LIST(zone->include_from) &&
-           zf_from != ZONEFILE_LOAD_DIFSE) {
+           zf_from != ZONEFILE_LOAD_DIFSE && !includes_configured) {
                zone_redis_err_t err;
                uint32_t db_serial = 0;
                ret = zone_redis_serial(db_ctx, db_instance, zone->name, &db_serial, err);
@@ -212,18 +211,13 @@ int event_load(conf_t *conf, zone_t *zone)
                zone->zonefile.exists = (zf_conts != NULL);
                zone->zonefile.mtime = mtime;
 
-zonefile_loaded: ;
+zonefile_loaded:
                // If configured, add reverse records to zone contents
-               const knot_dname_t *fail_fwd = NULL;
-               ret = zones_reverse(&zone->include_from, zf_conts, &fail_fwd);
-               if (ret == KNOT_ETRYAGAIN) {
-                       knot_dname_txt_storage_t forw_str;
-                       (void)knot_dname_to_str(forw_str, fail_fwd, sizeof(forw_str));
-                       log_zone_warning(zone->name, "waiting for source forward zone '%s'", forw_str);
-                       goto cleanup;
-               } else if (ret != KNOT_EOK) {
-                       log_zone_error(zone->name, "failed to generate reverse records");
-                       goto cleanup;
+               if (includes_configured) {
+                       ret = zones_reverse_log(zone, zf_conts);
+                       if (ret != KNOT_EOK) {
+                               goto cleanup;
+                       }
                }
 
                // If configured and possible, fix the SOA serial of zonefile.
index b77e5ad6eb6dc0daa98f23af19fa1896b602f665..074187da25888eaac791492e3ef96b1d4015eea5 100644 (file)
@@ -22,6 +22,7 @@
 #include "knot/updates/changesets.h"
 #include "knot/zone/adjust.h"
 #include "knot/zone/digest.h"
+#include "knot/zone/reverse.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone.h"
 #include "knot/zone/zonefile.h"
@@ -106,6 +107,7 @@ struct refresh_data {
        bool fallback_axfr;               //!< Flag allowing fallback to AXFR,
        bool ixfr_by_one;                 //!< Allow only single changeset within IXFR.
        bool ixfr_from_axfr;              //!< Diff computation of incremental update from AXFR allowed.
+       bool reverse_or_include;          //!< Auto reverse generation or subzone inclusion configured.
        uint32_t expire_timer;            //!< Result: expire timer from answer EDNS.
 
        // internal state, initialize with zeroes:
@@ -341,15 +343,25 @@ static int axfr_finalize(struct refresh_data *data)
        zone_skip_t skip = { 0 };
        int ret = KNOT_EOK;
 
-       if (dnssec_enable) {
+       if (dnssec_enable || data->reverse_or_include) {
                axfr_slave_sign_serial(new_zone, data->zone, data->conf, &master_serial);
                ret = zone_skip_add_dnssec_diff(&skip);
                assert(ret == KNOT_EOK); // static size of zone_skip is enough to cover dnssec types
        }
 
+       if (data->reverse_or_include) {
+               ret = zones_reverse_log(data->zone, new_zone);
+               if (ret != KNOT_EOK) {
+                       data->fallback->remote = false;
+                       return ret;
+               }
+       }
+
        zone_update_t up = { 0 };
 
-       if (data->ixfr_from_axfr && data->axfr_style_ixfr) {
+       // With ixfr-from-axfr configured, compute diff only when axfr-style-ixfr received. With include-from, do always.
+       if (data->ixfr_from_axfr && data->zone->contents != NULL &&
+           (data->axfr_style_ixfr || data->reverse_or_include)) {
                ret = zone_update_from_differences(&up, data->zone, NULL, new_zone, UPDATE_INCREMENTAL | UPDATE_EVREQ, &skip);
        } else {
                ret = zone_update_from_contents(&up, data->zone, new_zone, UPDATE_FULL | UPDATE_EVREQ);
@@ -397,7 +409,7 @@ static int axfr_finalize(struct refresh_data *data)
                return ret;
        }
 
-       if (dnssec_enable) {
+       if (dnssec_enable || data->reverse_or_include) {
                ret = zone_set_master_serial(data->zone, master_serial);
                if (ret != KNOT_EOK) {
                        log_zone_warning(data->zone->name,
@@ -407,7 +419,7 @@ static int axfr_finalize(struct refresh_data *data)
 
        finalize_timers(data);
        xfr_log_publish(data, old_serial, zone_contents_serial(data->zone->contents),
-                       master_serial, dnssec_enable, bootstrap);
+                       master_serial, dnssec_enable || data->reverse_or_include, bootstrap);
 
        data->fallback->remote = false;
        zone_set_last_master(data->zone, (const struct sockaddr_storage *)data->remote);
@@ -1341,6 +1353,7 @@ typedef struct {
        bool send_notify;
        bool ixfr_by_one;
        bool ixfr_from_axfr;
+       bool reverse_or_include;
        bool more_xfr;
 } try_refresh_ctx_t;
 
@@ -1379,6 +1392,7 @@ static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master,
                .fallback_axfr = false, // will be set upon IXFR consume
                .ixfr_by_one = trctx->ixfr_by_one,
                .ixfr_from_axfr = trctx->ixfr_from_axfr,
+               .reverse_or_include = trctx->reverse_or_include,
        };
 
        knot_requestor_t requestor;
@@ -1455,6 +1469,13 @@ int event_refresh(conf_t *conf, zone_t *zone)
        val = conf_zone_get(conf, C_IXFR_FROM_AXFR, zone->name);
        trctx.ixfr_from_axfr = conf_bool(&val);
 
+       if (zone_includes_configured(conf, zone)) {
+               trctx.force_axfr = true;
+               zone->zonefile.retransfer = true;
+               trctx.ixfr_from_axfr = true;
+               trctx.reverse_or_include = true;
+       }
+
        int ret = zone_master_try(conf, zone, try_refresh, &trctx, "refresh");
        zone_clear_preferred_master(zone);
        if (ret != KNOT_EOK) {
index 9dcb4a31c2883e871e7c8e2be4a9f04117b1fb6d..c8db835a93b40f128e5e12b651237e0103884e22 100644 (file)
@@ -677,6 +677,11 @@ static int commit_journal(conf_t *conf, zone_update_t *update)
 {
        conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, update->zone->name);
        unsigned content = conf_opt(&val);
+
+       if (zone_includes_configured(conf, update->zone)){
+               content = JOURNAL_CONTENT_ALL;
+       }
+
        int ret = KNOT_EOK;
        if (update->flags & UPDATE_NO_CHSET) {
                zone_diff_t diff;
@@ -1221,7 +1226,7 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
                log_zone_error(update->zone->name, "failed to deallocate unused memory");
        }
 
-       zone_local_notify(update->zone);
+       zone_local_notify(conf, update->zone);
 
        /* Sync zonefile immediately if configured. */
        val = conf_zone_get(conf, C_ZONEFILE_SYNC, update->zone->name);
index a629a9bdeff6eaff42819fdaa31e2d8ffb83f1fa..f0321124836f44b41ce48fa1570ba37942e99d4b 100644 (file)
@@ -6,6 +6,7 @@
 #include <string.h>
 #include <urcu.h>
 
+#include "knot/common/log.h"
 #include "knot/zone/reverse.h"
 
 static const uint8_t *reverse4postfix = (const uint8_t *)"\x07""in-addr""\x04""arpa";
@@ -212,3 +213,17 @@ int zones_reverse(list_t *zones, zone_contents_t *to_conts, const knot_dname_t *
        }
        return ret;
 }
+
+int zones_reverse_log(zone_t *zone, zone_contents_t *to_conts)
+{
+       const knot_dname_t *fail_fwd = NULL;
+       int ret = zones_reverse(&zone->include_from, to_conts, &fail_fwd);
+       if (ret == KNOT_ETRYAGAIN) {
+               knot_dname_txt_storage_t forw_str;
+               (void)knot_dname_to_str(forw_str, fail_fwd, sizeof(forw_str));
+               log_zone_warning(zone->name, "waiting for source forward zone '%s'", forw_str);
+       } else if (ret != KNOT_EOK) {
+               log_zone_error(zone->name, "failed to generate reverse records");
+       }
+       return ret;
+}
index eac1848878521fbfa15aa26742a14feeb8320b68..0732b3be45ed521b3b3416d8d2575aab00104c54 100644 (file)
@@ -42,3 +42,13 @@ inline static int changeset_reverse(changeset_t *from, zone_update_t *to)
  * \return KNOT_E*
  */
 int zones_reverse(list_t *zones, zone_contents_t *to_conts, const knot_dname_t **fail_fwd);
+
+/*!
+ * \brief Reverse/include from all configured zones (in zone->include_from) and log if failed.
+ *
+ * \param zone       Zone to include records from others.
+ * \param to_conts   Out: resulting reverse zone.
+ *
+ * \return KNOT_E*
+ */
+int zones_reverse_log(zone_t *zone, zone_contents_t *to_conts);
index 489e849d54c77738bf15e57f745036e5ad217aa7..624e309bf085d10aa74cf9ad21820b2f1ee67a60 100644 (file)
@@ -815,6 +815,12 @@ int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir)
        return zonefile_write_skip(target, zone->contents, conf);
 }
 
+bool zone_includes_configured(conf_t *conf, zone_t *zone)
+{
+       return conf_zone_get(conf, C_REVERSE_GEN, zone->name).code == KNOT_EOK ||
+              conf_zone_get(conf, C_INCLUDE_FROM, zone->name).code == KNOT_EOK;
+}
+
 int zone_includes_add(zone_t *zone, zone_t *include, zone_include_method_t method)
 {
        zone_include_t *n = calloc(1, sizeof(*n));
@@ -857,11 +863,12 @@ void zone_local_notify_unsubscribe(zone_t *zone, zone_t *subscribe)
        ptrlist_find_rem(&zone->internal_notify, subscribe, NULL);
 }
 
-void zone_local_notify(zone_t *zone)
+void zone_local_notify(conf_t *conf, zone_t *zone)
 {
        ptrnode_t *n;
        WALK_LIST(n, zone->internal_notify) {
-               zone_events_schedule_now(n->d, ZONE_EVENT_LOAD);
+               zone_t *to_notify = n->d;
+               zone_events_schedule_now(to_notify, zone_is_slave(conf, to_notify) ? ZONE_EVENT_REFRESH : ZONE_EVENT_LOAD);
        }
 }
 
@@ -901,7 +908,7 @@ int slave_zone_serial(zone_t *zone, conf_t *conf, uint32_t *serial)
        *serial = zone_contents_serial(zone->contents);
 
        conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name);
-       if (conf_bool(&val)) {
+       if (conf_bool(&val) || zone_includes_configured(conf, zone)) {
                ret = zone_get_master_serial(zone, serial);
        }
 
index 155fe510dd7a2e9d210f78cbd1c858b9066ec1c2..a04eed16e97c732a289a7cedcfe830878b4b8138 100644 (file)
@@ -298,13 +298,14 @@ int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir);
 /*!
  * \brief Zone inclusion (reverse generation) related ops.
  */
+bool zone_includes_configured(conf_t *conf, zone_t *zone);
 int zone_includes_add(zone_t *zone, zone_t *include, zone_include_method_t method);
 void zone_includes_rem(zone_t *zone, zone_t *include);
 void zone_includes_clear(zone_t *zone);
 
 void zone_local_notify_subscribe(zone_t *zone, zone_t *subscribe);
 void zone_local_notify_unsubscribe(zone_t *zone, zone_t *subscribe);
-void zone_local_notify(zone_t *zone);
+void zone_local_notify(conf_t *conf, zone_t *zone);
 
 int zone_set_master_serial(zone_t *zone, uint32_t serial);
 
index 22f9fbb614d88ad232f973413c6507909cf9d1f0..a5e1db1ea23ecc7a246a92da8500f22b89cb141b 100644 (file)
@@ -10,16 +10,23 @@ import random
 
 t = Test()
 
-master = t.server("knot") # only providing the subzones
+master = t.server("knot") # if not tld_axfr: 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=".")
 
+tld_axfr = random.choice([False, True])
+
 t.link(childs, master, flattener)
 t.link(parent, flattener, slave)
 
+if tld_axfr:
+    t.link(parent, master, flattener)
+
+parent_master = master if tld_axfr else flattener
+
 flattener.zones[parent[0].name].include_from = childs
 
 flattener.dnssec(parent).enable = random.choice([False, True])
@@ -51,9 +58,11 @@ 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)
+parent_zf = parent_master.zones[parent[0].name].zfile
+parent_zf.append_rndTXT("txt.cz.", rdata="added-txt")
+parent_zf.update_soa()
+if tld_axfr or random.choice([False, True]):
+    parent_master.ctl("zone-reload " + parent[0].name)
 else:
     up = master.update(childs[1])
     up.add("anything", 3600, "TXT", "dontcare")