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""" 15Example property-based test for wildcard synthesis. 16Verifies that otherwise-empty zone with single wildcard record * A 192.0.2.1 17produces synthesized answers for <random_label>.test. A, and returns NODATA for 18<random_label>.test. when rdtype is not A. 19 20Limitations - untested properties: 21 - expansion works with multiple labels 22 - asterisk in qname does not cause expansion 23 - empty non-terminals prevent expansion 24 - or more generally any existing node prevents expansion 25 - DNSSEC record inclusion 26 - possibly others, see RFC 4592 and company 27 - content of authority & additional sections 28 - flags beyond RCODE 29 - special behavior of rdtypes like CNAME 30""" 31import pytest 32 33pytest.importorskip("dns") 34import dns.message 35import dns.name 36import dns.query 37import dns.rcode 38import dns.rdataclass 39import dns.rdatatype 40import dns.rrset 41 42pytest.importorskip("hypothesis") 43from hypothesis import given 44from hypothesis.strategies import binary, integers 45 46 47# labels of a zone with * A 192.0.2.1 wildcard 48WILDCARD_ZONE = ("allwild", "test", "") 49WILDCARD_RDTYPE = dns.rdatatype.A 50WILDCARD_RDATA = "192.0.2.1" 51IPADDR = "10.53.0.1" 52TIMEOUT = 5 # seconds, just a sanity check 53 54 55# Helpers 56def is_nonexpanding_rdtype(rdtype): 57 """skip meta types to avoid weird rcodes caused by AXFR etc.; RFC 6895""" 58 return not ( 59 rdtype == WILDCARD_RDTYPE 60 or dns.rdatatype.is_metatype(rdtype) # known metatypes: OPT ... 61 or 128 <= rdtype <= 255 62 ) # unknown meta types 63 64 65def tcp_query(where, port, qname, qtype): 66 querymsg = dns.message.make_query(qname, qtype) 67 assert len(querymsg.question) == 1 68 return querymsg, dns.query.tcp(querymsg, where, port=port, timeout=TIMEOUT) 69 70 71def query(where, port, label, rdtype): 72 labels = (label,) + WILDCARD_ZONE 73 qname = dns.name.Name(labels) 74 return tcp_query(where, port, qname, rdtype) 75 76 77# Tests 78@given( 79 label=binary(min_size=1, max_size=63), 80 rdtype=integers(min_value=0, max_value=65535).filter(is_nonexpanding_rdtype), 81) 82def test_wildcard_rdtype_mismatch(label, rdtype, named_port): 83 """any label non-matching rdtype must result in to NODATA""" 84 check_answer_nodata(*query(IPADDR, named_port, label, rdtype)) 85 86 87def check_answer_nodata(querymsg, answer): 88 assert querymsg.is_response(answer), str(answer) 89 assert answer.rcode() == dns.rcode.NOERROR, str(answer) 90 assert answer.answer == [], str(answer) 91 92 93@given(label=binary(min_size=1, max_size=63)) 94def test_wildcard_match(label, named_port): 95 """any label with maching rdtype must result in wildcard data in answer""" 96 check_answer_noerror(*query(IPADDR, named_port, label, WILDCARD_RDTYPE)) 97 98 99def check_answer_noerror(querymsg, answer): 100 assert querymsg.is_response(answer), str(answer) 101 assert answer.rcode() == dns.rcode.NOERROR, str(answer) 102 assert len(querymsg.question) == 1, str(answer) 103 expected_answer = [ 104 dns.rrset.from_text( 105 querymsg.question[0].name, 106 300, # TTL, ignored by dnspython comparison 107 dns.rdataclass.IN, 108 WILDCARD_RDTYPE, 109 WILDCARD_RDATA, 110 ) 111 ] 112 assert answer.answer == expected_answer, str(answer) 113