From: Evan Hunt Date: Sun, 13 Apr 2025 07:28:49 +0000 (-0700) Subject: Add zone "initial-file" option X-Git-Tag: v9.21.9~5^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=60b129da253d72fd4b65d93f8590789ab92b8120;p=thirdparty%2Fbind9.git 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"; };' --- diff --git a/bin/check/check-tool.c b/bin/check/check-tool.c index 13c5ec7593d..3bd232c8be7 100644 --- a/bin/check/check-tool.c +++ b/bin/check/check-tool.c @@ -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) { diff --git a/bin/named/server.c b/bin/named/server.c index e539d778678..7c35cc4a51b 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -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); diff --git a/bin/named/zoneconf.c b/bin/named/zoneconf.c index 899d83bbcdd..ca24ec9851d 100644 --- a/bin/named/zoneconf.c +++ b/bin/named/zoneconf.c @@ -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; diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index b35dfe848ea..5c78ba73470 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -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 diff --git a/bin/tests/system/masterfile/ns2/named.conf.j2 b/bin/tests/system/masterfile/ns2/named.conf.j2 index 6333b05f40d..50a1d2e2d95 100644 --- a/bin/tests/system/masterfile/ns2/named.conf.j2 +++ b/bin/tests/system/masterfile/ns2/named.conf.j2 @@ -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 index 00000000000..9f57f5a9b30 --- /dev/null +++ b/bin/tests/system/masterfile/setup.sh @@ -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 diff --git a/bin/tests/system/masterfile/tests_masterfile.py b/bin/tests/system/masterfile/tests_masterfile.py index 9aaaa769a53..8b25f6db076 100644 --- a/bin/tests/system/masterfile/tests_masterfile.py +++ b/bin/tests/system/masterfile/tests_masterfile.py @@ -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") diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index efdcdc5b0aa..0c81a568008 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -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 ` 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. diff --git a/doc/misc/primary.zoneopt b/doc/misc/primary.zoneopt index d43f66a55ae..74a314999ab 100644 --- a/doc/misc/primary.zoneopt +++ b/doc/misc/primary.zoneopt @@ -27,6 +27,7 @@ zone [ ] { file ; forward ( first | only ); forwarders [ port ] [ tls ] { ( | ) [ port ] [ tls ]; ... }; + initial-file ; inline-signing ; ixfr-from-differences ; journal ; diff --git a/fuzz/dns_message_checksig.c b/fuzz/dns_message_checksig.c index e1c2eeefb3b..01ef02f5854 100644 --- a/fuzz/dns_message_checksig.c +++ b/fuzz/dns_message_checksig.c @@ -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); diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index fc3f6010286..bd015e7de9b 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -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. diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 7e61d6869ce..89fa19a3830 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -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"); diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index 7372a604152..82c50890f96 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -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 }, diff --git a/tests/dns/nsec3param_test.c b/tests/dns/nsec3param_test.c index 35e3ce03430..ec194f297de 100644 --- a/tests/dns/nsec3param_test.c +++ b/tests/dns/nsec3param_test.c @@ -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); diff --git a/tests/dns/zt_test.c b/tests/dns/zt_test.c index eea991afd64..803eabc15e5 100644 --- a/tests/dns/zt_test.c +++ b/tests/dns/zt_test.c @@ -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(); diff --git a/tests/libtest/ns.c b/tests/libtest/ns.c index 64c94d6f056..83e5c3f2b7b 100644 --- a/tests/libtest/ns.c +++ b/tests/libtest/ns.c @@ -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) {