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.rdatatype
39
40pytest.importorskip("hypothesis")
41from hypothesis import given
42from hypothesis.strategies import binary, integers
43
44
45# labels of a zone with * A 192.0.2.1 wildcard
46WILDCARD_ZONE = ('allwild', 'test', '')
47WILDCARD_RDTYPE = dns.rdatatype.A
48WILDCARD_RDATA = '192.0.2.1'
49IPADDR = '10.53.0.1'
50TIMEOUT = 5  # seconds, just a sanity check
51
52
53# Helpers
54def is_nonexpanding_rdtype(rdtype):
55    """skip meta types to avoid weird rcodes caused by AXFR etc.; RFC 6895"""
56    return not(rdtype == WILDCARD_RDTYPE
57               or dns.rdatatype.is_metatype(rdtype)  # known metatypes: OPT ...
58               or 128 <= rdtype <= 255)  # unknown meta types
59
60
61def tcp_query(where, port, qname, qtype):
62    querymsg = dns.message.make_query(qname, qtype)
63    assert len(querymsg.question) == 1
64    return querymsg, dns.query.tcp(querymsg, where, port=port, timeout=TIMEOUT)
65
66
67def query(where, port, label, rdtype):
68    labels = (label, ) + WILDCARD_ZONE
69    qname = dns.name.Name(labels)
70    return tcp_query(where, port, qname, rdtype)
71
72
73# Tests
74@given(label=binary(min_size=1, max_size=63),
75       rdtype=integers(min_value=0, max_value=65535).filter(
76           is_nonexpanding_rdtype))
77def test_wildcard_rdtype_mismatch(label, rdtype, named_port):
78    """any label non-matching rdtype must result in to NODATA"""
79    check_answer_nodata(*query(IPADDR, named_port, label, rdtype))
80
81
82def check_answer_nodata(querymsg, answer):
83    assert querymsg.is_response(answer), str(answer)
84    assert answer.rcode() == dns.rcode.NOERROR, str(answer)
85    assert answer.answer == [], str(answer)
86
87
88@given(label=binary(min_size=1, max_size=63))
89def test_wildcard_match(label, named_port):
90    """any label with maching rdtype must result in wildcard data in answer"""
91    check_answer_noerror(*query(IPADDR, named_port, label, WILDCARD_RDTYPE))
92
93
94def check_answer_noerror(querymsg, answer):
95    assert querymsg.is_response(answer), str(answer)
96    assert answer.rcode() == dns.rcode.NOERROR, str(answer)
97    assert len(querymsg.question) == 1, str(answer)
98    expected_answer = [dns.rrset.from_text(
99                            querymsg.question[0].name,
100                            300,  # TTL, ignored by dnspython comparison
101                            dns.rdataclass.IN,
102                            WILDCARD_RDTYPE,
103                            WILDCARD_RDATA)]
104    assert answer.answer == expected_answer, str(answer)
105