from isctest.hypothesis.strategies import dns_names
import isctest
+import isctest.name
SUFFIX = dns.name.from_text("nsec3.example.")
AUTH = "10.53.0.3"
RESOLVER = "10.53.0.4"
TIMEOUT = 5
-
-
-def get_known_names_and_delegations():
-
- # Read zone file
- system_test_root = Path(os.environ["srcdir"])
- with open(
- f"{system_test_root}/dnssec/ns3/nsec3.example.db.in", encoding="utf-8"
- ) as zf:
- content = dns.zone.from_file(zf, origin=SUFFIX, relativize=False)
- all_names = set(content)
- known_names = sorted(all_names)
-
- # Remove out of zone, obscured and glue names
- for known_name in known_names:
- relation, _, _ = known_name.fullcompare(SUFFIX)
- if relation == dns.name.NameRelation.EQUAL:
- continue
- if relation in (dns.name.NameRelation.NONE, dns.name.NameRelation.SUPERDOMAIN):
- known_names.remove(known_name)
- continue
- nsset = content.get_rdataset(known_name, rdtype=dns.rdatatype.NS)
- dname = content.get_rdataset(known_name, rdtype=dns.rdatatype.DNAME)
- if nsset is not None or dname is not None:
- for glue in known_names:
- relation, _, _ = glue.fullcompare(known_name)
- if relation == dns.name.NameRelation.SUBDOMAIN:
- known_names.remove(glue)
-
- # Add in possible ENT names
- for known_name in known_names:
- _, super_name = known_name.split(len(known_name.labels) - 1)
- while len(super_name.labels) > len(SUFFIX.labels):
- known_names.append(super_name)
- _, super_name = super_name.split(len(super_name.labels) - 1)
- known_names = set(known_names)
-
- # Build list of delegation points and DNAMES
- delegations = []
- for known_name in known_names:
- relation, _, _ = known_name.fullcompare(SUFFIX)
- if relation == dns.name.NameRelation.EQUAL:
- continue
- nsset = content.get_rdataset(known_name, rdtype=dns.rdatatype.NS)
- dname = content.get_rdataset(known_name, rdtype=dns.rdatatype.DNAME)
- if nsset is not None or dname is not None:
- delegations.append(known_name)
-
- # build list of WILDCARD named
- wildcards = []
- for known_name in known_names:
- if known_name.is_wild():
- wildcards.append(known_name)
- return known_names, delegations, wildcards
-
-
-KNOWN_NAMES, DELEGATIONS, WILDCARDS = get_known_names_and_delegations()
-
-
-def is_delegated(name, delegations):
- for delegation in delegations:
- relation, _, _ = name.fullcompare(delegation)
- if relation in (dns.name.NameRelation.EQUAL, dns.name.NameRelation.SUBDOMAIN):
- return True
- return False
+ZONE = isctest.name.ZoneAnalyzer.read_path(
+ Path(os.environ["builddir"]) / "dnssec/ns3/nsec3.example.db.in", origin=SUFFIX
+)
def nsec3_covers(rrset: dns.rrset.RRset, hashed_name: dns.name.Name) -> bool:
@pytest.mark.parametrize(
"server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
)
-@given(name=dns_names(suffix=KNOWN_NAMES))
+@given(name=dns_names(suffix=ZONE.reachable.union(ZONE.ents)))
def test_dnssec_nsec3_subdomain_nxdomain(
server, name: dns.name.Name, named_port: int
) -> None:
def noqname_test(server, name: dns.name.Name, named_port: int) -> None:
# Name must not exist.
- assume(name not in KNOWN_NAMES)
+ all_existing_names = (
+ ZONE.reachable.union(ZONE.ents).union(ZONE.delegations).union(ZONE.dnames)
+ )
+ assume(name not in (all_existing_names))
# Name must not be below a delegation or DNAME.
- assume(not is_delegated(name, DELEGATIONS))
+ assume(
+ not isctest.name.is_related_to_any(
+ name,
+ (dns.name.NameRelation.EQUAL, dns.name.NameRelation.SUBDOMAIN),
+ ZONE.reachable_delegations.union(ZONE.reachable_dnames),
+ )
+ )
query = dns.message.make_query(
name, dns.rdatatype.A, use_edns=True, want_dnssec=True
_, nce = name.split(ce_labels + 1)
else:
ce_labels = 0
- for zname in KNOWN_NAMES:
+ for zname in all_existing_names:
relation, _, nlabels = name.fullcompare(zname)
if relation == dns.name.NameRelation.SUBDOMAIN:
if nlabels > ce_labels:
if response.rcode() is dns.rcode.NOERROR:
# only NOERRORs should be from wildcards
found_wc = False
- for wildcard in WILDCARDS:
+ for wildcard in ZONE.reachable_wildcards:
if wildcard == wc:
found_wc = True
assert found_wc
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
-from dns.name import Name
+from typing import Container, Iterable, FrozenSet
+
+import pytest
+
+pytest.importorskip("dns", minversion="2.1.0") # NameRelation
+from dns.name import Name, NameRelation
+import dns.zone
+import dns.rdatatype
def prepend_label(label: str, name: Name) -> Name:
def len_wire_uncompressed(name: Name) -> int:
return len(name) + sum(map(len, name.labels))
+
+
+def get_wildcard_names(names: Iterable[Name]) -> FrozenSet[Name]:
+ return frozenset(name for name in names if name.is_wild())
+
+
+class ZoneAnalyzer:
+ """
+ Categorize names in zone and provide list of ENTs:
+
+ - delegations - names with NS RR
+ - dnames - names with DNAME RR
+ - wildcards - names with leftmost label '*'
+ - reachable - non-empty authoritative nodes in zone
+ - have at least one auth RR set and are not occluded
+ - ents - reachable empty non-terminals
+ - occluded - names under a parent node which has DNAME or a non-apex NS
+ - reachable_delegations
+ - 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
+
+ Warnings:
+ - Quadratic complexity ahead! Use only on small test zones.
+ - Zone must be constant.
+ """
+
+ @classmethod
+ def read_path(cls, zpath, origin):
+ with open(zpath, encoding="ascii") as zf:
+ zonedb = dns.zone.from_file(zf, origin, relativize=False)
+ return cls(zonedb)
+
+ def __init__(self, zone: dns.zone.Zone):
+ self.zone = zone
+ assert self.zone.origin # mypy hack
+ # based on individual nodes but not relationship between nodes
+ self.delegations = self.get_names_with_type(dns.rdatatype.NS) - {
+ self.zone.origin
+ }
+ self.dnames = self.get_names_with_type(dns.rdatatype.DNAME)
+ self.wildcards = get_wildcard_names(self.zone)
+
+ # takes relationship between nodes into account
+ self._categorize_names()
+ self.ents = self.generate_ents()
+ self.reachable_dnames = self.dnames.intersection(self.reachable)
+ self.reachable_wildcards = self.wildcards.intersection(self.reachable)
+
+ def get_names_with_type(self, rdtype) -> FrozenSet[Name]:
+ return frozenset(
+ name for name in self.zone if self.zone.get_rdataset(name, rdtype)
+ )
+
+ def _categorize_names(
+ self,
+ ) -> None:
+ """
+ Split names defined in a zone into three sets:
+ Generally reachable, reachable delegations, and occluded.
+
+ Delegations are set aside because they are a weird hybrid with different
+ rules for different RR types (NS, DS, NSEC, everything else).
+ """
+ assert self.zone.origin # mypy workaround
+ reachable = set(self.zone)
+ # assume everything is reachable until proven otherwise
+ reachable_delegations = set(self.delegations)
+ occluded = set()
+
+ def _mark_occluded(name: Name) -> None:
+ occluded.add(name)
+ if name in reachable:
+ reachable.remove(name)
+ if name in reachable_delegations:
+ reachable_delegations.remove(name)
+
+ # sanity check, should be impossible with dnspython 2.7.0 zone reader
+ for name in reachable:
+ relation, _, _ = name.fullcompare(self.zone.origin)
+ if relation in (
+ NameRelation.NONE, # out of zone?
+ NameRelation.SUPERDOMAIN, # parent of apex?!
+ ):
+ raise NotImplementedError
+
+ for maybe_occluded in reachable.copy():
+ for cut in self.delegations:
+ rel, _, _ = maybe_occluded.fullcompare(cut)
+ if rel == NameRelation.EQUAL:
+ # data _on_ a parent-side of a zone cut are in limbo:
+ # - are not really authoritative (except for DS)
+ # - but NS is not really 'occluded'
+ # We remove them from 'reachable' but do not add them to 'occluded' set,
+ # i.e. leave them in 'reachable_delegations'.
+ if maybe_occluded in reachable:
+ reachable.remove(maybe_occluded)
+
+ if rel == NameRelation.SUBDOMAIN:
+ _mark_occluded(maybe_occluded)
+ # do not break cycle - handle also nested NS and DNAME
+
+ # DNAME itself is authoritative but nothing under it is reachable
+ for dname in self.dnames:
+ rel, _, _ = maybe_occluded.fullcompare(dname)
+ if rel == NameRelation.SUBDOMAIN:
+ _mark_occluded(maybe_occluded)
+ # do not break cycle - handle also nested NS and DNAME
+
+ self.reachable = frozenset(reachable)
+ self.reachable_delegations = frozenset(reachable_delegations)
+ self.occluded = frozenset(occluded)
+
+ def generate_ents(self) -> FrozenSet[Name]:
+ """
+ Generate reachable names of empty nodes "between" all reachable
+ names with a RR and the origin.
+ """
+ assert self.zone.origin
+ all_reachable_names = self.reachable.union(self.reachable_delegations)
+ ents = set()
+ for name in all_reachable_names:
+ _, super_name = name.split(len(name) - 1)
+ while len(super_name) > len(self.zone.origin):
+ if super_name not in all_reachable_names:
+ ents.add(super_name)
+ _, super_name = super_name.split(len(super_name) - 1)
+
+ return frozenset(ents)
+
+
+def is_related_to_any(
+ test_name: Name,
+ acceptable_relations: Container[NameRelation],
+ candidates: Iterable[Name],
+) -> bool:
+ for maybe_parent in candidates:
+ relation, _, _ = test_name.fullcompare(maybe_parent)
+ if relation in acceptable_relations:
+ return True
+ return False