]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add zone "initial-file" option
authorEvan Hunt <each@isc.org>
Sun, 13 Apr 2025 07:28:49 +0000 (00:28 -0700)
committerEvan Hunt <each@isc.org>
Tue, 3 Jun 2025 19:03:07 +0000 (12:03 -0700)
When loading a primary zone for the first time, if the zonefile
does not exist but an "initial-file" option has been set, then a
new file will be copied into place from the path specified by
"initial-file".

This can be used to simplify the process of adding new zones. For
instance, a template zonefile could be used by running:

    $ rndc addzone example.com \
        '{ type primary; file "example.db"; initial-file "template.db"; };'

16 files changed:
bin/check/check-tool.c
bin/named/server.c
bin/named/zoneconf.c
bin/tests/system/isctest/check.py
bin/tests/system/masterfile/ns2/named.conf.j2
bin/tests/system/masterfile/setup.sh [new file with mode: 0644]
bin/tests/system/masterfile/tests_masterfile.py
doc/arm/reference.rst
doc/misc/primary.zoneopt
fuzz/dns_message_checksig.c
lib/dns/include/dns/zone.h
lib/dns/zone.c
lib/isccfg/namedconf.c
tests/dns/nsec3param_test.c
tests/dns/zt_test.c
tests/libtest/ns.c

index 13c5ec7593d1e99432b6667b87b6dc074a655db0..3bd232c8be7569cff47aa7729c108777b3344628 100644 (file)
@@ -656,7 +656,7 @@ load_zone(isc_mem_t *mctx, const char *zonename, const char *filename,
                dns_zone_setstream(zone, stdin, fileformat,
                                   &dns_master_style_default);
        } else {
-               dns_zone_setfile(zone, filename, fileformat,
+               dns_zone_setfile(zone, filename, NULL, fileformat,
                                 &dns_master_style_default);
        }
        if (journal != NULL) {
index e539d77867863197f8ac7ba02da59db5d4008233..7c35cc4a51bfe61a9a0fe774ed299a9e40ed9a66 100644 (file)
@@ -6599,7 +6599,7 @@ add_keydata_zone(dns_view_t *view, const char *directory, isc_mem_t *mctx) {
        CHECK(isc_file_sanitize(
                directory, defaultview ? "managed-keys" : view->name,
                defaultview ? "bind" : "mkeys", filename, sizeof(filename)));
-       dns_zone_setfile(zone, filename, dns_masterformat_text,
+       dns_zone_setfile(zone, filename, NULL, dns_masterformat_text,
                         &dns_master_style_default);
 
        dns_zone_setview(zone, view);
index 899d83bbcddb08317e669942381407b81e123631..ca24ec9851d4d656be5c74e1e615a66a03d89562 100644 (file)
@@ -878,6 +878,7 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig,
        const cfg_obj_t *options = NULL;
        const cfg_obj_t *obj;
        const char *filename = NULL;
+       const char *initial_file = NULL;
        const char *kaspname = NULL;
        const char *dupcheck;
        dns_checkdstype_t checkdstype = dns_checkdstype_yes;
@@ -996,6 +997,12 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig,
                filename = cfg_obj_asstring(obj);
        }
 
+       obj = NULL;
+       result = cfg_map_get(zoptions, "initial-file", &obj);
+       if (result == ISC_R_SUCCESS) {
+               initial_file = cfg_obj_asstring(obj);
+       }
+
        if (ztype == dns_zone_secondary || ztype == dns_zone_mirror) {
                masterformat = dns_masterformat_raw;
        } else {
@@ -1053,14 +1060,17 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig,
                size_t signedlen = strlen(filename) + sizeof(SIGNED);
                char *signedname;
 
-               dns_zone_setfile(raw, filename, masterformat, masterstyle);
+               dns_zone_setfile(raw, filename, initial_file, masterformat,
+                                masterstyle);
                signedname = isc_mem_get(mctx, signedlen);
 
                (void)snprintf(signedname, signedlen, "%s" SIGNED, filename);
-               dns_zone_setfile(zone, signedname, dns_masterformat_raw, NULL);
+               dns_zone_setfile(zone, signedname, NULL, dns_masterformat_raw,
+                                NULL);
                isc_mem_put(mctx, signedname, signedlen);
        } else {
-               dns_zone_setfile(zone, filename, masterformat, masterstyle);
+               dns_zone_setfile(zone, filename, initial_file, masterformat,
+                                masterstyle);
        }
 
        obj = NULL;
index b35dfe848eaed941cea6bd1df4554faf50d9d8bb..5c78ba73470023f3f8a7c809c2f8ff59e63f1b58 100644 (file)
@@ -11,6 +11,7 @@
 
 import difflib
 import shutil
+import os
 from typing import Optional
 
 import dns.rcode
@@ -150,3 +151,7 @@ def file_contents_equal(file1, file2):
         assert not line.startswith("+ ") and not line.startswith(
             "- "
         ), f'file contents of "{file1}" and "{file2}" differ'
+
+
+def file_empty(file):
+    assert os.path.getsize(file) == 0
index 6333b05f40d422c9761e981ddb0d9a77dcdd97fa..50a1d2e2d95ab71f49a0899e38e7986558ae9d48 100644 (file)
@@ -43,3 +43,15 @@ zone "missing" {
        type primary;
        file "missing.db";
 };
+
+zone "initial" {
+       type primary;
+       file "copied.db";
+       initial-file "example.db";
+};
+
+zone "present" {
+       type primary;
+       file "present.db";
+       initial-file "example.db";
+};
diff --git a/bin/tests/system/masterfile/setup.sh b/bin/tests/system/masterfile/setup.sh
new file mode 100644 (file)
index 0000000..9f57f5a
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0.  If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+# shellcheck source=conf.sh
+. ../conf.sh
+
+set -e
+
+touch ns2/present.db
index 9aaaa769a537778003ce2f00cf3a193bc5f56c3c..8b25f6db076dea219e633c8e84ffc4f5fc6c7e13 100644 (file)
@@ -15,6 +15,9 @@ import dns.message
 import dns.zone
 
 import isctest
+import pytest
+
+pytestmark = pytest.mark.extra_artifacts(["ns2/copied.db", "ns2/present.db"])
 
 
 def test_masterfile_include_semantics():
@@ -87,6 +90,24 @@ example.     300     IN      SOA     mname1. . 2010042407 20 20 1814400 3600
     isctest.check.rrsets_equal(res_soa.answer, expected.answer, compare_ttl=True)
 
 
+def test_masterfile_initial_file():
+    """Test zone configuration with initial template files"""
+    msg_soa = dns.message.make_query("initial.", "SOA")
+    res_soa = isctest.query.tcp(msg_soa, "10.53.0.2")
+    expected_soa_rr = """;ANSWER
+initial.       300     IN      SOA     mname1. . 2010042407 20 20 1814400 3600
+"""
+    expected = dns.message.from_text(expected_soa_rr)
+    isctest.check.rrsets_equal(res_soa.answer, expected.answer)
+    isctest.check.file_contents_equal("ns2/example.db", "ns2/copied.db")
+
+    # the 'present.db' file already existed and shouldn't load
+    msg_soa = dns.message.make_query("present.", "SOA")
+    res_soa = isctest.query.tcp(msg_soa, "10.53.0.2")
+    isctest.check.servfail(res_soa)
+    isctest.check.file_empty("ns2/present.db")
+
+
 def test_masterfile_missing_master_file_servfail():
     """Test nameserver returning SERVFAIL for a missing master file"""
     msg_soa = dns.message.make_query("missing.", "SOA")
index efdcdc5b0aa1f1e4814086d98a623a4ae26494d1..0c81a568008e49c2bcb9a36d0861fb3330f4236f 100644 (file)
@@ -7149,6 +7149,32 @@ Zone Options
    specified in a zone of type :any:`forward`, no forwarding is done for
    the zone and the global options are not used.
 
+.. namedconf:statement:: initial-file
+   :tags: zone
+   :short: Specifies a file with the initial contents of a newly created zone.
+
+   When a :any:`primary <type primary>` zone is loaded for the first time,
+   if the zone's :any:`file` does not exist but ``initial-file`` does, the
+   zone file is copied into place from the initial file before loading.
+   This can be used to simplify the process of adding new zones, removing
+   the need to create the zone file before configuring the zone. For example,
+   a template zonefile could be used by running:
+
+   ::
+
+      $ rndc addzone example.com \
+        '{ type primary; file "example.db"; initial-file "template.db"; };'
+
+    Using "@" to reference the zone origin name within ``template.db``
+    allows the same file to be used with multiple zones, as in:
+
+    ::
+
+        $TTL 300
+        @              IN SOA  ns hosmaster 1 1800 1800 86400 3600
+                        NS     ns
+        ns              A       192.0.2.1
+
 .. namedconf:statement:: journal
    :tags: zone
    :short: Allows the default journal's filename to be overridden.
index d43f66a55aeccd4bec41f027f0f13c14a048dbb0..74a314999abb3e804fcc6737b137f15b4e8462f9 100644 (file)
@@ -27,6 +27,7 @@ zone <string> [ <class> ] {
        file <quoted_string>;
        forward ( first | only );
        forwarders [ port <integer> ] [ tls <string> ] { ( <ipv4_address> | <ipv6_address> ) [ port <integer> ] [ tls <string> ]; ... };
+       initial-file <quoted_string>;
        inline-signing <boolean>;
        ixfr-from-differences <boolean>;
        journal <quoted_string>;
index e1c2eeefb3b3b7dab673b5d96afc659c5bbfe81d..01ef02f58541a8c434f0fce71319362c6d2962a8 100644 (file)
@@ -212,7 +212,7 @@ LLVMFuzzerInitialize(int *argc ISC_ATTR_UNUSED, char ***argv ISC_ATTR_UNUSED) {
        dns_zone_setclass(zone, view->rdclass);
        dns_zone_settype(zone, dns_zone_primary);
        dns_zone_setkeydirectory(zone, wd);
-       dns_zone_setfile(zone, pathbuf, dns_masterformat_text,
+       dns_zone_setfile(zone, pathbuf, NULL, dns_masterformat_text,
                         &dns_master_style_default);
 
        result = dns_zone_load(zone, false);
index fc3f601028614be94e2f9696bd311e633822c6f6..bd015e7de9b0180bd89c447af80c05a0b9cc2982 100644 (file)
@@ -290,16 +290,18 @@ dns_zone_getorigin(dns_zone_t *zone);
  */
 
 void
-dns_zone_setfile(dns_zone_t *zone, const char *file, dns_masterformat_t format,
-                const dns_master_style_t *style);
+dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file,
+                dns_masterformat_t format, const dns_master_style_t *style);
 /*%<
  *    Sets the name of the master file in the format of 'format' from which
  *    the zone loads its database to 'file'.
  *
  *    For zones that have no associated master file, 'file' will be NULL.
+ *    For some zone types, e.g. secondary zones, 'file' is optional, but
+ *    for primary zones it is mandatory. If the master file does not exist
+ *    during loading, then it will be copied into place from 'initial_file'.
  *
- *     For zones with persistent databases, the file name
- *     setting is ignored.
+ *    For zones with persistent databases, the file name setting is ignored.
  *
  * Require:
  *\li  'zone' to be a valid zone.
index 7e61d6869ceff2c9b799bf9383e432932a070314..89fa19a3830c53b753c6ba0fbc76e081770fcafc 100644 (file)
@@ -284,6 +284,7 @@ struct dns_zone {
        dns_name_t origin;
        dns_name_t rad;
        char *masterfile;
+       char *initfile;
        const FILE *stream;                  /* loading from a stream? */
        ISC_LIST(dns_include_t) includes;    /* Include files */
        ISC_LIST(dns_include_t) newincludes; /* Loading */
@@ -1289,9 +1290,13 @@ zone_free(dns_zone_t *zone) {
        if (zone->masterfile != NULL) {
                isc_mem_free(zone->mctx, zone->masterfile);
        }
+       if (zone->initfile != NULL) {
+               isc_mem_free(zone->mctx, zone->initfile);
+       }
        if (zone->keydirectory != NULL) {
                isc_mem_free(zone->mctx, zone->keydirectory);
        }
+
        if (zone->kasp != NULL) {
                dns_kasp_detach(&zone->kasp);
        }
@@ -1793,13 +1798,14 @@ setstring(dns_zone_t *zone, char **field, const char *value) {
 }
 
 void
-dns_zone_setfile(dns_zone_t *zone, const char *file, dns_masterformat_t format,
-                const dns_master_style_t *style) {
+dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file,
+                dns_masterformat_t format, const dns_master_style_t *style) {
        REQUIRE(DNS_ZONE_VALID(zone));
        REQUIRE(zone->stream == NULL);
 
        LOCK_ZONE(zone);
        setstring(zone, &zone->masterfile, file);
+       setstring(zone, &zone->initfile, initial_file);
        zone->masterformat = format;
        if (format == dns_masterformat_text) {
                zone->masterstyle = style;
@@ -2105,6 +2111,42 @@ zone_touched(dns_zone_t *zone) {
        return false;
 }
 
+static isc_result_t
+copy_initfile(dns_zone_t *zone) {
+       isc_result_t result;
+       FILE *input = NULL, *output = NULL;
+       size_t len;
+
+       CHECK(isc_stdio_open(zone->initfile, "r", &input));
+       CHECK(isc_stdio_open(zone->masterfile, "w", &output));
+
+       CHECK(isc_file_getsizefd(fileno(input), (off_t *)&len));
+
+       do {
+               char buf[BUFSIZ];
+               size_t rval;
+
+               result = isc_stdio_read(buf, 1, sizeof(buf), input, &rval);
+               if (result != ISC_R_SUCCESS && result != ISC_R_EOF) {
+                       goto failure;
+               }
+               CHECK(isc_stdio_write(buf, rval, 1, output, NULL));
+               len -= rval;
+       } while (len > 0);
+
+failure:
+       if (input != NULL) {
+               isc_stdio_close(input);
+       }
+       if (output != NULL) {
+               if (result != ISC_R_SUCCESS) {
+                       isc_file_remove(zone->masterfile);
+               }
+               isc_stdio_close(output);
+       }
+       return result;
+}
+
 /*
  * Note: when dealing with inline-signed zones, external callers will always
  * call zone_load() for the secure zone; zone_load() calls itself recursively
@@ -2347,6 +2389,22 @@ zone_load(dns_zone_t *zone, unsigned int flags, bool locked) {
                }
        }
 
+       if (zone->type == dns_zone_primary && zone->masterfile != NULL &&
+           !isc_file_exists(zone->masterfile) && zone->initfile != NULL)
+       {
+               dns_zone_logc(zone, DNS_LOGCATEGORY_ZONELOAD, ISC_LOG_INFO,
+                             "zone file %s not found; copying initial "
+                             "file %s",
+                             zone->masterfile, zone->initfile);
+               result = copy_initfile(zone);
+               if (result != ISC_R_SUCCESS) {
+                       dns_zone_logc(zone, DNS_LOGCATEGORY_ZONELOAD,
+                                     ISC_LOG_ERROR, "copy from %s failed: %s",
+                                     zone->initfile,
+                                     isc_result_totext(result));
+               }
+       }
+
        dns_zone_logc(zone, DNS_LOGCATEGORY_ZONELOAD, ISC_LOG_DEBUG(1),
                      "starting load");
 
index 7372a604152a1632e0aa0e033af64f550890b663..82c50890f9679213486f1ef8ad390d444c25ca9d 100644 (file)
@@ -2430,6 +2430,7 @@ static cfg_clausedef_t zone_only_clauses[] = {
          CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR |
                  CFG_ZONE_STUB | CFG_ZONE_HINT | CFG_ZONE_REDIRECT },
        { "in-view", &cfg_type_astring, CFG_ZONE_INVIEW },
+       { "initial-file", &cfg_type_qstring, CFG_ZONE_PRIMARY },
        { "inline-signing", &cfg_type_boolean,
          CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY },
        { "ixfr-base", NULL, CFG_CLAUSEFLAG_ANCIENT },
index 35e3ce034302e9d7d4095b7421279c1337a7d715..ec194f297de0c16f669d20bb98ea778eba34821c 100644 (file)
@@ -120,7 +120,8 @@ nsec3param_change_test(const nsec3param_change_test_params_t *test) {
        assert_int_equal(result, ISC_R_SUCCESS);
 
        dns_zone_setfile(zone, TESTS_DIR "/testdata/nsec3param/nsec3.db.signed",
-                        dns_masterformat_text, &dns_master_style_default);
+                        NULL, dns_masterformat_text,
+                        &dns_master_style_default);
 
        result = dns_zone_load(zone, false);
        assert_int_equal(result, ISC_R_SUCCESS);
index eea991afd645d2bb09986f9dbe96a04d971bce6f..803eabc15e54d0bd68497c1f78979b1421e17c4e 100644 (file)
@@ -184,7 +184,7 @@ ISC_LOOP_TEST_IMPL(asyncload_zone) {
        fwrite(buf, 1, n, zonefile);
        fflush(zonefile);
 
-       dns_zone_setfile(zone, "./zone.data", dns_masterformat_text,
+       dns_zone_setfile(zone, "./zone.data", NULL, dns_masterformat_text,
                         &dns_master_style_default);
 
        dns_zone_asyncload(zone, false, load_done_first, zone);
@@ -235,19 +235,19 @@ ISC_LOOP_TEST_IMPL(asyncload_zt) {
 
        result = dns_test_makezone("foo", &zone1, NULL, true);
        assert_int_equal(result, ISC_R_SUCCESS);
-       dns_zone_setfile(zone1, TESTS_DIR "/testdata/zt/zone1.db",
+       dns_zone_setfile(zone1, TESTS_DIR "/testdata/zt/zone1.db", NULL,
                         dns_masterformat_text, &dns_master_style_default);
        view = dns_zone_getview(zone1);
 
        result = dns_test_makezone("bar", &zone2, view, false);
        assert_int_equal(result, ISC_R_SUCCESS);
-       dns_zone_setfile(zone2, TESTS_DIR "/testdata/zt/zone1.db",
+       dns_zone_setfile(zone2, TESTS_DIR "/testdata/zt/zone1.db", NULL,
                         dns_masterformat_text, &dns_master_style_default);
 
        /* This one will fail to load */
        result = dns_test_makezone("fake", &zone3, view, false);
        assert_int_equal(result, ISC_R_SUCCESS);
-       dns_zone_setfile(zone3, TESTS_DIR "/testdata/zt/nonexistent.db",
+       dns_zone_setfile(zone3, TESTS_DIR "/testdata/zt/nonexistent.db", NULL,
                         dns_masterformat_text, &dns_master_style_default);
 
        rcu_read_lock();
index 64c94d6f0563cb851cfd030e96ca52558a3607c0..83e5c3f2b7bb3f2935b2c4bdc460a32f8ab0a294 100644 (file)
@@ -168,7 +168,7 @@ ns_test_serve_zone(const char *zonename, const char *filename,
        /*
         * Set path to the master file for the zone and then load it.
         */
-       dns_zone_setfile(served_zone, filename, dns_masterformat_text,
+       dns_zone_setfile(served_zone, filename, NULL, dns_masterformat_text,
                         &dns_master_style_default);
        result = dns_zone_load(served_zone, false);
        if (result != ISC_R_SUCCESS) {