Browse Source

Merge branch 'master' into dockerfile

Benjamin Sago 4 years ago
parent
commit
348fc8f93e
100 changed files with 1928 additions and 660 deletions
  1. 1 2
      .gitignore
  2. 218 119
      Cargo.lock
  3. 13 4
      Cargo.toml
  4. 66 26
      Justfile
  5. 5 3
      README.md
  6. 34 11
      build.rs
  7. 5 1
      dns-transport/Cargo.toml
  8. 1 24
      dns-transport/src/auto.rs
  9. 0 23
      dns-transport/src/https.rs
  10. 0 23
      dns-transport/src/tcp.rs
  11. 0 23
      dns-transport/src/tls.rs
  12. 3 21
      dns-transport/src/udp.rs
  13. 11 2
      dns/Cargo.toml
  14. 4 1
      dns/src/lib.rs
  15. 12 13
      dns/src/record/a.rs
  16. 10 15
      dns/src/record/aaaa.rs
  17. 2 11
      dns/src/record/caa.rs
  18. 1 1
      dns/src/record/cname.rs
  19. 109 0
      dns/src/record/eui48.rs
  20. 109 0
      dns/src/record/eui64.rs
  21. 1 1
      dns/src/record/hinfo.rs
  22. 1 1
      dns/src/record/loc.rs
  23. 24 0
      dns/src/record/mod.rs
  24. 1 1
      dns/src/record/mx.rs
  25. 2 2
      dns/src/record/naptr.rs
  26. 1 1
      dns/src/record/ns.rs
  27. 87 0
      dns/src/record/openpgpkey.rs
  28. 2 2
      dns/src/record/opt.rs
  29. 1 1
      dns/src/record/ptr.rs
  30. 1 1
      dns/src/record/soa.rs
  31. 1 1
      dns/src/record/srv.rs
  32. 19 3
      dns/src/record/sshfp.rs
  33. 21 3
      dns/src/record/tlsa.rs
  34. 1 1
      dns/src/record/txt.rs
  35. 128 0
      dns/src/record/uri.rs
  36. 26 3
      dns/src/strings.rs
  37. 25 174
      dns/src/wire.rs
  38. 40 0
      dns/tests/wire_building_tests.rs
  39. 120 1
      dns/tests/wire_parsing_tests.rs
  40. 8 0
      src/colours.rs
  41. 1 3
      src/connect.rs
  42. 25 15
      src/main.rs
  43. 14 10
      src/options.rs
  44. 89 51
      src/output.rs
  45. 14 13
      src/requests.rs
  46. 62 27
      src/resolve.rs
  47. 19 15
      src/table.rs
  48. 25 7
      xtests/README.md
  49. 0 0
      xtests/live/badssl.toml
  50. 0 0
      xtests/live/basics.toml
  51. 0 0
      xtests/live/bins.toml
  52. 0 0
      xtests/live/https.toml
  53. 0 0
      xtests/live/json.toml
  54. 0 0
      xtests/live/tcp.toml
  55. 0 0
      xtests/live/tls.toml
  56. 0 0
      xtests/live/udp.toml
  57. 44 0
      xtests/madns/a-records.toml
  58. 44 0
      xtests/madns/aaaa-records.toml
  59. 44 0
      xtests/madns/caa-records.toml
  60. 28 0
      xtests/madns/cname-records.toml
  61. 44 0
      xtests/madns/eui48-records.toml
  62. 44 0
      xtests/madns/eui64-records.toml
  63. 28 0
      xtests/madns/hinfo-records.toml
  64. 90 0
      xtests/madns/loc-records.toml
  65. 28 0
      xtests/madns/mx-records.toml
  66. 36 0
      xtests/madns/naptr-records.toml
  67. 28 0
      xtests/madns/ns-records.toml
  68. 28 0
      xtests/madns/openpgpkey-records.toml
  69. 44 0
      xtests/madns/opt-records.toml
  70. 1 0
      xtests/madns/outputs/a.example.ansitxt
  71. 1 0
      xtests/madns/outputs/aaaa.example.ansitxt
  72. 1 0
      xtests/madns/outputs/ansi.str.example.ansitxt
  73. 1 0
      xtests/madns/outputs/bad-regex.naptr.example.ansitxt
  74. 1 0
      xtests/madns/outputs/caa.example.ansitxt
  75. 1 0
      xtests/madns/outputs/cname.example.ansitxt
  76. 1 0
      xtests/madns/outputs/critical.caa.example.ansitxt
  77. 2 0
      xtests/madns/outputs/do-flag.opt.example.ansitxt
  78. 1 0
      xtests/madns/outputs/eui48.example.ansitxt
  79. 1 0
      xtests/madns/outputs/eui64.example.ansitxt
  80. 1 0
      xtests/madns/outputs/far-negative-latitude.loc.invalid.ansitxt
  81. 1 0
      xtests/madns/outputs/far-negative-longitude.loc.invalid.ansitxt
  82. 1 0
      xtests/madns/outputs/far-positive-latitude.loc.invalid.ansitxt
  83. 1 0
      xtests/madns/outputs/far-positive-longitude.loc.invalid.ansitxt
  84. 1 0
      xtests/madns/outputs/hinfo.example.ansitxt
  85. 1 0
      xtests/madns/outputs/loc.example.ansitxt
  86. 1 0
      xtests/madns/outputs/mx.example.ansitxt
  87. 2 0
      xtests/madns/outputs/named.opt.invalid.ansitxt
  88. 1 0
      xtests/madns/outputs/naptr.example.ansitxt
  89. 1 0
      xtests/madns/outputs/newline.str.example.ansitxt
  90. 1 0
      xtests/madns/outputs/ns.example.ansitxt
  91. 1 0
      xtests/madns/outputs/null.str.example.ansitxt
  92. 1 0
      xtests/madns/outputs/openpgpkey.example.ansitxt
  93. 2 0
      xtests/madns/outputs/opt.example.ansitxt
  94. 2 0
      xtests/madns/outputs/other-flags.opt.example.ansitxt
  95. 1 0
      xtests/madns/outputs/others.caa.example.ansitxt
  96. 1 0
      xtests/madns/outputs/ptr.example.ansitxt
  97. 1 0
      xtests/madns/outputs/slash.uri.example.ansitxt
  98. 1 0
      xtests/madns/outputs/soa.example.ansitxt
  99. 1 0
      xtests/madns/outputs/srv.example.ansitxt
  100. 1 0
      xtests/madns/outputs/sshfp.example.ansitxt

+ 1 - 2
.gitignore

@@ -1,5 +1,4 @@
 /target
 /tarpaulin-report.html
 fuzz-*.log
-.bash_history
-.idea
+/cargo-timing*.html

+ 218 - 119
Cargo.lock

@@ -2,27 +2,18 @@
 # It is not intended for manual editing.
 [[package]]
 name = "addr2line"
-version = "0.14.0"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
+checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
 dependencies = [
  "gimli",
 ]
 
 [[package]]
 name = "adler"
-version = "0.2.3"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
-
-[[package]]
-name = "ansi_term"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-dependencies = [
- "winapi",
-]
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
 name = "ansi_term"
@@ -52,18 +43,24 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backtrace"
-version = "0.3.54"
+version = "0.3.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2baad346b2d4e94a24347adeee9c7a93f412ee94b9cc26e5b59dea23848e9f28"
+checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
 dependencies = [
  "addr2line",
- "cfg-if 1.0.0",
+ "cfg-if",
  "libc",
  "miniz_oxide",
  "object",
  "rustc-demangle",
 ]
 
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
 [[package]]
 name = "bitflags"
 version = "1.2.1"
@@ -72,21 +69,15 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
 [[package]]
 name = "byteorder"
-version = "1.3.4"
+version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "cc"
-version = "1.0.61"
+version = "1.0.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d"
-
-[[package]]
-name = "cfg-if"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
 
 [[package]]
 name = "cfg-if"
@@ -96,9 +87,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "core-foundation"
-version = "0.7.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -106,15 +97,15 @@ dependencies = [
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.7.0"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
 [[package]]
 name = "ctor"
-version = "0.1.16"
+version = "0.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484"
+checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
 dependencies = [
  "quote",
  "syn",
@@ -127,29 +118,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0fcb4df22ae812fa2f6d5e3b577247584cc67fce06ad0779168d1dd41cbcce3"
 dependencies = [
  "libc",
- "redox_syscall",
+ "redox_syscall 0.1.57",
  "winapi",
 ]
 
 [[package]]
-name = "difference"
-version = "2.0.0"
+name = "diff"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
 
 [[package]]
 name = "dns"
-version = "0.1.0"
+version = "0.2.0-pre"
 dependencies = [
+ "base64",
  "byteorder",
  "log",
  "mutagen",
  "pretty_assertions",
+ "unic-idna",
 ]
 
 [[package]]
 name = "dns-transport"
-version = "0.1.0"
+version = "0.2.0-pre"
 dependencies = [
  "dns",
  "httparse",
@@ -159,9 +152,9 @@ dependencies = [
 
 [[package]]
 name = "dog"
-version = "0.1.0"
+version = "0.2.0-pre"
 dependencies = [
- "ansi_term 0.12.1",
+ "ansi_term",
  "atty",
  "datetime",
  "dns",
@@ -171,7 +164,6 @@ dependencies = [
  "log",
  "pretty_assertions",
  "rand",
- "regex",
  "serde",
  "serde_json",
 ]
@@ -224,11 +216,11 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.1.15"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if",
  "libc",
  "wasi",
 ]
@@ -241,18 +233,18 @@ checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.17"
+version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "httparse"
-version = "1.3.4"
+version = "1.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"
 
 [[package]]
 name = "ipconfig"
@@ -268,9 +260,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
 [[package]]
 name = "json"
@@ -286,24 +278,30 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.80"
+version = "0.2.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
+checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
 
 [[package]]
 name = "log"
-version = "0.4.11"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if",
 ]
 
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
 [[package]]
 name = "miniz_oxide"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
 dependencies = [
  "adler",
  "autocfg",
@@ -312,7 +310,7 @@ dependencies = [
 [[package]]
 name = "mutagen"
 version = "0.2.0"
-source = "git+https://github.com/llogiq/mutagen#c7abc956a10e8a3e2cc71f21279ea0a42f7b7c10"
+source = "git+https://github.com/llogiq/mutagen#933bbaf4edaa22f6237b2201dff2940ff7f4193c"
 dependencies = [
  "mutagen-core",
  "mutagen-transform",
@@ -321,7 +319,7 @@ dependencies = [
 [[package]]
 name = "mutagen-core"
 version = "0.2.0"
-source = "git+https://github.com/llogiq/mutagen#c7abc956a10e8a3e2cc71f21279ea0a42f7b7c10"
+source = "git+https://github.com/llogiq/mutagen#933bbaf4edaa22f6237b2201dff2940ff7f4193c"
 dependencies = [
  "failure",
  "json",
@@ -336,7 +334,7 @@ dependencies = [
 [[package]]
 name = "mutagen-transform"
 version = "0.2.0"
-source = "git+https://github.com/llogiq/mutagen#c7abc956a10e8a3e2cc71f21279ea0a42f7b7c10"
+source = "git+https://github.com/llogiq/mutagen#933bbaf4edaa22f6237b2201dff2940ff7f4193c"
 dependencies = [
  "mutagen-core",
  "proc-macro2",
@@ -344,9 +342,9 @@ dependencies = [
 
 [[package]]
 name = "native-tls"
-version = "0.2.4"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
 dependencies = [
  "lazy_static",
  "libc",
@@ -362,21 +360,27 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.22.0"
+version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
+checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
+
+[[package]]
+name = "once_cell"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
 
 [[package]]
 name = "openssl"
-version = "0.10.30"
+version = "0.10.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
+checksum = "a61075b62a23fef5a29815de7536d940aa35ce96d18ce0cc5076272db678a577"
 dependencies = [
  "bitflags",
- "cfg-if 0.1.10",
+ "cfg-if",
  "foreign-types",
- "lazy_static",
  "libc",
+ "once_cell",
  "openssl-sys",
 ]
 
@@ -388,9 +392,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.58"
+version = "0.9.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
+checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f"
 dependencies = [
  "autocfg",
  "cc",
@@ -422,13 +426,13 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
 [[package]]
 name = "pretty_assertions"
-version = "0.6.1"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
+checksum = "f297542c27a7df8d45de2b0e620308ab883ad232d06c14b76ac3e144bda50184"
 dependencies = [
- "ansi_term 0.11.0",
+ "ansi_term",
  "ctor",
- "difference",
+ "diff",
  "output_vt100",
 ]
 
@@ -443,20 +447,19 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.7"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "rand"
-version = "0.7.3"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
 dependencies = [
- "getrandom",
  "libc",
  "rand_chacha",
  "rand_core",
@@ -465,9 +468,9 @@ dependencies = [
 
 [[package]]
 name = "rand_chacha"
-version = "0.2.2"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
 dependencies = [
  "ppv-lite86",
  "rand_core",
@@ -475,18 +478,18 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.5.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
 dependencies = [
  "getrandom",
 ]
 
 [[package]]
 name = "rand_hc"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
 dependencies = [
  "rand_core",
 ]
@@ -498,20 +501,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
 
 [[package]]
-name = "regex"
-version = "1.4.2"
+name = "redox_syscall"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
 dependencies = [
- "regex-syntax",
+ "bitflags",
 ]
 
-[[package]]
-name = "regex-syntax"
-version = "0.6.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
-
 [[package]]
 name = "remove_dir_all"
 version = "0.5.3"
@@ -545,9 +542,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "0.4.4"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
+checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -558,9 +555,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "0.4.3"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
+checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -568,18 +565,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.117"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
+checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.117"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
+checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -588,9 +585,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.59"
+version = "1.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
 dependencies = [
  "itoa",
  "ryu",
@@ -599,21 +596,20 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.3.15"
+version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44"
+checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if",
  "libc",
- "redox_syscall",
  "winapi",
 ]
 
 [[package]]
 name = "syn"
-version = "1.0.48"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
+checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -634,18 +630,121 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.1.0"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if",
  "libc",
  "rand",
- "redox_syscall",
+ "redox_syscall 0.2.5",
  "remove_dir_all",
  "winapi",
 ]
 
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-idna"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "621e9cf526f2094d2c2ced579766458a92f8f422d6bb934c503ba1a95823a62d"
+dependencies = [
+ "matches",
+ "unic-idna-mapping",
+ "unic-idna-punycode",
+ "unic-normal",
+ "unic-ucd-bidi",
+ "unic-ucd-normal",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-idna-mapping"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4de70fd4e5331537347a50a0dbc938efb1f127c9f6e5efec980fc90585aa1343"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-idna-punycode"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06feaedcbf9f1fc259144d833c0d630b8b15207b0486ab817d29258bc89f2f8a"
+
+[[package]]
+name = "unic-normal"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09d64d33589a94628bc2aeb037f35c2e25f3f049c7348b5aa5580b48e6bba62"
+dependencies = [
+ "unic-ucd-normal",
+]
+
+[[package]]
+name = "unic-ucd-bidi"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-hangul"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb1dc690e19010e1523edb9713224cba5ef55b54894fe33424439ec9a40c0054"
+dependencies = [
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-normal"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86aed873b8202d22b13859dda5fe7c001d271412c31d411fd9b827e030569410"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-hangul",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
 [[package]]
 name = "unicode-width"
 version = "0.1.8"
@@ -660,15 +759,15 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
 [[package]]
 name = "vcpkg"
-version = "0.2.10"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
+checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
 
 [[package]]
 name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
+version = "0.10.2+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
 [[package]]
 name = "widestring"

+ 13 - 4
Cargo.toml

@@ -11,11 +11,14 @@ exclude = [
 ]
 homepage = "https://dns.lookup.dog/"
 license = "EUPL-1.2"
-version = "0.1.0"
+version = "0.2.0-pre"
+
 
 [[bin]]
 name = "dog"
 path = "src/main.rs"
+doctest = false
+
 
 [workspace]
 members = [
@@ -23,11 +26,18 @@ members = [
   "dns-transport",
 ]
 
+
+# make dev builds faster by excluding debug symbols
+[profile.dev]
+debug = false
+
+# use LTO for smaller binaries (that take longer to build)
 [profile.release]
 lto = true
 overflow-checks = true
 panic = "abort"
 
+
 [dependencies]
 
 # dns stuff
@@ -40,7 +50,7 @@ atty = "0.2"
 getopts = "0.2"
 
 # transaction ID generation
-rand = "0.7"
+rand = "0.8"
 
 # json
 serde = "1.0"
@@ -55,10 +65,9 @@ ipconfig = { version = "0.2" }
 
 [build-dependencies]
 datetime = { version = "0.5.1", default_features = false }
-regex = { version = "1.3", default_features = false, features = ["std"] }
 
 [dev-dependencies]
-pretty_assertions = "0.6"
+pretty_assertions = "0.7"
 
 [features]
 default = ["tls", "https"]

+ 66 - 26
Justfile

@@ -5,106 +5,146 @@ all-quick: build-quick test-quick xtests-quick
 export DOG_DEBUG := ""
 
 
-# compiles the dog binary
+#----------#
+# building #
+#----------#
+
+# compile the dog binary
 @build:
     cargo build
 
-# compiles the dog binary (in release mode)
+# compile the dog binary (in release mode)
 @build-release:
     cargo build --release --verbose
     strip "${CARGO_TARGET_DIR:-target}/release/dog"
 
-# compiles the dog binary (without some features)
+# produce an HTML chart of compilation timings
+@build-time:
+    cargo +nightly clean
+    cargo +nightly build -Z timings
+
+# compile the dog binary (without some features)
 @build-quick:
     cargo build --no-default-features
 
 
-# runs unit tests
+#---------------#
+# running tests #
+#---------------#
+
+# run unit tests
 @test:
     cargo test --workspace -- --quiet
 
-# runs unit tests (in release mode)
+# run unit tests (in release mode)
 @test-release:
-    cargo test --release --workspace --verbose
+    cargo test --workspace --release --verbose
 
-# runs unit tests (without some features)
+# run unit tests (without some features)
 @test-quick:
     cargo test --workspace --no-default-features -- --quiet
 
-# runs mutation tests
+# run mutation tests
 @test-mutation:
     cargo +nightly test    --package dns --features=dns/with_mutagen -- --quiet
     cargo +nightly mutagen --package dns --features=dns/with_mutagen
 
 
-# runs extended tests
+#------------------------#
+# running extended tests #
+#------------------------#
+
+# run extended tests
 @xtests:
-    specsheet xtests/*.toml -O cmd.target.dog="${CARGO_TARGET_DIR:-../target}/debug/dog"
+    specsheet xtests/*/*.toml -shide \
+        -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/debug/dog"
 
-# runs extended tests (in release mode)
+# run extended tests (in release mode)
 @xtests-release:
-    specsheet xtests/*.toml -O cmd.target.dog="${CARGO_TARGET_DIR:-../target}/release/dog"
+    specsheet xtests/*/*.toml \
+        -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/release/dog"
 
-# runs extended tests (omitting certain feature tests)
+# run extended tests (omitting certain feature tests)
 @xtests-quick:
-    specsheet xtests/*.toml -O cmd.target.dog="${CARGO_TARGET_DIR:-../target}/debug/dog" --skip-tags=udp,tls,https,json
+    specsheet xtests/options/*.toml xtests/live/{basics,tcp}.toml -shide \
+        -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/debug/dog"
+
+# display the number of extended tests that get run
+@count-xtests:
+    grep -F '[[cmd]]' -R xtests | wc -l
 
 
-# runs fuzzing on the dns crate
+#---------#
+# fuzzing #
+#---------#
+
+# run fuzzing on the dns crate
 @fuzz:
     cargo +nightly fuzz --version
     cd dns; cargo +nightly fuzz run fuzz_parsing -- -jobs=`nproc` -workers=`nproc` -runs=69105
 
-# prints out the data that caused crashes during fuzzing as hexadecimal
+# print out the data that caused crashes during fuzzing as hexadecimal
 @fuzz-hex:
     for crash in dns/fuzz/artifacts/fuzz_parsing/crash-*; do echo; echo $crash; hexyl $crash; done
 
-# removes fuzz log files
+# remove fuzz log files
 @fuzz-clean:
     rm dns/fuzz/fuzz-*.log
 
 
-# lints the code
+#-----------------------#
+# code quality and misc #
+#-----------------------#
+
+# lint the code
 @clippy:
     touch dns/src/lib.rs
     cargo clippy
 
-# generates a code coverage report using tarpaulin via docker
+# generate a code coverage report using tarpaulin via docker
 @coverage-docker:
     docker run --security-opt seccomp=unconfined -v "${PWD}:/volume" xd009642/tarpaulin cargo tarpaulin --all --out Html
 
-# updates dependency versions, and checks for outdated ones
+# update dependency versions, and check for outdated ones
 @update-deps:
     cargo update
     command -v cargo-outdated >/dev/null || (echo "cargo-outdated not installed" && exit 1)
     cargo outdated
 
-# lists unused dependencies
+# list unused dependencies
 @unused-deps:
     command -v cargo-udeps >/dev/null || (echo "cargo-udeps not installed" && exit 1)
     cargo +nightly udeps
 
-# prints versions of the necessary build tools
+# print versions of the necessary build tools
 @versions:
     rustc --version
     cargo --version
 
 
-# renders the documentation
+#---------------#
+# documentation #
+#---------------#
+
+# render the documentation
 @doc:
     cargo doc --no-deps --workspace
 
-# builds the man pages
+# build the man pages
 @man:
     mkdir -p "${CARGO_TARGET_DIR:-target}/man"
     pandoc --standalone -f markdown -t man man/dog.1.md > "${CARGO_TARGET_DIR:-target}/man/dog.1"
 
-# builds and previews the man page
+# build and preview the man page
 @man-preview: man
     man "${CARGO_TARGET_DIR:-target}/man/dog.1"
 
 
-# creates a distributable package
+#-----------#
+# packaging #
+#-----------#
+
+# create a distributable package
 zip desc exe="dog":
     #!/usr/bin/env perl
     use Archive::Zip;

+ 5 - 3
README.md

@@ -72,7 +72,7 @@ To install dog, you can download a pre-compiled binary, or you can compile it fr
 
 ### Packages
 
-- For Arch Linux, install the [`dog-dns`](https://aur.archlinux.org/packages/dog-dns/) package from the AUR.
+- For Arch Linux, install the [`dog`](https://www.archlinux.org/packages/community/x86_64/dog/) package.
 - For Homebrew on macOS, install the [`dog`](https://formulae.brew.sh/formula/dog) formula.
 
 
@@ -93,7 +93,7 @@ To build, download the source code and run:
     $ cargo test
 
 - The [just](https://github.com/casey/just) command runner can be used to run some helpful development commands, in a manner similar to `make`.
-Run `just --tasks` to get an overview of what’s available.
+Run `just --list` to get an overview of what’s available.
 
 - If you are compiling a copy for yourself, be sure to run `cargo build --release` or `just build-release` to benefit from release-mode optimisations.
 Copy the resulting binary, which will be in the `target/release` directory, into a folder in your `$PATH`.
@@ -125,7 +125,9 @@ If you have a copy installed, you can run:
 
     just xtests
 
-Specsheet will test the compiled binary by making DNS requests over the network, checking that dog returns results and does not crash.
+Specsheet will test the compiled binary by making DNS requests over the network, checking that dog returns the correct results and does not crash.
+Note that this will expose your IP address.
+For more information, read [the xtests README](xtests/README.md).
 
 
 ---

+ 34 - 11
build.rs

@@ -19,7 +19,6 @@ use std::io::{self, Write};
 use std::path::PathBuf;
 
 use datetime::{LocalDateTime, ISO};
-use regex::Regex;
 
 
 /// The build script entry point.
@@ -30,7 +29,11 @@ fn main() -> io::Result<()> {
     let tagline = "dog \\1;32m●\\0m command-line DNS client";
     let url     = "https://dns.lookup.dog/";
 
-    let ver = if is_development_version() {
+    let ver =
+        if is_debug_build() {
+            format!("{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, cargo_version(), url)
+        }
+        else if is_development_version() {
             format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, cargo_version(), git_hash(), build_date(), url)
         }
         else {
@@ -40,28 +43,46 @@ fn main() -> io::Result<()> {
     // We need to create these files in the Cargo output directory.
     let out = PathBuf::from(env::var("OUT_DIR").unwrap());
 
-    // The bits .txt files contain ANSI escape codes, ish.
-    let control_code = Regex::new(r##"\\.+?m"##).unwrap();
-
     // Pretty version text
     let mut f = File::create(&out.join("version.pretty.txt"))?;
-    write!(f, "{}\n", ver.replace("\\", "\x1B["))?;
+    writeln!(f, "{}", convert_codes(&ver))?;
 
     // Bland version text
     let mut f = File::create(&out.join("version.bland.txt"))?;
-    write!(f, "{}\n", control_code.replace_all(&ver, ""))?;
+    writeln!(f, "{}", strip_codes(&ver))?;
 
     // Pretty usage text
     let mut f = File::create(&out.join("usage.pretty.txt"))?;
-    write!(f, "{}\n\n{}", tagline.replace("\\", "\x1B["), usage.replace("\\", "\x1B["))?;
+    writeln!(f, "{}", convert_codes(&tagline))?;
+    writeln!(f)?;
+    write!(f, "{}", convert_codes(&usage))?;
 
     // Bland usage text
     let mut f = File::create(&out.join("usage.bland.txt"))?;
-    write!(f, "{}\n\n{}", control_code.replace_all(tagline, ""), control_code.replace_all(usage, ""))?;
+    writeln!(f, "{}", strip_codes(&tagline))?;
+    writeln!(f)?;
+    write!(f, "{}", strip_codes(&usage))?;
 
     Ok(())
 }
 
+/// Converts the escape codes in ‘usage.txt’ to ANSI escape codes.
+fn convert_codes(input: &str) -> String {
+    input.replace("\\", "\x1B[")
+}
+
+/// Removes escape codes from ‘usage.txt’.
+fn strip_codes(input: &str) -> String {
+    input.replace("\\0m", "")
+         .replace("\\1m", "")
+         .replace("\\4m", "")
+         .replace("\\32m", "")
+         .replace("\\33m", "")
+         .replace("\\1;31m", "")
+         .replace("\\1;32m", "")
+         .replace("\\1;33m", "")
+         .replace("\\1;4;34", "")
+}
 
 /// Retrieve the project’s current Git hash, as a string.
 fn git_hash() -> String {
@@ -74,7 +95,6 @@ fn git_hash() -> String {
             .stdout).trim().to_string()
 }
 
-
 /// Whether we should show pre-release info in the version string.
 ///
 /// Both weekly releases and actual releases are --release releases,
@@ -83,13 +103,16 @@ fn is_development_version() -> bool {
     cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug"
 }
 
+/// Whether we are building in debug mode.
+fn is_debug_build() -> bool {
+    env::var("PROFILE").unwrap() == "debug"
+}
 
 /// Retrieves the [package] version in Cargo.toml as a string.
 fn cargo_version() -> String {
     env::var("CARGO_PKG_VERSION").unwrap()
 }
 
-
 /// Formats the current date as an ISO 8601 string.
 fn build_date() -> String {
     let now = LocalDateTime::now();

+ 5 - 1
dns-transport/Cargo.toml

@@ -1,9 +1,13 @@
 [package]
 name = "dns-transport"
-version = "0.1.0"
+version = "0.2.0-pre"
 authors = ["Benjamin Sago <ogham@bsago.me>"]
 edition = "2018"
 
+[lib]
+doctest = false
+test = false
+
 
 [dependencies]
 

+ 1 - 24
dns-transport/src/auto.rs

@@ -6,32 +6,9 @@ use super::{Transport, Error, UdpTransport, TcpTransport};
 
 /// The **automatic transport**, which sends DNS wire data using the UDP
 /// transport, then tries using the TCP transport if the first one fails
-/// because the response wouldn't fit in a single UDP packet.
+/// because the response wouldnt fit in a single UDP packet.
 ///
 /// This is the default behaviour for many DNS clients.
-///
-/// # Examples
-///
-/// ```no_run
-/// use dns_transport::{Transport, AutoTransport};
-/// use dns::{Request, Flags, Query, Labels, QClass, qtype, record::NS};
-///
-/// let query = Query {
-///     qname: Labels::encode("dns.lookup.dog").unwrap(),
-///     qclass: QClass::IN,
-///     qtype: qtype!(NS),
-/// };
-///
-/// let request = Request {
-///     transaction_id: 0xABCD,
-///     flags: Flags::query(),
-///     query: query,
-///     additional: None,
-/// };
-///
-/// let transport = AutoTransport::new("8.8.8.8");
-/// transport.send(&request);
-/// ```
 pub struct AutoTransport {
     addr: String,
 }

+ 0 - 23
dns-transport/src/https.rs

@@ -11,29 +11,6 @@ use super::{Transport, Error};
 
 /// The **HTTPS transport**, which sends DNS wire data inside HTTP packets
 /// encrypted with TLS, using TCP.
-///
-/// # Examples
-///
-/// ```no_run
-/// use dns_transport::{Transport, HttpsTransport};
-/// use dns::{Request, Flags, Query, Labels, QClass, qtype, record::A};
-///
-/// let query = Query {
-///     qname: Labels::encode("dns.lookup.dog").unwrap(),
-///     qclass: QClass::IN,
-///     qtype: qtype!(A),
-/// };
-///
-/// let request = Request {
-///     transaction_id: 0xABCD,
-///     flags: Flags::query(),
-///     query: query,
-///     additional: None,
-/// };
-///
-/// let transport = HttpsTransport::new("https://cloudflare-dns.com/dns-query");
-/// transport.send(&request);
-/// ```
 pub struct HttpsTransport {
     url: String,
 }

+ 0 - 23
dns-transport/src/tcp.rs

@@ -10,29 +10,6 @@ use super::{Transport, Error};
 
 /// The **TCP transport**, which sends DNS wire data over a TCP stream.
 ///
-/// # Examples
-///
-/// ```no_run
-/// use dns_transport::{Transport, TcpTransport};
-/// use dns::{Request, Flags, Query, Labels, QClass, qtype, record::MX};
-///
-/// let query = Query {
-///     qname: Labels::encode("dns.lookup.dog").unwrap(),
-///     qclass: QClass::IN,
-///     qtype: qtype!(MX),
-/// };
-///
-/// let request = Request {
-///     transaction_id: 0xABCD,
-///     flags: Flags::query(),
-///     query: query,
-///     additional: None,
-/// };
-///
-/// let transport = TcpTransport::new("8.8.8.8");
-/// transport.send(&request);
-/// ```
-///
 /// # References
 ///
 /// - [RFC 1035 §4.2.2](https://tools.ietf.org/html/rfc1035) — Domain Names,

+ 0 - 23
dns-transport/src/tls.rs

@@ -11,29 +11,6 @@ use super::{Transport, Error, TcpTransport};
 
 /// The **TLS transport**, which sends DNS wire data using TCP through an
 /// encrypted TLS connection.
-///
-/// # Examples
-///
-/// ```no_run
-/// use dns_transport::{Transport, TlsTransport};
-/// use dns::{Request, Flags, Query, Labels, QClass, qtype, record::SRV};
-///
-/// let query = Query {
-///     qname: Labels::encode("dns.lookup.dog").unwrap(),
-///     qclass: QClass::IN,
-///     qtype: qtype!(SRV),
-/// };
-///
-/// let request = Request {
-///     transaction_id: 0xABCD,
-///     flags: Flags::query(),
-///     query: query,
-///     additional: None,
-/// };
-///
-/// let transport = TlsTransport::new("dns.google");
-/// transport.send(&request);
-/// ```
 pub struct TlsTransport {
     addr: String,
 }

+ 3 - 21
dns-transport/src/udp.rs

@@ -8,28 +8,10 @@ use super::{Transport, Error};
 
 /// The **UDP transport**, which sends DNS wire data inside a UDP datagram.
 ///
-/// # Examples
+/// # References
 ///
-/// ```no_run
-/// use dns_transport::{Transport, UdpTransport};
-/// use dns::{Request, Flags, Query, Labels, QClass, qtype, record::NS};
-///
-/// let query = Query {
-///     qname: Labels::encode("dns.lookup.dog").unwrap(),
-///     qclass: QClass::IN,
-///     qtype: qtype!(NS),
-/// };
-///
-/// let request = Request {
-///     transaction_id: 0xABCD,
-///     flags: Flags::query(),
-///     query: query,
-///     additional: None,
-/// };
-///
-/// let transport = UdpTransport::new("8.8.8.8");
-/// transport.send(&request);
-/// ```
+/// - [RFC 1035 §4.2.1](https://tools.ietf.org/html/rfc1035) — Domain Names,
+///   Implementation and Specification (November 1987)
 pub struct UdpTransport {
     addr: String,
 }

+ 11 - 2
dns/Cargo.toml

@@ -1,9 +1,12 @@
 [package]
 name = "dns"
-version = "0.1.0"
+version = "0.2.0-pre"
 authors = ["Benjamin Sago <ogham@bsago.me>"]
 edition = "2018"
 
+[lib]
+doctest = false
+
 
 [dependencies]
 
@@ -13,11 +16,17 @@ log = "0.4"
 # protocol parsing
 byteorder = "1.3"
 
+# packet printing
+base64 = "0.13"
+
 # testing
 mutagen = { git = "https://github.com/llogiq/mutagen", optional = true }
 
+# idna encoding
+unic-idna = "0.9.0"
+
 [dev-dependencies]
-pretty_assertions = "0.6"
+pretty_assertions = "0.7"
 
 [features]
 with_mutagen = ["mutagen"]  # needs nightly

+ 4 - 1
dns/src/lib.rs

@@ -10,12 +10,15 @@
 #![warn(unused)]
 
 #![warn(clippy::all, clippy::pedantic)]
-#![allow(clippy::find_map)]
+#![allow(clippy::doc_markdown)]
+#![allow(clippy::len_without_is_empty)]
 #![allow(clippy::missing_errors_doc)]
 #![allow(clippy::module_name_repetitions)]
 #![allow(clippy::must_use_candidate)]
 #![allow(clippy::non_ascii_literal)]
+#![allow(clippy::redundant_else)]
 #![allow(clippy::struct_excessive_bools)]
+#![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::wildcard_imports)]
 
 #![deny(clippy::cast_possible_truncation)]

+ 12 - 13
dns/src/record/a.rs

@@ -22,22 +22,21 @@ impl Wire for A {
     const NAME: &'static str = "A";
     const RR_TYPE: u16 = 1;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
-        let mut buf = Vec::new();
-        for _ in 0 .. stated_length {
-            buf.push(c.read_u8()?);
-        }
-
-        if let [a, b, c, d] = *buf {
-            let address = Ipv4Addr::new(a, b, c, d);
-            trace!("Parsed IPv4 address -> {:?}", address);
-            Ok(Self { address })
-        }
-        else {
+        if stated_length != 4 {
             warn!("Length is incorrect (record length {:?}, but should be four)", stated_length);
-            Err(WireError::WrongRecordLength { stated_length, mandated_length: MandatedLength::Exactly(4) })
+            let mandated_length = MandatedLength::Exactly(4);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
         }
+
+        let mut buf = [0_u8; 4];
+        c.read_exact(&mut buf)?;
+
+        let address = Ipv4Addr::from(buf);
+        trace!("Parsed IPv4 address -> {:?}", address);
+
+        Ok(Self { address })
     }
 }
 

+ 10 - 15
dns/src/record/aaaa.rs

@@ -22,26 +22,21 @@ impl Wire for AAAA {
     const NAME: &'static str = "AAAA";
     const RR_TYPE: u16 = 28;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
-        let mut buf = Vec::new();
-        for _ in 0 .. stated_length {
-            buf.push(c.read_u8()?);
+        if stated_length != 16 {
+            warn!("Length is incorrect (stated length {:?}, but should be sixteen)", stated_length);
+            let mandated_length = MandatedLength::Exactly(16);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
         }
 
-        if let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = *buf {
-            let address = Ipv6Addr::from([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]);
-            // probably the best two lines of code I have ever written
+        let mut buf = [0_u8; 16];
+        c.read_exact(&mut buf)?;
 
-            trace!("Parsed IPv6 address -> {:?}", address);
-            Ok(Self { address })
-        }
-        else {
-            warn!("Length is incorrect (stated length {:?}, but should be sixteen)", stated_length);
+        let address = Ipv6Addr::from(buf);
+        trace!("Parsed IPv6 address -> {:?}", address);
 
-            let mandated_length = MandatedLength::Exactly(16);
-            Err(WireError::WrongRecordLength { stated_length, mandated_length })
-        }
+        Ok(Self { address })
     }
 }
 

+ 2 - 11
dns/src/record/caa.rs

@@ -28,7 +28,7 @@ impl Wire for CAA {
     const NAME: &'static str = "CAA";
     const RR_TYPE: u16 = 257;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let flags = c.read_u8()?;
         trace!("Parsed flags -> {:#08b}", flags);
@@ -59,16 +59,7 @@ impl Wire for CAA {
         let value = String::from_utf8_lossy(&value_buf).to_string();
         trace!("Parsed value -> {:?}", value);
 
-        let got_length = 1 + 1 + u16::from(tag_length) + remaining_length;
-        if stated_length == got_length {
-            // This one’s a little weird, because remaining_len is based on len
-            trace!("Length is correct");
-            Ok(Self { critical, tag, value })
-        }
-        else {
-            warn!("Length is incorrect (stated length {:?}, flags plus tag plus data length {:?}", stated_length, got_length);
-            Err(WireError::WrongLabelLength { stated_length, length_after_labels: got_length })
-        }
+        Ok(Self { critical, tag, value })
     }
 }
 

+ 1 - 1
dns/src/record/cname.rs

@@ -21,7 +21,7 @@ impl Wire for CNAME {
     const NAME: &'static str = "CNAME";
     const RR_TYPE: u16 = 5;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let (domain, domain_length) = c.read_labels()?;
         trace!("Parsed domain -> {:?}", domain);

+ 109 - 0
dns/src/record/eui48.rs

@@ -0,0 +1,109 @@
+use log::*;
+
+use crate::wire::*;
+
+/// A **EUI48** record, which holds a six-octet (48-bit) Extended Unique
+/// Identifier. These identifiers can be used as MAC addresses.
+///
+/// # References
+///
+/// - [RFC 7043](https://tools.ietf.org/html/rfc7043) — Resource Records for
+///   EUI-48 and EUI-64 Addresses in the DNS (October 2013)
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct EUI48 {
+
+    /// The six octets that make up the identifier.
+    pub octets: [u8; 6],
+}
+
+impl Wire for EUI48 {
+    const NAME: &'static str = "EUI48";
+    const RR_TYPE: u16 = 108;
+
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
+    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        if stated_length != 6 {
+            warn!("Length is incorrect (record length {:?}, but should be six)", stated_length);
+            let mandated_length = MandatedLength::Exactly(6);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
+        }
+
+        let mut octets = [0_u8; 6];
+        c.read_exact(&mut octets)?;
+
+        Ok(Self { octets })
+    }
+}
+
+
+impl EUI48 {
+
+    /// Returns this EUI as hexadecimal numbers, separated by dashes.
+    pub fn formatted_address(self) -> String {
+        format!("{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}",
+                self.octets[0], self.octets[1], self.octets[2],
+                self.octets[3], self.octets[4], self.octets[5])
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn parses() {
+        let buf = &[
+            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56,  // identifier
+        ];
+
+        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   EUI48 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56 ] });
+    }
+
+    #[test]
+    fn record_too_short() {
+        let buf = &[
+            0x00, 0x7F, 0x23,  // a mere OUI
+        ];
+
+        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)),
+                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(6) }));
+    }
+
+    #[test]
+    fn record_too_long() {
+        let buf = &[
+            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56,  // identifier
+            0x01,  // an unexpected extra byte
+        ];
+
+        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)),
+                   Err(WireError::WrongRecordLength { stated_length: 7, mandated_length: MandatedLength::Exactly(6) }));
+    }
+
+    #[test]
+    fn record_empty() {
+        assert_eq!(EUI48::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(6) }));
+    }
+
+    #[test]
+    fn buffer_ends_abruptly() {
+        let buf = &[
+            0x00, 0x7F, 0x23,  // a mere OUI
+        ];
+
+        assert_eq!(EUI48::read(6, &mut Cursor::new(buf)),
+                   Err(WireError::IO));
+    }
+
+    #[test]
+    fn hex_rep() {
+        let record = EUI48 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56 ] };
+
+        assert_eq!(record.formatted_address(),
+                   "00-7f-23-12-34-56");
+    }
+}

+ 109 - 0
dns/src/record/eui64.rs

@@ -0,0 +1,109 @@
+use log::*;
+
+use crate::wire::*;
+
+/// A **EUI64** record, which holds an eight-octet (64-bit) Extended Unique
+/// Identifier.
+///
+/// # References
+///
+/// - [RFC 7043](https://tools.ietf.org/html/rfc7043) — Resource Records for
+///   EUI-48 and EUI-64 Addresses in the DNS (October 2013)
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct EUI64 {
+
+    /// The eight octets that make up the identifier.
+    pub octets: [u8; 8],
+}
+
+impl Wire for EUI64 {
+    const NAME: &'static str = "EUI64";
+    const RR_TYPE: u16 = 109;
+
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
+    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        if stated_length != 8 {
+            warn!("Length is incorrect (record length {:?}, but should be eight)", stated_length);
+            let mandated_length = MandatedLength::Exactly(8);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
+        }
+
+        let mut octets = [0_u8; 8];
+        c.read_exact(&mut octets)?;
+
+        Ok(Self { octets })
+    }
+}
+
+
+impl EUI64 {
+
+    /// Returns this EUI as hexadecimal numbers, separated by dashes.
+    pub fn formatted_address(self) -> String {
+        format!("{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}",
+                self.octets[0], self.octets[1], self.octets[2], self.octets[3],
+                self.octets[4], self.octets[5], self.octets[6], self.octets[7])
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn parses() {
+        let buf = &[
+            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90,  // identifier
+        ];
+
+        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   EUI64 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90 ] });
+    }
+
+    #[test]
+    fn record_too_short() {
+        let buf = &[
+            0x00, 0x7F, 0x23,  // a mere OUI
+        ];
+
+        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)),
+                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(8) }));
+    }
+
+    #[test]
+    fn record_too_long() {
+        let buf = &[
+            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90,  // identifier
+            0x01,  // an unexpected extra byte
+        ];
+
+        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)),
+                   Err(WireError::WrongRecordLength { stated_length: 9, mandated_length: MandatedLength::Exactly(8) }));
+    }
+
+    #[test]
+    fn record_empty() {
+        assert_eq!(EUI64::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(8) }));
+    }
+
+    #[test]
+    fn buffer_ends_abruptly() {
+        let buf = &[
+            0x00, 0x7F, 0x23,  // a mere OUI
+        ];
+
+        assert_eq!(EUI64::read(8, &mut Cursor::new(buf)),
+                   Err(WireError::IO));
+    }
+
+    #[test]
+    fn hex_rep() {
+        let record = EUI64 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90 ] };
+
+        assert_eq!(record.formatted_address(),
+                   "00-7f-23-12-34-56-78-90");
+    }
+}

+ 1 - 1
dns/src/record/hinfo.rs

@@ -28,7 +28,7 @@ impl Wire for HINFO {
     const NAME: &'static str = "HINFO";
     const RR_TYPE: u16 = 13;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
 
         let cpu_length = c.read_u8()?;

+ 1 - 1
dns/src/record/loc.rs

@@ -78,7 +78,7 @@ impl Wire for LOC {
     const NAME: &'static str = "LOC";
     const RR_TYPE: u16 = 29;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let version = c.read_u8()?;
         trace!("Parsed version -> {:?}", version);

+ 24 - 0
dns/src/record/mod.rs

@@ -13,6 +13,12 @@ pub use self::caa::CAA;
 mod cname;
 pub use self::cname::CNAME;
 
+mod eui48;
+pub use self::eui48::EUI48;
+
+mod eui64;
+pub use self::eui64::EUI64;
+
 mod hinfo;
 pub use self::hinfo::HINFO;
 
@@ -28,6 +34,9 @@ pub use self::naptr::NAPTR;
 mod ns;
 pub use self::ns::NS;
 
+mod openpgpkey;
+pub use self::openpgpkey::OPENPGPKEY;
+
 mod opt;
 pub use self::opt::OPT;
 
@@ -49,6 +58,9 @@ pub use self::tlsa::TLSA;
 mod txt;
 pub use self::txt::TXT;
 
+mod uri;
+pub use self::uri::URI;
+
 
 mod others;
 pub use self::others::{UnknownQtype, find_other_qtype_number};
@@ -70,6 +82,12 @@ pub enum Record {
     /// A **CNAME** record.
     CNAME(CNAME),
 
+    /// An **EUI48** record.
+    EUI48(EUI48),
+
+    /// An **EUI64** record.
+    EUI64(EUI64),
+
     /// A **HINFO** record.
     HINFO(HINFO),
 
@@ -85,6 +103,9 @@ pub enum Record {
     /// A **NS** record.
     NS(NS),
 
+    /// An **OPENPGPKEY** record.
+    OPENPGPKEY(OPENPGPKEY),
+
     // OPT is not included here.
 
     /// A **PTR** record.
@@ -105,6 +126,9 @@ pub enum Record {
     /// A **TXT** record.
     TXT(TXT),
 
+    /// A **URI** record.
+    URI(URI),
+
     /// A record with a type that we don’t recognise.
     Other {
 

+ 1 - 1
dns/src/record/mx.rs

@@ -26,7 +26,7 @@ impl Wire for MX {
     const NAME: &'static str = "MX";
     const RR_TYPE: u16 = 15;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let preference = c.read_u16::<BigEndian>()?;
         trace!("Parsed preference -> {:?}", preference);

+ 2 - 2
dns/src/record/naptr.rs

@@ -37,10 +37,10 @@ pub struct NAPTR {
 }
 
 impl Wire for NAPTR {
-    const NAME: &'static str = "MX";
+    const NAME: &'static str = "NAPTR";
     const RR_TYPE: u16 = 35;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let order = c.read_u16::<BigEndian>()?;
         trace!("Parsed order -> {:?}", order);

+ 1 - 1
dns/src/record/ns.rs

@@ -22,7 +22,7 @@ impl Wire for NS {
     const NAME: &'static str = "NS";
     const RR_TYPE: u16 = 2;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let (nameserver, nameserver_length) = c.read_labels()?;
         trace!("Parsed nameserver -> {:?}", nameserver);

+ 87 - 0
dns/src/record/openpgpkey.rs

@@ -0,0 +1,87 @@
+use crate::wire::*;
+
+
+/// A **OPENPGPKEY** record, which holds a PGP key.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc7929) — DNS-Based
+///   Authentication of Named Entities Bindings for OpenPGP (August 2016)
+#[derive(PartialEq, Debug)]
+pub struct OPENPGPKEY {
+
+    /// The PGP key, as unencoded bytes.
+    pub key: Vec<u8>,
+}
+
+impl Wire for OPENPGPKEY {
+    const NAME: &'static str = "OPENPGPKEY";
+    const RR_TYPE: u16 = 61;
+
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
+    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        if stated_length == 0 {
+            let mandated_length = MandatedLength::AtLeast(1);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
+        }
+
+        let mut key = vec![0_u8; usize::from(stated_length)];
+        c.read_exact(&mut key)?;
+        Ok(Self { key })
+    }
+}
+
+impl OPENPGPKEY {
+
+    /// The base64-encoded PGP key.
+    pub fn base64_key(&self) -> String {
+        base64::encode(&self.key)
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn parses() {
+        let buf = &[
+            0x12, 0x34, 0x56, 0x78,  // key
+        ];
+
+        assert_eq!(OPENPGPKEY::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   OPENPGPKEY {
+                       key: vec![ 0x12, 0x34, 0x56, 0x78 ],
+                   });
+    }
+
+    #[test]
+    fn one_byte_of_uri() {
+        let buf = &[
+            0x2b,  // one byte of key
+        ];
+
+        assert_eq!(OPENPGPKEY::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   OPENPGPKEY {
+                       key: vec![ 0x2b ],
+                   });
+    }
+
+    #[test]
+    fn record_empty() {
+        assert_eq!(OPENPGPKEY::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::AtLeast(1) }));
+    }
+
+    #[test]
+    fn buffer_ends_abruptly() {
+        let buf = &[
+            0x12, 0x34,  // the beginning of a key
+        ];
+
+        assert_eq!(OPENPGPKEY::read(23, &mut Cursor::new(buf)),
+                   Err(WireError::IO));
+    }
+}

+ 2 - 2
dns/src/record/opt.rs

@@ -30,7 +30,7 @@ use crate::wire::*;
 ///
 /// - [RFC 6891](https://tools.ietf.org/html/rfc6891) — Extension Mechanisms
 ///   for DNS (April 2013)
-#[derive(PartialEq, Debug)]
+#[derive(PartialEq, Debug, Clone)]
 pub struct OPT {
 
     /// The maximum size of a UDP packet that the client supports.
@@ -62,7 +62,7 @@ impl OPT {
     /// See §6.1.3 of the RFC, “OPT Record TTL Field Use”.
     ///
     /// Unlike the `Wire::read` function, this does not require a length.
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     pub fn read(c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let udp_payload_size = c.read_u16::<BigEndian>()?;  // replaces the class field
         trace!("Parsed UDP payload size -> {:?}", udp_payload_size);

+ 1 - 1
dns/src/record/ptr.rs

@@ -27,7 +27,7 @@ impl Wire for PTR {
     const NAME: &'static str = "PTR";
     const RR_TYPE: u16 = 12;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let (cname, cname_length) = c.read_labels()?;
         trace!("Parsed cname -> {:?}", cname);

+ 1 - 1
dns/src/record/soa.rs

@@ -47,7 +47,7 @@ impl Wire for SOA {
     const RR_TYPE: u16 = 6;
 
     #[allow(clippy::similar_names)]
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let (mname, mname_length) = c.read_labels()?;
         trace!("Parsed mname -> {:?}", mname);

+ 1 - 1
dns/src/record/srv.rs

@@ -33,7 +33,7 @@ impl Wire for SRV {
     const NAME: &'static str = "SRV";
     const RR_TYPE: u16 = 33;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let priority = c.read_u16::<BigEndian>()?;
         trace!("Parsed priority -> {:?}", priority);

+ 19 - 3
dns/src/record/sshfp.rs

@@ -30,7 +30,7 @@ impl Wire for SSHFP {
     const NAME: &'static str = "SSHFP";
     const RR_TYPE: u16 = 44;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let algorithm = c.read_u8()?;
         trace!("Parsed algorithm -> {:?}", algorithm);
@@ -73,14 +73,30 @@ mod test {
         let buf = &[
             0x01,  // algorithm
             0x01,  // fingerprint type
-            0x21, 0x22, 0x23, 0x24,  // an extremely short fingerprint
+            0x21, 0x22, 0x23, 0x24, 0x25, 0x26,  // a short fingerprint
         ];
 
         assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
                    SSHFP {
                        algorithm: 1,
                        fingerprint_type: 1,
-                       fingerprint: vec![ 0x21, 0x22, 0x23, 0x24 ],
+                       fingerprint: vec![ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26 ],
+                   });
+    }
+
+    #[test]
+    fn one_byte_fingerprint() {
+        let buf = &[
+            0x01,  // algorithm
+            0x01,  // fingerprint type
+            0x21,  // an extremely short fingerprint
+        ];
+
+        assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   SSHFP {
+                       algorithm: 1,
+                       fingerprint_type: 1,
+                       fingerprint: vec![ 0x21 ],
                    });
     }
 

+ 21 - 3
dns/src/record/tlsa.rs

@@ -34,7 +34,7 @@ impl Wire for TLSA {
     const NAME: &'static str = "TLSA";
     const RR_TYPE: u16 = 52;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
 
         let certificate_usage = c.read_u8()?;
@@ -82,7 +82,7 @@ mod test {
             0x03,  // certificate usage
             0x01,  // selector
             0x01,  // matching type
-            0x05, 0x95, 0x98,  // data
+            0x05, 0x95, 0x98, 0x11, 0x22, 0x33 // data
         ];
 
         assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
@@ -90,7 +90,25 @@ mod test {
                        certificate_usage: 3,
                        selector: 1,
                        matching_type: 1,
-                       certificate_data: vec![ 0x05, 0x95, 0x98 ],
+                       certificate_data: vec![ 0x05, 0x95, 0x98, 0x11, 0x22, 0x33 ],
+                   });
+    }
+
+    #[test]
+    fn one_byte_certificate() {
+        let buf = &[
+            0x03,  // certificate usage
+            0x01,  // selector
+            0x01,  // matching type
+            0x05,  // one byte of data
+        ];
+
+        assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   TLSA {
+                       certificate_usage: 3,
+                       selector: 1,
+                       matching_type: 1,
+                       certificate_data: vec![ 0x05 ],
                    });
     }
 

+ 1 - 1
dns/src/record/txt.rs

@@ -25,7 +25,7 @@ impl Wire for TXT {
     const NAME: &'static str = "TXT";
     const RR_TYPE: u16 = 16;
 
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let mut messages = Vec::new();
         let mut total_length = 0_u16;

+ 128 - 0
dns/src/record/uri.rs

@@ -0,0 +1,128 @@
+use log::*;
+
+use crate::wire::*;
+
+
+/// A **URI** record, which holds a URI along with weight and priority values
+/// to balance between several records.
+///
+/// # References
+///
+/// - [RFC 7553](https://tools.ietf.org/html/rfc7553) — The Uniform Resource
+///   Identifier (URI) DNS Resource Record (June 2015)
+/// - [RFC 3986](https://tools.ietf.org/html/rfc3986) — Uniform Resource
+///   Identifier (URI): Generic Syntax (January 2005)
+#[derive(PartialEq, Debug)]
+pub struct URI {
+
+    /// The priority of the URI. Clients are supposed to contact the URI with
+    /// the lowest priority out of all the ones it can reach.
+    pub priority: u16,
+
+    /// The weight of the URI, which specifies a relative weight for entries
+    /// with the same priority.
+    pub weight: u16,
+
+    /// The URI contained in the record. Since all we are doing is displaying
+    /// it to the user, we do not need to parse it for accuracy.
+    pub target: String,
+}
+
+impl Wire for URI {
+    const NAME: &'static str = "URI";
+    const RR_TYPE: u16 = 256;
+
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
+    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let priority = c.read_u16::<BigEndian>()?;
+        trace!("Parsed priority -> {:?}", priority);
+
+        let weight = c.read_u16::<BigEndian>()?;
+        trace!("Parsed weight -> {:?}", weight);
+
+        // The target must not be empty.
+        if stated_length <= 4 {
+            let mandated_length = MandatedLength::AtLeast(5);
+            return Err(WireError::WrongRecordLength { stated_length, mandated_length });
+        }
+
+        let remaining_length = stated_length - 4;
+        let mut buf = Vec::with_capacity(remaining_length.into());
+
+        for _ in 0 .. remaining_length {
+            buf.push(c.read_u8()?);
+        }
+
+        let target = String::from_utf8_lossy(&buf).to_string();
+        trace!("Parsed target -> {:?}", target);
+
+        Ok(Self { priority, weight, target })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn parses() {
+        let buf = &[
+            0x00, 0x0A,  // priority
+            0x00, 0x10,  // weight
+            0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x66, 0x63,
+            0x73, 0x2e, 0x69, 0x6f, 0x2f,  // uri
+        ];
+
+        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   URI {
+                       priority: 10,
+                       weight: 16,
+                       target: String::from("https://rfcs.io/"),
+                   });
+    }
+
+    #[test]
+    fn one_byte_of_uri() {
+        let buf = &[
+            0x00, 0x0A,  // priority
+            0x00, 0x10,  // weight
+            0x2f,  // one byte of uri (invalid but still a legitimate DNS record)
+        ];
+
+        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),
+                   URI {
+                       priority: 10,
+                       weight: 16,
+                       target: String::from("/"),
+                   });
+    }
+
+    #[test]
+    fn missing_any_data() {
+        let buf = &[
+            0x00, 0x0A,  // priority
+            0x00, 0x10,  // weight
+        ];
+
+        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)),
+                   Err(WireError::WrongRecordLength { stated_length: 4, mandated_length: MandatedLength::AtLeast(5) }));
+    }
+
+    #[test]
+    fn record_empty() {
+        assert_eq!(URI::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+
+    #[test]
+    fn buffer_ends_abruptly() {
+        let buf = &[
+            0x00, 0x0A,  // half a priority
+        ];
+
+        assert_eq!(URI::read(23, &mut Cursor::new(buf)),
+                   Err(WireError::IO));
+    }
+}

+ 26 - 3
dns/src/strings.rs

@@ -20,6 +20,11 @@ pub struct Labels {
     segments: Vec<(u8, String)>,
 }
 
+fn label_to_ascii(label: &str) -> Result<String, unic_idna::Errors> {
+    let flags = unic_idna::Flags{use_std3_ascii_rules: true, transitional_processing: false, verify_dns_length: true};
+    unic_idna::to_ascii(label, flags)
+}
+
 impl Labels {
 
     /// Creates a new empty set of labels, which represent the root of the DNS
@@ -38,9 +43,15 @@ impl Labels {
                 continue;
             }
 
-            match u8::try_from(label.len()) {
+            let label_idn = label_to_ascii(label)
+                    .map_err(|e| {
+                        warn!("Could not encode label {:?}: {:?}", label, e);
+                        label
+                    })?;
+
+            match u8::try_from(label_idn.len()) {
                 Ok(length) => {
-                    segments.push((length, label.to_owned()));
+                    segments.push((length, label_idn));
                 }
                 Err(e) => {
                     warn!("Could not encode label {:?}: {}", label, e);
@@ -51,6 +62,18 @@ impl Labels {
 
         Ok(Self { segments })
     }
+
+    /// Returns the number of segments.
+    pub fn len(&self) -> usize {
+        self.segments.len()
+    }
+
+    /// Returns a new set of labels concatenating two names.
+    pub fn extend(&self, other: &Self) -> Self {
+        let mut segments = self.segments.clone();
+        segments.extend_from_slice(&other.segments);
+        Self { segments }
+    }
 }
 
 impl fmt::Display for Labels {
@@ -116,7 +139,7 @@ const RECURSION_LIMIT: usize = 8;
 /// recursions to track backtracking positions. Returns the count of bytes
 /// that had to be read to produce the string, including the bytes to signify
 /// backtracking, but not including the bytes read _during_ backtracking.
-#[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+#[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
 fn read_string_recursive(labels: &mut Labels, c: &mut Cursor<&[u8]>, recursions: &mut Vec<u16>) -> Result<u16, WireError> {
     let mut bytes_read = 0;
 

+ 25 - 174
dns/src/wire.rs

@@ -1,6 +1,6 @@
 //! Parsing the DNS wire protocol.
 
-pub(crate) use std::io::Cursor;
+pub(crate) use std::io::{Cursor, Read};
 pub(crate) use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
 
 use std::io;
@@ -54,7 +54,7 @@ impl Request {
 impl Response {
 
     /// Reads bytes off of the given slice, parsing them into a response.
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
         info!("Parsing response");
         trace!("Bytes -> {:?}", bytes);
@@ -108,7 +108,7 @@ impl Query {
 
     /// Reads bytes from the given cursor, and parses them into a query with
     /// the given domain name.
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let qtype = c.read_u16::<BigEndian>()?;
         trace!("Read qtype -> {:?}", qtype);
@@ -125,7 +125,7 @@ impl Answer {
 
     /// Reads bytes from the given cursor, and parses them into an answer with
     /// the given domain name.
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         let qtype = c.read_u16::<BigEndian>()?;
         trace!("Read qtype -> {:?}", qtype);
@@ -156,10 +156,14 @@ impl Record {
 
     /// Reads at most `len` bytes from the given curser, and parses them into
     /// a record structure depending on the type number, which has already been read.
-    #[cfg_attr(all(test, feature = "with_mutagen"), ::mutagen::mutate)]
+    #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn from_bytes(qtype: TypeInt, len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         use crate::record::*;
 
+        if cfg!(feature = "with_mutagen") {
+            warn!("Mutation is enabled!");
+        }
+
         macro_rules! try_record {
             ($record:tt) => {
                 if $record::RR_TYPE == qtype {
@@ -175,11 +179,14 @@ impl Record {
         try_record!(AAAA);
         try_record!(CAA);
         try_record!(CNAME);
+        try_record!(EUI48);
+        try_record!(EUI64);
         try_record!(HINFO);
         try_record!(LOC);
         try_record!(MX);
         try_record!(NAPTR);
         try_record!(NS);
+        try_record!(OPENPGPKEY);
         // OPT is handled separately
         try_record!(PTR);
         try_record!(SSHFP);
@@ -187,6 +194,7 @@ impl Record {
         try_record!(SRV);
         try_record!(TLSA);
         try_record!(TXT);
+        try_record!(URI);
 
         // Otherwise, collect the bytes into a vector and return an unknown
         // record type.
@@ -238,11 +246,14 @@ pub fn find_qtype_number(record_type: &str) -> Option<TypeInt> {
     try_record!(AAAA);
     try_record!(CAA);
     try_record!(CNAME);
+    try_record!(EUI48);
+    try_record!(EUI64);
     try_record!(HINFO);
     try_record!(LOC);
     try_record!(MX);
     try_record!(NAPTR);
     try_record!(NS);
+    try_record!(OPENPGPKEY);
     // OPT is elsewhere
     try_record!(PTR);
     try_record!(SSHFP);
@@ -250,6 +261,7 @@ pub fn find_qtype_number(record_type: &str) -> Option<TypeInt> {
     try_record!(SRV);
     try_record!(TLSA);
     try_record!(TXT);
+    try_record!(URI);
 
     None
 }
@@ -270,18 +282,18 @@ impl Flags {
     /// Converts the flags into a two-byte number.
     pub fn to_u16(self) -> u16 {                 // 0123 4567 89AB CDEF
         let mut                          bits  = 0b_0000_0000_0000_0000;
-        if self.response               { bits += 0b_1000_0000_0000_0000; }
+        if self.response               { bits |= 0b_1000_0000_0000_0000; }
         match self.opcode {
-            Opcode::Query     =>       { bits += 0b_0000_0000_0000_0000; }
+            Opcode::Query     =>       { bits |= 0b_0000_0000_0000_0000; }
             Opcode::Other(_)  =>       { unimplemented!(); }
         }
-        if self.authoritative          { bits += 0b_0000_0100_0000_0000; }
-        if self.truncated              { bits += 0b_0000_0010_0000_0000; }
-        if self.recursion_desired      { bits += 0b_0000_0001_0000_0000; }
-        if self.recursion_available    { bits += 0b_0000_0000_1000_0000; }
+        if self.authoritative          { bits |= 0b_0000_0100_0000_0000; }
+        if self.truncated              { bits |= 0b_0000_0010_0000_0000; }
+        if self.recursion_desired      { bits |= 0b_0000_0001_0000_0000; }
+        if self.recursion_available    { bits |= 0b_0000_0000_1000_0000; }
         // (the Z bit is reserved)               0b_0000_0000_0100_0000
-        if self.authentic_data         { bits += 0b_0000_0000_0010_0000; }
-        if self.checking_disabled      { bits += 0b_0000_0000_0001_0000; }
+        if self.authentic_data         { bits |= 0b_0000_0000_0010_0000; }
+        if self.checking_disabled      { bits |= 0b_0000_0000_0001_0000; }
 
         bits
     }
@@ -480,164 +492,3 @@ impl From<io::Error> for WireError {
         Self::IO
     }
 }
-
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use crate::record::{Record, A, SOA, OPT, UnknownQtype};
-    use std::net::Ipv4Addr;
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn build_request() {
-        let request = Request {
-            transaction_id: 0xceac,
-            flags: Flags::query(),
-            query: Query {
-                qname: Labels::encode("rfcs.io").unwrap(),
-                qclass: QClass::Other(0x42),
-                qtype: 0x1234,
-            },
-            additional: Some(Request::additional_record()),
-        };
-
-        let result = vec![
-            0xce, 0xac,  // transaction ID
-            0x01, 0x00,  // flags (standard query)
-            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,  // counts (1, 0, 0, 1)
-
-            // query:
-            0x04, 0x72, 0x66, 0x63, 0x73, 0x02, 0x69, 0x6f, 0x00,  // qname
-            0x12, 0x34,  // type
-            0x00, 0x42,  // class
-
-            // OPT record:
-            0x00,  // name
-            0x00, 0x29,  // type OPT
-            0x02, 0x00,  // UDP payload size
-            0x00,  // higher bits
-            0x00,  // EDNS(0) version
-            0x00, 0x00,  // more flags
-            0x00, 0x00,  // no data
-        ];
-
-        assert_eq!(request.to_bytes().unwrap(), result);
-    }
-
-    #[test]
-    fn complete_response() {
-
-        // This is an artifical amalgam of DNS, not a real-world response!
-        let buf = &[
-            0xce, 0xac,  // transaction ID
-            0x81, 0x80,  // flags (standard query, response, no error)
-            0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02,  // counts (1, 1, 1, 2)
-
-            // query:
-            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00,  // name
-            0x00, 0x01,  // type A
-            0x00, 0x01,  // class IN
-
-            // answer:
-            0xc0, 0x0c,  // name (backreference)
-            0x00, 0x01,  // type A
-            0x00, 0x01,  // class IN
-            0x00, 0x00, 0x03, 0x77,  // TTL
-            0x00, 0x04,  // data length 4
-            0x8a, 0x44, 0x75, 0x5e,  // IP address
-
-            // authoritative:
-            0x00,  // name
-            0x00, 0x06,  // type SOA
-            0x00, 0x01,  // class IN
-            0xFF, 0xFF, 0xFF, 0xFF,  // TTL (maximum possible!)
-            0x00, 0x1B,  // data length
-            0x01, 0x61, 0x00,  // primary name server ("a")
-            0x02, 0x6d, 0x78, 0x00,  // mailbox ("mx")
-            0x78, 0x68, 0x52, 0x2c,  // serial number
-            0x00, 0x00, 0x07, 0x08,  // refresh interval
-            0x00, 0x00, 0x03, 0x84,  // retry interval
-            0x00, 0x09, 0x3a, 0x80,  // expire limit
-            0x00, 0x01, 0x51, 0x80,  // minimum TTL
-
-            // additional 1:
-            0x00,  // name
-            0x00, 0x99,  // unknown type
-            0x00, 0x99,  // unknown class
-            0x12, 0x34, 0x56, 0x78,  // TTL
-            0x00, 0x04,  // data length 4
-            0x12, 0x34, 0x56, 0x78,  // data
-
-            // additional 2:
-            0x00,  // name
-            0x00, 0x29,  // type OPT
-            0x02, 0x00,  // UDP payload size
-            0x00,  // higher bits
-            0x00,  // EDNS(0) version
-            0x00, 0x00,  // more flags
-            0x00, 0x00,  // no data
-        ];
-
-        let response = Response {
-            transaction_id: 0xceac,
-            flags: Flags::standard_response(),
-            queries: vec![
-                Query {
-                    qname: Labels::encode("bsago.me").unwrap(),
-                    qclass: QClass::IN,
-                    qtype: qtype!(A),
-                },
-            ],
-            answers: vec![
-                Answer::Standard {
-                    qname: Labels::encode("bsago.me").unwrap(),
-                    qclass: QClass::IN,
-                    ttl: 887,
-                    record: Record::A(A {
-                        address: Ipv4Addr::new(138, 68, 117, 94),
-                    }),
-                }
-            ],
-            authorities: vec![
-                Answer::Standard {
-                    qname: Labels::root(),
-                    qclass: QClass::IN,
-                    ttl: 4294967295,
-                    record: Record::SOA(SOA {
-                        mname: Labels::encode("a").unwrap(),
-                        rname: Labels::encode("mx").unwrap(),
-                        serial: 2020102700,
-                        refresh_interval: 1800,
-                        retry_interval: 900,
-                        expire_limit: 604800,
-                        minimum_ttl: 86400,
-                    }),
-                }
-            ],
-            additionals: vec![
-                Answer::Standard {
-                    qname: Labels::root(),
-                    qclass: QClass::Other(153),
-                    ttl: 305419896,
-                    record: Record::Other {
-                        type_number: UnknownQtype::UnheardOf(153),
-                        bytes: vec![ 0x12, 0x34, 0x56, 0x78 ],
-                    },
-                },
-                Answer::Pseudo {
-                    qname: Labels::root(),
-                    opt: OPT {
-                        udp_payload_size: 512,
-                        higher_bits: 0,
-                        edns0_version: 0,
-                        flags: 0,
-                        data: vec![],
-                    },
-                },
-            ],
-        };
-
-        assert_eq!(Response::from_bytes(buf), Ok(response));
-    }
-}

+ 40 - 0
dns/tests/wire_building_tests.rs

@@ -0,0 +1,40 @@
+use dns::{Request, Flags, Query, Labels, QClass};
+
+use pretty_assertions::assert_eq;
+
+
+#[test]
+fn build_request() {
+    let request = Request {
+        transaction_id: 0xceac,
+        flags: Flags::query(),
+        query: Query {
+            qname: Labels::encode("rfcs.io").unwrap(),
+            qclass: QClass::Other(0x42),
+            qtype: 0x1234,
+        },
+        additional: Some(Request::additional_record()),
+    };
+
+    let result = vec![
+        0xce, 0xac,  // transaction ID
+        0x01, 0x00,  // flags (standard query)
+        0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,  // counts (1, 0, 0, 1)
+
+        // query:
+        0x04, 0x72, 0x66, 0x63, 0x73, 0x02, 0x69, 0x6f, 0x00,  // qname
+        0x12, 0x34,  // type
+        0x00, 0x42,  // class
+
+        // OPT record:
+        0x00,  // name
+        0x00, 0x29,  // type OPT
+        0x02, 0x00,  // UDP payload size
+        0x00,  // higher bits
+        0x00,  // EDNS(0) version
+        0x00, 0x00,  // more flags
+        0x00, 0x00,  // no data
+    ];
+
+    assert_eq!(request.to_bytes().unwrap(), result);
+}

+ 120 - 1
dns/tests/wire_parsing_tests.rs

@@ -1,7 +1,9 @@
 use std::net::Ipv4Addr;
 
 use dns::{Response, Query, Answer, Labels, Flags, Opcode, QClass, qtype};
-use dns::record::{Record, A, CNAME, OPT};
+use dns::record::{Record, A, CNAME, OPT, SOA, UnknownQtype};
+
+use pretty_assertions::assert_eq;
 
 
 #[test]
@@ -150,3 +152,120 @@ fn parse_response_with_mixed_string() {
 
     assert_eq!(Response::from_bytes(buf), Ok(response));
 }
+
+
+#[test]
+fn parse_response_with_multiple_additionals() {
+
+    // This is an artifical amalgam of DNS, not a real-world response!
+    let buf = &[
+        0xce, 0xac,  // transaction ID
+        0x81, 0x80,  // flags (standard query, response, no error)
+        0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02,  // counts (1, 1, 1, 2)
+
+        // query:
+        0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00,  // name
+        0x00, 0x01,  // type A
+        0x00, 0x01,  // class IN
+
+        // answer:
+        0xc0, 0x0c,  // name (backreference)
+        0x00, 0x01,  // type A
+        0x00, 0x01,  // class IN
+        0x00, 0x00, 0x03, 0x77,  // TTL
+        0x00, 0x04,  // data length 4
+        0x8a, 0x44, 0x75, 0x5e,  // IP address
+
+        // authoritative:
+        0x00,  // name
+        0x00, 0x06,  // type SOA
+        0x00, 0x01,  // class IN
+        0xFF, 0xFF, 0xFF, 0xFF,  // TTL (maximum possible!)
+        0x00, 0x1B,  // data length
+        0x01, 0x61, 0x00,  // primary name server ("a")
+        0x02, 0x6d, 0x78, 0x00,  // mailbox ("mx")
+        0x78, 0x68, 0x52, 0x2c,  // serial number
+        0x00, 0x00, 0x07, 0x08,  // refresh interval
+        0x00, 0x00, 0x03, 0x84,  // retry interval
+        0x00, 0x09, 0x3a, 0x80,  // expire limit
+        0x00, 0x01, 0x51, 0x80,  // minimum TTL
+
+        // additional 1:
+        0x00,  // name
+        0x00, 0x99,  // unknown type
+        0x00, 0x99,  // unknown class
+        0x12, 0x34, 0x56, 0x78,  // TTL
+        0x00, 0x04,  // data length 4
+        0x12, 0x34, 0x56, 0x78,  // data
+
+        // additional 2:
+        0x00,  // name
+        0x00, 0x29,  // type OPT
+        0x02, 0x00,  // UDP payload size
+        0x00,  // higher bits
+        0x00,  // EDNS(0) version
+        0x00, 0x00,  // more flags
+        0x00, 0x00,  // no data
+    ];
+
+    let response = Response {
+        transaction_id: 0xceac,
+        flags: Flags::standard_response(),
+        queries: vec![
+            Query {
+                qname: Labels::encode("bsago.me").unwrap(),
+                qclass: QClass::IN,
+                qtype: qtype!(A),
+            },
+        ],
+        answers: vec![
+            Answer::Standard {
+                qname: Labels::encode("bsago.me").unwrap(),
+                qclass: QClass::IN,
+                ttl: 887,
+                record: Record::A(A {
+                    address: Ipv4Addr::new(138, 68, 117, 94),
+                }),
+            }
+        ],
+        authorities: vec![
+            Answer::Standard {
+                qname: Labels::root(),
+                qclass: QClass::IN,
+                ttl: 4294967295,
+                record: Record::SOA(SOA {
+                    mname: Labels::encode("a").unwrap(),
+                    rname: Labels::encode("mx").unwrap(),
+                    serial: 2020102700,
+                    refresh_interval: 1800,
+                    retry_interval: 900,
+                    expire_limit: 604800,
+                    minimum_ttl: 86400,
+                }),
+            }
+        ],
+        additionals: vec![
+            Answer::Standard {
+                qname: Labels::root(),
+                qclass: QClass::Other(153),
+                ttl: 305419896,
+                record: Record::Other {
+                    type_number: UnknownQtype::UnheardOf(153),
+                    bytes: vec![ 0x12, 0x34, 0x56, 0x78 ],
+                },
+            },
+            Answer::Pseudo {
+                qname: Labels::root(),
+                opt: OPT {
+                    udp_payload_size: 512,
+                    higher_bits: 0,
+                    edns0_version: 0,
+                    flags: 0,
+                    data: vec![],
+                },
+            },
+        ],
+    };
+
+    assert_eq!(Response::from_bytes(buf), Ok(response));
+}

+ 8 - 0
src/colours.rs

@@ -17,11 +17,14 @@ pub struct Colours {
     pub aaaa: Style,
     pub caa: Style,
     pub cname: Style,
+    pub eui48: Style,
+    pub eui64: Style,
     pub hinfo: Style,
     pub loc: Style,
     pub mx: Style,
     pub ns: Style,
     pub naptr: Style,
+    pub openpgpkey: Style,
     pub opt: Style,
     pub ptr: Style,
     pub sshfp: Style,
@@ -29,6 +32,7 @@ pub struct Colours {
     pub srv: Style,
     pub tlsa: Style,
     pub txt: Style,
+    pub uri: Style,
     pub unknown: Style,
 }
 
@@ -48,11 +52,14 @@ impl Colours {
             aaaa: Green.bold(),
             caa: Red.normal(),
             cname: Yellow.normal(),
+            eui48: Yellow.normal(),
+            eui64: Yellow.bold(),
             hinfo: Yellow.normal(),
             loc: Yellow.normal(),
             mx: Cyan.normal(),
             naptr: Green.normal(),
             ns: Red.normal(),
+            openpgpkey: Cyan.normal(),
             opt: Purple.normal(),
             ptr: Red.normal(),
             sshfp: Cyan.normal(),
@@ -60,6 +67,7 @@ impl Colours {
             srv: Cyan.normal(),
             tlsa: Yellow.normal(),
             txt: Yellow.normal(),
+            uri: Yellow.normal(),
             unknown: White.on(Red),
         }
     }

+ 1 - 3
src/connect.rs

@@ -2,8 +2,6 @@
 
 use dns_transport::*;
 
-use crate::resolve::Nameserver;
-
 
 /// A **transport type** creates a `Transport` that determines which protocols
 /// should be used to send and receive DNS wire data over the network.
@@ -34,7 +32,7 @@ pub enum TransportType {
 impl TransportType {
 
     /// Creates a boxed `Transport` depending on the transport type.
-    pub fn make_transport(self, ns: Nameserver) -> Box<dyn Transport> {
+    pub fn make_transport(self, ns: String) -> Box<dyn Transport> {
         match self {
             Self::Automatic  => Box::new(AutoTransport::new(ns)),
             Self::UDP        => Box::new(UdpTransport::new(ns)),

+ 25 - 15
src/main.rs

@@ -18,6 +18,7 @@
 #![allow(clippy::too_many_lines)]
 #![allow(clippy::unit_arg)]
 #![allow(clippy::unused_self)]
+#![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::useless_let_if_seq)]
 #![allow(clippy::wildcard_imports)]
 
@@ -107,22 +108,31 @@ fn run(Options { requests, format, measure_time }: Options) -> i32 {
     let timer = if measure_time { Some(Instant::now()) } else { None };
 
     let mut errored = false;
-    for (request, transport) in requests.generate() {
-        let result = transport.send(&request);
-
-        match result {
-            Ok(mut response) => {
-                if ! should_show_opt {
-                    response.answers.retain(dns::Answer::is_standard);
-                    response.authorities.retain(dns::Answer::is_standard);
-                    response.additionals.retain(dns::Answer::is_standard);
+    for (request_list, transport) in requests.generate() {
+        let request_list_len = request_list.len();
+        for (i, request) in request_list.into_iter().enumerate() {
+            let result = transport.send(&request);
+
+            match result {
+                Ok(mut response) => {
+                    if response.flags.error_code.is_some() && i != request_list_len - 1 {
+                        continue;
+                    }
+
+                    if ! should_show_opt {
+                        response.answers.retain(dns::Answer::is_standard);
+                        response.authorities.retain(dns::Answer::is_standard);
+                        response.additionals.retain(dns::Answer::is_standard);
+                    }
+
+                    responses.push(response);
+                    break;
+                }
+                Err(e) => {
+                    format.print_error(e);
+                    errored = true;
+                    break;
                 }
-
-                responses.push(response);
-            }
-            Err(e) => {
-                format.print_error(e);
-                errored = true;
             }
         }
     }

+ 14 - 10
src/options.rs

@@ -194,7 +194,7 @@ impl Inputs {
     }
 
     fn add_nameserver(&mut self, input: &str) -> Result<(), OptionsError> {
-        self.resolvers.push(Resolver::Specified(input.into()));
+        self.resolvers.push(Resolver::specified(input.into()));
         Ok(())
     }
 
@@ -223,7 +223,7 @@ impl Inputs {
                 trace!("Got nameserver -> {:?}", nameserver);
                 self.add_nameserver(nameserver)?;
             }
-            else if a.chars().all(char::is_uppercase) {
+            else if Self::is_constant_name(&a) {
                 if let Some(class) = self.parse_class_name(&a) {
                     trace!("Got qclass -> {:?}", &a);
                     self.classes.push(class);
@@ -245,6 +245,10 @@ impl Inputs {
         Ok(())
     }
 
+    fn is_constant_name(a: &str) -> bool {
+        a.chars().all(char::is_uppercase) || a == "EUI48" || a == "EUI64"
+    }
+
     fn load_fallbacks(&mut self) {
         if self.types.is_empty() {
             self.types.push(qtype!(A));
@@ -255,7 +259,7 @@ impl Inputs {
         }
 
         if self.resolvers.is_empty() {
-            self.resolvers.push(Resolver::SystemDefault);
+            self.resolvers.push(Resolver::system_default());
         }
 
         if self.transport_types.is_empty() {
@@ -488,7 +492,7 @@ mod test {
                 domains:         vec![ /* No domains by default */ ],
                 types:           vec![ qtype!(A) ],
                 classes:         vec![ QClass::IN ],
-                resolvers:       vec![ Resolver::SystemDefault ],
+                resolvers:       vec![ Resolver::system_default() ],
                 transport_types: vec![ TransportType::Automatic ],
             }
         }
@@ -583,7 +587,7 @@ mod test {
         let options = Options::getopts(&[ "lookup.dog", "@1.1.1.1" ]).unwrap();
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
-            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
     }
@@ -605,7 +609,7 @@ mod test {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
             types:      vec![ qtype!(NS) ],
-            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
     }
@@ -617,7 +621,7 @@ mod test {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
             types:      vec![ qtype!(SOA) ],
-            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
     }
@@ -649,7 +653,7 @@ mod test {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
             types:      vec![ qtype!(SOA) ],
-            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
     }
@@ -670,8 +674,8 @@ mod test {
         let options = Options::getopts(&[ "lookup.dog", "--nameserver", "1.1.1.1", "--nameserver", "1.0.0.1" ]).unwrap();
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
-            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()),
-                              Resolver::Specified("1.0.0.1".into()), ],
+            resolvers:  vec![ Resolver::specified("1.1.1.1".into()),
+                              Resolver::specified("1.0.0.1".into()), ],
             .. Inputs::fallbacks()
         });
     }

+ 89 - 51
src/output.rs

@@ -194,6 +194,12 @@ impl TextFormat {
             Record::CNAME(ref cname) => {
                 format!("{:?}", cname.domain.to_string())
             }
+            Record::EUI48(ref eui48) => {
+                format!("{:?}", eui48.formatted_address())
+            }
+            Record::EUI64(ref eui64) => {
+                format!("{:?}", eui64.formatted_address())
+            }
             Record::HINFO(ref hinfo) => {
                 format!("{:?} {:?}", hinfo.cpu, hinfo.os)
             }
@@ -223,6 +229,9 @@ impl TextFormat {
             Record::NS(ref ns) => {
                 format!("{:?}", ns.nameserver.to_string())
             }
+            Record::OPENPGPKEY(ref opgp) => {
+                format!("{:?}", opgp.base64_key())
+            }
             Record::PTR(ref ptr) => {
                 format!("{:?}", ptr.cname.to_string())
             }
@@ -259,6 +268,9 @@ impl TextFormat {
                 let messages = txt.messages.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>();
                 messages.join(", ")
             }
+            Record::URI(ref uri) => {
+                format!("{} {} {:?}", uri.priority, uri.weight, uri.target)
+            }
             Record::Other { ref bytes, .. } => {
                 format!("{:?}", bytes)
             }
@@ -358,119 +370,145 @@ fn json_answers(answers: &[Answer]) -> JsonValue {
 /// Serialises a received DNS record as a JSON value.
 fn json_record(record: &Record) -> JsonValue {
     match record {
-        Record::A(rec) => {
+        Record::A(a) => {
             json!({
                 "type": "A",
-                "address": rec.address.to_string(),
+                "address": a.address.to_string(),
             })
         }
-        Record::AAAA(rec) => {
+        Record::AAAA(aaaa) => {
             json!({
                 "type": "AAAA",
-                "address": rec.address.to_string(),
+                "address": aaaa.address.to_string(),
             })
         }
-        Record::CAA(rec) => {
+        Record::CAA(caa) => {
             json!({
                 "type": "CAA",
-                "critical": rec.critical,
-                "tag": rec.tag,
-                "value": rec.value,
+                "critical": caa.critical,
+                "tag": caa.tag,
+                "value": caa.value,
             })
         }
-        Record::CNAME(rec) => {
+        Record::CNAME(cname) => {
             json!({
                 "type": "CNAME",
-                "domain": rec.domain.to_string(),
+                "domain": cname.domain.to_string(),
+            })
+        }
+        Record::EUI48(eui48) => {
+            json!({
+                "type": "EUI48",
+                "identifier": eui48.formatted_address(),
+            })
+        }
+        Record::EUI64(eui64) => {
+            json!({
+                "type": "EUI64",
+                "identifier": eui64.formatted_address(),
             })
         }
-        Record::HINFO(rec) => {
+        Record::HINFO(hinfo) => {
             json!({
                 "type": "HINFO",
-                "cpu": rec.cpu,
-                "os": rec.os,
+                "cpu": hinfo.cpu,
+                "os": hinfo.os,
             })
         }
-        Record::LOC(rec) => {
+        Record::LOC(loc) => {
             json!({
                 "type": "LOC",
-                "size": rec.size.to_string(),
+                "size": loc.size.to_string(),
                 "precision": {
-                    "horizontal": rec.horizontal_precision,
-                    "vertical": rec.vertical_precision,
+                    "horizontal": loc.horizontal_precision,
+                    "vertical": loc.vertical_precision,
                 },
                 "point": {
-                    "latitude": rec.latitude.map(|e| e.to_string()),
-                    "longitude": rec.longitude.map(|e| e.to_string()),
-                    "altitude": rec.altitude.to_string(),
+                    "latitude": loc.latitude.map(|e| e.to_string()),
+                    "longitude": loc.longitude.map(|e| e.to_string()),
+                    "altitude": loc.altitude.to_string(),
                 },
             })
         }
-        Record::MX(rec) => {
+        Record::MX(mx) => {
             json!({
                 "type": "MX",
-                "preference": rec.preference,
-                "exchange": rec.exchange.to_string(),
+                "preference": mx.preference,
+                "exchange": mx.exchange.to_string(),
             })
         }
-        Record::NAPTR(rec) => {
+        Record::NAPTR(naptr) => {
             json!({
                 "type": "NAPTR",
-                "order": rec.order,
-                "flags": rec.flags,
-                "service": rec.service,
-                "regex": rec.service,
-                "replacement": rec.replacement.to_string(),
+                "order": naptr.order,
+                "flags": naptr.flags,
+                "service": naptr.service,
+                "regex": naptr.service,
+                "replacement": naptr.replacement.to_string(),
             })
         }
-        Record::NS(rec) => {
+        Record::NS(ns) => {
             json!({
                 "type": "NS",
-                "nameserver": rec.nameserver.to_string(),
+                "nameserver": ns.nameserver.to_string(),
+            })
+        }
+        Record::OPENPGPKEY(opgp) => {
+            json!({
+                "type": "OPENPGPKEY",
+                "key": opgp.base64_key(),
             })
         }
-        Record::PTR(rec) => {
+        Record::PTR(ptr) => {
             json!({
                 "type": "PTR",
-                "cname": rec.cname.to_string(),
+                "cname": ptr.cname.to_string(),
             })
         }
-        Record::SSHFP(rec) => {
+        Record::SSHFP(sshfp) => {
             json!({
                 "type": "SSHFP",
-                "algorithm": rec.algorithm,
-                "fingerprint_type": rec.fingerprint_type,
-                "fingerprint": rec.hex_fingerprint(),
+                "algorithm": sshfp.algorithm,
+                "fingerprint_type": sshfp.fingerprint_type,
+                "fingerprint": sshfp.hex_fingerprint(),
             })
         }
-        Record::SOA(rec) => {
+        Record::SOA(soa) => {
             json!({
                 "type": "SOA",
-                "mname": rec.mname.to_string(),
+                "mname": soa.mname.to_string(),
             })
         }
-        Record::SRV(rec) => {
+        Record::SRV(srv) => {
             json!({
                 "type": "SRV",
-                "priority": rec.priority,
-                "weight": rec.weight,
-                "port": rec.port,
-                "target": rec.target.to_string(),
+                "priority": srv.priority,
+                "weight": srv.weight,
+                "port": srv.port,
+                "target": srv.target.to_string(),
             })
         }
-        Record::TLSA(rec) => {
+        Record::TLSA(tlsa) => {
             json!({
                 "type": "TLSA",
-                "certificate_usage": rec.certificate_usage,
-                "selector": rec.selector,
-                "matching_type": rec.matching_type,
-                "certificate_data": rec.hex_certificate_data(),
+                "certificate_usage": tlsa.certificate_usage,
+                "selector": tlsa.selector,
+                "matching_type": tlsa.matching_type,
+                "certificate_data": tlsa.hex_certificate_data(),
             })
         }
-        Record::TXT(rec) => {
+        Record::TXT(txt) => {
             json!({
                 "type": "TXT",
-                "messages": rec.messages,
+                "messages": txt.messages,
+            })
+        }
+        Record::URI(uri) => {
+            json!({
+                "type": "URI",
+                "priority": uri.priority,
+                "weight": uri.weight,
+                "target": uri.target,
             })
         }
         Record::Other { type_number, bytes } => {

+ 14 - 13
src/requests.rs

@@ -81,21 +81,16 @@ pub enum UseEDNS {
 
 impl RequestGenerator {
 
-    /// Iterate through the inputs matrix, returning pairs of DNS requests and
-    /// the details of the transport to send them down.
-    pub fn generate(self) -> Vec<(dns::Request, Box<dyn dns_transport::Transport>)> {
-        let nameservers = self.inputs.resolvers.into_iter()
-                              .map(|e| e.lookup().expect("Failed to get nameserver").expect("No nameserver found"))
-                              .collect::<Vec<_>>();
-
+    /// Iterate through the inputs matrix, returning pairs of DNS request list
+    /// and the details of the transport to send them down.
+    pub fn generate(self) -> Vec<(Vec<dns::Request>, Box<dyn dns_transport::Transport>)> {
         let mut requests = Vec::new();
         for domain in &self.inputs.domains {
             for qtype in self.inputs.types.iter().copied() {
                 for qclass in self.inputs.classes.iter().copied() {
-                    for nameserver in &nameservers {
+                    for resolver in &self.inputs.resolvers {
                         for transport_type in &self.inputs.transport_types {
 
-                            let transaction_id = self.txid_generator.generate();
                             let mut flags = dns::Flags::query();
                             self.protocol_tweaks.set_request_flags(&mut flags);
 
@@ -106,11 +101,17 @@ impl RequestGenerator {
                                 additional = Some(opt);
                             }
 
-                            let query = dns::Query { qname: domain.clone(), qtype, qclass };
-                            let request = dns::Request { transaction_id, flags, query, additional };
+                            let nameserver = resolver.nameserver();
+                            let transport = transport_type.make_transport(nameserver);
 
-                            let transport = transport_type.make_transport(nameserver.clone());
-                            requests.push((request, transport));
+                            let mut request_list = Vec::new();
+                            for qname in resolver.name_list(domain) {
+                                let transaction_id = self.txid_generator.generate();
+                                let query = dns::Query { qname, qtype, qclass };
+                                let request = dns::Request { transaction_id, flags, query, additional: additional.clone() };
+                                request_list.push(request);
+                            }
+                            requests.push((request_list, transport));
                         }
                     }
                 }

+ 62 - 27
src/resolve.rs

@@ -4,31 +4,58 @@ use std::io;
 
 use log::*;
 
+use dns::Labels;
 
-/// A **resolver** is used to obtain the IP address of the server we should
-/// send DNS requests to.
+
+/// A **resolver** knows the address of the server we should
+/// send DNS requests to, and the search list for name lookup.
 #[derive(PartialEq, Debug)]
-pub enum Resolver {
+pub struct Resolver {
 
-    /// Read the list of nameservers from the system, and use that.
-    SystemDefault,
+    /// The address of the name server.
+    pub nameserver: String,
 
-    // Use a specific nameserver specified by the user.
-    Specified(Nameserver),
+    /// The search list for name lookup.
+    pub search_list: Vec<String>,
 }
 
-pub type Nameserver = String;
-
 impl Resolver {
 
-    /// Returns a nameserver that queries should be sent to, possibly by
-    /// obtaining one based on the system, returning an error if there was a
-    /// problem looking one up.
-    pub fn lookup(self) -> io::Result<Option<Nameserver>> {
-        match self {
-            Self::Specified(ns)  => Ok(Some(ns)),
-            Self::SystemDefault  => system_nameservers(),
+    /// Returns a resolver with the specified nameserver and an empty
+    /// search list.
+    pub fn specified(nameserver: String) -> Self {
+        let search_list = Vec::new();
+        Self { nameserver, search_list }
+    }
+
+    /// Returns a resolver that is default for the system.
+    pub fn system_default() -> Self {
+        let (nameserver_opt, search_list) = system_nameservers().expect("Failed to get nameserver");
+        let nameserver = nameserver_opt.expect("No nameserver found");
+        Self { nameserver, search_list }
+    }
+
+    /// Returns a nameserver that queries should be sent to.
+    pub fn nameserver(&self) -> String {
+        self.nameserver.clone()
+    }
+
+    /// Returns a sequence of names to be queried, taking into account
+    /// of the search list.
+    pub fn name_list(&self, name: &Labels) -> Vec<Labels> {
+        let mut list = Vec::new();
+        if name.len() > 1 {
+            list.push(name.clone());
+            return list;
+        }
+        for search in &self.search_list {
+            match Labels::encode(search) {
+                Ok(suffix) => list.push(name.extend(&suffix)),
+                Err(_) => panic!("Invalid search list {}", search),
+            }
         }
+        list.push(name.clone());
+        list
     }
 }
 
@@ -38,7 +65,7 @@ impl Resolver {
 /// Returns an error if there’s a problem reading the file, or `None` if no
 /// nameserver is specified in the file.
 #[cfg(unix)]
-fn system_nameservers() -> io::Result<Option<Nameserver>> {
+fn system_nameservers() -> io::Result<(Option<String>, Vec<String>)> {
     use std::io::{BufRead, BufReader};
     use std::fs::File;
 
@@ -46,6 +73,7 @@ fn system_nameservers() -> io::Result<Option<Nameserver>> {
     let reader = BufReader::new(f);
 
     let mut nameservers = Vec::new();
+    let mut search_list = Vec::new();
     for line in reader.lines() {
         let line = line?;
 
@@ -58,43 +86,50 @@ fn system_nameservers() -> io::Result<Option<Nameserver>> {
                 Err(e)  => warn!("Failed to parse nameserver line {:?}: {}", line, e),
             }
         }
+
+        if let Some(search_str) = line.strip_prefix("search ") {
+            search_list.clear();
+            search_list.extend(search_str.split_ascii_whitespace().map(|s| s.into()));
+        }
     }
 
-    Ok(nameservers.first().cloned())
+    Ok((nameservers.first().cloned(), search_list))
 }
 
 
 /// Looks up the system default nameserver on Windows, by iterating through
 /// the list of network adapters and returning the first nameserver it finds.
 #[cfg(windows)]
-fn system_nameservers() -> io::Result<Option<Nameserver>> {
+fn system_nameservers() -> io::Result<(Option<String>, Vec<String>)> {
     let adapters = match ipconfig::get_adapters() {
         Ok(a) => a,
         Err(e) => {
             warn!("Error getting network adapters: {}", e);
-            return Ok(None);
+            return Ok((None, Vec::new()));
         }
     };
 
-    for dns_server in adapters
-        .iter()
-        .flat_map(|adapter| adapter.dns_servers().iter()) {
+    for adapter in adapters.iter().filter(|a| {
+        a.oper_status() == ipconfig::OperStatus::IfOperStatusUp && !a.gateways().is_empty()
+    }) {
+        for dns_server in adapter.dns_servers().iter() {
             // TODO: This will need to be changed for IPv6 support.
             if dns_server.is_ipv4() {
                 debug!("Found first nameserver {:?}", dns_server);
-                return Ok(Some(dns_server.to_string()))
+                return Ok((Some(dns_server.to_string()), Vec::new()));
             }
+        }
     }
 
     warn!("No nameservers available");
-    return Ok(None)
+    return Ok((None, Vec::new()))
 }
 
 
 /// The fall-back system default nameserver determinator that is not very
 /// determined as it returns nothing without actually checking anything.
 #[cfg(all(not(unix), not(windows)))]
-fn system_nameservers() -> io::Result<Option<Nameserver>> {
+fn system_nameservers() -> io::Result<(Option<String>, Vec<String>)> {
     warn!("Unable to fetch default nameservers on this platform.");
-    Ok(None)
+    Ok((None, Vec::new()))
 }

+ 19 - 15
src/table.rs

@@ -114,21 +114,25 @@ impl Table {
 
     fn coloured_record_type(&self, record: &Record) -> ANSIString<'static> {
         match *record {
-            Record::A(_)      => self.colours.a.paint("A"),
-            Record::AAAA(_)   => self.colours.aaaa.paint("AAAA"),
-            Record::CAA(_)    => self.colours.caa.paint("CAA"),
-            Record::CNAME(_)  => self.colours.cname.paint("CNAME"),
-            Record::HINFO(_)  => self.colours.hinfo.paint("HINFO"),
-            Record::LOC(_)    => self.colours.loc.paint("LOC"),
-            Record::MX(_)     => self.colours.mx.paint("MX"),
-            Record::NAPTR(_)  => self.colours.ns.paint("NAPTR"),
-            Record::NS(_)     => self.colours.ns.paint("NS"),
-            Record::PTR(_)    => self.colours.ptr.paint("PTR"),
-            Record::SSHFP(_)  => self.colours.sshfp.paint("SSHFP"),
-            Record::SOA(_)    => self.colours.soa.paint("SOA"),
-            Record::SRV(_)    => self.colours.srv.paint("SRV"),
-            Record::TLSA(_)   => self.colours.tlsa.paint("TLSA"),
-            Record::TXT(_)    => self.colours.txt.paint("TXT"),
+            Record::A(_)           => self.colours.a.paint("A"),
+            Record::AAAA(_)        => self.colours.aaaa.paint("AAAA"),
+            Record::CAA(_)         => self.colours.caa.paint("CAA"),
+            Record::CNAME(_)       => self.colours.cname.paint("CNAME"),
+            Record::EUI48(_)       => self.colours.eui48.paint("EUI48"),
+            Record::EUI64(_)       => self.colours.eui64.paint("EUI64"),
+            Record::HINFO(_)       => self.colours.hinfo.paint("HINFO"),
+            Record::LOC(_)         => self.colours.loc.paint("LOC"),
+            Record::MX(_)          => self.colours.mx.paint("MX"),
+            Record::NAPTR(_)       => self.colours.ns.paint("NAPTR"),
+            Record::NS(_)          => self.colours.ns.paint("NS"),
+            Record::OPENPGPKEY(_)  => self.colours.openpgpkey.paint("OPENPGPKEY"),
+            Record::PTR(_)         => self.colours.ptr.paint("PTR"),
+            Record::SSHFP(_)       => self.colours.sshfp.paint("SSHFP"),
+            Record::SOA(_)         => self.colours.soa.paint("SOA"),
+            Record::SRV(_)         => self.colours.srv.paint("SRV"),
+            Record::TLSA(_)        => self.colours.tlsa.paint("TLSA"),
+            Record::TXT(_)         => self.colours.txt.paint("TXT"),
+            Record::URI(_)         => self.colours.uri.paint("URI"),
 
             Record::Other { ref type_number, .. } => self.colours.unknown.paint(type_number.to_string()),
         }

+ 25 - 7
xtests/README.md

@@ -1,16 +1,34 @@
 # dog › xtests
 
-This is dog’s extended test suite. It gets run using [Specsheet]. They run a complete end-to-end set of tests, covering network connections, DNS protocol parsing, command-line options, and error handling.
+This is dog’s extended test suite. The checks herein form a complete end-to-end set of tests, covering things like network connections, DNS protocol parsing, command-line options, error handling, and edge case behaviour.
 
-For completeness, this makes connections over the network. This means that the outcome of some of the tests is dependent on your own machine’s connectivity! It also means that your own IP address will be recorded as making the requests.
+The checks are written as [Specsheet] documents, which you’ll need to have installed.
 
-The tests have the following set of Specsheet tags:
+Because these tests make connections over the network, the outcome of the test suite will depend on your own machine‘s Internet connection! It also means that your own IP address will be recorded as making the requests.
+
+
+### Test hierarchy
+
+The tests have been divided into three sections:
+
+1. **live**, which uses both your computer’s default resolver and the [public Cloudflare DNS resolver] to access records that have been created using a public-facing DNS host. This checks that dog works using whatever software is on the Internet right now. Because these are _live_ records, the output will vary as things like the TTL vary, so we cannot assert on the _exact_ output; nevertheless, it’s a good check to see if the basic functionality is working.
+2. **madns**, which sends requests to the [madns resolver]. This resolver has been pre-programmed with deliberately incorrect responses to see how dog handles edge cases in the DNS specification. These are not live records, so things like the TTLs of the responses are fixed, meaning the output should never change over time; however, it does not mean dog will hold up against the network infrastructure of the real world.
+3. **options**, which runs dog using various command-line options and checks that the correct output is returned. These tests should not make network requests when behaving correctly.
+
+All three categories of check are needed to ensure dog is working correctly.
+
+
+### Tags
+
+To run a subset of the checks, you can filter with the following tags:
 
-- `live`: All tests that use the network.
-- `isp`: Tests that use your computer’s default resolver.
-- `google`: Tests that use the [public Google DNS resolver].
 - `cloudflare`: Tests that use the [public Cloudflare DNS resolver].
+- `isp`: Tests that use your computer’s default resolver.
+- `madns`: Tests that use the [madns resolver].
+- `options`: Tests that check the command-line options.
+
+You can also use a DNS record type as a tag to only run the checks for that particular type.
 
 [Specsheet]: https://specsheet.software/
-[public Google DNS resolver]: https://developers.google.com/speed/public-dns
 [public Cloudflare DNS resolver]: https://developers.cloudflare.com/1.1.1.1/
+[madns resolver]: https://madns.binarystar.systems/

+ 0 - 0
xtests/badssl.toml → xtests/live/badssl.toml


+ 0 - 0
xtests/basics.toml → xtests/live/basics.toml


+ 0 - 0
xtests/bins.toml → xtests/live/bins.toml


+ 0 - 0
xtests/https.toml → xtests/live/https.toml


+ 0 - 0
xtests/json.toml → xtests/live/json.toml


+ 0 - 0
xtests/tcp.toml → xtests/live/tcp.toml


+ 0 - 0
xtests/tls.toml → xtests/live/tls.toml


+ 0 - 0
xtests/udp.toml → xtests/live/udp.toml


+ 44 - 0
xtests/madns/a-records.toml

@@ -0,0 +1,44 @@
+# A record successes
+
+[[cmd]]
+name = "Running with ‘a.example’ prints the correct A record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A a.example"
+stdout = { file = "outputs/a.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "a", "madns" ]
+
+
+# A record invalid packets
+
+[[cmd]]
+name = "Running with ‘too-long.a.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A too-long.a.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 4, got 5" }
+status = 1
+tags = [ "a", "madns" ]
+
+[[cmd]]
+name = "Running with ‘too-short.a.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A too-short.a.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 4, got 3" }
+status = 1
+tags = [ "a", "madns" ]
+
+[[cmd]]
+name = "Running with ‘empty.a.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A empty.a.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 4, got 0" }
+status = 1
+tags = [ "a", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.a.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A incomplete.a.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "a", "madns" ]

+ 44 - 0
xtests/madns/aaaa-records.toml

@@ -0,0 +1,44 @@
+# AAAA record successes
+
+[[cmd]]
+name = "Running with ‘aaaa.example’ prints the correct AAAA record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 AAAA aaaa.example"
+stdout = { file = "outputs/aaaa.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "aaaa", "madns" ]
+
+
+# AAAA record invalid packets
+
+[[cmd]]
+name = "Running with ‘too-long.aaaa.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 AAAA too-long.aaaa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 16, got 17" }
+status = 1
+tags = [ "aaaa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘too-short.aaaa.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 AAAA too-short.aaaa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 16, got 8" }
+status = 1
+tags = [ "aaaa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘empty.aaaa.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 AAAA empty.aaaa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 16, got 0" }
+status = 1
+tags = [ "aaaa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.aaaa.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 AAAA incomplete.aaaa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "aaaa", "madns" ]

+ 44 - 0
xtests/madns/caa-records.toml

@@ -0,0 +1,44 @@
+# CAA record successes
+
+[[cmd]]
+name = "Running with ‘caa.example’ prints the correct CAA record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CAA caa.example"
+stdout = { file = "outputs/caa.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "caa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘critical.caa.example’ prints the correct CAA record with the flag"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CAA critical.caa.example"
+stdout = { file = "outputs/critical.caa.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "caa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘others.caa.example’ prints the correct CAA record and ignores the flags"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CAA others.caa.example"
+stdout = { file = "outputs/others.caa.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "caa", "madns" ]
+
+
+# CAA record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.caa.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CAA empty.caa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "caa", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.caa.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CAA incomplete.caa.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "caa", "madns" ]

+ 28 - 0
xtests/madns/cname-records.toml

@@ -0,0 +1,28 @@
+# CNAME record successes
+
+[[cmd]]
+name = "Running with ‘cname.example’ prints the correct CNAME record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CNAME cname.example"
+stdout = { file = "outputs/cname.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "cname", "madns" ]
+
+
+# CNAME record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.cname.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CNAME empty.cname.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "cname", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.cname.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 CNAME incomplete.cname.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "cname", "madns" ]

+ 44 - 0
xtests/madns/eui48-records.toml

@@ -0,0 +1,44 @@
+# EUI48 record successes
+
+[[cmd]]
+name = "Running with ‘eui48.example’ prints the correct EUI48 record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI48 eui48.example"
+stdout = { file = "outputs/eui48.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "eui48", "madns" ]
+
+
+# EUI48 record invalid packets
+
+[[cmd]]
+name = "Running with ‘too-long.eui48.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI48 too-long.eui48.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 6, got 7" }
+status = 1
+tags = [ "eui48", "madns" ]
+
+[[cmd]]
+name = "Running with ‘too-short.eui48.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI48 too-short.eui48.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 6, got 5" }
+status = 1
+tags = [ "eui48", "madns" ]
+
+[[cmd]]
+name = "Running with ‘empty.eui48.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI48 empty.eui48.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 6, got 0" }
+status = 1
+tags = [ "eui48", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.eui48.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI48 incomplete.eui48.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "eui48", "madns" ]

+ 44 - 0
xtests/madns/eui64-records.toml

@@ -0,0 +1,44 @@
+# EUI64 record successes
+
+[[cmd]]
+name = "Running with ‘eui64.example’ prints the correct EUI64 record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI64 eui64.example"
+stdout = { file = "outputs/eui64.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "eui64", "madns" ]
+
+
+# EUI64 record invalid packets
+
+[[cmd]]
+name = "Running with ‘too-long.eui64.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI64 too-long.eui64.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 8, got 9" }
+status = 1
+tags = [ "eui64", "madns" ]
+
+[[cmd]]
+name = "Running with ‘too-short.eui64.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI64 too-short.eui64.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 8, got 7" }
+status = 1
+tags = [ "eui64", "madns" ]
+
+[[cmd]]
+name = "Running with ‘empty.eui64.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI64 empty.eui64.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be 8, got 0" }
+status = 1
+tags = [ "eui64", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.eui64.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 EUI64 incomplete.eui64.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "eui64", "madns" ]

+ 28 - 0
xtests/madns/hinfo-records.toml

@@ -0,0 +1,28 @@
+# HINFO record successes
+
+[[cmd]]
+name = "Running with ‘hinfo.example’ prints the correct HINFO record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 HINFO hinfo.example"
+stdout = { file = "outputs/hinfo.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "hinfo", "madns" ]
+
+
+# HINFO record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.hinfo.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 HINFO empty.hinfo.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "hinfo", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.hinfo.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 HINFO incomplete.hinfo.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "hinfo", "madns" ]

+ 90 - 0
xtests/madns/loc-records.toml

@@ -0,0 +1,90 @@
+# LOC record successes
+
+[[cmd]]
+name = "Running with ‘loc.example’ prints the correct LOC record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC loc.example"
+stdout = { file = "outputs/loc.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "loc", "madns" ]
+
+
+# LOC record out-of-range positions
+
+[[cmd]]
+name = "Running with ‘far-negative-longitude.loc.invalid’ displays a record with an out-of-range field"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC far-negative-longitude.loc.invalid"
+stdout = { file = "outputs/far-negative-longitude.loc.invalid.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘far-positive-longitude.loc.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC far-positive-longitude.loc.invalid"
+stdout = { file = "outputs/far-positive-longitude.loc.invalid.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘far-negative-latitude.loc.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC far-negative-latitude.loc.invalid"
+stdout = { file = "outputs/far-negative-latitude.loc.invalid.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘far-positive-latitude.loc.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC far-positive-latitude.loc.invalid"
+stdout = { file = "outputs/far-positive-latitude.loc.invalid.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "loc", "madns" ]
+
+
+# LOC record version 1
+
+[[cmd]]
+name = "Running with ‘v1-conform.loc.invalid’ displays a version error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC v1-conform.loc.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record specifies version 1, expected up to 0" }
+status = 1
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘v1-nonconform.loc.invalid’ displays a version error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC v1-nonconform.loc.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record specifies version 1, expected up to 0" }
+status = 1
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘v1-empty.loc.invalid’ displays a version error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC v1-empty.loc.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record specifies version 1, expected up to 0" }
+status = 1
+tags = [ "loc", "madns" ]
+
+
+# LOC record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.loc.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC empty.loc.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "loc", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.loc.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 LOC incomplete.loc.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "loc", "madns" ]

+ 28 - 0
xtests/madns/mx-records.toml

@@ -0,0 +1,28 @@
+# MX record successes
+
+[[cmd]]
+name = "Running with ‘mx.example’ prints the correct MX record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 MX mx.example"
+stdout = { file = "outputs/mx.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "mx", "madns" ]
+
+
+# MX record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.mx.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 MX empty.mx.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "mx", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.mx.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 MX incomplete.mx.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "mx", "madns" ]

+ 36 - 0
xtests/madns/naptr-records.toml

@@ -0,0 +1,36 @@
+# NAPTR record successes
+
+[[cmd]]
+name = "Running with ‘naptr.example’ prints the correct NAPTR record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NAPTR naptr.example"
+stdout = { file = "outputs/naptr.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "naptr", "madns" ]
+
+[[cmd]]
+name = "Running with ‘bad-regex.naptr.example’ still prints the correct NAPTR record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NAPTR bad-regex.naptr.example"
+stdout = { file = "outputs/bad-regex.naptr.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "naptr", "madns" ]
+
+
+# NAPTR record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.naptr.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NAPTR empty.naptr.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "naptr", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.naptr.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NAPTR incomplete.naptr.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "naptr", "madns" ]

+ 28 - 0
xtests/madns/ns-records.toml

@@ -0,0 +1,28 @@
+# NS record successes
+
+[[cmd]]
+name = "Running with ‘ns.example’ prints the correct NS record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NS ns.example"
+stdout = { file = "outputs/ns.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "ns", "madns" ]
+
+
+# NS record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.ns.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NS empty.ns.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "ns", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.ns.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 NS incomplete.ns.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "ns", "madns" ]

+ 28 - 0
xtests/madns/openpgpkey-records.toml

@@ -0,0 +1,28 @@
+# OPENPGPKEY record successes
+
+[[cmd]]
+name = "Running with ‘openpgpkey.example’ prints the correct OPENPGPKEY record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 OPENPGPKEY openpgpkey.example"
+stdout = { file = "outputs/openpgpkey.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "openpgpkey", "madns" ]
+
+
+# OPENPGPKEY record invalid packets
+
+[[cmd]]
+name = "Running with ‘empty.openpgpkey.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 OPENPGPKEY empty.openpgpkey.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: record length should be at least 1, got 0" }
+status = 1
+tags = [ "openpgpkey", "madns" ]
+
+[[cmd]]
+name = "Running with ‘incomplete.openpgpkey.invalid’ displays a protocol error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 OPENPGPKEY incomplete.openpgpkey.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "openpgpkey", "madns" ]

+ 44 - 0
xtests/madns/opt-records.toml

@@ -0,0 +1,44 @@
+# OPT record successes
+
+[[cmd]]
+name = "Running with ‘opt.example’ prints the correct OPT record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A opt.example --edns=show"
+stdout = { file = "outputs/opt.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "opt", "madns" ]
+
+[[cmd]]
+name = "Running with ‘do-flag.opt.example’ prints the correct OPT record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A do-flag.opt.example --edns=show"
+stdout = { file = "outputs/do-flag.opt.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "opt", "madns" ]
+
+[[cmd]]
+name = "Running with ‘other-flags.opt.example’ prints the correct OPT record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A other-flags.opt.example --edns=show"
+stdout = { file = "outputs/other-flags.opt.example.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "opt", "madns" ]
+
+[[cmd]]
+name = "Running with ‘named.opt.invalid’ prints the correct OPT record"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A named.opt.invalid --edns=show"
+stdout = { file = "outputs/named.opt.invalid.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ "opt", "madns" ]
+
+
+# OPT record invalid packets
+
+[[cmd]]
+name = "Running with ‘incomplete.opt.invalid’ displays a record length error"
+shell = "dog --colour=always --tcp @madns.binarystar.systems:5301 A incomplete.opt.invalid"
+stdout = { empty = true }
+stderr = { string = "Error [protocol]: Malformed packet: insufficient data" }
+status = 1
+tags = [ "opt", "madns" ]

+ 1 - 0
xtests/madns/outputs/a.example.ansitxt

@@ -0,0 +1 @@
+A a.example. 10m00s   127.0.0.1

+ 1 - 0
xtests/madns/outputs/aaaa.example.ansitxt

@@ -0,0 +1 @@
+AAAA aaaa.example. 10m00s   ::1

+ 1 - 0
xtests/madns/outputs/ansi.str.example.ansitxt

@@ -0,0 +1 @@
+CNAME ansi.str.example. 10m00s   "\u{1b}[32mgreen.\u{1b}[34mblue.\u{1b}[31mred.\u{1b}[0m."

+ 1 - 0
xtests/madns/outputs/bad-regex.naptr.example.ansitxt

@@ -0,0 +1 @@
+NAPTR bad-regex.naptr.example. 10m00s   5 10 s "SRV" /(((((((((((((((((((((((((/ "srv.example."

+ 1 - 0
xtests/madns/outputs/caa.example.ansitxt

@@ -0,0 +1 @@
+CAA caa.example. 10m00s   "issuewild" "trustworthy.example" (non-critical)

+ 1 - 0
xtests/madns/outputs/cname.example.ansitxt

@@ -0,0 +1 @@
+CNAME cname.example. 10m00s   "dns.lookup.dog."

+ 1 - 0
xtests/madns/outputs/critical.caa.example.ansitxt

@@ -0,0 +1 @@
+CAA critical.caa.example. 10m00s   "issuewild" "trustworthy.example" (critical)

+ 2 - 0
xtests/madns/outputs/do-flag.opt.example.ansitxt

@@ -0,0 +1,2 @@
+  A do-flag.opt.example. 10m00s   127.0.0.1
+OPT                             + 1452 0 0 32768 []

+ 1 - 0
xtests/madns/outputs/eui48.example.ansitxt

@@ -0,0 +1 @@
+EUI48 eui48.example. 10m00s   "12-34-56-78-90-ab"

+ 1 - 0
xtests/madns/outputs/eui64.example.ansitxt

@@ -0,0 +1 @@
+EUI64 eui64.example. 10m00s   "12-34-56-ff-fe-78-90-ab"

+ 1 - 0
xtests/madns/outputs/far-negative-latitude.loc.invalid.ansitxt

@@ -0,0 +1 @@
+LOC far-negative-latitude.loc.invalid. 10m00s   3e2 (0, 0) (Out of range, 0°0′0″ E, 0m)

+ 1 - 0
xtests/madns/outputs/far-negative-longitude.loc.invalid.ansitxt

@@ -0,0 +1 @@
+LOC far-negative-longitude.loc.invalid. 10m00s   3e2 (0, 0) (0°0′0″ N, Out of range, 0m)

+ 1 - 0
xtests/madns/outputs/far-positive-latitude.loc.invalid.ansitxt

@@ -0,0 +1 @@
+LOC far-positive-latitude.loc.invalid. 10m00s   3e2 (0, 0) (Out of range, 0°0′0″ E, 0m)

+ 1 - 0
xtests/madns/outputs/far-positive-longitude.loc.invalid.ansitxt

@@ -0,0 +1 @@
+LOC far-positive-longitude.loc.invalid. 10m00s   3e2 (0, 0) (0°0′0″ N, Out of range, 0m)

+ 1 - 0
xtests/madns/outputs/hinfo.example.ansitxt

@@ -0,0 +1 @@
+HINFO hinfo.example. 10m00s   "some-kinda-cpu" "some-kinda-os"

+ 1 - 0
xtests/madns/outputs/loc.example.ansitxt

@@ -0,0 +1 @@
+LOC loc.example. 10m00s   3e2 (0, 0) (51°30′12.748″ N, 0°7′39.611″ W, 0m)

+ 1 - 0
xtests/madns/outputs/mx.example.ansitxt

@@ -0,0 +1 @@
+MX mx.example. 10m00s   10 "exchange.example."

+ 2 - 0
xtests/madns/outputs/named.opt.invalid.ansitxt

@@ -0,0 +1,2 @@
+  A named.opt.invalid.           10m00s   127.0.0.1
+OPT bingle.bongle.dingle.dangle.        + 1452 0 0 0 []

+ 1 - 0
xtests/madns/outputs/naptr.example.ansitxt

@@ -0,0 +1 @@
+NAPTR naptr.example. 10m00s   5 10 s "SRV" /\d\d:\d\d:\d\d/ "srv.example."

+ 1 - 0
xtests/madns/outputs/newline.str.example.ansitxt

@@ -0,0 +1 @@
+CNAME newline.str.example. 10m00s   "some\nnew\r\nlines\n.example."

+ 1 - 0
xtests/madns/outputs/ns.example.ansitxt

@@ -0,0 +1 @@
+NS ns.example. 10m00s   "a.gtld-servers.net."

+ 1 - 0
xtests/madns/outputs/null.str.example.ansitxt

@@ -0,0 +1 @@
+CNAME null.str.example. 10m00s   "some\u{0}null\u{0}\u{0}chars\u{0}.example."

+ 1 - 0
xtests/madns/outputs/openpgpkey.example.ansitxt

@@ -0,0 +1 @@
+OPENPGPKEY openpgpkey.example. 10m00s   "EjRWeA=="

+ 2 - 0
xtests/madns/outputs/opt.example.ansitxt

@@ -0,0 +1,2 @@
+  A opt.example. 10m00s   127.0.0.1
+OPT                     + 1452 0 0 0 []

+ 2 - 0
xtests/madns/outputs/other-flags.opt.example.ansitxt

@@ -0,0 +1,2 @@
+  A other-flags.opt.example. 10m00s   127.0.0.1
+OPT                                 + 1452 0 0 32767 []

+ 1 - 0
xtests/madns/outputs/others.caa.example.ansitxt

@@ -0,0 +1 @@
+CAA caa.example. 10m00s   "issuewild" "trustworthy.example" (non-critical)

+ 1 - 0
xtests/madns/outputs/ptr.example.ansitxt

@@ -0,0 +1 @@
+PTR ptr.example. 10m00s   "dns.example."

+ 1 - 0
xtests/madns/outputs/slash.uri.example.ansitxt

@@ -0,0 +1 @@
+URI slash.uri.example. 10m00s   10 1 "/"

+ 1 - 0
xtests/madns/outputs/soa.example.ansitxt

@@ -0,0 +1 @@
+SOA soa.example. 10m00s   "mname.example." "rname.example." 1564274434 1d0h00m00s 2h00m00s 7d0h00m00s 5m00s

+ 1 - 0
xtests/madns/outputs/srv.example.ansitxt

@@ -0,0 +1 @@
+SRV srv.example. 10m00s   1 1 "service.example.":37500

+ 1 - 0
xtests/madns/outputs/sshfp.example.ansitxt

@@ -0,0 +1 @@
+SSHFP sshfp.example. 10m00s   1 1 212223242526

Some files were not shown because too many files changed in this diff