]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Check that a zone that serves A/AAAA is served over IPv4/IPv6
authorMark Andrews <marka@isc.org>
Tue, 17 Oct 2023 03:49:14 +0000 (14:49 +1100)
committerMark Andrews <marka@isc.org>
Wed, 11 Dec 2024 21:32:21 +0000 (21:32 +0000)
named-checkzone will now, as part of the zone's integrity checks,
look to see if there are A or AAAA records being served and if so
check that the nameservers have A or AAAA records respectively.

These are a sometimes overlooked checks that, if not met, can mean
that a service that is supposed to reachable over IPv6 will not be
resolvable when the recursive resolver is IPv6 only.  Similarly for
IPv4 servers when there are IPv4 only resolvers.

bin/check/check-tool.c
bin/check/named-checkzone.rst
lib/dns/include/dns/types.h
lib/dns/include/dns/zone.h
lib/dns/zone.c

index 5ad43f29935426ea26b55b1edf34a20629fea369..98924e3dfc0d17699c77d6f27df0c2fc2b33543e 100644 (file)
@@ -144,12 +144,97 @@ logged(char *key, int value) {
        return false;
 }
 
+static bool
+checkisservedby(dns_zone_t *zone, dns_rdatatype_t type,
+               const dns_name_t *name) {
+       char namebuf[DNS_NAME_FORMATSIZE + 1];
+       char ownerbuf[DNS_NAME_FORMATSIZE + 1];
+       /*
+        * Not all getaddrinfo implementations distinguish NODATA
+        * from NXDOMAIN with PF_INET6 so use PF_UNSPEC and look at
+        * the returned ai_family values.
+        */
+       struct addrinfo hints = {
+               .ai_flags = AI_CANONNAME,
+               .ai_family = PF_UNSPEC,
+               .ai_socktype = SOCK_STREAM,
+               .ai_protocol = IPPROTO_TCP,
+       };
+       struct addrinfo *ai = NULL, *cur;
+       bool has_type = false;
+       int eai;
+
+       dns_name_format(name, namebuf, sizeof(namebuf) - 1);
+       /*
+        * Turn off search.
+        */
+       if (dns_name_countlabels(name) > 1U) {
+               strlcat(namebuf, ".", sizeof(namebuf));
+       }
+       eai = getaddrinfo(namebuf, NULL, &hints, &ai);
+
+       switch (eai) {
+       case 0:
+               cur = ai;
+               while (cur != NULL) {
+                       if (cur->ai_family == AF_INET &&
+                           type == dns_rdatatype_a)
+                       {
+                               has_type = true;
+                               break;
+                       }
+                       if (cur->ai_family == AF_INET6 &&
+                           type == dns_rdatatype_aaaa)
+                       {
+                               has_type = true;
+                               break;
+                       }
+                       cur = cur->ai_next;
+               }
+               freeaddrinfo(ai);
+               return has_type;
+#if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME)
+       case EAI_NODATA:
+#endif /* if defined(EAI_NODATA) && (EAI_NODATA != EAI_NONAME) */
+       case EAI_NONAME:
+               if (!logged(namebuf, ERR_NO_ADDRESSES)) {
+                       dns_name_format(dns_zone_getorigin(zone), ownerbuf,
+                                       sizeof(ownerbuf));
+                       dns_name_format(name, namebuf, sizeof(namebuf) - 1);
+                       dns_zone_log(zone, ISC_LOG_ERROR,
+                                    "%s/NS '%s' (out of zone) "
+                                    "has no addresses records (A or AAAA)",
+                                    ownerbuf, namebuf);
+                       add(namebuf, ERR_NO_ADDRESSES);
+               }
+               return false;
+       default:
+               if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
+                       dns_name_format(dns_zone_getorigin(zone), ownerbuf,
+                                       sizeof(ownerbuf));
+                       dns_name_format(name, namebuf, sizeof(namebuf) - 1);
+                       dns_zone_log(zone, ISC_LOG_WARNING,
+                                    "getaddrinfo(%s) failed: %s", namebuf,
+                                    gai_strerror(eai));
+                       add(namebuf, ERR_LOOKUP_FAILURE);
+               }
+               return true;
+       }
+}
+
 static bool
 checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
        dns_rdataset_t *a, dns_rdataset_t *aaaa) {
        dns_rdataset_t *rdataset;
        dns_rdata_t rdata = DNS_RDATA_INIT;
-       struct addrinfo hints, *ai, *cur;
+       isc_result_t result;
+       struct addrinfo hints = {
+               .ai_flags = AI_CANONNAME,
+               .ai_family = PF_UNSPEC,
+               .ai_socktype = SOCK_STREAM,
+               .ai_protocol = IPPROTO_TCP,
+       };
+       struct addrinfo *ai = NULL, *cur;
        char namebuf[DNS_NAME_FORMATSIZE + 1];
        char ownerbuf[DNS_NAME_FORMATSIZE];
        char addrbuf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:123.123.123.123")];
@@ -157,7 +242,7 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
        bool match;
        const char *type;
        void *ptr = NULL;
-       int result;
+       int eai;
 
        REQUIRE(a == NULL || !dns_rdataset_isassociated(a) ||
                a->type == dns_rdatatype_a);
@@ -168,12 +253,6 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
                return answer;
        }
 
-       memset(&hints, 0, sizeof(hints));
-       hints.ai_flags = AI_CANONNAME;
-       hints.ai_family = PF_UNSPEC;
-       hints.ai_socktype = SOCK_STREAM;
-       hints.ai_protocol = IPPROTO_TCP;
-
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
        /*
         * Turn off search.
@@ -183,9 +262,9 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
        }
        dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
 
-       result = getaddrinfo(namebuf, NULL, &hints, &ai);
+       eai = getaddrinfo(namebuf, NULL, &hints, &ai);
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
-       switch (result) {
+       switch (eai) {
        case 0:
                /*
                 * Work around broken getaddrinfo() implementations that
@@ -228,7 +307,7 @@ checkns(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner,
                if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
                        dns_zone_log(zone, ISC_LOG_WARNING,
                                     "getaddrinfo(%s) failed: %s", namebuf,
-                                    gai_strerror(result));
+                                    gai_strerror(eai));
                        add(namebuf, ERR_LOOKUP_FAILURE);
                }
                return true;
@@ -358,25 +437,27 @@ checkmissing:
                        add(namebuf, ERR_MISSING_GLUE);
                }
        }
-       freeaddrinfo(ai);
+       if (ai != NULL) {
+               freeaddrinfo(ai);
+       }
        return answer;
 }
 
 static bool
 checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
-       struct addrinfo hints, *ai, *cur;
+       struct addrinfo hints = {
+               .ai_flags = AI_CANONNAME,
+               .ai_family = PF_UNSPEC,
+               .ai_socktype = SOCK_STREAM,
+               .ai_protocol = IPPROTO_TCP,
+       };
+       struct addrinfo *ai = NULL, *cur;
        char namebuf[DNS_NAME_FORMATSIZE + 1];
        char ownerbuf[DNS_NAME_FORMATSIZE];
-       int result;
+       int eai;
        int level = ISC_LOG_ERROR;
        bool answer = true;
 
-       memset(&hints, 0, sizeof(hints));
-       hints.ai_flags = AI_CANONNAME;
-       hints.ai_family = PF_UNSPEC;
-       hints.ai_socktype = SOCK_STREAM;
-       hints.ai_protocol = IPPROTO_TCP;
-
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
        /*
         * Turn off search.
@@ -386,9 +467,9 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
        }
        dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
 
-       result = getaddrinfo(namebuf, NULL, &hints, &ai);
+       eai = getaddrinfo(namebuf, NULL, &hints, &ai);
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
-       switch (result) {
+       switch (eai) {
        case 0:
                /*
                 * Work around broken getaddrinfo() implementations that
@@ -421,7 +502,9 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
                                }
                        }
                }
-               freeaddrinfo(ai);
+               if (ai != NULL) {
+                       freeaddrinfo(ai);
+               }
                return answer;
 
        case EAI_NONAME:
@@ -442,7 +525,7 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
                if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
                        dns_zone_log(zone, ISC_LOG_WARNING,
                                     "getaddrinfo(%s) failed: %s", namebuf,
-                                    gai_strerror(result));
+                                    gai_strerror(eai));
                        add(namebuf, ERR_LOOKUP_FAILURE);
                }
                return true;
@@ -451,19 +534,19 @@ checkmx(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
 
 static bool
 checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
-       struct addrinfo hints, *ai, *cur;
+       struct addrinfo hints = {
+               .ai_flags = AI_CANONNAME,
+               .ai_family = PF_UNSPEC,
+               .ai_socktype = SOCK_STREAM,
+               .ai_protocol = IPPROTO_TCP,
+       };
+       struct addrinfo *ai = NULL, *cur;
        char namebuf[DNS_NAME_FORMATSIZE + 1];
        char ownerbuf[DNS_NAME_FORMATSIZE];
-       int result;
+       int eai;
        int level = ISC_LOG_ERROR;
        bool answer = true;
 
-       memset(&hints, 0, sizeof(hints));
-       hints.ai_flags = AI_CANONNAME;
-       hints.ai_family = PF_UNSPEC;
-       hints.ai_socktype = SOCK_STREAM;
-       hints.ai_protocol = IPPROTO_TCP;
-
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
        /*
         * Turn off search.
@@ -473,9 +556,9 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
        }
        dns_name_format(owner, ownerbuf, sizeof(ownerbuf));
 
-       result = getaddrinfo(namebuf, NULL, &hints, &ai);
+       eai = getaddrinfo(namebuf, NULL, &hints, &ai);
        dns_name_format(name, namebuf, sizeof(namebuf) - 1);
-       switch (result) {
+       switch (eai) {
        case 0:
                /*
                 * Work around broken getaddrinfo() implementations that
@@ -508,7 +591,9 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
                                }
                        }
                }
-               freeaddrinfo(ai);
+               if (ai != NULL) {
+                       freeaddrinfo(ai);
+               }
                return answer;
 
        case EAI_NONAME:
@@ -529,7 +614,7 @@ checksrv(dns_zone_t *zone, const dns_name_t *name, const dns_name_t *owner) {
                if (!logged(namebuf, ERR_LOOKUP_FAILURE)) {
                        dns_zone_log(zone, ISC_LOG_WARNING,
                                     "getaddrinfo(%s) failed: %s", namebuf,
-                                    gai_strerror(result));
+                                    gai_strerror(eai));
                        add(namebuf, ERR_LOOKUP_FAILURE);
                }
                return true;
@@ -603,6 +688,7 @@ load_zone(isc_mem_t *mctx, const char *zonename, const char *filename,
        }
        if (docheckns) {
                dns_zone_setcheckns(zone, checkns);
+               dns_zone_setcheckisservedby(zone, checkisservedby);
        }
        if (dochecksrv) {
                dns_zone_setchecksrv(zone, checksrv);
index 9b8a0909a4daccf65e637d4d36a9a223fcb6bd95..a58cfb7a1445fded687556959ce4dc379e6a0f86 100644 (file)
@@ -91,9 +91,13 @@ Options
    (both in-zone and out-of-zone hostnames). Mode ``local`` only
    checks SRV records which refer to in-zone hostnames.
 
+   Mode ``full`` checks that a zone that has A or AAAA records it is served
+   by a server with the same type of address records.
+
    Mode ``full`` checks that delegation NS records refer to A or AAAA
    records (both in-zone and out-of-zone hostnames). It also checks that
    glue address records in the zone match those advertised by the child.
+
    Mode ``local`` only checks NS records which refer to in-zone
    hostnames or verifies that some required glue exists, i.e., when the
    name server is in a child zone.
index 1e56d1d256b16b0a4ddce700c8044b132bc67f3c..7ed00cb4118e82b7f6826f25e4215e56c951a0df 100644 (file)
@@ -448,6 +448,9 @@ typedef bool (*dns_checknsfunc_t)(dns_zone_t *, const dns_name_t *,
                                  const dns_name_t *, dns_rdataset_t *,
                                  dns_rdataset_t *);
 
+typedef bool (*dns_checkisservedbyfunc_t)(dns_zone_t *, dns_rdatatype_t type,
+                                         const dns_name_t *);
+
 typedef bool (*dns_isselffunc_t)(dns_view_t *, dns_tsigkey_t *,
                                 const isc_sockaddr_t *, const isc_sockaddr_t *,
                                 dns_rdataclass_t, void *);
index 8165c5a67f00cfaaffee74fa6a6311882e4f1636..23c89e519779b42f4b3c612561708cc8d0b99bc5 100644 (file)
@@ -2187,6 +2187,18 @@ dns_zone_setcheckns(dns_zone_t *zone, dns_checknsfunc_t checkns);
  *     'zone' to be a valid zone.
  */
 
+void
+dns_zone_setcheckisservedby(dns_zone_t              *zone,
+                           dns_checkisservedbyfunc_t checkisserverby);
+/*%<
+ *     Set the post load integrity callback function 'checkisserverby'.
+ *     'checkisserverby' will be called if the NS TARGET is not within
+ *     the zone and there are A or AAAA records in the the zone.
+ *
+ * Require:
+ *     'zone' to be a valid zone.
+ */
+
 void
 dns_zone_setnotifydelay(dns_zone_t *zone, uint32_t delay);
 /*%<
index 3e08b77da1dee22397d4ceb76617f22d22580127..00e2bdb55d92f1259e3b5c9a21e9f7ccd29cb696 100644 (file)
@@ -377,6 +377,7 @@ struct dns_zone {
        dns_checkmxfunc_t checkmx;
        dns_checksrvfunc_t checksrv;
        dns_checknsfunc_t checkns;
+       dns_checkisservedbyfunc_t checkisservedby;
        /*%
         * Zones in certain states such as "waiting for zone transfer"
         * or "zone transfer in progress" are kept on per-state linked lists
@@ -2875,8 +2876,8 @@ zone_check_srv(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
 }
 
 static bool
-zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
-               dns_name_t *owner) {
+zone_check_glue(dns_zone_t *zone, dns_db_t *db, bool *has_a, bool *has_aaaa,
+               dns_name_t *name, dns_name_t *owner) {
        bool answer = true;
        isc_result_t result, tresult;
        char ownerbuf[DNS_NAME_FORMATSIZE];
@@ -2928,8 +2929,22 @@ zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
                                     NULL);
        }
        if (result == ISC_R_SUCCESS) {
+               SET_IF_NOT_NULL(has_a, true);
                dns_rdataset_disassociate(&a);
+               if (has_aaaa != NULL && !*has_aaaa) {
+                       result = dns_db_find(db, name, NULL, dns_rdatatype_aaaa,
+                                            DNS_DBFIND_GLUEOK, 0, NULL,
+                                            foundname, &aaaa, NULL);
+                       if (result == ISC_R_SUCCESS) {
+                               *has_aaaa = true;
+                       }
+                       if (dns_rdataset_isassociated(&aaaa)) {
+                               dns_rdataset_disassociate(&aaaa);
+                       }
+               }
                return true;
+       } else if (result == DNS_R_GLUE && has_a != NULL) {
+               *has_a = true;
        } else if (result == DNS_R_DELEGATION) {
                dns_rdataset_disassociate(&a);
        }
@@ -2944,12 +2959,16 @@ zone_check_glue(dns_zone_t *zone, dns_db_t *db, dns_name_t *name,
                        if (dns_rdataset_isassociated(&a)) {
                                dns_rdataset_disassociate(&a);
                        }
+                       SET_IF_NOT_NULL(has_aaaa, true);
                        dns_rdataset_disassociate(&aaaa);
                        return true;
                }
                if (tresult == DNS_R_DELEGATION || tresult == DNS_R_DNAME) {
                        dns_rdataset_disassociate(&aaaa);
                }
+               if (tresult == DNS_R_GLUE && has_aaaa != NULL) {
+                       *has_aaaa = true;
+               }
                if (result == DNS_R_GLUE || tresult == DNS_R_GLUE) {
                        /*
                         * Check glue against child zone.
@@ -3173,6 +3192,46 @@ isspf(const dns_rdata_t *rdata) {
        return false;
 }
 
+static bool
+zone_is_served_by(dns_zone_t *zone, dns_db_t *db, dns_rdatatype_t type,
+                 dns_name_t *name) {
+       dns_rdataset_t rdataset;
+       dns_fixedname_t found;
+       dns_name_t *foundname = dns_fixedname_initname(&found);
+       isc_result_t result;
+
+       /*
+        * Outside of zone, assume good when loading in named.
+        */
+       if (!dns_name_issubdomain(name, &zone->origin)) {
+               if (zone->checkisservedby != NULL) {
+                       return zone->checkisservedby(zone, type, name);
+               }
+               return true;
+       }
+
+       dns_rdataset_init(&rdataset);
+       result = dns_db_find(db, name, NULL, type, 0, 0, NULL, foundname,
+                            &rdataset, NULL);
+       if (dns_rdataset_isassociated(&rdataset)) {
+               dns_rdataset_disassociate(&rdataset);
+       }
+       switch (result) {
+       case DNS_R_DELEGATION:
+               if (zone->checkisservedby != NULL) {
+                       return zone->checkisservedby(zone, type, name);
+               }
+               /*
+                * Treat as success.
+                */
+               return true;
+       case ISC_R_SUCCESS:
+               return true;
+       default:
+               return false;
+       }
+}
+
 static bool
 integrity_checks(dns_zone_t *zone, dns_db_t *db) {
        dns_dbiterator_t *dbiterator = NULL;
@@ -3188,6 +3247,8 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
        dns_name_t *bottom;
        isc_result_t result;
        bool ok = true, have_spf, have_txt;
+       bool has_a = false;
+       bool has_aaaa = false;
        int level;
        char namebuf[DNS_NAME_FORMATSIZE];
 
@@ -3242,7 +3303,9 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
                        dns_rdataset_current(&rdataset, &rdata);
                        result = dns_rdata_tostruct(&rdata, &ns, NULL);
                        RUNTIME_CHECK(result == ISC_R_SUCCESS);
-                       if (!zone_check_glue(zone, db, &ns.name, name)) {
+                       if (!zone_check_glue(zone, db, &has_a, &has_aaaa,
+                                            &ns.name, name))
+                       {
                                ok = false;
                        }
                        dns_rdata_reset(&rdata);
@@ -3306,7 +3369,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
                result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_srv,
                                             0, 0, &rdataset, NULL);
                if (result != ISC_R_SUCCESS) {
-                       goto checkspf;
+                       goto checkforaaaa;
                }
                result = dns_rdataset_first(&rdataset);
                while (result == ISC_R_SUCCESS) {
@@ -3321,7 +3384,29 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
                }
                dns_rdataset_disassociate(&rdataset);
 
-       checkspf:
+       checkforaaaa:
+               /*
+                * Check if there is an A or AAAA RRset in the zone.
+                */
+               if (!has_a) {
+                       result = dns_db_findrdataset(db, node, NULL,
+                                                    dns_rdatatype_a, 0, 0,
+                                                    &rdataset, NULL);
+                       if (result == ISC_R_SUCCESS) {
+                               has_a = true;
+                               dns_rdataset_disassociate(&rdataset);
+                       }
+               }
+               if (!has_aaaa) {
+                       result = dns_db_findrdataset(db, node, NULL,
+                                                    dns_rdatatype_aaaa, 0, 0,
+                                                    &rdataset, NULL);
+                       if (result == ISC_R_SUCCESS) {
+                               has_aaaa = true;
+                               dns_rdataset_disassociate(&rdataset);
+                       }
+               }
+
                /*
                 * Check if there is a type SPF record without an
                 * SPF-formatted type TXT record also being present.
@@ -3371,6 +3456,70 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) {
                result = dns_dbiterator_next(dbiterator);
        }
 
+       if (has_a) {
+               has_a = false;
+               result = dns_db_find(db, &zone->origin, NULL, dns_rdatatype_ns,
+                                    0, 0, NULL, name, &rdataset, NULL);
+               if (result != ISC_R_SUCCESS) {
+                       if (dns_rdataset_isassociated(&rdataset)) {
+                               dns_rdataset_disassociate(&rdataset);
+                       }
+                       goto cleanup;
+               }
+               result = dns_rdataset_first(&rdataset);
+               while (result == ISC_R_SUCCESS) {
+                       dns_rdataset_current(&rdataset, &rdata);
+                       result = dns_rdata_tostruct(&rdata, &ns, NULL);
+                       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+                       dns_rdata_reset(&rdata);
+                       if (zone_is_served_by(zone, db, dns_rdatatype_a,
+                                             &ns.name))
+                       {
+                               has_a = true;
+                               break;
+                       }
+                       result = dns_rdataset_next(&rdataset);
+               }
+               dns_rdataset_disassociate(&rdataset);
+               if (!has_a) {
+                       dns_zone_log(zone, ISC_LOG_WARNING,
+                                    "zone has A records but is not served "
+                                    "by IPv4 servers");
+               }
+       }
+
+       if (has_aaaa) {
+               has_aaaa = false;
+               result = dns_db_find(db, &zone->origin, NULL, dns_rdatatype_ns,
+                                    0, 0, NULL, name, &rdataset, NULL);
+               if (result != ISC_R_SUCCESS) {
+                       if (dns_rdataset_isassociated(&rdataset)) {
+                               dns_rdataset_disassociate(&rdataset);
+                       }
+                       goto cleanup;
+               }
+               result = dns_rdataset_first(&rdataset);
+               while (result == ISC_R_SUCCESS) {
+                       dns_rdataset_current(&rdataset, &rdata);
+                       result = dns_rdata_tostruct(&rdata, &ns, NULL);
+                       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+                       dns_rdata_reset(&rdata);
+                       if (zone_is_served_by(zone, db, dns_rdatatype_aaaa,
+                                             &ns.name))
+                       {
+                               has_aaaa = true;
+                               break;
+                       }
+                       result = dns_rdataset_next(&rdataset);
+               }
+               dns_rdataset_disassociate(&rdataset);
+               if (!has_aaaa) {
+                       dns_zone_log(zone, ISC_LOG_WARNING,
+                                    "zone has AAAA records but is not served "
+                                    "by IPv6 servers");
+               }
+       }
+
 cleanup:
        if (node != NULL) {
                dns_db_detachnode(db, &node);
@@ -20092,6 +20241,13 @@ dns_zone_setcheckns(dns_zone_t *zone, dns_checknsfunc_t checkns) {
        zone->checkns = checkns;
 }
 
+void
+dns_zone_setcheckisservedby(dns_zone_t *zone,
+                           dns_checkisservedbyfunc_t checkisservedby) {
+       REQUIRE(DNS_ZONE_VALID(zone));
+       zone->checkisservedby = checkisservedby;
+}
+
 void
 dns_zone_setisself(dns_zone_t *zone, dns_isselffunc_t isself, void *arg) {
        REQUIRE(DNS_ZONE_VALID(zone));