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