]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
XFR/master-selection: try all masters if more than one sent NOTIFY
authorLibor Peltan <libor.peltan@nic.cz>
Wed, 20 Aug 2025 14:02:53 +0000 (16:02 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Fri, 5 Sep 2025 05:42:14 +0000 (07:42 +0200)
src/knot/zone/zone.c
src/knot/zone/zone.h
tests-extra/tests/notify/multi-master2/test.py [new file with mode: 0644]

index ba11aa1134259af81b6aea24d026226a17531299..0084dccab7a825ad0c209b0586cd951670f0bae1 100644 (file)
@@ -517,10 +517,16 @@ void zone_set_preferred_master(zone_t *zone, const struct sockaddr_storage *addr
 
        pthread_mutex_lock(&zone->preferred_lock);
        if (zone->preferred_master == NULL) {
-               zone->preferred_master = malloc(sizeof(*zone->preferred_master));
+               zone->preferred_master = calloc(1, sizeof(*zone->preferred_master));
                assert(zone->preferred_master != NULL);
        }
-       memcpy(zone->preferred_master, addr, sockaddr_len(addr));
+
+       if (!sockaddr_net_match(zone->preferred_master, addr, -1)) {
+               if (zone->preferred_master->ss_family != AF_UNSPEC) {
+                       zone->flags |= ZONE_PREF_MASTER_2X; // not zone_set_flag() as the mutex is already locked
+               }
+               memcpy(zone->preferred_master, addr, sockaddr_len(addr));
+       }
        pthread_mutex_unlock(&zone->preferred_lock);
 }
 
@@ -533,6 +539,7 @@ void zone_clear_preferred_master(zone_t *zone)
        pthread_mutex_lock(&zone->preferred_lock);
        free(zone->preferred_master);
        zone->preferred_master = NULL;
+       zone->flags &= ~ZONE_PREF_MASTER_2X;
        pthread_mutex_unlock(&zone->preferred_lock);
 }
 
@@ -698,6 +705,7 @@ int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback,
        const char *last_id = NULL, *preferred_id = NULL;
        conf_val_t last = { 0 }, preferred = { 0 };
        int idx = 0, last_idx = -1, preferred_idx = -1;
+       bool preferred_2x = false;
 
        conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name);
        conf_mix_iter_t iter;
@@ -714,6 +722,7 @@ int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback,
                                preferred_id = conf_str(iter.id);
                                preferred = *iter.id;
                                preferred_idx = idx;
+                               preferred_2x = (zone->flags & ZONE_PREF_MASTER_2X);
                        }
                        if (pin_tolerance > 0 &&
                            sockaddr_net_match(&remote.addr, (struct sockaddr_storage *)&zone->timers.last_master, -1)) {
@@ -738,7 +747,7 @@ int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback,
                };
                ret = try_remote(conf, zone, callback, callback_data, err_str,
                                 preferred_id, &preferred, &fallback, "notifier ");
-               if (ret == KNOT_EOK || !fallback.remote) {
+               if ((ret == KNOT_EOK && !preferred_2x) || !fallback.remote) {
                        return ret; // Success or local error.
                }
        }
index ef861f2a397a0a7dcedc5a1bcf489fef7397ad92..561264e6712e57640594c27313f789396414e7ec 100644 (file)
@@ -39,6 +39,7 @@ typedef enum {
        ZONE_XFR_FROZEN     = 1 << 7, /*!< Outgoing AXFR/IXFR temporarily disabled. */
        ZONE_USER_FLUSH     = 1 << 8, /*!< User-triggered flush. */
        ZONE_LAST_SIGN_OK   = 1 << 9, /*!< Last full-sign event finished OK. */
+       ZONE_PREF_MASTER_2X = 1 << 10, /*!< Preferred master has been overwritten at least once. */
 } zone_flag_t;
 
 /*!
diff --git a/tests-extra/tests/notify/multi-master2/test.py b/tests-extra/tests/notify/multi-master2/test.py
new file mode 100644 (file)
index 0000000..475132c
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+
+'''Test of not inhibiting received NOTIFY by another NOTIFY from outdated master.'''
+
+from dnstest.test import Test
+
+t = Test(address=4)
+
+master1 = t.server("knot", address="127.0.0.11", via=True)
+master2 = t.server("knot", address="127.0.0.12", via=True)
+slave = t.server("knot")
+
+zone = t.zone_rnd(1, records=300)
+ZONE = zone[0].name
+
+t.link(zone, master1, slave)
+t.link(zone, master2, slave)
+
+t.start()
+
+serial = slave.zone_wait(zone)
+
+# TEST 1: both masters send NOTIFY, but only the first one has higher serial than slave
+
+slave.ctl("zone-freeze", wait=True)
+slave.ctl("zone-flush", wait=True)
+
+up = master1.update(zone)
+up.add(ZONE, 3600, "SOA", "dns1 hostmaster %d 10800 3600 1209600 7200" % (serial + 2))
+up.add("add1", 3600, "TXT", master1.name)
+up.send()
+t.sleep(2)
+
+up = master2.update(zone)
+up.add("add1", 3600, "TXT", master2.name)
+up.send()
+t.sleep(2)
+
+slave.zones[ZONE].zfile.update_soa()
+slave.ctl("zone-reload", wait=True)
+serial = slave.zone_wait(zone, serial)
+slave.ctl("zone-thaw")
+
+# in case of failure: slave only tries master2 as the last one sending NOTIFY
+serial = slave.zone_wait(zone, serial)
+resp = slave.dig("add1." + ZONE, "TXT")
+resp.check(rcode="NOERROR", rdata=master1.name, nordata=master2.name)
+
+# TEST 2: master1 doesn't send NOTIFY, only master2 is attempted
+
+slave.ctl("zone-freeze", wait=True)
+slave.ctl("zone-flush", wait=True)
+
+master1.disable_notify = True
+master1.gen_confile()
+master1.reload()
+
+up = master2.update(zone)
+up.add(ZONE, 3600, "SOA", "dns1 hostmaster %d 10800 3600 1209600 7200" % (serial + 1))
+up.add("add2", 3600, "TXT", master2.name)
+up.send()
+t.sleep(2)
+
+up = master1.update(zone)
+up.add(ZONE, 3600, "SOA", "dns1 hostmaster %d 10800 3600 1209600 7200" % (serial + 2))
+up.add("add2", 3600, "TXT", master1.name)
+up.send()
+t.sleep(2)
+
+slave.zones[ZONE].zfile.update_soa()
+slave.ctl("zone-reload", wait=True)
+serial = slave.zone_wait(zone, serial)
+slave.ctl("zone-thaw")
+
+t.sleep(5)
+
+# in case of failure: slave also tries master1 as the last one with greatest (greater) serial
+slave.zone_wait(zone, serial, equal=True, greater=False)
+resp = slave.dig("add2." + ZONE, "TXT")
+resp.check(rcode="NXDOMAIN")
+
+t.end()