From: Petr Špaček Date: Tue, 3 Jun 2025 15:20:54 +0000 (+0200) Subject: Extract closest encloser and source of synthesis logic into ZoneAnalyzer X-Git-Tag: v9.21.11~22^2~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f0592de608af06792dbc14829a0ac3671b9ed868;p=thirdparty%2Fbind9.git 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. --- diff --git a/bin/tests/system/dnssec/tests_nsec3.py b/bin/tests/system/dnssec/tests_nsec3.py index 1f01074edb2..f7f98be3646 100755 --- a/bin/tests/system/dnssec/tests_nsec3.py +++ b/bin/tests/system/dnssec/tests_nsec3.py @@ -111,13 +111,11 @@ def test_dnssec_nsec3_subdomain_nxdomain( def noqname_test(server, name: dns.name.Name, named_port: int) -> None: - # Name must not exist. - all_existing_names = ( - ZONE.reachable.union(ZONE.ents).union(ZONE.delegations).union(ZONE.dnames) - ) - assume(name not in (all_existing_names)) + # randomly generated name must not exist + assume(name not in (ZONE.all_existing_names)) - # Name must not be below a delegation or DNAME. + # name must not be under a delegation or DNAME: + # it would not work with resolver ns4 assume( not isctest.name.is_related_to_any( name, @@ -133,11 +131,16 @@ def noqname_test(server, name: dns.name.Name, named_port: int) -> None: isctest.check.is_response_to(response, query) assert response.rcode() in (dns.rcode.NOERROR, dns.rcode.NXDOMAIN) - # Retrieve closest encloser (ce) and next closest encloser (nce). - ce = None - nce = None - if response.rcode() is dns.rcode.NOERROR: - # this should only be a wild card response + ce, nce = ZONE.closest_encloser(name) + # Response has NSEC3 that covers the next closer name + check_nsec3_covers(nce, response) + + wname = ZONE.source_of_synthesis(name) + if wname in ZONE.reachable_wildcards: + wname_parent = dns.name.Name(wname[1:]) + assert name.is_subdomain(wname_parent) + # expecting wildcard response with a signed A RRset + assert response.rcode() is dns.rcode.NOERROR answer_sig = response.get_rrset( section="ANSWER", name=name, @@ -147,26 +150,18 @@ def noqname_test(server, name: dns.name.Name, named_port: int) -> None: ) assert answer_sig is not None assert len(answer_sig) == 1 - # root label is not being counted in labels field, RFC 4034 section 3.1.3 - ce_labels = answer_sig[0].labels + 1 - # wildcard labels < QNAME labels - assert ce_labels < len(name.labels) - # ce is wildcard name w/o wildcard label - _, ce = name.split(ce_labels) - _, nce = name.split(ce_labels + 1) + # RRSIG labels field, RFC 4034 section 3.1.3 does not count: + # - root label + # - leftmost * label + wildcard_parent_labels = answer_sig[0].labels + 1 # add root but not leftmost * + assert wildcard_parent_labels < len(name) + # ce should be wildcard name w/o wildcard label, nce one label longer + assert ce == name.split(wildcard_parent_labels)[1] + assert nce == name.split(wildcard_parent_labels + 1)[1] else: - ce_labels = 0 - for zname in all_existing_names: - relation, _, nlabels = name.fullcompare(zname) - if relation == dns.name.NameRelation.SUBDOMAIN: - if nlabels > ce_labels: - ce_labels = nlabels - ce = zname - _, nce = name.split(ce_labels + 1) - assert ce is not None - assert nce is not None - - # Response has closest encloser NSEC3. + # no wildcard synthesis -> NXDOMAIN + assert response.rcode() is dns.rcode.NXDOMAIN + # Response must have closest encloser NSEC3 ce_hash = dns.dnssec.nsec3_hash( ce, salt=None, iterations=0, algorithm=NSEC3Hash.SHA1 ) @@ -182,18 +177,5 @@ def noqname_test(server, name: dns.name.Name, named_port: int) -> None: ce_nsec3_match ), f"Expected matching NSEC3 for {ce} (hash={ce_hash}) not found:\n {response}" - # Response has NSEC3 that covers the next closer name. - check_nsec3_covers(nce, response) - - wc = dns.name.from_text("*", ce) - if response.rcode() is dns.rcode.NOERROR: - # only NOERRORs should be from wildcards - found_wc = False - for wildcard in ZONE.reachable_wildcards: - if wildcard == wc: - found_wc = True - assert found_wc - - if response.rcode() == dns.rcode.NXDOMAIN: - # Response has NSEC3 that covers the wildcard. - check_nsec3_covers(wc, response) + # Response has NSEC3 that covers the wildcard + check_nsec3_covers(wname, response) diff --git a/bin/tests/system/isctest/name.py b/bin/tests/system/isctest/name.py index 75c22720f68..255150e8372 100644 --- a/bin/tests/system/isctest/name.py +++ b/bin/tests/system/isctest/name.py @@ -46,6 +46,7 @@ class ZoneAnalyzer: - have NS RR on it, are not zone's apex, and are not occluded - reachable_dnames - have DNAME RR on it and are not occluded - reachable_wildcards - have leftmost label '*' and are not occluded + - reachable_wildcard_parents - reachable_wildcards with leftmost '*' stripped Warnings: - Quadratic complexity ahead! Use only on small test zones. @@ -73,6 +74,16 @@ class ZoneAnalyzer: self.ents = self.generate_ents() self.reachable_dnames = self.dnames.intersection(self.reachable) self.reachable_wildcards = self.wildcards.intersection(self.reachable) + self.reachable_wildcard_parents = { + Name(wname[1:]) for wname in self.reachable_wildcards + } + + # (except for wildcard expansions) all names in zone which result in NOERROR answers + self.all_existing_names = ( + self.reachable.union(self.ents) + .union(self.reachable_delegations) + .union(self.reachable_dnames) + ) def get_names_with_type(self, rdtype) -> FrozenSet[Name]: return frozenset( @@ -155,6 +166,31 @@ class ZoneAnalyzer: return frozenset(ents) + def closest_encloser(self, qname: Name): + """ + Get (closest encloser, next closer name) for given qname. + """ + ce = None # Closest encloser, RFC 4592 + nce = None # Next closer name, RFC 5155 + for zname in self.all_existing_names: + relation, _, common_labels = qname.fullcompare(zname) + if relation == NameRelation.SUBDOMAIN: + if not ce or common_labels > len(ce): + # longest match so far + ce = zname + _, nce = qname.split(len(ce) + 1) + assert ce is not None + assert nce is not None + return ce, nce + + def source_of_synthesis(self, qname: Name) -> Name: + """ + Return source of synthesis according to RFC 4592 section 3.3.1. + Name is not guaranteed to exist or be reachable. + """ + ce, _ = self.closest_encloser(qname) + return Name("*") + ce + def is_related_to_any( test_name: Name, diff --git a/bin/tests/system/selftest/tests_zone_analyzer.py b/bin/tests/system/selftest/tests_zone_analyzer.py index 9115d3ca64b..9cea8c7986e 100755 --- a/bin/tests/system/selftest/tests_zone_analyzer.py +++ b/bin/tests/system/selftest/tests_zone_analyzer.py @@ -29,6 +29,7 @@ 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", @@ -37,6 +38,7 @@ CATEGORIES = frozenset( "reachable_delegations", "reachable_dnames", "reachable_wildcards", + "reachable_wildcard_parents", "wildcards", ] ) @@ -73,6 +75,7 @@ def name2tags(name): 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") @@ -110,12 +113,25 @@ def add_ents(nodes): except ValueError: break entname = Name(name[entidx:]) - new_ents[entname] = {"ents"} + 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 @@ -180,10 +196,11 @@ def generate_test_data(): for labelseq in filter(is_non_ent, itertools.product(LABELS, repeat=length)): gen_node(nodes, labelseq) - nodes.update(add_ents(nodes)) - # special-case to make this look as a valid DNS zone - it needs zone origin node - nodes[Name([])] = {"reachable"} + 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))