diff --git a/bin/tests/system/masterfile/ns2/named.conf.j2 b/bin/tests/system/masterfile/ns2/named.conf.j2 index 50a1d2e2d9..442d273a8c 100644 --- a/bin/tests/system/masterfile/ns2/named.conf.j2 +++ b/bin/tests/system/masterfile/ns2/named.conf.j2 @@ -36,12 +36,12 @@ zone "." { zone "example" { type primary; - file "example.db"; + file "$name.db"; }; zone "missing" { type primary; - file "missing.db"; + file "$name.db"; }; zone "initial" { diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index 0c81a56800..63535eddde 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -7132,13 +7132,21 @@ Zone Options :tags: zone :short: Specifies the zone's filename. - This sets the zone's filename. In :any:`primary `, :any:`hint `, and :any:`redirect ` + This sets the zone's filename. In :any:`primary `, + :any:`hint `, and :any:`redirect ` zones which do not have :any:`primaries` defined, zone data is loaded from this file. In :any:`secondary `, :any:`mirror `, :any:`stub `, and :any:`redirect ` zones which do have :any:`primaries` defined, zone data is retrieved from another server and saved in this file. This option is not applicable to other zone types. + The filename can be generated parametrically by including special + tokens in the string: the first instance of ``$name`` in the string + is replaced with the zone name in lower case; the first instance of + ``$type`` is replaced with the zone type -- i.e., ``primary``, + ``secondary``, etc); and the first instance of ``$view`` is replaced + with the view name. These tokens are case-insensitive. + :any:`forward` This option is only meaningful if the zone has a forwarders list. The ``only`` value causes the lookup to fail after trying the forwarders and getting no @@ -7163,10 +7171,12 @@ Zone Options :: $ rndc addzone example.com \ - '{ type primary; file "example.db"; initial-file "template.db"; };' + '{ type primary; file "$name.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: + This creates a zone ``example.com``, with filename ``example.com.db``. + + Using "@" to reference the zone origin within the initial file + allows the same file to be used for multiple zones, as in: :: diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 89fa19a383..8edc1beb5f 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -1797,6 +1797,108 @@ setstring(dns_zone_t *zone, char **field, const char *value) { *field = copy; } +static int +position_order(const void *a, const void *b) { + /* sort char pointers in order of which occurs first in memory */ + return (char *)*(char **)a - (char *)*(char **)b; +} + +static isc_result_t +putmem(isc_buffer_t *b, const char *base, size_t length) { + size_t space = isc_buffer_availablelength(b) - 1; + if (space < length) { + isc_buffer_putmem(b, (const unsigned char *)base, space); + return ISC_R_NOSPACE; + } + + isc_buffer_putmem(b, (const unsigned char *)base, length); + return ISC_R_SUCCESS; +} + +/* + * Set the masterfile field, expanding $name to the zone name, + * $type to the zone type, and $view to the view name. Cap the + * length at PATH_MAX. + */ +static void +setfilename(dns_zone_t *zone, char **field, const char *value) { + isc_result_t result; + char *t = NULL, *n = NULL, *v = NULL; + char *positions[3]; + char filename[PATH_MAX]; + isc_buffer_t b; + size_t tags = 0; + + if (value == NULL) { + *field = NULL; + return; + } + + t = strcasestr(value, "$type"); + if (t != NULL) { + positions[tags++] = t; + } + + n = strcasestr(value, "$name"); + if (n != NULL) { + positions[tags++] = n; + } + + v = strcasestr(value, "$view"); + if (v != NULL) { + positions[tags++] = v; + } + + if (tags == 0) { + setstring(zone, field, value); + return; + } + + isc_buffer_init(&b, filename, sizeof(filename)); + + /* sort the tag offsets in order of occurrence */ + qsort(positions, tags, sizeof(char *), position_order); + + const char *p = value; + for (size_t i = 0; i < tags; i++) { + size_t tokenlen = 0; + + CHECK(putmem(&b, p, (positions[i] - p))); + + p = positions[i]; + INSIST(p != NULL); + if (p == n) { + dns_fixedname_t fn; + dns_name_t *name = dns_fixedname_initname(&fn); + char namebuf[DNS_NAME_FORMATSIZE]; + + result = dns_name_downcase(&zone->origin, name); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_name_format(name, namebuf, sizeof(namebuf)); + CHECK(putmem(&b, namebuf, strlen(namebuf))); + tokenlen = 5; /* "$name" */ + } else if (p == t) { + const char *typename = dns_zonetype_name(zone->type); + CHECK(putmem(&b, typename, strlen(typename))); + tokenlen = 5; /* "$type" */ + } else if (p == v) { + CHECK(putmem(&b, zone->view->name, + strlen(zone->view->name))); + tokenlen = 5; /* "$view" */ + } + + /* Advance the input pointer past the token */ + p += tokenlen; + } + + const char *end = value + strlen(value); + putmem(&b, p, end - p); + +failure: + isc_buffer_putuint8(&b, 0); + setstring(zone, field, filename); +} + void dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file, dns_masterformat_t format, const dns_master_style_t *style) { @@ -1804,7 +1906,7 @@ dns_zone_setfile(dns_zone_t *zone, const char *file, const char *initial_file, REQUIRE(zone->stream == NULL); LOCK_ZONE(zone); - setstring(zone, &zone->masterfile, file); + setfilename(zone, &zone->masterfile, file); setstring(zone, &zone->initfile, initial_file); zone->masterformat = format; if (format == dns_masterformat_text) { diff --git a/tests/dns/Makefile.am b/tests/dns/Makefile.am index 0eb30dcabf..8d4cc4c151 100644 --- a/tests/dns/Makefile.am +++ b/tests/dns/Makefile.am @@ -51,6 +51,7 @@ check_PROGRAMS = \ transport_test \ tsig_test \ update_test \ + zonefile_test \ zonemgr_test \ zt_test diff --git a/tests/dns/zonefile_test.c b/tests/dns/zonefile_test.c new file mode 100644 index 0000000000..a25bf65fd4 --- /dev/null +++ b/tests/dns/zonefile_test.c @@ -0,0 +1,121 @@ +/* + * 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. + */ + +#include +#include /* IWYU pragma: keep */ +#include +#include +#include +#include +#include +#include +#include + +#define UNIT_TESTING +#include + +#include +#include + +#include +#include + +#include + +typedef struct { + const char *input, *expected; +} zonefile_test_params_t; + +static int +setup_test(void **state) { + setup_loopmgr(state); + return 0; +} + +static int +teardown_test(void **state) { + teardown_loopmgr(state); + return 0; +} + +ISC_LOOP_TEST_IMPL(filename) { + isc_result_t result; + dns_zone_t *zone = NULL; + const zonefile_test_params_t tests[] = { + { "$name", "example.com" }, + { "$name.db", "example.com.db" }, + { "./dir/$name.db", "./dir/example.com.db" }, + { "$type", "primary" }, + { "$type-file", "primary-file" }, + { "./dir/$type", "./dir/primary" }, + { "./$type/$name.db", "./primary/example.com.db" }, + { "./$TyPe/$NAmE.db", "./primary/example.com.db" }, + { "./$name/$type", "./example.com/primary" }, + { "$name.$type", "example.com.primary" }, + { "$type$name", "primaryexample.com" }, + { "$type$type", "primary$type" }, + { "$name$name", "example.com$name" }, + { "typename", "typename" }, + { "$view", "local" }, + { "./$type/$view-$name.db", "./primary/local-example.com.db" }, + { "./$view/$type-$name.db", "./local/primary-example.com.db" }, + { "./$name/$view-$type.db", "./example.com/local-primary.db" }, + { "", "" }, + }; + + dns_view_t *view = NULL; + result = dns_test_makeview("local", false, false, &view); + assert_int_equal(result, ISC_R_SUCCESS); + + /* use .COM here to test that the name is correctly downcased */ + result = dns_test_makezone("example.COM", &zone, view, false); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_zone_setview(zone, view); + dns_view_detach(&view); + + for (size_t i = 0; i < ARRAY_SIZE(tests); i++) { + dns_zone_setfile(zone, tests[i].input, NULL, + dns_masterformat_text, + &dns_master_style_default); + assert_string_equal(dns_zone_getfile(zone), tests[i].expected); + } + + /* test PATH_MAX overrun */ + char longname[PATH_MAX] = { 0 }; + memset(longname, 'x', sizeof(longname) - 1); + dns_zone_setfile(zone, longname, NULL, dns_masterformat_text, + &dns_master_style_default); + assert_string_equal(dns_zone_getfile(zone), longname); + + /* + * overwrite the beginning of the long name with $name. when + * it's expanded to the zone name, the resulting string should + * still be capped at PATH_MAX characters. + */ + memmove(longname, "$name", 5); + dns_zone_setfile(zone, longname, NULL, dns_masterformat_text, + &dns_master_style_default); + assert_int_equal(strlen(longname), PATH_MAX - 1); + memmove(longname, "example.com", 11); + assert_string_equal(dns_zone_getfile(zone), longname); + + dns_zone_detach(&zone); + isc_loopmgr_shutdown(loopmgr); +} + +ISC_TEST_LIST_START +ISC_TEST_ENTRY_CUSTOM(filename, setup_test, teardown_test) +ISC_TEST_LIST_END + +ISC_TEST_MAIN