mirror of
https://github.com/isc-projects/bind9.git
synced 2026-06-11 06:19:59 -04:00
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.
(cherry picked from commit f0592de608)
This commit is contained in:
parent
2cf035b87d
commit
da51bfed8c
3 changed files with 84 additions and 49 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue