Commit 1e67e0833b for bind
commit 1e67e0833bad3b3d53eb8e56232cdc0452a5a694
Author: Nicki Křížek <nicki@isc.org>
Date: Tue Aug 25 13:15:28 2026 +0000
Improve server-less zone handling in isctest.zone.Zone
The NO_NS expressed "this zone has no nameserver" as Nameserver(".", 0,
"", ""), relying on "." doubling as the current directory and as a magic
value for templates to compare against. The empty ip/ip6 strings would
render silently into a zone file if a template ever dereferenced them.
Instead, make the nameserver optional for Zone. This ensures consistent
and robust handling across all templates and use-cases.
Assisted-by: Claude:claude-fable-5
diff --git a/bin/tests/system/_common/zones.conf.j2 b/bin/tests/system/_common/zones.conf.j2
index edfe0713fc..5b5df244cf 100644
--- a/bin/tests/system/_common/zones.conf.j2
+++ b/bin/tests/system/_common/zones.conf.j2
@@ -1,6 +1,6 @@
{% if zones is defined and zones %}
{% for zone in zones.values() %}
-{% if zone.ns.name == ns.name %}
+{% if zone.ns and zone.ns.name == ns.name %}
zone "@zone.name@" {
type @zone.type@;
{% if zone.type == "static-stub" %}
diff --git a/bin/tests/system/_common/zones/ns.partial.db.j2 b/bin/tests/system/_common/zones/ns.partial.db.j2
index 5c0198b036..30144ccdc9 100644
--- a/bin/tests/system/_common/zones/ns.partial.db.j2
+++ b/bin/tests/system/_common/zones/ns.partial.db.j2
@@ -1,4 +1,4 @@
-{% if ns.name != "." %}
+{% if zone.ns %}
@zone.name@. NS @zone.ns.name@.@zone.name@.
@zone.ns.name@.@zone.name@. A @zone.ns.ip@
{% else %}
diff --git a/bin/tests/system/_common/zones/soa.partial.db.j2 b/bin/tests/system/_common/zones/soa.partial.db.j2
index 7a084c4b0d..acf97ec9d1 100644
--- a/bin/tests/system/_common/zones/soa.partial.db.j2
+++ b/bin/tests/system/_common/zones/soa.partial.db.j2
@@ -1,11 +1,11 @@
-{% if not ns.name.startswith("ans") %}
+{% if not (ns and ns.name.startswith("ans")) %}
$ORIGIN @zone.name@.
{% endif %}
$TTL 300
-{% if zone.ns.name == "." %}
-{% raw %}@{% endraw %} IN SOA @zone.name@. . (
-{% else %}
+{% if zone.ns %}
{% raw %}@{% endraw %} IN SOA @zone.ns.name@.@zone.name@. . (
+{% else %}
+{% raw %}@{% endraw %} IN SOA @zone.name@. . (
{% endif %}
1 ; serial
20 ; refresh (20 seconds)
diff --git a/bin/tests/system/isctest/template.py b/bin/tests/system/isctest/template.py
index 522887c0af..75661b88bc 100644
--- a/bin/tests/system/isctest/template.py
+++ b/bin/tests/system/isctest/template.py
@@ -200,7 +200,6 @@ NS8 = Nameserver("ns8")
NS9 = Nameserver("ns9")
NS10 = Nameserver("ns10")
NS11 = Nameserver("ns11")
-NO_NS = Nameserver(".", 0, "", "")
ANS1 = Nameserver("ans1")
ANS2 = Nameserver("ans2")
@@ -219,7 +218,7 @@ ANS11 = Nameserver("ans11")
class Zone:
name: str
- ns: Nameserver
+ ns: Nameserver | None = None
type: str = "primary"
filepath: Path | None = field(default=None)
diff --git a/bin/tests/system/isctest/zone.py b/bin/tests/system/isctest/zone.py
index b92f0d2591..4060279767 100644
--- a/bin/tests/system/isctest/zone.py
+++ b/bin/tests/system/isctest/zone.py
@@ -221,7 +221,7 @@ class FileZoneKey(ZoneKey):
Zone.copy_dssets enforces this.
"""
assert self.zone is not None, "write_dsset requires a zone-attached key"
- src = Path(self.zone.ns.name) / f"dsset-{self.zone.name}."
+ src = self.zone.directory / f"dsset-{self.zone.name}."
dst = Path(target_dir) / src.name
if src.resolve() == dst.resolve():
debug(f"{self.zone.name}: dsset already in {target_dir}")
@@ -238,19 +238,19 @@ class FileZoneKey(ZoneKey):
"""
Generate a DNSSEC key via dnssec-keygen for zone and return it.
- Runs dnssec-keygen in zone.ns.name/keys/, stores the key there, and
+ Runs dnssec-keygen in zone.directory/keys/, stores the key there, and
returns the resulting FileZoneKey. Pass params="-f KSK" to generate a
Key Signing Key; omit it (or pass "") for a Zone Signing Key.
"""
debug(f"{zone.name}: generating key using dnssec-keygen")
- keydir = Path(zone.ns.name) / KEYDIR
+ keydir = zone.directory / KEYDIR
keydir.mkdir(exist_ok=True)
if alg is None:
alg = Algorithm.default()
keygen = EnvCmd(
"KEYGEN", f"-q -a {alg.number} -b {alg.bits} -K {KEYDIR} -L {DNSKEY_TTL}"
)
- key_name = keygen(f"{params} {zone.name}", cwd=zone.ns.name).out.strip()
+ key_name = keygen(f"{params} {zone.name}", cwd=zone.directory).out.strip()
return FileZoneKey(key_name, keydir=keydir, zone=zone)
@@ -401,7 +401,7 @@ class Zone:
def __init__(
self,
name: str | dns.name.Name,
- ns: Nameserver,
+ ns: Nameserver | None = None,
signed: bool = False,
subdir: str | None = "zones",
filepath_unsigned: Path | str | None = None,
@@ -438,6 +438,27 @@ class Zone:
"""
return self.filepath_signed if self.signed else self.filepath_unsigned
+ @property
+ def directory(self) -> Path:
+ """
+ Directory the zone's files are rooted in, relative to the test directory.
+ """
+ return Path(self.ns.name) if self.ns else Path(".")
+
+ @property
+ def path_unsigned(self) -> Path:
+ """
+ Path of the unsigned zone file, relative to the test directory.
+ """
+ return self.directory / self.filepath_unsigned
+
+ @property
+ def path_signed(self) -> Path:
+ """
+ Path of the signed zone file, relative to the test directory.
+ """
+ return self.directory / self.filepath_signed
+
def add_keys(self, ksk: bool = True, zsk: bool = True) -> None:
"""
Generate KSK and/or ZSK via dnssec-keygen and append to self.keys.
@@ -449,7 +470,7 @@ class Zone:
def copy_dssets(self) -> None:
"""
- Write dsset-* files for each signed delegation into self.ns dir.
+ Write dsset-* files for each signed delegation into self.directory.
"""
for zone in self.delegations:
ksks = [k for k in zone.keys if k.is_ksk()]
@@ -463,7 +484,7 @@ class Zone:
)
if ksks:
for key in ksks:
- key.write_dsset(Path(self.ns.name))
+ key.write_dsset(self.directory)
else:
debug(f"{zone.name}: delegation is insecure (no KSK)")
@@ -473,7 +494,7 @@ class Zone:
"""
debug(f"{self.name}: rendering zone file")
templates = TemplateEngine(".")
- output = Path(self.ns.name) / self.filepath_unsigned
+ output = self.path_unsigned
output.parent.mkdir(exist_ok=True)
if template is None:
@@ -508,7 +529,7 @@ class Zone:
signer(
f"-P -x -O full -o {self.name}"
f" -f {self.filepath_signed} {self.filepath_unsigned}",
- cwd=self.ns.name,
+ cwd=self.directory,
)
def configure(
diff --git a/bin/tests/system/verify/tests_verify.py b/bin/tests/system/verify/tests_verify.py
index ca82f81f68..4ea2bee643 100644
--- a/bin/tests/system/verify/tests_verify.py
+++ b/bin/tests/system/verify/tests_verify.py
@@ -9,7 +9,6 @@
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
-from pathlib import Path
from re import compile as Re
import os
@@ -28,7 +27,7 @@ import dns.rdatatype
import dns.zone
import pytest
-from isctest.template import NO_NS, zones
+from isctest.template import zones
from isctest.zone import Zone
import isctest
@@ -74,15 +73,13 @@ def bootstrap():
return ksk_private_key, ksk_dnskey
def gen_and_sign(name, nsec3=False):
- zone = Zone(name, NO_NS, signed=True)
+ zone = Zone(name, signed=True)
ksk_private_key, ksk_dnskey = generate_keys()
keys = [(ksk_private_key, ksk_dnskey)]
zone.render()
# read the rendered zone
- unsigned_path = str(Path(zone.ns.name) / zone.filepath_unsigned)
- signed_path = str(Path(zone.ns.name) / zone.filepath_signed)
- zoneobj = dns.zone.from_file(unsigned_path, origin=f"{name}.")
+ zoneobj = dns.zone.from_file(str(zone.path_unsigned), origin=f"{name}.")
lifetime = 30 * 86400
# sign the zone
@@ -134,7 +131,7 @@ def bootstrap():
nsec3_rrsig.ttl = 300
nsec3_rrsig.add(nsec3_sigdata)
- zoneobj.to_file(signed_path)
+ zoneobj.to_file(str(zone.path_signed))
return zone