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