2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-22 10:10:06 +00:00

Add zone "initial-file" option

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"; };'
This commit is contained in:
Evan Hunt 2025-04-13 00:28:49 -07:00
parent 2ad9516a72
commit 60b129da25
16 changed files with 174 additions and 18 deletions

View File

@ -656,7 +656,7 @@ load_zone(isc_mem_t *mctx, const char *zonename, const char *filename,
dns_zone_setstream(zone, stdin, fileformat, dns_zone_setstream(zone, stdin, fileformat,
&dns_master_style_default); &dns_master_style_default);
} else { } else {
dns_zone_setfile(zone, filename, fileformat, dns_zone_setfile(zone, filename, NULL, fileformat,
&dns_master_style_default); &dns_master_style_default);
} }
if (journal != NULL) { if (journal != NULL) {

View File

@ -6599,7 +6599,7 @@ add_keydata_zone(dns_view_t *view, const char *directory, isc_mem_t *mctx) {
CHECK(isc_file_sanitize( CHECK(isc_file_sanitize(
directory, defaultview ? "managed-keys" : view->name, directory, defaultview ? "managed-keys" : view->name,
defaultview ? "bind" : "mkeys", filename, sizeof(filename))); 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_master_style_default);
dns_zone_setview(zone, view); dns_zone_setview(zone, view);

View 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 *options = NULL;
const cfg_obj_t *obj; const cfg_obj_t *obj;
const char *filename = NULL; const char *filename = NULL;
const char *initial_file = NULL;
const char *kaspname = NULL; const char *kaspname = NULL;
const char *dupcheck; const char *dupcheck;
dns_checkdstype_t checkdstype = dns_checkdstype_yes; 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); 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) { if (ztype == dns_zone_secondary || ztype == dns_zone_mirror) {
masterformat = dns_masterformat_raw; masterformat = dns_masterformat_raw;
} else { } 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); size_t signedlen = strlen(filename) + sizeof(SIGNED);
char *signedname; char *signedname;
dns_zone_setfile(raw, filename, masterformat, masterstyle); dns_zone_setfile(raw, filename, initial_file, masterformat,
masterstyle);
signedname = isc_mem_get(mctx, signedlen); signedname = isc_mem_get(mctx, signedlen);
(void)snprintf(signedname, signedlen, "%s" SIGNED, filename); (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); isc_mem_put(mctx, signedname, signedlen);
} else { } else {
dns_zone_setfile(zone, filename, masterformat, masterstyle); dns_zone_setfile(zone, filename, initial_file, masterformat,
masterstyle);
} }
obj = NULL; obj = NULL;

View File

@ -11,6 +11,7 @@
import difflib import difflib
import shutil import shutil
import os
from typing import Optional from typing import Optional
import dns.rcode import dns.rcode
@ -150,3 +151,7 @@ def file_contents_equal(file1, file2):
assert not line.startswith("+ ") and not line.startswith( assert not line.startswith("+ ") and not line.startswith(
"- " "- "
), f'file contents of "{file1}" and "{file2}" differ' ), f'file contents of "{file1}" and "{file2}" differ'
def file_empty(file):
assert os.path.getsize(file) == 0

View File

@ -43,3 +43,15 @@ zone "missing" {
type primary; type primary;
file "missing.db"; 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";
};

View File

@ -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

View File

@ -15,6 +15,9 @@ import dns.message
import dns.zone import dns.zone
import isctest import isctest
import pytest
pytestmark = pytest.mark.extra_artifacts(["ns2/copied.db", "ns2/present.db"])
def test_masterfile_include_semantics(): 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) 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(): def test_masterfile_missing_master_file_servfail():
"""Test nameserver returning SERVFAIL for a missing master file""" """Test nameserver returning SERVFAIL for a missing master file"""
msg_soa = dns.message.make_query("missing.", "SOA") msg_soa = dns.message.make_query("missing.", "SOA")

View File

@ -7149,6 +7149,32 @@ Zone Options
specified in a zone of type :any:`forward`, no forwarding is done for specified in a zone of type :any:`forward`, no forwarding is done for
the zone and the global options are not used. 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 .. namedconf:statement:: journal
:tags: zone :tags: zone
:short: Allows the default journal's filename to be overridden. :short: Allows the default journal's filename to be overridden.

View File

@ -27,6 +27,7 @@ zone <string> [ <class> ] {
file <quoted_string>; file <quoted_string>;
forward ( first | only ); forward ( first | only );
forwarders [ port <integer> ] [ tls <string> ] { ( <ipv4_address> | <ipv6_address> ) [ port <integer> ] [ tls <string> ]; ... }; forwarders [ port <integer> ] [ tls <string> ] { ( <ipv4_address> | <ipv6_address> ) [ port <integer> ] [ tls <string> ]; ... };
initial-file <quoted_string>;
inline-signing <boolean>; inline-signing <boolean>;
ixfr-from-differences <boolean>; ixfr-from-differences <boolean>;
journal <quoted_string>; journal <quoted_string>;

View File

@ -212,7 +212,7 @@ LLVMFuzzerInitialize(int *argc ISC_ATTR_UNUSED, char ***argv ISC_ATTR_UNUSED) {
dns_zone_setclass(zone, view->rdclass); dns_zone_setclass(zone, view->rdclass);
dns_zone_settype(zone, dns_zone_primary); dns_zone_settype(zone, dns_zone_primary);
dns_zone_setkeydirectory(zone, wd); 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); &dns_master_style_default);
result = dns_zone_load(zone, false); result = dns_zone_load(zone, false);

View File

@ -290,16 +290,18 @@ dns_zone_getorigin(dns_zone_t *zone);
*/ */
void void
dns_zone_setfile(dns_zone_t *zone, const char *file, dns_masterformat_t format, dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file,
const dns_master_style_t *style); dns_masterformat_t format, const dns_master_style_t *style);
/*%< /*%<
* Sets the name of the master file in the format of 'format' from which * Sets the name of the master file in the format of 'format' from which
* the zone loads its database to 'file'. * the zone loads its database to 'file'.
* *
* For zones that have no associated master file, 'file' will be NULL. * 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 * For zones with persistent databases, the file name setting is ignored.
* setting is ignored.
* *
* Require: * Require:
*\li 'zone' to be a valid zone. *\li 'zone' to be a valid zone.

View File

@ -284,6 +284,7 @@ struct dns_zone {
dns_name_t origin; dns_name_t origin;
dns_name_t rad; dns_name_t rad;
char *masterfile; char *masterfile;
char *initfile;
const FILE *stream; /* loading from a stream? */ const FILE *stream; /* loading from a stream? */
ISC_LIST(dns_include_t) includes; /* Include files */ ISC_LIST(dns_include_t) includes; /* Include files */
ISC_LIST(dns_include_t) newincludes; /* Loading */ ISC_LIST(dns_include_t) newincludes; /* Loading */
@ -1289,9 +1290,13 @@ zone_free(dns_zone_t *zone) {
if (zone->masterfile != NULL) { if (zone->masterfile != NULL) {
isc_mem_free(zone->mctx, zone->masterfile); isc_mem_free(zone->mctx, zone->masterfile);
} }
if (zone->initfile != NULL) {
isc_mem_free(zone->mctx, zone->initfile);
}
if (zone->keydirectory != NULL) { if (zone->keydirectory != NULL) {
isc_mem_free(zone->mctx, zone->keydirectory); isc_mem_free(zone->mctx, zone->keydirectory);
} }
if (zone->kasp != NULL) { if (zone->kasp != NULL) {
dns_kasp_detach(&zone->kasp); dns_kasp_detach(&zone->kasp);
} }
@ -1793,13 +1798,14 @@ setstring(dns_zone_t *zone, char **field, const char *value) {
} }
void void
dns_zone_setfile(dns_zone_t *zone, const char *file, dns_masterformat_t format, dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file,
const dns_master_style_t *style) { dns_masterformat_t format, const dns_master_style_t *style) {
REQUIRE(DNS_ZONE_VALID(zone)); REQUIRE(DNS_ZONE_VALID(zone));
REQUIRE(zone->stream == NULL); REQUIRE(zone->stream == NULL);
LOCK_ZONE(zone); LOCK_ZONE(zone);
setstring(zone, &zone->masterfile, file); setstring(zone, &zone->masterfile, file);
setstring(zone, &zone->initfile, initial_file);
zone->masterformat = format; zone->masterformat = format;
if (format == dns_masterformat_text) { if (format == dns_masterformat_text) {
zone->masterstyle = style; zone->masterstyle = style;
@ -2105,6 +2111,42 @@ zone_touched(dns_zone_t *zone) {
return false; 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 * Note: when dealing with inline-signed zones, external callers will always
* call zone_load() for the secure zone; zone_load() calls itself recursively * 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), dns_zone_logc(zone, DNS_LOGCATEGORY_ZONELOAD, ISC_LOG_DEBUG(1),
"starting load"); "starting load");

View File

@ -2430,6 +2430,7 @@ static cfg_clausedef_t zone_only_clauses[] = {
CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR | CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY | CFG_ZONE_MIRROR |
CFG_ZONE_STUB | CFG_ZONE_HINT | CFG_ZONE_REDIRECT }, CFG_ZONE_STUB | CFG_ZONE_HINT | CFG_ZONE_REDIRECT },
{ "in-view", &cfg_type_astring, CFG_ZONE_INVIEW }, { "in-view", &cfg_type_astring, CFG_ZONE_INVIEW },
{ "initial-file", &cfg_type_qstring, CFG_ZONE_PRIMARY },
{ "inline-signing", &cfg_type_boolean, { "inline-signing", &cfg_type_boolean,
CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY }, CFG_ZONE_PRIMARY | CFG_ZONE_SECONDARY },
{ "ixfr-base", NULL, CFG_CLAUSEFLAG_ANCIENT }, { "ixfr-base", NULL, CFG_CLAUSEFLAG_ANCIENT },

View File

@ -120,7 +120,8 @@ nsec3param_change_test(const nsec3param_change_test_params_t *test) {
assert_int_equal(result, ISC_R_SUCCESS); assert_int_equal(result, ISC_R_SUCCESS);
dns_zone_setfile(zone, TESTS_DIR "/testdata/nsec3param/nsec3.db.signed", 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); result = dns_zone_load(zone, false);
assert_int_equal(result, ISC_R_SUCCESS); assert_int_equal(result, ISC_R_SUCCESS);

View File

@ -184,7 +184,7 @@ ISC_LOOP_TEST_IMPL(asyncload_zone) {
fwrite(buf, 1, n, zonefile); fwrite(buf, 1, n, zonefile);
fflush(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_master_style_default);
dns_zone_asyncload(zone, false, load_done_first, zone); 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); result = dns_test_makezone("foo", &zone1, NULL, true);
assert_int_equal(result, ISC_R_SUCCESS); 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); dns_masterformat_text, &dns_master_style_default);
view = dns_zone_getview(zone1); view = dns_zone_getview(zone1);
result = dns_test_makezone("bar", &zone2, view, false); result = dns_test_makezone("bar", &zone2, view, false);
assert_int_equal(result, ISC_R_SUCCESS); 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); dns_masterformat_text, &dns_master_style_default);
/* This one will fail to load */ /* This one will fail to load */
result = dns_test_makezone("fake", &zone3, view, false); result = dns_test_makezone("fake", &zone3, view, false);
assert_int_equal(result, ISC_R_SUCCESS); 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); dns_masterformat_text, &dns_master_style_default);
rcu_read_lock(); rcu_read_lock();

View 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. * 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); &dns_master_style_default);
result = dns_zone_load(served_zone, false); result = dns_zone_load(served_zone, false);
if (result != ISC_R_SUCCESS) { if (result != ISC_R_SUCCESS) {