Home | History | Annotate | Line # | Download | only in wildcard
      1 #!/usr/bin/python3
      2 
      3 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
      4 #
      5 # SPDX-License-Identifier: MPL-2.0
      6 #
      7 # This Source Code Form is subject to the terms of the Mozilla Public
      8 # License, v. 2.0.  If a copy of the MPL was not distributed with this
      9 # file, you can obtain one at https://mozilla.org/MPL/2.0/.
     10 #
     11 # See the COPYRIGHT file distributed with this work for additional
     12 # information regarding copyright ownership.
     13 
     14 """
     15 Example property-based test for wildcard synthesis.
     16 Verifies that otherwise-empty zone with single wildcard record * A 192.0.2.1
     17 produces synthesized answers for <random_label>.test. A, and returns NODATA for
     18 <random_label>.test. when rdtype is not A.
     19 
     20 Limitations - untested properties:
     21     - empty non-terminals prevent expansion
     22     - or more generally any existing node prevents expansion
     23     - DNSSEC record inclusion
     24     - possibly others, see RFC 4592 and company
     25     - content of authority & additional sections
     26     - flags beyond RCODE
     27     - special behavior of rdtypes like CNAME
     28 """
     29 
     30 import pytest
     31 
     32 pytest.importorskip("dns", minversion="2.0.0")
     33 import dns.message
     34 import dns.name
     35 import dns.query
     36 import dns.rcode
     37 import dns.rdataclass
     38 import dns.rdatatype
     39 import dns.rrset
     40 
     41 from isctest.hypothesis.strategies import dns_names, dns_rdatatypes_without_meta
     42 from hypothesis import assume, example, given, settings
     43 
     44 import isctest.check
     45 import isctest.name
     46 import isctest.query
     47 
     48 pytestmark = pytest.mark.extra_artifacts(
     49     [
     50         "ns1/K*",
     51         "ns1/dsset-*",
     52         "ns1/*.signed",
     53         "ns1/allwild.db",
     54         "ns1/example.db",
     55         "ns1/nestedwild.db",
     56         "ns1/nsec.db",
     57         "ns1/nsec3.db",
     58         "ns1/private.nsec.conf",
     59         "ns1/private.nsec.db",
     60         "ns1/private.nsec3.conf",
     61         "ns1/private.nsec3.db",
     62         "ns1/root.db",
     63         "ns1/signer.err",
     64         "ns1/trusted.conf",
     65     ]
     66 )
     67 
     68 
     69 # labels of a zone with * A 192.0.2.1 wildcard
     70 SUFFIX = dns.name.from_text("allwild.test.")
     71 WILDCARD_RDTYPE = dns.rdatatype.A
     72 WILDCARD_RDATA = "192.0.2.1"
     73 IP_ADDR = "10.53.0.1"
     74 TIMEOUT = 5  # seconds, just a sanity check
     75 
     76 
     77 @settings(deadline=None)
     78 @given(name=dns_names(suffix=SUFFIX), rdtype=dns_rdatatypes_without_meta)
     79 def test_wildcard_rdtype_mismatch(
     80     name: dns.name.Name, rdtype: dns.rdatatype.RdataType, named_port: int
     81 ) -> None:
     82     """Any label non-matching rdtype must result in NODATA."""
     83     assume(rdtype != WILDCARD_RDTYPE)
     84 
     85     # NS and SOA are present in the zone and DS gets answered from parent.
     86     assume(
     87         not (
     88             name == SUFFIX
     89             and rdtype in (dns.rdatatype.SOA, dns.rdatatype.NS, dns.rdatatype.DS)
     90         )
     91     )
     92 
     93     # Subdomains of *.allwild.test. are not to be synthesized.
     94     # See RFC 4592 section 2.2.1.
     95     assume(name == SUFFIX or name.labels[-len(SUFFIX) - 1] != b"*")
     96 
     97     query_msg = isctest.query.create(name, rdtype)
     98     response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT)
     99 
    100     isctest.check.is_response_to(response_msg, query_msg)
    101     isctest.check.noerror(response_msg)
    102     isctest.check.empty_answer(response_msg)
    103 
    104 
    105 @settings(deadline=None)
    106 @given(name=dns_names(suffix=SUFFIX, min_labels=len(SUFFIX) + 1))
    107 def test_wildcard_match(name: dns.name.Name, named_port: int) -> None:
    108     """Any label with maching rdtype must result in wildcard data in answer."""
    109 
    110     # Subdomains of *.allwild.test. are not to be synthesized.
    111     # See RFC 4592 section 2.2.1.
    112     assume(name.labels[-len(SUFFIX) - 1] != b"*")
    113 
    114     query_msg = isctest.query.create(name, WILDCARD_RDTYPE)
    115     response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT)
    116 
    117     isctest.check.is_response_to(response_msg, query_msg)
    118     isctest.check.noerror(response_msg)
    119     expected_answer = [
    120         dns.rrset.from_text(
    121             query_msg.question[0].name,
    122             300,  # TTL, ignored by dnspython comparison
    123             dns.rdataclass.IN,
    124             WILDCARD_RDTYPE,
    125             WILDCARD_RDATA,
    126         )
    127     ]
    128     assert response_msg.answer == expected_answer, str(response_msg)
    129 
    130 
    131 # Force the `*.*.allwild.test.` corner case to be checked.
    132 @settings(deadline=None)
    133 @example(name=isctest.name.prepend_label("*", isctest.name.prepend_label("*", SUFFIX)))
    134 @given(
    135     name=dns_names(
    136         suffix=isctest.name.prepend_label("*", SUFFIX), min_labels=len(SUFFIX) + 2
    137     )
    138 )
    139 def test_wildcard_with_star_not_synthesized(
    140     name: dns.name.Name, named_port: int
    141 ) -> None:
    142     """RFC 4592 section 2.2.1 ghost.*.example."""
    143     query_msg = isctest.query.create(name, WILDCARD_RDTYPE)
    144     response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT)
    145 
    146     isctest.check.is_response_to(response_msg, query_msg)
    147     isctest.check.nxdomain(response_msg)
    148     isctest.check.empty_answer(query_msg)
    149 
    150 
    151 NESTED_SUFFIX = dns.name.from_text("*.*.nestedwild.test.")
    152 
    153 
    154 # Force `*.*.*.nestedwild.test.` to be checked.
    155 @settings(deadline=None)
    156 @example(name=isctest.name.prepend_label("*", NESTED_SUFFIX))
    157 @given(name=dns_names(suffix=NESTED_SUFFIX, min_labels=len(NESTED_SUFFIX) + 1))
    158 def test_name_in_between_wildcards(name: dns.name.Name, named_port: int) -> None:
    159     """Check nested wildcard cases.
    160 
    161     There are `*.nestedwild.test. A` and `*.*.*.nestedwild.test. A` records present in their zone.
    162     This means that `foo.*.nestedwild.test. A` must not be synthetized (see test above)
    163     but `foo.*.*.nestedwild.test A` must.
    164     """
    165 
    166     # `*.*.*.nestedwild.test.` and `*.foo.*.*.nestedwild.test.` must be NOERROR
    167     # `foo.*.*.*.nestedwild.test` must be NXDOMAIN (see test below).
    168     assume(
    169         len(name) == len(NESTED_SUFFIX) + 1
    170         or name.labels[-len(NESTED_SUFFIX) - 1] != b"*"
    171     )
    172 
    173     query_msg = isctest.query.create(name, WILDCARD_RDTYPE)
    174     response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT)
    175 
    176     isctest.check.is_response_to(response_msg, query_msg)
    177     isctest.check.noerror(response_msg)
    178     expected_answer = [
    179         dns.rrset.from_text(
    180             query_msg.question[0].name,
    181             300,  # TTL, ignored by dnspython comparison
    182             dns.rdataclass.IN,
    183             WILDCARD_RDTYPE,
    184             WILDCARD_RDATA,
    185         )
    186     ]
    187     assert response_msg.answer == expected_answer, str(response_msg)
    188 
    189 
    190 @settings(deadline=None)
    191 @given(
    192     name=dns_names(
    193         suffix=isctest.name.prepend_label("*", NESTED_SUFFIX),
    194         min_labels=len(NESTED_SUFFIX) + 2,
    195     )
    196 )
    197 def test_name_nested_wildcard_subdomains_not_synthesized(
    198     name: dns.name.Name, named_port: int
    199 ):
    200     """Check nested wildcard cases.
    201 
    202     `foo.*.*.*.nestedwild.test. A` must not be synthesized.
    203     """
    204     query_msg = isctest.query.create(name, WILDCARD_RDTYPE)
    205     response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT)
    206 
    207     isctest.check.is_response_to(response_msg, query_msg)
    208     isctest.check.nxdomain(response_msg)
    209     isctest.check.empty_answer(query_msg)
    210