mirror of
https://gitlab.isc.org/isc-projects/bind9
synced 2025-08-29 13:38:26 +00:00
Separate zone analyzer from NSEC3 test
Code to generate ENTs, detect wildcards, occlusion etc. is generic enough to be in an utility module.
This commit is contained in:
parent
3fb6b990af
commit
dbba59f48b
@ -31,76 +31,15 @@ from hypothesis import assume, given
|
|||||||
|
|
||||||
from isctest.hypothesis.strategies import dns_names
|
from isctest.hypothesis.strategies import dns_names
|
||||||
import isctest
|
import isctest
|
||||||
|
import isctest.name
|
||||||
|
|
||||||
SUFFIX = dns.name.from_text("nsec3.example.")
|
SUFFIX = dns.name.from_text("nsec3.example.")
|
||||||
AUTH = "10.53.0.3"
|
AUTH = "10.53.0.3"
|
||||||
RESOLVER = "10.53.0.4"
|
RESOLVER = "10.53.0.4"
|
||||||
TIMEOUT = 5
|
TIMEOUT = 5
|
||||||
|
ZONE = isctest.name.ZoneAnalyzer.read_path(
|
||||||
|
Path(os.environ["builddir"]) / "dnssec/ns3/nsec3.example.db.in", origin=SUFFIX
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def nsec3_covers(rrset: dns.rrset.RRset, hashed_name: dns.name.Name) -> bool:
|
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(
|
@pytest.mark.parametrize(
|
||||||
"server", [pytest.param(AUTH, id="ns3"), pytest.param(RESOLVER, id="ns4")]
|
"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(
|
def test_dnssec_nsec3_subdomain_nxdomain(
|
||||||
server, name: dns.name.Name, named_port: int
|
server, name: dns.name.Name, named_port: int
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -173,10 +112,19 @@ def test_dnssec_nsec3_subdomain_nxdomain(
|
|||||||
|
|
||||||
def noqname_test(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.
|
# 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.
|
# 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(
|
query = dns.message.make_query(
|
||||||
name, dns.rdatatype.A, use_edns=True, want_dnssec=True
|
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)
|
_, nce = name.split(ce_labels + 1)
|
||||||
else:
|
else:
|
||||||
ce_labels = 0
|
ce_labels = 0
|
||||||
for zname in KNOWN_NAMES:
|
for zname in all_existing_names:
|
||||||
relation, _, nlabels = name.fullcompare(zname)
|
relation, _, nlabels = name.fullcompare(zname)
|
||||||
if relation == dns.name.NameRelation.SUBDOMAIN:
|
if relation == dns.name.NameRelation.SUBDOMAIN:
|
||||||
if nlabels > ce_labels:
|
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:
|
if response.rcode() is dns.rcode.NOERROR:
|
||||||
# only NOERRORs should be from wildcards
|
# only NOERRORs should be from wildcards
|
||||||
found_wc = False
|
found_wc = False
|
||||||
for wildcard in WILDCARDS:
|
for wildcard in ZONE.reachable_wildcards:
|
||||||
if wildcard == wc:
|
if wildcard == wc:
|
||||||
found_wc = True
|
found_wc = True
|
||||||
assert found_wc
|
assert found_wc
|
||||||
|
@ -13,7 +13,6 @@ from . import check
|
|||||||
from . import instance
|
from . import instance
|
||||||
from . import query
|
from . import query
|
||||||
from . import kasp
|
from . import kasp
|
||||||
from . import name
|
|
||||||
from . import rndc
|
from . import rndc
|
||||||
from . import run
|
from . import run
|
||||||
from . import template
|
from . import template
|
||||||
|
@ -9,7 +9,14 @@
|
|||||||
# See the COPYRIGHT file distributed with this work for additional
|
# See the COPYRIGHT file distributed with this work for additional
|
||||||
# information regarding copyright ownership.
|
# 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 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:
|
def len_wire_uncompressed(name: Name) -> int:
|
||||||
return len(name) + sum(map(len, name.labels))
|
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…
x
Reference in New Issue
Block a user