From 0e1f13e05c0f1117d5d05ebb27b1e1d1afcb51a0 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Thu, 24 Jul 2025 14:54:07 +0200 Subject: [PATCH] add extra tokens to the zone file name template Extend the `$name`, `$view` and `$type` tokens (expanding into the zone name, zone's view name and type); the new following tokens are now also accepted: - $name or %s is replaced with the zone name in lower case; - $type or %t is replaced with the zone type -- i.e., primary, secondary, etc); - $view or %v is replaced with the view name; - $char1 or %1 is replaced with the first character of the zone name; - $char2 or %2 is replaced with the second character of the zone name (or a dot if there is no second character); - $char3 or %3 is replaced with the third character of the zone name (or a dot if there is no third character); - $label1 or %z is replaced with the toplevel domain of the zone (or a dot if it is the root zone); - $label2 or %y is replaced with the next label under the toplevel domain (or a dot if there is no next label); - $label3 or %x is replaced with the next-next label under the toplevel domain (or a dot if there is no next-next label). --- doc/arm/reference.rst | 38 ++++++- lib/dns/zone.c | 216 ++++++++++++++++++++++++++++++-------- tests/dns/zonefile_test.c | 103 +++++++++++++----- 3 files changed, 282 insertions(+), 75 deletions(-) diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index b41429d1d4..6f64db05e6 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -7177,11 +7177,39 @@ Zone Options 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. + tokens in the string. The first instance of each token is replaced + as shown in the table below; any subsequent uses of the same token are + ignored. + + An alternate set of tokens has been provided for compatibility with other + servers which use a different format for filename templates. + + All tokens are case-insensitive. + + +-------------+------------+-----------------------------------------------+ + | Token | Alternate | Replaced with: | + +=============+============+===============================================+ + | ``$name`` | ``%s`` | Zone name, in lower case | + +-------------+------------+-----------------------------------------------+ + | ``$type`` | ``%t`` | Zone type (``primary``, ``secondary``, etc) | + +-------------+------------+-----------------------------------------------+ + | ``$view`` | ``%v`` | View name | + +-------------+------------+-----------------------------------------------+ + | ``$label1`` | ``%z`` | Top-level domain name (or ``.`` for the | + | | | root zone) | + +-------------+------------+-----------------------------------------------+ + | ``$label2`` | ``%y`` | Second label under the TLD (or ``.`` if | + | | | there is no second label) | + +-------------+------------+-----------------------------------------------+ + | ``$label3`` | ``%x`` | Third label under the TLD (or ``.`` if | + | | | there is no third label) | + +-------------+------------+-----------------------------------------------+ + | ``$char1`` | ``%1`` | First character of the zone name | + +-------------+------------+-----------------------------------------------+ + | ``$char2`` | ``%2`` | Second character of the zone name (or ``.``) | + +-------------+------------+-----------------------------------------------+ + | ``$char3`` | ``%3`` | Third character of the zone name (or ``.``) | + +-------------+------------+-----------------------------------------------+ :any:`forward` This option is only meaningful if the zone has a forwarders list. The ``only`` value diff --git a/lib/dns/zone.c b/lib/dns/zone.c index a25aeb8967..b7350b9c2a 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -1783,10 +1783,20 @@ setstring(dns_zone_t *zone, char **field, const char *value) { *field = copy; } +typedef struct foundtoken foundtoken_t; +typedef isc_result_t (*tokenparse_t)(const dns_zone_t *zone, + const foundtoken_t *token, + isc_buffer_t *b); +struct foundtoken { + const char *pos; + size_t len; + tokenparse_t parse; +}; + static int -position_order(const void *a, const void *b) { +foundtoken_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; + return ((foundtoken_t *)a)->pos - ((foundtoken_t *)b)->pos; } static isc_result_t @@ -1801,16 +1811,157 @@ putmem(isc_buffer_t *b, const char *base, size_t length) { return ISC_R_SUCCESS; } +static isc_result_t +tokenparse_type(const dns_zone_t *zone, const foundtoken_t *token, + isc_buffer_t *b) { + const char *typename = dns_zonetype_name(zone->type); + + UNUSED(token); + + return putmem(b, typename, strlen(typename)); +} + +static isc_result_t +tokenparse_name(const dns_zone_t *zone, const foundtoken_t *token, + isc_buffer_t *b) { + isc_result_t result; + char buf[DNS_NAME_FORMATSIZE]; + dns_fixedname_t fn; + dns_name_t *name = dns_fixedname_initname(&fn); + + UNUSED(token); + + result = dns_name_downcase(&zone->origin, name); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_name_format(name, buf, sizeof(buf)); + result = putmem(b, buf, strlen(buf)); + + return result; +} + +static isc_result_t +tokenparse_view(const dns_zone_t *zone, const foundtoken_t *token, + isc_buffer_t *b) { + UNUSED(token); + + return putmem(b, zone->view->name, strlen(zone->view->name)); +} + +static isc_result_t +tokenparse_char(const dns_zone_t *zone, const foundtoken_t *token, + isc_buffer_t *b) { + isc_result_t result; + char buf[DNS_NAME_FORMATSIZE]; + dns_fixedname_t fn; + dns_name_t *name = dns_fixedname_initname(&fn); + size_t chartokidx; + char c; + + result = dns_name_downcase(&zone->origin, name); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + dns_name_format(name, buf, sizeof(buf)); + + chartokidx = token->pos[token->len - 1] - '1'; + INSIST(chartokidx <= 2); + if (chartokidx < strlen(buf)) { + c = buf[chartokidx]; + } else { + c = '.'; + } + result = putmem(b, &c, 1); + + return result; +} + +static isc_result_t +tokenparse_label(const dns_zone_t *zone, const foundtoken_t *token, + isc_buffer_t *b) { + isc_result_t result; + char buf[DNS_NAME_FORMATSIZE]; + dns_fixedname_t fn; + dns_name_t *name = dns_fixedname_initname(&fn); + unsigned int labels = dns_name_countlabels(&zone->origin); + char labeltokidx; + int ilabel = -1; + + result = dns_name_fromstring(name, ".", NULL, 0, NULL); + INSIST(result == ISC_R_SUCCESS); + + labeltokidx = token->pos[token->len - 1]; + if (token->len == 2) { + /* + * %z, %y, %x pattern + */ + INSIST(labeltokidx >= 'x' && labeltokidx <= 'z'); + } else { + /* + * $label1, $label2, $label3 pattern + */ + INSIST(labeltokidx >= '1' && labeltokidx <= '3'); + } + + /* + * Even if implicit, the root label counts as one label. + */ + if (labeltokidx == '1' || labeltokidx == 'z') { + ilabel = labels - 2; + } else if (labeltokidx == '2' || labeltokidx == 'y') { + ilabel = labels - 3; + } else if (labeltokidx == '3' || labeltokidx == 'x') { + ilabel = labels - 4; + } + + if (ilabel >= 0) { + dns_name_getlabelsequence(&zone->origin, ilabel, 1, name); + result = dns_name_downcase(name, name); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + } + dns_name_format(name, buf, sizeof(buf)); + result = putmem(b, buf, strlen(buf)); + + return result; +} + +typedef struct { + const char *name; + tokenparse_t parse; +} token_t; +static const token_t tokens[] = { + { "$type", tokenparse_type }, { "$name", tokenparse_name }, + { "$view", tokenparse_view }, { "$char1", tokenparse_char }, + { "$char2", tokenparse_char }, { "$char3", tokenparse_char }, + { "$label1", tokenparse_label }, { "$label2", tokenparse_label }, + { "$label3", tokenparse_label }, { "%t", tokenparse_type }, + { "%s", tokenparse_name }, { "%v", tokenparse_view }, + { "%1", tokenparse_char }, { "%2", tokenparse_char }, + { "%3", tokenparse_char }, { "%z", tokenparse_label }, + { "%y", tokenparse_label }, { "%x", tokenparse_label } +}; + /* - * 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. + * Set the masterfile field, expanding: + * + * - $name or "%s" to the zone name + * - $type or "%t" to the zone type + * - $view or "%v" to the view name. + * - $char1 or "%1" to the first character of the zone name + * - $char2 or "%2" to the second character of the zone name (or a dot if + * there is no second character) + * - $char3 or "%3" to the third character of the zone name (or a dot if + * there is no third character) + * - $label1 or "%z" to the toplevel domain of the zone (or a dot if it is + * the TLD) + * - $label2 or "%y" to the next label under the toplevel domain (or a dot if + * there is no next label) + * - $label2 or "%x" to the next-next label under the toplevel domain (or a + * dot if there is no next-next label) + * + * 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]; + foundtoken_t founds[ARRAY_SIZE(tokens)]; char filename[PATH_MAX]; isc_buffer_t b; size_t tags = 0; @@ -1820,19 +1971,16 @@ setfilename(dns_zone_t *zone, char **field, const char *value) { return; } - t = strcasestr(value, "$type"); - if (t != NULL) { - positions[tags++] = t; - } + for (size_t i = 0; i < ARRAY_SIZE(tokens); i++) { + const token_t *token = &tokens[i]; + const char *p = strcasestr(value, token->name); - n = strcasestr(value, "$name"); - if (n != NULL) { - positions[tags++] = n; - } - - v = strcasestr(value, "$view"); - if (v != NULL) { - positions[tags++] = v; + if (p != NULL) { + founds[tags++] = + (foundtoken_t){ .pos = p, + .len = strlen(token->name), + .parse = token->parse }; + } } if (tags == 0) { @@ -1843,38 +1991,20 @@ setfilename(dns_zone_t *zone, char **field, const char *value) { isc_buffer_init(&b, filename, sizeof(filename)); /* sort the tag offsets in order of occurrence */ - qsort(positions, tags, sizeof(char *), position_order); + qsort(founds, tags, sizeof(foundtoken_t), foundtoken_order); const char *p = value; for (size_t i = 0; i < tags; i++) { - size_t tokenlen = 0; + foundtoken_t *token = &founds[i]; - CHECK(putmem(&b, p, positions[i] - p)); + CHECK(putmem(&b, p, token->pos - p)); - p = positions[i]; + p = token->pos; 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" */ - } + CHECK(token->parse(zone, token, &b)); /* Advance the input pointer past the token */ - p += tokenlen; + p += founds[i].len; } const char *end = value + strlen(value); diff --git a/tests/dns/zonefile_test.c b/tests/dns/zonefile_test.c index 1525798f91..58fad0a1aa 100644 --- a/tests/dns/zonefile_test.c +++ b/tests/dns/zonefile_test.c @@ -33,7 +33,7 @@ #include typedef struct { - const char *input, *expected; + const char *name, *input, *expected; } zonefile_test_params_t; static int @@ -52,31 +52,87 @@ 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" }, - { "", "" }, + { "example.COM", "$name", "example.com" }, + { "example.COM", "$name.db", "example.com.db" }, + { "example.COM", "./dir/$name.db", "./dir/example.com.db" }, + { "example.COM", "%s", "example.com" }, + { "example.COM", "%s.db", "example.com.db" }, + { "example.COM", "./dir/%s.db", "./dir/example.com.db" }, + { "example.COM", "$type", "primary" }, + { "example.COM", "$type-file", "primary-file" }, + { "example.COM", "./dir/$type", "./dir/primary" }, + { "example.COM", "./$type/$name.db", + "./primary/example.com.db" }, + { "example.COM", "%t", "primary" }, + { "example.COM", "%t-file", "primary-file" }, + { "example.COM", "./dir/%t", "./dir/primary" }, + { "example.COM", "./%t/%s.db", "./primary/example.com.db" }, + { "example.COM", "./$TyPe/$NAmE.db", + "./primary/example.com.db" }, + { "example.COM", "./$name/$type", "./example.com/primary" }, + { "example.COM", "$name.$type", "example.com.primary" }, + { "example.COM", "$type$name", "primaryexample.com" }, + { "example.COM", "$type$type", "primary$type" }, + { "example.COM", "$name$name", "example.com$name" }, + { "example.COM", "typename", "typename" }, + { "example.COM", "$view", "local" }, + { "example.COM", "%v", "local" }, + { "example.COM", "./$type/$view-$name.db", + "./primary/local-example.com.db" }, + { "example.COM", "./$view/$type-$name.db", + "./local/primary-example.com.db" }, + { "example.COM", "./$name/$view-$type.db", + "./example.com/local-primary.db" }, + { "example.COM", "./%s/%v-%t.db", + "./example.com/local-primary.db" }, + { "example.COM", "", "" }, + { "example.COM", "$char1", "e" }, + { "example.COM", "$char2", "x" }, + { "example.COM", "$char3", "a" }, + { "example.COM", "%1", "e" }, + { "example.COM", "%2", "x" }, + { "example.COM", "%3", "a" }, + { "example.COM", "$label1", "com" }, + { "example.COM", "$label2", "example" }, + { "example.COM", "$label3", "." }, + { "example.COM", "%z", "com" }, + { "example.COM", "%y", "example" }, + { "example.COM", "%x", "." }, + { "example", "$label1", "example" }, + { "example", "$label2", "." }, + { "example", "$label3", "." }, + { "a.b.c.d.e", "$label1", "e" }, + { "a.b.c.d.e", "$label2", "d" }, + { "a.b.c.d.e", "$label3", "c" }, + { "a.b.c", "$char1", "a" }, + { "a.b.c", "$char2", "." }, + { "a.b.c", "$char3", "b" }, + { "a.b.c", "%1", "a" }, + { "a.b.c", "%2", "." }, + { "a.b.c", "%3", "b" }, + { "a", "%1", "a" }, + { "a", "%2", "." }, + { "a", "%3", "." }, + { "a.b.c.d", "%1$char2%3$label1%x", "a.bdb" } }; dns_view_t *view = NULL; result = dns_test_makeview("local", false, false, &view); assert_int_equal(result, ISC_R_SUCCESS); + for (size_t i = 0; i < ARRAY_SIZE(tests); i++) { + result = dns_test_makezone(tests[i].name, &zone, view, false); + assert_int_equal(result, ISC_R_SUCCESS); + + dns_zone_setview(zone, view); + 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); + + dns_zone_detach(&zone); + } + /* 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); @@ -84,13 +140,6 @@ ISC_LOOP_TEST_IMPL(filename) { 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);