Home | History | Annotate | Line # | Download | only in digdelv
      1 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
      2 #
      3 # SPDX-License-Identifier: MPL-2.0
      4 #
      5 # This Source Code Form is subject to the terms of the Mozilla Public
      6 # License, v. 2.0.  If a copy of the MPL was not distributed with this
      7 # file, you can obtain one at https://mozilla.org/MPL/2.0/.
      8 #
      9 # See the COPYRIGHT file distributed with this work for additional
     10 # information regarding copyright ownership.
     11 
     12 """
     13 Tests for the dig tool.
     14 """
     15 
     16 from re import compile as Re
     17 
     18 import ipaddress
     19 import os
     20 import re
     21 
     22 import pytest
     23 
     24 from digdelv.common import ARTIFACTS, check_ttl_range, parse_yaml
     25 from isctest.util import param
     26 
     27 import isctest
     28 import isctest.mark
     29 
     30 pytestmark = [
     31     pytest.mark.extra_artifacts(ARTIFACTS),
     32 ]
     33 
     34 
     35 @pytest.fixture(name="dig")
     36 def dig_fixture(named_port):
     37     return isctest.run.EnvCmd("DIG", f"-p {named_port}")
     38 
     39 
     40 def edns_yaml(text, direction="query"):
     41     """Get the EDNS OPT pseudosection mapping of the first message in
     42     dig +yaml output."""
     43     message = parse_yaml(text)[0]["message"]
     44     return message[f"{direction}_message_data"]["OPT_PSEUDOSECTION"]["EDNS"]
     45 
     46 
     47 def test_update_response(dig, ans6):
     48     """Check that dig rejects a response with the UPDATE opcode."""
     49     result = dig(
     50         f"@{ans6.ip} +tries=1 +timeout=1 cname foo.bar", raise_on_exception=False
     51     )
     52     assert result.rc != 0
     53     assert "Opcode mismatch" in result.out
     54 
     55 
     56 def test_short(dig, ns3):
     57     """Check that dig +short returns a single-line answer."""
     58     result = dig(f"@{ns3.ip} +short a a.example")
     59     assert len(result.out.splitlines()) == 1
     60 
     61 
     62 @pytest.mark.parametrize("option", ["+split=4", "+sp=4"])
     63 def test_split_width(dig, ns3, option):
     64     """Check that dig +split (and its +sp abbreviation) splits hex data
     65     into fields of the requested width."""
     66     result = dig(f"@{ns3.ip} {option} -t sshfp foo.example")
     67     assert " 9ABC DEF6 7890 " in result.out
     68     assert check_ttl_range(result.out, "SSHFP", 300)
     69 
     70 
     71 def test_unknownformat(dig, ns3):
     72     """Check that dig +unknownformat prints RFC 3597 format."""
     73     result = dig(f"@{ns3.ip} +unknownformat a a.example")
     74     assert Re(r"CLASS1\s+TYPE1\s+\\# 4 0A000001") in result.out
     75     assert check_ttl_range(result.out, "TYPE1", 300)
     76 
     77 
     78 def test_reverse_lookup(dig, ns3):
     79     """Check that dig -x works."""
     80     result = dig(f"@{ns3.ip} -x 127.0.0.1")
     81     # doesn't matter if has answer
     82     assert Re(r"127\.in-addr\.arpa\.", re.IGNORECASE) in result.out
     83     assert check_ttl_range(result.out, "SOA", 86400)
     84 
     85 
     86 def test_tcp(dig, ns3):
     87     """Check that dig over TCP works."""
     88     result = dig(f"+tcp @{ns3.ip} a a.example")
     89     assert Re(r"10\.0\.0\.1$") in result.out
     90     assert check_ttl_range(result.out, "A", 300)
     91 
     92 
     93 @pytest.mark.parametrize(
     94     "args,expect_rrcomment",
     95     [
     96         param("+multi +norrcomments -t DNSKEY example", False, id="multi-norrcomments"),
     97         param("+rrcomments DNSKEY example", True, id="rrcomments"),
     98         param("+short +rrcomments DNSKEY example", True, id="short-rrcomments"),
     99     ],
    100 )
    101 def test_dnskey_rrcomments(dig, ns3, zsk, args, expect_rrcomment):
    102     """Check that +[no]rrcomments controls the DNSKEY comment
    103     (the default is rrcomments, even with +multi)."""
    104     result = dig(f"+tcp @{ns3.ip} {args}")
    105     assert (zsk.rrcomment in result.out) == expect_rrcomment
    106     if "+short" not in args:
    107         assert check_ttl_range(result.out, "DNSKEY", 300)
    108 
    109 
    110 def test_soa_norrcomments(dig, ns3):
    111     """Check that +multi +norrcomments suppresses the SOA field comments."""
    112     result = dig(f"+tcp @{ns3.ip} +multi +norrcomments -t SOA example")
    113     assert "; serial" not in result.out
    114     assert check_ttl_range(result.out, "SOA", 300)
    115 
    116 
    117 def test_short_nosplit(dig, ns3, zsk):
    118     """Check that dig +short +nosplit does not split the key data."""
    119     result = dig(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example")
    120     assert zsk.keydata.replace(" ", "") in result.out
    121 
    122 
    123 def test_short_rrcomments_line(dig, ns3, zsk):
    124     """Check the exact dig +short +rrcomments output line."""
    125     result = dig(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example")
    126     expected = re.escape(f"{zsk.keydata}  {zsk.rrcomment}")
    127     assert Re(expected + "$") in result.out
    128 
    129 
    130 def test_multi_flag_is_local(dig, ns3):
    131     """Check that +[no]multi applies to a single lookup only."""
    132     lines = {}
    133     for flags in [
    134         ("nomulti", "nomulti"),
    135         ("multi", "nomulti"),
    136         ("nomulti", "multi"),
    137         ("multi", "multi"),
    138     ]:
    139         first, second = flags
    140         result = dig(f"+tcp @{ns3.ip} -t DNSKEY example +{first} example +{second}")
    141         assert check_ttl_range(result.out, "DNSKEY", 300)
    142         lines[flags] = len(result.out.splitlines())
    143     assert lines[("multi", "multi")] >= lines[("nomulti", "multi")]
    144     assert lines[("multi", "multi")] >= lines[("multi", "nomulti")]
    145     assert lines[("nomulti", "multi")] >= lines[("nomulti", "nomulti")]
    146     assert lines[("multi", "nomulti")] >= lines[("nomulti", "nomulti")]
    147 
    148 
    149 def test_noheader_only(dig, ns3):
    150     """Check that dig +noheader-only sends a full query."""
    151     result = dig(f"+tcp @{ns3.ip} +noheader-only A example")
    152     assert "Got answer:" in result.out
    153     assert check_ttl_range(result.out, "SOA", 300)
    154 
    155 
    156 @pytest.mark.parametrize(
    157     "class_type",
    158     [
    159         param("", id="default"),
    160         param("-c IN -t A", id="with-class-and-type"),
    161     ],
    162 )
    163 def test_header_only(dig, ns3, class_type):
    164     """Check that dig +header-only sends a query without a question."""
    165     result = dig(f"+tcp @{ns3.ip} +header-only {class_type} example")
    166     assert Re(r"^;; flags: qr rd; QUERY: 0, ANSWER: 0,") in result.out
    167     assert Re(r"^;; QUESTION SECTION:") not in result.out
    168 
    169 
    170 @pytest.mark.parametrize(
    171     "qname,ttl",
    172     [
    173         param("weeks", "3w"),
    174         param("days", "3d"),
    175         param("hours", "3h"),
    176         param("minutes", "45m"),
    177         param("seconds", "45s"),
    178     ],
    179 )
    180 def test_ttl_units(dig, ns2, qname, ttl):
    181     """Check that dig +ttlunits prints TTLs in time units."""
    182     result = dig(f"+tcp @{ns2.ip} +ttlunits A {qname}.example")
    183     assert Re(rf"^{qname}\.example\.\s+{ttl}\s") in result.out
    184 
    185 
    186 @pytest.mark.parametrize(
    187     "options,field",
    188     [
    189         param("+ttlunits +nottlid", "IN", id="nottlid-wins"),
    190         param("+nottlid +ttlunits", "3w", id="ttlunits-wins"),
    191         param("+nottlid +nottlunits", "1814400", id="plain-seconds"),
    192     ],
    193 )
    194 def test_ttl_units_precedence(dig, ns2, options, field):
    195     """Check that the last of the +ttlid/+ttlunits options wins."""
    196     result = dig(f"+tcp @{ns2.ip} {options} A weeks.example")
    197     assert Re(rf"^weeks\.example\.\s+{re.escape(field)}\s") in result.out
    198 
    199 
    200 def test_class_chaos(dig, ns3):
    201     """Check that dig -c CHAOS works."""
    202     result = dig(f"@{ns3.ip} -c CHAOS -t txt version.bind")
    203     assert "version.bind.\t\t0\tCH\tTXT" in result.out
    204 
    205 
    206 def test_bad_escape(dig, ns3):
    207     """Check that dig gracefully rejects a bad escape in the domain name."""
    208     result = dig(rf"@{ns3.ip} \0.", raise_on_exception=False)
    209     assert result.rc == 10
    210     assert "REQUIRE" not in result.err
    211     assert "is not a legal name (bad escape)" in result.err
    212 
    213 
    214 def test_q_m(dig, ns3):
    215     """Check that -q -m treats -m as a query name, not as the memory
    216     debugging flag."""
    217     result = dig(f"@{ns3.ip} -q -m", raise_on_exception=False)
    218     assert Re(r"^;-m\..*IN.*A$") in result.out
    219     assert "Dump of all outstanding memory allocations" not in result.out
    220 
    221 
    222 @pytest.mark.parametrize(
    223     "options,pattern",
    224     [
    225         param(
    226             "+expandaaaa",
    227             r"ns2\.example.*fd92:7065:0b8e:ffff:0000:0000:0000:0002",
    228             id="expandaaaa",
    229         ),
    230         param(
    231             "+noexpandaaaa", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="noexpandaaaa"
    232         ),
    233         param("", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="default"),
    234         param(
    235             "+short +expandaaaa",
    236             r"^fd92:7065:0b8e:ffff:0000:0000:0000:0002$",
    237             id="short-expandaaaa",
    238         ),
    239     ],
    240 )
    241 def test_expandaaaa(dig, ns3, options, pattern):
    242     """Check that +[no]expandaaaa controls AAAA address formatting
    243     (the default is +noexpandaaaa)."""
    244     result = dig(f"@{ns3.ip} {options} AAAA ns2.example")
    245     assert Re(pattern) in result.out
    246 
    247 
    248 def test_bufsize_zero(dig, ns3):
    249     """Check that +bufsize=0 just sets the advertised buffer size to 0
    250     instead of disabling EDNS."""
    251     result = dig(f"@{ns3.ip} a.example +bufsize=0 +qr")
    252     assert "EDNS:" in result.out
    253 
    254 
    255 def test_bufsize_restores_default(dig, ns3):
    256     """Check that a later +bufsize restores the default buffer size."""
    257     result = dig(f"@{ns3.ip} a.example +bufsize=0 +bufsize +qr")
    258     assert len(result.out.grep(Re(r"EDNS:.* udp:"))) == 2
    259     assert len(result.out.grep(Re(r"EDNS:.* udp: 1232"))) == 2
    260 
    261 
    262 @pytest.mark.parametrize(
    263     "options,unit",
    264     [
    265         param("", "msec", id="msec"),
    266         param("-u", "usec", id="usec"),
    267     ],
    268 )
    269 def test_query_time_units(dig, ns3, options, unit):
    270     """Check that Query time is in milliseconds, or in microseconds
    271     with -u."""
    272     result = dig(f"{options} @{ns3.ip} a.example")
    273     assert Re(rf";; Query time: \d+ {unit}") in result.out
    274 
    275 
    276 @pytest.mark.parametrize(
    277     "options,digits",
    278     [
    279         param("+yaml", 3, id="msec"),
    280         param("-u +yaml", 6, id="usec"),
    281     ],
    282 )
    283 def test_yaml_timestamp_precision(dig, ns3, options, digits):
    284     """Check that +yaml timestamps have millisecond precision, or
    285     microsecond precision with -u."""
    286     result = dig(f"{options} @{ns3.ip} a.example")
    287     for field in ("query_time", "response_time"):
    288         pattern = (
    289             rf"{field}: !!timestamp \d{{4}}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{{{digits}}}Z"
    290         )
    291         assert Re(pattern) in result.out
    292 
    293 
    294 def test_local_reserved_warning(dig, ns3):
    295     """Check that dig warns about .local queries."""
    296     result = dig(f"@{ns3.ip} local soa")
    297     assert ";; WARNING: .local is reserved for Multicast DNS" in result.out
    298 
    299 
    300 def test_nocrypto(dig, ns1):
    301     """Check that +nocrypto omits the key and signature data."""
    302     alg_num = os.environ["DEFAULT_ALGORITHM_NUMBER"]
    303     result = dig(f"+dnssec +norec +nocrypto DNSKEY . @{ns1.ip}")
    304     assert Re(rf"256 \d+ {alg_num} \[key id = [1-9]\d*]") in result.out
    305     assert Re(r"RRSIG.* \[omitted]") in result.out
    306     result = dig(f"+norec +nocrypto DS example @{ns1.ip}")
    307     assert Re(r"DS.* \d+ [12] \[omitted]") in result.out
    308 
    309 
    310 def test_coflag(dig, ns3):
    311     """Check that dig +coflag sets the EDNS CO flag in the sent query."""
    312     result = dig(f"+tcp @{ns3.ip} +coflag +qr example")
    313     assert Re(r"^; EDNS: version: 0, flags: co;") in result.out
    314     assert check_ttl_range(result.out, "SOA", 300)
    315 
    316 
    317 def test_coflag_yaml(dig, ns3):
    318     """Check that dig +coflag +yaml shows the CO flag in the sent query."""
    319     result = dig(f"+yaml +tcp @{ns3.ip} +coflag +qr example")
    320     assert edns_yaml(result.out)["flags"] == "co"
    321 
    322 
    323 @pytest.mark.parametrize(
    324     "option,sent_flags",
    325     [
    326         param("+raflag", "rd ra ad"),
    327         param("+tcflag", "tc rd ad"),
    328     ],
    329 )
    330 def test_header_flag_options(dig, ns3, option, sent_flags):
    331     """Check that +raflag/+tcflag set the flag in the sent query and that
    332     the response is unaffected."""
    333     result = dig(f"+tcp @{ns3.ip} {option} +qr example")
    334     assert Re(rf"^;; flags: {sent_flags}; QUERY: 1, ANSWER: 0") in result.out
    335     assert Re(r"^;; flags: qr rd ra; QUERY: 1, ANSWER: 0,") in result.out
    336     assert check_ttl_range(result.out, "SOA", 300)
    337 
    338 
    339 def test_zflag(dig, ns3):
    340     """Check that dig +zflag sets the MBZ bit and that named ignores it."""
    341     result = dig(f"+tcp @{ns3.ip} +zflag +qr A example")
    342     assert Re(r"^;; flags: rd ad; MBZ: 0x4;") in result.out
    343     assert Re(r"^;; flags: qr rd ra; QUERY: 1") in result.out
    344     assert check_ttl_range(result.out, "SOA", 300)
    345 
    346 
    347 def test_ednsopt_08_no_insist(dig, ns3):
    348     """Check that +qr +ednsopt=08 does not cause an INSIST failure."""
    349     result = dig(f"@{ns3.ip} +ednsopt=08 +qr a a.example")
    350     assert "INSIST" not in result.out
    351     assert "FORMERR" in result.out
    352 
    353 
    354 @pytest.mark.parametrize(
    355     "option",
    356     [
    357         param("3", id="number"),
    358         param("nsid", id="name"),
    359     ],
    360 )
    361 def test_ednsopt_nsid(dig, ns3, option):
    362     """Check that +ednsopt accepts an option number as well as a name."""
    363     result = dig(f"@{ns3.ip} +ednsopt={option} a.example")
    364     assert Re(r'NSID: .* \("ns3"\)') in result.out
    365     assert check_ttl_range(result.out, "A", 300)
    366 
    367 
    368 def test_ednsopt_update_lease(dig, ns3):
    369     """Check that a single-lease UPDATE-LEASE option prints as expected."""
    370     result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e10 +qr a.example")
    371     assert "UPDATE-LEASE: 3600 (1 hour)" in result.out
    372 
    373 
    374 def test_ednsopt_update_lease_yaml(dig, ns3):
    375     """Check that a single-lease UPDATE-LEASE option prints as expected
    376     with +yaml."""
    377     result = dig(f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e10 +qr a.example")
    378     assert edns_yaml(result.out)["UPDATE-LEASE"]["LEASE"] == 3600
    379     assert "LEASE: 3600 # 1 hour" in result.out
    380 
    381 
    382 def test_ednsopt_update_lease_split(dig, ns3):
    383     """Check that a split-lease UPDATE-LEASE option prints as expected."""
    384     result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example")
    385     assert "UPDATE-LEASE: 3600/1209600 (1 hour/2 weeks)" in result.out
    386 
    387 
    388 def test_ednsopt_update_lease_split_yaml(dig, ns3):
    389     """Check that a split-lease UPDATE-LEASE option prints as expected
    390     with +yaml."""
    391     result = dig(
    392         f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example"
    393     )
    394     update_lease = edns_yaml(result.out)["UPDATE-LEASE"]
    395     assert update_lease["LEASE"] == 3600
    396     assert update_lease["KEY-LEASE"] == 1209600
    397     assert "LEASE: 3600 # 1 hour" in result.out
    398     assert "KEY-LEASE: 1209600 # 2 weeks" in result.out
    399 
    400 
    401 def test_ednsopt_llq(dig, ns3):
    402     """Check that the LLQ option prints as expected."""
    403     result = dig(
    404         f"@{ns3.ip} +ednsopt=llq:0001000200001234567812345678fefefefe +qr a.example"
    405     )
    406     pattern = (
    407         r"LLQ: Version: 1, Opcode: 2, Error: 0, "
    408         r"Identifier: 1311768465173141112, Lifetime: 4278124286$"
    409     )
    410     assert Re(pattern) in result.out
    411 
    412 
    413 def test_ednsopt_llq_yaml(dig, ns3):
    414     """Check that the LLQ option prints as expected with +yaml."""
    415     result = dig(
    416         f"@{ns3.ip} +yaml +ednsopt=llq:0001000200001234567812345678fefefefe "
    417         "+qr a.example"
    418     )
    419     llq = edns_yaml(result.out)["LLQ"]
    420     assert llq["LLQ-VERSION"] == 1
    421     assert llq["LLQ-OPCODE"] == 2
    422     assert llq["LLQ-ERROR"] == 0
    423     assert llq["LLQ-ID"] == 1311768465173141112
    424     assert llq["LLQ-LEASE"] == 4278124286
    425 
    426 
    427 def test_ednsopt_key_tag_empty(dig, ns3):
    428     """Check that an empty key-tag option is sent and FORMERR is returned."""
    429     result = dig(f"@{ns3.ip} +ednsopt=key-tag a.example +qr")
    430     assert Re(r"; KEY-TAG: *$") in result.out
    431     assert "status: FORMERR" in result.out
    432 
    433 
    434 def test_ednsopt_key_tag(dig, ns3):
    435     """Check that a key-tag value list is sent and accepted."""
    436     result = dig(f"@{ns3.ip} +ednsopt=key-tag:00010002 a.example +qr")
    437     assert Re(r"; KEY-TAG: 1, 2$") in result.out
    438     assert "status: FORMERR" not in result.out
    439     assert check_ttl_range(result.out, "A", 300)
    440 
    441 
    442 def test_ednsopt_key_tag_yaml(dig, ns3):
    443     """Check that a key-tag value list prints as a list with +yaml."""
    444     result = dig(f"@{ns3.ip} +yaml +ednsopt=key-tag:00010002 a.example +qr")
    445     assert edns_yaml(result.out)["KEY-TAG"] == [1, 2]
    446 
    447 
    448 def test_ednsopt_key_tag_malformed(dig, ns3):
    449     """Check that a malformed key-tag value list is sent as raw data and
    450     FORMERR is returned."""
    451     result = dig(f"@{ns3.ip} +ednsopt=key-tag:0001000201 a.example +qr")
    452     assert "; KEY-TAG: 00 01 00 02 01" in result.out
    453     assert "status: FORMERR" in result.out
    454 
    455 
    456 @pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
    457 def test_ednsopt_tag(dig, ns3, tag):
    458     """Check that a valid client/server-tag value is sent and accepted."""
    459     result = dig(f"@{ns3.ip} +ednsopt={tag}:0001 a.example +qr")
    460     assert Re(rf"; {tag.upper()}: 1$") in result.out
    461     assert "status: FORMERR" not in result.out
    462 
    463 
    464 @pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
    465 def test_ednsopt_tag_yaml(dig, ns3, tag):
    466     """Check that a client/server-tag value prints as expected with +yaml."""
    467     result = dig(f"@{ns3.ip} +yaml +ednsopt={tag}:0001 a.example +qr")
    468     assert edns_yaml(result.out)[tag.upper()] == 1
    469 
    470 
    471 @pytest.mark.parametrize(
    472     "value",
    473     [
    474         param("01", id="too-short"),
    475         param("000001", id="too-long"),
    476     ],
    477 )
    478 @pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
    479 def test_ednsopt_tag_bad_length(dig, ns3, tag, value):
    480     """Check that FORMERR is returned for a client/server-tag value of the
    481     wrong length."""
    482     result = dig(f"@{ns3.ip} +ednsopt={tag}:{value} a.example +qr")
    483     assert f"; {tag.upper()}" in result.out
    484     assert "status: FORMERR" in result.out
    485 
    486 
    487 def test_ednsopt_chain(dig, ns3):
    488     """Check that the CHAIN option prints special characters escaped."""
    489     result = dig(rf'@{ns3.ip} +ednsopt=chain:02002200 a.\000" +qr')
    490     assert r'; CHAIN: "\000\""' in result.out
    491 
    492 
    493 def test_ednsopt_chain_yaml(dig, ns3):
    494     """Check that the CHAIN option prints special characters escaped
    495     with +yaml."""
    496     result = dig(rf'@{ns3.ip} +yaml +ednsopt=chain:02002200 a.\000" +qr')
    497     assert edns_yaml(result.out)["CHAIN"] == r"\000\""
    498 
    499 
    500 def test_expire(dig, ns1):
    501     """Check that dig processes +expire."""
    502     result = dig(f"@{ns1.ip} +expire . soa")
    503     assert "; EXPIRE: 1200 (20 minutes)" in result.out
    504 
    505 
    506 def test_expire_yaml(dig, ns1):
    507     """Check that dig processes +expire with +yaml."""
    508     result = dig(f"@{ns1.ip} +yaml +expire . soa")
    509     assert edns_yaml(result.out, "response")["EXPIRE"] == 1200
    510     assert "EXPIRE: 1200 # 20 minutes" in result.out
    511 
    512 
    513 def test_keepalive(dig, ns1):
    514     """Check that dig processes +keepalive."""
    515     result = dig(f"@{ns1.ip} +keepalive . soa +tcp")
    516     assert "; TCP-KEEPALIVE: 30.0 secs" in result.out
    517 
    518 
    519 def test_keepalive_yaml(dig, ns1):
    520     """Check that dig processes +keepalive with +yaml."""
    521     result = dig(f"@{ns1.ip} +yaml +keepalive . soa +tcp")
    522     assert edns_yaml(result.out, "response")["TCP-KEEPALIVE"] == "30.0 secs"
    523 
    524 
    525 @pytest.mark.parametrize(
    526     "payload,expected",
    527     [
    528         param("ede:0000666f6f", "; EDE: 0 (Other): (foo)", id="first-defined-code"),
    529         param("ede:0018", "; EDE: 24 (Invalid Data)", id="last-defined-code"),
    530         param("ede:0019666f6f", "; EDE: 25: (foo)", id="undefined-code"),
    531         param("ede", "; EDE:", id="empty"),
    532         param("ede:00", '; EDE: 00 (".")', id="too-short"),
    533     ],
    534 )
    535 def test_ednsopt_ede(dig, ns3, payload, expected):
    536     """Check that Extended DNS Error options, including invalid ones with
    537     a too short payload, are printed correctly."""
    538     result = dig(f"@{ns3.ip} +ednsopt={payload} a.example +qr")
    539     assert Re("^" + re.escape(expected) + "$") in result.out
    540 
    541 
    542 @pytest.mark.parametrize(
    543     "payload,info_code,extra_text",
    544     [
    545         param("ede:0000666f6f", "0 (Other)", "foo", id="first-defined-code"),
    546         param("ede:0018", "24 (Invalid Data)", None, id="last-defined-code"),
    547         param("ede:0019666f6f", 25, "foo", id="undefined-code"),
    548     ],
    549 )
    550 def test_ednsopt_ede_yaml(dig, ns3, payload, info_code, extra_text):
    551     """Check that Extended DNS Error options are printed correctly
    552     with +yaml."""
    553     result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr")
    554     ede = edns_yaml(result.out)["EDE"]
    555     assert ede["INFO-CODE"] == info_code
    556     if extra_text is None:
    557         assert "EXTRA-TEXT" not in ede
    558     else:
    559         assert ede["EXTRA-TEXT"] == extra_text
    560 
    561 
    562 def test_ednsopt_ede_yaml_specials(dig, ns3):
    563     """Check that EDE extra text with '"' and '\\' specials survives YAML
    564     quoting."""
    565     result = dig(f"@{ns3.ip} +yaml +ednsopt=ede:0000666f6f225c a.example +qr")
    566     assert edns_yaml(result.out)["EDE"]["EXTRA-TEXT"] == 'foo"\\'
    567 
    568 
    569 @pytest.mark.parametrize(
    570     "payload,expected",
    571     [
    572         param("ede", None, id="empty"),
    573         param("ede:00", '00 (".")', id="too-short"),
    574     ],
    575 )
    576 def test_ednsopt_ede_yaml_invalid(dig, ns3, payload, expected):
    577     """Check that invalid Extended DNS Error options with a too short
    578     payload are printed correctly with +yaml."""
    579     result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr")
    580     assert edns_yaml(result.out)["EDE"] == expected
    581 
    582 
    583 def test_ednsopt_malformed(dig, ns3):
    584     """Check that dig handles the malformed option '+ednsopt=:'
    585     gracefully."""
    586     result = dig(f"@{ns3.ip} +ednsopt=: a.example", raise_on_exception=False)
    587     assert result.rc != 0
    588     assert "ednsopt no code point specified" in result.err
    589 
    590 
    591 def test_ednsflags_reenables_edns(dig, ns3):
    592     """Check that +noedns +ednsflags=<nonzero> re-enables EDNS."""
    593     result = dig(f"@{ns3.ip} +qr +noedns +ednsflags=0x70 a.example")
    594     assert "; EDNS: version: 0, flags:; MBZ: 0x0070, udp: 1232" in result.out
    595     assert "; EDNS: version: 0, flags:; udp: 1232" in result.out
    596 
    597 
    598 def test_showbadvers(dig, ns3):
    599     """Check that +showbadvers displays the BADVERS response as well as
    600     the retry without EDNS version 1."""
    601     result = dig(f"@{ns3.ip} +edns=1 +qr +showbadvers a.example")
    602     assert "; EDNS: version: 1, flags:; udp: 1232" in result.out
    603     assert "; EDNS: version: 0, flags:; udp: 1232" in result.out
    604     assert "status: BADVERS" in result.out
    605     assert "status: NOERROR" in result.out
    606 
    607 
    608 def test_subnet(dig, ns2):
    609     """Check that dig +subnet sends the client subnet."""
    610     result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.1 A a.example")
    611     assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out
    612     assert check_ttl_range(result.out, "A", 300)
    613 
    614 
    615 def test_subnet_last_wins(dig, ns2):
    616     """Check that the last of multiple +subnet options wins."""
    617     result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.0 +subnet=127.0.0.1 A a.example")
    618     assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out
    619     assert check_ttl_range(result.out, "A", 300)
    620 
    621 
    622 @pytest.mark.parametrize("plen", range(1, 25))
    623 def test_subnet_prefix_lengths(dig, ns2, plen):
    624     """Check that dig +subnet masks the address to various prefix
    625     lengths."""
    626     result = dig(f"+tcp @{ns2.ip} +subnet=255.255.255.255/{plen} A a.example")
    627     addr = ipaddress.ip_address((0xFFFFFFFF << (32 - plen)) & 0xFFFFFFFF)
    628     assert "FORMERR" not in result.out
    629     assert f"CLIENT-SUBNET: {addr}/{plen}/0" in result.out
    630     assert check_ttl_range(result.out, "A", 300)
    631 
    632 
    633 @pytest.mark.parametrize("plen", range(9, 16))
    634 def test_subnet_prefix_between_byte_boundaries(dig, ns2, plen):
    635     """Check dig +subnet with prefix lengths between byte boundaries."""
    636     result = dig(f"+tcp @{ns2.ip} +subnet=10.53/{plen} A a.example")
    637     assert "FORMERR" not in result.out
    638     assert Re(rf"CLIENT-SUBNET.*/{plen}/0") in result.out
    639     assert check_ttl_range(result.out, "A", 300)
    640 
    641 
    642 ZERO_SUBNETS = [
    643     param("+subnet=0/0", "0.0.0.0/0/0"),
    644     param("+subnet=0", "0.0.0.0/0/0"),
    645     param("+subnet=::/0", "::/0/0"),
    646 ]
    647 
    648 
    649 @pytest.mark.parametrize("option,subnet", ZERO_SUBNETS)
    650 def test_subnet_zero(dig, ns2, option, subnet):
    651     """Check that a zero-length client subnet is sent and answered."""
    652     result = dig(f"+tcp @{ns2.ip} {option} A a.example")
    653     assert "status: NOERROR" in result.out
    654     assert f"CLIENT-SUBNET: {subnet}" in result.out
    655     assert "10.0.0.1" in result.out
    656     assert check_ttl_range(result.out, "A", 300)
    657 
    658 
    659 @pytest.mark.parametrize("option,subnet", ZERO_SUBNETS)
    660 def test_subnet_zero_yaml(dig, ns2, option, subnet):
    661     """Check that a zero-length client subnet is echoed in the +yaml
    662     response."""
    663     result = dig(f"+yaml +tcp @{ns2.ip} {option} A a.example")
    664     assert edns_yaml(result.out, "response")["CLIENT-SUBNET"] == subnet
    665 
    666 
    667 def test_subnet_yaml(dig, ns2):
    668     """Check that +subnet=dead::/16 is shown in the +yaml query."""
    669     result = dig(f"+yaml +tcp @{ns2.ip} +qr +subnet=dead::/16 A a.example")
    670     assert edns_yaml(result.out)["CLIENT-SUBNET"] == "dead::/16/0"
    671 
    672 
    673 def test_subnet_raw_zero(dig, ns2):
    674     """Check that a raw zero-length ECS option (family 0, source 0,
    675     scope 0) is rejected by the server with FORMERR."""
    676     result = dig(f"+tcp @{ns2.ip} +ednsopt=8:00000000 A a.example")
    677     assert "status: FORMERR" in result.out
    678     assert "CLIENT-SUBNET" not in result.out
    679 
    680 
    681 def test_subnet_raw_unknown_family(dig, ns2):
    682     """Check that a raw ECS option with an unknown family (3) is sent
    683     as-is and rejected by the server with FORMERR."""
    684     result = dig(f"+qr +tcp @{ns2.ip} +ednsopt=8:00030000 A a.example")
    685     assert "status: FORMERR" in result.out
    686     assert len(result.out.grep("CLIENT-SUBNET: 00 03 00 00")) == 1
    687 
    688 
    689 def test_origin_preserved_on_tcp_retries(dig, ans4):
    690     """Check that dig preserves the search origin when retrying over
    691     TCP."""
    692     result = dig(
    693         f"-d +tcp @{ans4.ip} +retry=1 +time=1 +domain=bar foo",
    694         raise_on_exception=False,
    695     )
    696     assert result.rc != 0
    697     assert len(result.err.grep("trying origin bar")) == 2
    698     assert "using root origin" not in result.err
    699 
    700 
    701 def test_4_and_6_mutually_exclusive(dig, ns2):
    702     """Check that dig rejects -4 combined with -6."""
    703     result = dig(f"+tcp @{ns2.ip} -4 -6 A a.example", raise_on_exception=False)
    704     assert result.rc != 0
    705     assert "only one of -4 and -6 allowed" in result.err
    706 
    707 
    708 @isctest.mark.with_ipv6
    709 def test_ipv6_server_with_ipv4_only(dig):
    710     """Check that dig -4 rejects an IPv6 server address."""
    711     result = dig("+tcp @fd92:7065:b8e:ffff::2 -4 A a.example", raise_on_exception=False)
    712     assert result.rc != 0
    713     assert "address family not supported" in result.err
    714 
    715 
    716 @isctest.mark.with_ipv6
    717 @pytest.mark.parametrize("option", ["+tcp", "+notcp"])
    718 def test_ipv4_server_with_ipv6_only(dig, ns2, option):
    719     """Check that dig -6 does not use a mapped form of an IPv4 server
    720     address."""
    721     result = dig(f"{option} @{ns2.ip} -6 A a.example")
    722     assert f"SERVER: ::ffff:{ns2.ip}#" not in result.out
    723 
    724 
    725 @pytest.fixture(name="set_response_sequence")
    726 def set_response_sequence_fixture(dig, ans5):
    727     """Arm the sequence of AXFR responses served by ans5."""
    728 
    729     def _set(sequence):
    730         dig(f"@{ans5.ip} {sequence}.response-sequence._control TXT")
    731 
    732     return _set
    733 
    734 
    735 @pytest.mark.parametrize(
    736     "sequence,tries,expect_failure,eof_errors",
    737     [
    738         param("no-response", 2, True, 2, id="immediate-immediate"),
    739         param("partial-axfr", 2, True, 2, id="partial-partial"),
    740         param("no-response.partial-axfr", 2, True, 2, id="immediate-partial"),
    741         param("partial-axfr.no-response", 2, True, 2, id="partial-immediate"),
    742         param("no-response.complete-axfr", 2, False, 1, id="immediate-complete"),
    743         param("partial-axfr.complete-axfr", 2, False, 1, id="partial-complete"),
    744         param("no-response", 1, True, 1, id="tries-1-no-second-retry"),
    745     ],
    746 )
    747 def test_axfr_retry_upon_tcp_eof(
    748     dig, ans5, set_response_sequence, sequence, tries, expect_failure, eof_errors
    749 ):
    750     """Check the exit code and the number of retries for an AXFR retried
    751     upon TCP EOF."""
    752     set_response_sequence(sequence)
    753     result = dig(f"@{ans5.ip} example AXFR +tries={tries}", raise_on_exception=False)
    754     assert (result.rc != 0) == expect_failure
    755     # Sanity check: ensure ans5 behaves as expected.
    756     eof_pattern = Re("communications error.*end of file")
    757     assert len(result.out.grep(eof_pattern)) == eof_errors
    758 
    759 
    760 def test_axfr_no_retry_with_retry_0(dig, ans5, set_response_sequence):
    761     """Check that +retry=0 does not retry upon TCP EOF."""
    762     set_response_sequence("no-response")
    763     result = dig(f"@{ans5.ip} example AXFR +retry=0", raise_on_exception=False)
    764     assert result.rc != 0
    765     # Sanity check: ensure ans5 behaves as expected.
    766     eof_pattern = Re("communications error.*end of file")
    767     assert len(result.out.grep(eof_pattern)) == 1
    768 
    769 
    770 @pytest.mark.parametrize(
    771     "option",
    772     [
    773         param("", id="udp"),
    774         param("+tcp", id="tcp"),
    775     ],
    776 )
    777 def test_timeout_then_servfail(dig, ans7, option):
    778     """Check that dig handles a timeout followed by a SERVFAIL
    779     correctly.  See GL #3020 for more information."""
    780     result = dig(f"+timeout=1 +nofail {option} @{ans7.ip} silent-then-servfail.example")
    781     assert "status: SERVFAIL" in result.out
    782 
    783 
    784 def test_comments_retry_comment(dig, ans7):
    785     """Check that dig +comments emits the retry comment."""
    786     result = dig(
    787         f"+timeout=1 +nofail +comments @{ans7.ip} silent-then-servfail.example"
    788     )
    789     assert ";; Got SERVFAIL reply from" in result.out
    790 
    791 
    792 def test_short_comments_suppresses_retry_comment(dig, ans7):
    793     """Check that dig +short +comments does not leak the ";; " comments
    794     into the short-form output.  +short normally turns comments off, but
    795     "+short +comments" re-enables them while short form is still in
    796     effect; the comment output then belongs to the verbose form and
    797     would corrupt the short output."""
    798     result = dig(
    799         f"+timeout=1 +nofail +short +comments @{ans7.ip} silent-then-servfail.example"
    800     )
    801     assert ";; Got SERVFAIL reply from" not in result.out
    802 
    803 
    804 ERROR_PATTERN = Re("connection refused|timed out|network unreachable|host unreachable")
    805 
    806 
    807 @pytest.mark.parametrize(
    808     "option",
    809     [
    810         param("", id="udp"),
    811         param("+tcp", id="tcp"),
    812     ],
    813 )
    814 def test_next_server_after_network_unreachable(dig, ns3, option):
    815     """Check that dig tries the next server after a socket network
    816     unreachable error."""
    817     result = dig(f"{option} @192.0.2.128 @{ns3.ip} a.example")
    818     assert len(result.out.grep(ERROR_PATTERN)) == 3
    819     assert "status: NOERROR" in result.out
    820 
    821 
    822 def test_next_server_after_udp_read_error(dig, ns3):
    823     """Check that dig tries the next server after a UDP socket read
    824     error."""
    825     result = dig(f"@10.53.0.99 @{ns3.ip} a.example")
    826     assert "status: NOERROR" in result.out
    827 
    828 
    829 def test_next_server_after_tcp_read_error(dig, ans7, ns3):
    830     """Check that dig tries the next server after a TCP socket read
    831     error."""
    832     result = dig(f"+tcp @{ans7.ip} @{ns3.ip} close.example")
    833     assert "status: NOERROR" in result.out
    834 
    835 
    836 def test_next_server_after_tcp_connection_error(dig, ns3):
    837     """Check that dig tries the next server after a TCP socket connection
    838     error/timeout.  The connection error and timeout cases are combined,
    839     because it is not trivial to simulate the timeout case in a system
    840     test in Linux without a firewall, but the code which handles error
    841     cases during connection establishment does not differentiate between
    842     timeout and other types of errors (unlike during reading), so this
    843     one check should be sufficient for both cases."""
    844     result = dig(f"+tcp @10.53.0.99 @{ns3.ip} a.example")
    845     assert len(result.out.grep(ERROR_PATTERN)) == 3
    846     assert "status: NOERROR" in result.out
    847 
    848 
    849 @pytest.mark.parametrize(
    850     "option",
    851     [
    852         param("", id="udp"),
    853         param("+tcp", id="tcp"),
    854     ],
    855 )
    856 def test_next_server_after_read_timeout(dig, ans7, ns3, option):
    857     """Check that dig tries the next server after socket read timeouts."""
    858     result = dig(f"+timeout=1 {option} @{ans7.ip} @{ns3.ip} silent.example")
    859     assert "status: NOERROR" in result.out
    860 
    861 
    862 def test_mapped_ipv6_server_refused(dig, ans7):
    863     """Check that dig refuses to use a server with an IPv4-mapped IPv6
    864     address after failing with the regular IP address.  See GL #3248
    865     for more information."""
    866     result = dig(f"@{ans7.ip} @::ffff:{ans7.ip} silent.example")
    867     assert ";; Skipping mapped address" in result.out
    868     assert ";; No acceptable nameservers" in result.out
    869 
    870 
    871 def test_qr_and_y_with_failed_query(dig, ns3):
    872     """Check that dig handles printing query information with +qr and +y
    873     when multiple queries are involved, including a failed one.  See
    874     GL #3244 for more information."""
    875     result = dig(f"+timeout=1 +qr +y @127.0.0.1 @{ns3.ip} a.example")
    876     assert "IN A 10.0.0.1" in result.out
    877 
    878 
    879 def test_startup_banner_default(dig, ans7):
    880     """Check that dig prints the startup banner by default, including on
    881     the error path.  This makes the absence check with +nocmd
    882     meaningful."""
    883     result = dig(
    884         f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1",
    885         raise_on_exception=False,
    886     )
    887     assert result.rc != 0
    888     assert "<<>> DiG" in result.out
    889     assert "no servers could be reached" in result.out
    890 
    891 
    892 def test_nocmd_after_query_name(dig, ans7):
    893     """Check that +nocmd placed after the query name suppresses the
    894     startup banner, including on the error path.  This regressed because
    895     the banner was built as soon as the query name was seen, before
    896     +nocmd had been parsed."""
    897     result = dig(
    898         f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +nocmd",
    899         raise_on_exception=False,
    900     )
    901     assert result.rc != 0
    902     assert "<<>> DiG" not in result.out
    903     assert "no servers could be reached" in result.out
    904 
    905 
    906 def test_yaml_valid_when_no_server_reached(dig, ans7):
    907     """Check that dig +yaml produces valid YAML when no servers could be
    908     reached; the ";"-prefixed startup banner must not precede the
    909     DIG_ERROR block.  The query name is deliberately placed before +yaml
    910     on the command line: that is what makes dig build the banner (while
    911     +cmd is still in effect) before switching to YAML output, which is
    912     the ordering that regressed."""
    913     result = dig(
    914         f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +yaml",
    915         raise_on_exception=False,
    916     )
    917     assert result.rc != 0
    918     assert parse_yaml(result.out)[0]["type"] == "DIG_ERROR"
    919 
    920 
    921 @isctest.mark.with_ipv6
    922 def test_source_address_both_families_no_crash(dig, ns1):
    923     """Check that dig with an IPv4 source address and a server with both
    924     IPv4 and IPv6 addresses does not crash.  @localhost is not really
    925     expected to have an answer for the query; only a crash (termination
    926     by a signal) is an error.  Without IPv6, @localhost resolves to the
    927     IPv4 address only and the address-family mismatch under test never
    928     happens.  See GL #5609 for more information."""
    929     result = dig(f"@localhost example -b {ns1.ip}", raise_on_exception=False)
    930     assert result.rc >= 0
    931 
    932 
    933 def test_yaml_any_output(dig, ns3):
    934     """Check the structure of dig +yaml output for an ANY query."""
    935     result = dig(f"+qr +yaml @{ns3.ip} any ns2.example")
    936     messages = parse_yaml(result.out)
    937     query = messages[0]["message"]["query_message_data"]
    938     assert query["status"] == "NOERROR"
    939     response = messages[1]["message"]["response_message_data"]
    940     assert response["status"] == "NOERROR"
    941     assert response["QUESTION_SECTION"][0] == "ns2.example. IN ANY"
    942 
    943 
    944 def test_yaml_ipv6_trailing_zeroes(dig, ns3):
    945     """Check dig +yaml output of an IPv6 address ending in zeroes."""
    946     result = dig(f"+qr +yaml @{ns3.ip} aaaa d.example")
    947     response = parse_yaml(result.out)[1]["message"]["response_message_data"]
    948     answer = response["ANSWER_SECTION"][0]
    949     assert answer == "d.example. 300 IN AAAA fd92:7065:b8e:ffff::0"
    950 
    951 
    952 @pytest.mark.parametrize(
    953     "qname", ["yaml", "'.yaml", "[.yaml", "{.yaml", "&.yaml", "#.yaml"]
    954 )
    955 def test_yaml_special_characters_in_qname(dig, ns3, qname):
    956     """Check that qnames containing characters special to YAML are quoted
    957     correctly in dig +yaml output."""
    958     result = dig(f"@{ns3.ip} +yaml {qname}.example TXT +qr")
    959     query = parse_yaml(result.out)[0]["message"]["query_message_data"]
    960     question = query["QUESTION_SECTION"][0]
    961     assert question == f"{qname}.example. IN TXT"
    962     response = parse_yaml(result.out)[1]["message"]["response_message_data"]
    963     answer = response["ANSWER_SECTION"][0]
    964     assert answer == f'{qname}.example. 300 IN TXT "a: b"'
    965 
    966 
    967 def test_yaml_character_values(dig, ns3):
    968     """Check the quoting of all 256 character values in dig +yaml TXT
    969     output."""
    970 
    971     def quoted(i):
    972         char = chr(i)
    973         if char in ('"', "\\"):
    974             return f'"\\{char}"'
    975         if 32 <= i <= 126:
    976             return f'"{char}"'
    977         return f'"\\{i:03d}"'
    978 
    979     result = dig(f"@{ns3.ip} +yaml all.yaml.example TXT +qr")
    980     response = parse_yaml(result.out)[1]["message"]["response_message_data"]
    981     answer = response["ANSWER_SECTION"][0]
    982     strings = " ".join(quoted(i) for i in range(256))
    983     assert answer == f"all.yaml.example. 300 IN TXT {strings}"
    984