mirror of
https://github.com/isc-projects/bind9.git
synced 2026-07-15 14:20:56 -04:00
Separate zone analyzer from NSEC3 test
Code to generate ENTs, detect wildcards, occlusion etc. is generic
enough to be in an utility module.
(cherry picked from commit dbba59f48b)
This commit is contained in:
parent
b6b6b6f45b
commit
26bd3f2019
3 changed files with 167 additions and 72 deletions
|
|
@ -31,76 +31,15 @@ from hypothesis import assume, given
|
|||
|
||||
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:
|
||||
|
|
@ -164,7 +103,7 @@ def test_dnssec_nsec3_nxdomain(server, name: dns.name.Name, named_port: int) ->
|
|||
@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:
|
||||
|
|
@ -173,10 +112,19 @@ def test_dnssec_nsec3_subdomain_nxdomain(
|
|||
|
||||
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
|
||||
|
|
@ -208,7 +156,7 @@ def noqname_test(server, name: dns.name.Name, named_port: int) -> None:
|
|||
_, 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:
|
||||
|
|
@ -241,7 +189,7 @@ def noqname_test(server, name: dns.name.Name, named_port: int) -> None:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from . import check
|
|||
from . import instance
|
||||
from . import query
|
||||
from . import kasp
|
||||
from . import name
|
||||
from . import rndc
|
||||
from . import run
|
||||
from . import template
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@
|
|||
# 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:
|
||||
|
|
@ -18,3 +25,144 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue