2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-10-05 13:16:31 +00:00
Files
bind/bin/tests/system/selftest/tests_zone_analyzer.py
Petr Špaček f0592de608 Extract closest encloser and source of synthesis logic into ZoneAnalyzer
As a side-effect, we now have set of all existing names in a zone with a
test, too. These parts should be shared with new NSEC tests.
2025-07-29 10:00:45 +02:00

229 lines
6.4 KiB
Python
Executable File

#!/usr/bin/python3
# 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.
"""
isctest.name.ZoneAnalyzer self-test
Generate insane test zone and check expected output of ZoneAnalyzer utility class
"""
import collections
import itertools
from pathlib import Path
import dns.name
from dns.name import Name
import pytest
import isctest
import isctest.name
# set of properies present in the tested zone - read by tests_zone_analyzer.py
CATEGORIES = frozenset(
[
"all_existing_names",
"delegations",
"dnames",
"ents",
"occluded",
"reachable",
"reachable_delegations",
"reachable_dnames",
"reachable_wildcards",
"reachable_wildcard_parents",
"wildcards",
]
)
pytestmark = pytest.mark.extra_artifacts(["analyzer.db"])
SUFFIX = dns.name.from_text("nsec3.example.")
LABELS = (b"*", b"dname", b"ent", b"ns", b"txt")
LABEL2RRTYPE = { # leftmost label encodes RR type we will synthesize for given name
b"*": "TXT",
b"dname": "DNAME",
b"ent": None, # ENT is not really a 'type'
b"ns": "NS",
b"txt": "TXT",
}
LABEL2TAGS = { # leftmost label encodes 'initial' meaning of a complete name
b"*": {"wildcards"},
b"dname": {"dnames"},
b"ns": {"delegations"},
b"txt": set(), # perhaps reachable, perhaps not, we need to decide based on other labels
}
def name2tags(name):
"""
Decode meaning hidden in labels and their relationships
and return all tags expected from ZoneAnalyzer
"""
tags = LABEL2TAGS[name[0]].copy()
parent_labels = name[1:]
if b"ns" in parent_labels or b"dname" in parent_labels:
tags.add("occluded")
if "occluded" not in tags:
tags.add("all_existing_names")
if "delegations" in tags:
# delegations are ambiguous and don't count as 'reachable'
tags.add("reachable_delegations")
elif "dnames" in tags:
tags.add("reachable")
tags.add("reachable_dnames")
elif "wildcards" in tags:
tags.add("reachable")
tags.add("reachable_wildcards")
else:
tags.add("reachable")
return tags
def gen_node(nodes, labels):
name = Name(labels)
nodes[name] = name2tags(name)
def add_ents(nodes):
"""
Non-occluded nodes with 'ent' as a parent label imply existence of 'ent' nodes.
"""
new_ents = {}
for name, tags in nodes.items():
if "occluded" in tags:
continue
# check if any parent is ENT
entidx = 1
while True:
try:
entidx = name.labels.index(b"ent", entidx)
except ValueError:
break
entname = Name(name[entidx:])
new_ents[entname] = {"all_existing_names", "ents"}
entidx += 1
return new_ents
def tag_wildcard_parents(nodes):
"""
Non-occluded nodes with '*' as a leftmost label tag their immediate parent
nodes as 'reachable_wildcard_parents'.
"""
for name, tags in nodes.items():
if "occluded" in tags or not name.is_wild():
continue
parent_name = Name(name[1:])
nodes[parent_name].add("reachable_wildcard_parents")
def is_non_ent(labels):
"""
Filter out nodes with 'ent' at leftmost position. To become ENT a name must
not have data by itself but have some other node defined underneath it,
and must not be occluded, which is something itertools.product() cannot
decide.
"""
return labels[0] != b"ent"
def gen_zone(nodes):
"""
Generate zone file in text format.
All names are relative.
Right-hand side of RRs contains dot-separated list of categories a node
belongs to (except for zone origin).
"""
for name, tags in sorted(nodes.items()):
if len(name) == 0:
# origin, very special case
yield "@\tSOA\treachable. origin-special-case. 0 0 0 0 0\n"
yield "@\tNS\treachable.\n"
yield "@\tA\t192.0.2.1\n"
continue
rrtype = LABEL2RRTYPE[name[0]]
if rrtype is None: # ENT
prefix = "; "
else:
prefix = ""
assert tags
yield f"{prefix}{name}\t{rrtype}\t{'.'.join(sorted(tags))}.\n"
def gen_expected_output(nodes):
"""
{category: set(names)} mapping used by the pytest check
"""
categories = collections.defaultdict(set)
for name, tags in nodes.items():
for tag in tags:
categories[tag].add(name)
assert set(categories.keys()) == CATEGORIES, (
"CATEGORIES needs updating",
CATEGORIES.symmetric_difference(set(categories.keys())),
)
return categories
def generate_test_data():
"""
Prepare the analyzer.db zone file in the current working directory and
return the expected attribute values for the ZoneAnalyzer instance that
will be tested using that file.
"""
nodes = {}
for length in range(1, len(LABELS) + 1):
for labelseq in filter(is_non_ent, itertools.product(LABELS, repeat=length)):
gen_node(nodes, labelseq)
# special-case to make this look as a valid DNS zone - it needs zone origin node
nodes[Name([])] = {"all_existing_names", "reachable"}
nodes.update(add_ents(nodes))
tag_wildcard_parents(nodes)
with open("analyzer.db", "w", encoding="ascii") as outf:
outf.writelines(gen_zone(nodes))
return gen_expected_output(nodes)
if __name__ == "__main__":
generate_test_data()
@pytest.fixture(scope="module")
def analyzer_fixture():
expected_results = generate_test_data() # creates the "analyzer.db" file
analyzer = isctest.name.ZoneAnalyzer.read_path(Path("analyzer.db"), origin=SUFFIX)
return expected_results, analyzer
# pylint: disable=redefined-outer-name
@pytest.mark.parametrize("category", sorted(CATEGORIES))
def test_analyzer_attrs(category, analyzer_fixture):
expected_results, analyzer = analyzer_fixture
# relativize results to zone name to make debugging easier
results = {name.relativize(SUFFIX) for name in getattr(analyzer, category)}
assert results == expected_results[category]