]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Allow zone names to be generated parametrically
authorEvan Hunt <each@isc.org>
Wed, 16 Apr 2025 04:17:44 +0000 (21:17 -0700)
committerEvan Hunt <each@isc.org>
Tue, 3 Jun 2025 19:03:07 +0000 (12:03 -0700)
Special tokens can now be specified in a zone "file" option
in order to generate the filename parametrically. The first
instead of "$name" in the "file" option is replaced with the
zone origin, 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..

This simplifies the creation of zones using initial-file templates.
For example:

   $ rndc addzone <zonename> \
     { type primary; file "$name.db"; initial-file "template.db"

bin/tests/system/masterfile/ns2/named.conf.j2
doc/arm/reference.rst
lib/dns/zone.c
tests/dns/Makefile.am
tests/dns/zonefile_test.c [new file with mode: 0644]

index 50a1d2e2d95ab71f49a0899e38e7986558ae9d48..442d273a8c762b37f787894f308009824ceb6f70 100644 (file)
@@ -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" {
index 0c81a568008e49c2bcb9a36d0861fb3330f4236f..63535eddde415ef74adff8233072399cf06230da 100644 (file)
@@ -7132,13 +7132,21 @@ Zone Options
    :tags: zone
    :short: Specifies the zone's filename.
 
-   This sets the zone's filename. In :any:`primary <type primary>`, :any:`hint <type hint>`, and :any:`redirect <type redirect>`
+   This sets the zone's filename. In :any:`primary <type primary>`,
+   :any:`hint <type hint>`, and :any:`redirect <type redirect>`
    zones which do not have :any:`primaries` defined, zone data is loaded from
    this file. In :any:`secondary <type secondary>`, :any:`mirror <type mirror>`, :any:`stub <type stub>`, and :any:`redirect <type 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"; };'
+
+    This creates a zone ``example.com``, with filename ``example.com.db``.
 
-    Using "@" to reference the zone origin name within ``template.db``
-    allows the same file to be used with multiple zones, as in:
+    Using "@" to reference the zone origin within the initial file
+    allows the same file to be used for multiple zones, as in:
 
     ::
 
index 89fa19a3830c53b753c6ba0fbc76e081770fcafc..8edc1beb5f75b71df724803e715a227129b7d048 100644 (file)
@@ -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) {
index 0eb30dcabf478c0c16585d3419420acb4dee8f17..8d4cc4c1511d8ac33194ead313d2685466c0b221 100644 (file)
@@ -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 (file)
index 0000000..a25bf65
--- /dev/null
@@ -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 <inttypes.h>
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/atomic.h>
+#include <isc/lib.h>
+
+#include <dns/lib.h>
+#include <dns/view.h>
+
+#include <tests/dns.h>
+
+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