Repository: ogham/dog Branch: master Commit: 721440b12ef0 Files: 208 Total size: 410.9 KB Directory structure: gitextract_m0bcnqsg/ ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ ├── compilation_error.md │ ├── config.yml │ ├── feature_request.md │ └── question.md ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── Cargo.toml ├── Dockerfile ├── Justfile ├── LICENCE ├── README.md ├── build.rs ├── completions/ │ ├── dog.bash │ ├── dog.fish │ ├── dog.ps1 │ └── dog.zsh ├── dns/ │ ├── Cargo.toml │ ├── fuzz/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── fuzz_targets/ │ │ └── fuzz_parsing.rs │ ├── src/ │ │ ├── lib.rs │ │ ├── record/ │ │ │ ├── a.rs │ │ │ ├── aaaa.rs │ │ │ ├── caa.rs │ │ │ ├── cname.rs │ │ │ ├── eui48.rs │ │ │ ├── eui64.rs │ │ │ ├── hinfo.rs │ │ │ ├── loc.rs │ │ │ ├── mod.rs │ │ │ ├── mx.rs │ │ │ ├── naptr.rs │ │ │ ├── ns.rs │ │ │ ├── openpgpkey.rs │ │ │ ├── opt.rs │ │ │ ├── others.rs │ │ │ ├── ptr.rs │ │ │ ├── soa.rs │ │ │ ├── srv.rs │ │ │ ├── sshfp.rs │ │ │ ├── tlsa.rs │ │ │ ├── txt.rs │ │ │ └── uri.rs │ │ ├── strings.rs │ │ ├── types.rs │ │ └── wire.rs │ └── tests/ │ ├── wire_building_tests.rs │ └── wire_parsing_tests.rs ├── dns-transport/ │ ├── Cargo.toml │ └── src/ │ ├── auto.rs │ ├── error.rs │ ├── https.rs │ ├── lib.rs │ ├── tcp.rs │ ├── tls.rs │ ├── tls_stream.rs │ └── udp.rs ├── man/ │ └── dog.1.md ├── src/ │ ├── colours.rs │ ├── connect.rs │ ├── hints.rs │ ├── logger.rs │ ├── main.rs │ ├── options.rs │ ├── output.rs │ ├── requests.rs │ ├── resolve.rs │ ├── table.rs │ ├── txid.rs │ └── usage.txt └── xtests/ ├── README.md ├── features/ │ ├── none.toml │ └── outputs/ │ ├── disabled_https.txt │ └── disabled_tls.txt ├── live/ │ ├── badssl.toml │ ├── basics.toml │ ├── bins.toml │ ├── https.toml │ ├── json.toml │ ├── tcp.toml │ ├── tls.toml │ └── udp.toml ├── madns/ │ ├── a-records.toml │ ├── aaaa-records.toml │ ├── caa-records.toml │ ├── cname-records.toml │ ├── eui48-records.toml │ ├── eui64-records.toml │ ├── hinfo-records.toml │ ├── loc-records.toml │ ├── mx-records.toml │ ├── naptr-records.toml │ ├── ns-records.toml │ ├── openpgpkey-records.toml │ ├── opt-records.toml │ ├── outputs/ │ │ ├── a.example.ansitxt │ │ ├── a.example.json │ │ ├── aaaa.example.ansitxt │ │ ├── aaaa.example.json │ │ ├── ansi.str.example.ansitxt │ │ ├── ansi.str.example.json │ │ ├── bad-regex.naptr.example.ansitxt │ │ ├── bad-utf8.caa.example.ansitxt │ │ ├── bad-utf8.caa.example.json │ │ ├── bad-utf8.hinfo.example.ansitxt │ │ ├── bad-utf8.hinfo.example.json │ │ ├── bad-utf8.naptr.invalid.ansitxt │ │ ├── bad-utf8.naptr.invalid.json │ │ ├── bad-utf8.txt.example.ansitxt │ │ ├── bad-utf8.txt.example.json │ │ ├── bad-utf8.uri.example.ansitxt │ │ ├── bad-utf8.uri.example.json │ │ ├── caa.example.ansitxt │ │ ├── caa.example.json │ │ ├── cname.example.ansitxt │ │ ├── cname.example.json │ │ ├── critical.caa.example.ansitxt │ │ ├── critical.caa.example.json │ │ ├── do-flag.opt.example.ansitxt │ │ ├── do-flag.opt.example.json │ │ ├── eui48.example.ansitxt │ │ ├── eui48.example.json │ │ ├── eui64.example.ansitxt │ │ ├── eui64.example.json │ │ ├── far-negative-latitude.loc.invalid.ansitxt │ │ ├── far-negative-latitude.loc.invalid.json │ │ ├── far-negative-longitude.loc.invalid.ansitxt │ │ ├── far-negative-longitude.loc.invalid.json │ │ ├── far-positive-latitude.loc.invalid.ansitxt │ │ ├── far-positive-latitude.loc.invalid.json │ │ ├── far-positive-longitude.loc.invalid.ansitxt │ │ ├── far-positive-longitude.loc.invalid.json │ │ ├── hinfo.example.ansitxt │ │ ├── hinfo.example.json │ │ ├── loc.example.ansitxt │ │ ├── loc.example.json │ │ ├── mx.example.ansitxt │ │ ├── mx.example.json │ │ ├── named.opt.invalid.ansitxt │ │ ├── named.opt.invalid.json │ │ ├── naptr.example.ansitxt │ │ ├── naptr.example.json │ │ ├── newline.str.example.ansitxt │ │ ├── newline.str.example.json │ │ ├── ns.example.ansitxt │ │ ├── ns.example.json │ │ ├── null.str.example.ansitxt │ │ ├── null.str.example.json │ │ ├── openpgpkey.example.ansitxt │ │ ├── openpgpkey.example.json │ │ ├── opt.example.ansitxt │ │ ├── opt.example.json │ │ ├── other-flags.opt.example.ansitxt │ │ ├── other-flags.opt.example.json │ │ ├── others.caa.example.ansitxt │ │ ├── others.caa.example.json │ │ ├── ptr.example.ansitxt │ │ ├── ptr.example.json │ │ ├── slash.uri.example.ansitxt │ │ ├── soa.example.ansitxt │ │ ├── soa.example.json │ │ ├── srv.example.ansitxt │ │ ├── srv.example.json │ │ ├── sshfp.example.ansitxt │ │ ├── sshfp.example.json │ │ ├── tab.str.example.ansitxt │ │ ├── tab.str.example.json │ │ ├── tlsa.example.ansitxt │ │ ├── tlsa.example.json │ │ ├── txt.example.ansitxt │ │ ├── txt.example.json │ │ ├── upperbit.str.example.ansitxt │ │ ├── upperbit.str.example.json │ │ ├── uri.example.ansitxt │ │ ├── uri.example.json │ │ ├── utf8.caa.example.ansitxt │ │ ├── utf8.caa.example.json │ │ ├── utf8.hinfo.example.ansitxt │ │ ├── utf8.hinfo.example.json │ │ ├── utf8.naptr.invalid.ansitxt │ │ ├── utf8.naptr.invalid.json │ │ ├── utf8.txt.example.ansitxt │ │ ├── utf8.txt.example.json │ │ ├── utf8.uri.example.ansitxt │ │ └── utf8.uri.example.json │ ├── protocol-chars.toml │ ├── protocol-compression.toml │ ├── protocol-error-codes.toml │ ├── ptr-records.toml │ ├── soa-records.toml │ ├── srv-records.toml │ ├── sshfp-records.toml │ ├── tlsa-records.toml │ ├── txt-records.toml │ └── uri-records.toml └── options/ ├── errors.toml ├── help.toml └── outputs/ ├── huge-domain.txt ├── invalid-argument.txt ├── invalid-protocol-tweak.txt ├── invalid-query-class.txt ├── invalid-query-type.txt ├── missing-nameserver.txt ├── missing-parameter.txt └── opt-query.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: ogham ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a crash, runtime error, or invalid output in dog --- If dog does something unexpected, or displays an error on the screen, or if it outright crashes, then please include the following information in your report: - The version of dog being used (`dog --version`) - The command-line arguments you are using - Your operating system and hardware platform If it’s a crash, please include the full text of the crash that gets printed to the screen. If you’re seeing unexpected behaviour, a screenshot of the issue will help a lot. --- ================================================ FILE: .github/ISSUE_TEMPLATE/compilation_error.md ================================================ --- name: Compilation error about: Report a problem compiling dog --- If dog fails to compile, or if there is a problem during the build process, then please include the following information in your report: - The exact dog commit you are building (`git rev-parse --short HEAD`) - The version of rustc you are compiling it with (`rustc --version`) - Your operating system and hardware platform - The Rust build target (the _exact_ output of `rustc --print cfg`) If you are seeing compilation errors, please include the output of the build process. --- ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Request a feature or enhancement to dog --- ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Ask a question about dog --- ================================================ FILE: .gitignore ================================================ /target /tarpaulin-report.html fuzz-*.log /cargo-timing*.html ================================================ FILE: .rustfmt.toml ================================================ disable_all_formatting = true ================================================ FILE: .travis.yml ================================================ language: rust rust: - 1.45.0 - stable - beta - nightly script: - cargo build --verbose --workspace - cargo test --verbose --workspace --no-run - cargo test --verbose --workspace os: - windows - linux - osx jobs: fast_finish: true allow_failures: - rust: nightly include: - name: 'Rust: lint with Clippy' rust: stable install: - rustup component add clippy script: - cargo clippy - name: 'Rust: mutation testing' rust: nightly install: - git clone https://github.com/llogiq/mutagen.git - cd mutagen/mutagen-runner - cargo install --path . - cd ../.. script: - cargo test --package dns --features=dns/with_mutagen -- --quiet - cargo mutagen --package dns --features=dns/with_mutagen ================================================ FILE: Cargo.toml ================================================ [package] name = "dog" description = "A command-line DNS client" authors = ["Benjamin Sago "] categories = ["command-line-utilities"] edition = "2018" exclude = [ "/completions/*", "/man/*", "/xtests/*", "/dog-screenshot.png", "/Justfile", "/README.md", "/.rustfmt.toml", "/.travis.yml", ] homepage = "https://dns.lookup.dog/" license = "EUPL-1.2" version = "0.2.0-pre" [[bin]] name = "dog" path = "src/main.rs" doctest = false [workspace] members = [ "dns", "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 dns = { path = "./dns" } dns-transport = { path = "./dns-transport" } # command-line ansi_term = "0.12" atty = "0.2" getopts = "0.2" # transaction ID generation rand = "0.8" # json output json = "0.12" # logging log = "0.4" # windows default nameserver determination [target.'cfg(windows)'.dependencies] ipconfig = { version = "0.2" } [build-dependencies] datetime = { version = "0.5.1", default_features = false } [dev-dependencies] pretty_assertions = "0.7" [features] default = ["with_idna", "with_tls", "with_https", "with_nativetls"] with_idna = ["dns/with_idna"] with_tls = ["dns-transport/with_tls"] with_https = ["dns-transport/with_https"] with_nativetls = ["dns-transport/with_nativetls"] with_nativetls_vendored = ["with_nativetls", "dns-transport/with_nativetls", "dns-transport/with_nativetls_vendored"] with_rustls = ["dns-transport/with_rustls"] ================================================ FILE: Dockerfile ================================================ FROM rust as build WORKDIR /build COPY /src /build/src COPY /dns /build/dns COPY /dns-transport /build/dns-transport COPY /man /build/man COPY build.rs Cargo.toml /build/ RUN cargo build --release FROM debian:buster-slim RUN apt update && apt install -y libssl1.1 ca-certificates && apt clean all COPY --from=build /build/target/release/dog /dog ENTRYPOINT ["/dog"] ================================================ FILE: Justfile ================================================ all: build test xtests all-release: build-release test-release xtests-release all-quick: build-quick test-quick xtests-quick export DOG_DEBUG := "" #----------# # building # #----------# # compile the dog binary @build: cargo build # compile the dog binary (in release mode) @build-release: cargo build --release --verbose strip "${CARGO_TARGET_DIR:-target}/release/dog" # 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 # check that the dog binary can compile @check: cargo check #---------------# # running tests # #---------------# # run unit tests @test: cargo test --workspace -- --quiet # run unit tests (in release mode) @test-release: cargo test --workspace --release --verbose # run unit tests (without some features) @test-quick: cargo test --workspace --no-default-features -- --quiet # run mutation tests @test-mutation: cargo +nightly test --package dns --features=dns/with_mutagen -- --quiet cargo +nightly mutagen --package dns --features=dns/with_mutagen #------------------------# # running extended tests # #------------------------# # run extended tests @xtests *args: specsheet xtests/{options,live,madns}/*.toml -shide {{args}} \ -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/debug/dog" # run extended tests (in release mode) @xtests-release *args: specsheet xtests/{options,live,madns}/*.toml {{args}} \ -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/release/dog" # run extended tests (omitting certain feature tests) @xtests-quick *args: specsheet xtests/options/*.toml xtests/live/{basics,tcp}.toml -shide {{args}} \ -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/debug/dog" # run extended tests against a local madns instance @xtests-madns-local *args: env MADNS_ARGS="@localhost:5301 --tcp" \ specsheet xtests/madns/*.toml -shide {{args}} \ -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 #---------# # 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 # 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 # remove fuzz log files @fuzz-clean: rm dns/fuzz/fuzz-*.log #-----------------------# # code quality and misc # #-----------------------# # lint the code @clippy: touch dns/src/lib.rs cargo clippy # 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 # 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 # list unused dependencies @unused-deps: command -v cargo-udeps >/dev/null || (echo "cargo-udeps not installed" && exit 1) cargo +nightly udeps # builds dog and runs extended tests with features disabled @feature-checks *args: cargo build --no-default-features specsheet xtests/features/none.toml -shide {{args}} \ -O cmd.target.dog="${CARGO_TARGET_DIR:-../../target}/debug/dog" # print versions of the necessary build tools @versions: rustc --version cargo --version #---------------# # documentation # #---------------# # render the documentation @doc: cargo doc --no-deps --workspace # 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" # build and preview the man page @man-preview: man man "${CARGO_TARGET_DIR:-target}/man/dog.1" #-----------# # packaging # #-----------# # create a distributable package zip desc exe="dog": #!/usr/bin/env perl use Archive::Zip; -e 'target/release/{{ exe }}' || die 'Binary not built!'; -e 'target/man/dog.1' || die 'Man page not built!'; my $zip = Archive::Zip->new(); $zip->addFile('completions/dog.bash'); $zip->addFile('completions/dog.zsh'); $zip->addFile('completions/dog.fish'); $zip->addFile('target/man/dog.1', 'man/dog.1'); $zip->addFile('target/release/{{ exe }}', 'bin/{{ exe }}'); $zip->writeToFileNamed('dog-{{ desc }}.zip') == AZ_OK || die 'Zip write error!'; system 'unzip -l "dog-{{ desc }}".zip' ================================================ FILE: LICENCE ================================================ EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: - ‘The Licence’: this Licence. - ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. - ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. - ‘The Work’: the Original Work or its Derivative Works. - ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. - ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. - ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. - ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: - use the Work in any circumstance and for all usage, - reproduce the Work, - modify the Work, and make Derivative Works based upon the Work, - communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, - distribute the Work or copies thereof, - lend and rent the Work or copies thereof, - sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, - any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, - any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, - this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, - this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: - GNU General Public License (GPL) v. 2, v. 3 - GNU Affero General Public License (AGPL) v. 3 - Open Software License (OSL) v. 2.1, v. 3.0 - Eclipse Public License (EPL) v. 1.0 - CeCILL v. 2.0, v. 2.1 - Mozilla Public Licence (MPL) v. 2 - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software - European Union Public Licence (EUPL) v. 1.1, v. 1.2 - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. ================================================ FILE: README.md ================================================

dog

[dog](https://dns.lookup.dog/) is a command-line DNS client. Build status Say thanks!
![A screenshot of dog making a DNS request](dog-screenshot.png) --- Dogs _can_ look up! **dog** is a command-line DNS client, like `dig`. It has colourful output, understands normal command-line argument syntax, supports the DNS-over-TLS and DNS-over-HTTPS protocols, and can emit JSON. ## Examples dog example.net Query a domain using default settings dog example.net MX ...looking up MX records instead dog example.net MX @1.1.1.1 ...using a specific nameserver instead dog example.net MX @1.1.1.1 -T ...using TCP rather than UDP dog -q example.net -t MX -n 1.1.1.1 -T As above, but using explicit arguments --- ## Command-line options ### Query options Human-readable host names, nameservers, types, or classes -q, --query=HOST Host name or domain name to query -t, --type=TYPE Type of the DNS record being queried (A, MX, NS...) -n, --nameserver=ADDR Address of the nameserver to send packets to --class=CLASS Network class of the DNS record being queried (IN, CH, HS) ### Sending options --edns=SETTING Whether to OPT in to EDNS (disable, hide, show) --txid=NUMBER Set the transaction ID to a specific value -Z=TWEAKS Set uncommon protocol-level tweaks ### Protocol options -U, --udp Use the DNS protocol over UDP -T, --tcp Use the DNS protocol over TCP -S, --tls Use the DNS-over-TLS protocol -H, --https Use the DNS-over-HTTPS protocol ### Output options -1, --short Short mode: display nothing but the first result -J, --json Display the output as JSON --color, --colour=WHEN When to colourise the output (always, automatic, never) --seconds Do not format durations, display them as seconds --time Print how long the response took to arrive --- ## Installation To install dog, you can download a pre-compiled binary, or you can compile it from source. You _may_ be able to install dog using your OS’s package manager, depending on your platform. ### Packages - 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. - For NixOS, install the [`dogdns`](https://search.nixos.org/packages?channel=unstable&show=dogdns&query=dogdns) package. ### Downloads Binary downloads of dog are available from [the releases section on GitHub](https://github.com/ogham/dog/releases/) for 64-bit Windows, macOS, and Linux targets. They contain the compiled executable, the manual page, and shell completions. ### Compilation dog is written in [Rust](https://www.rust-lang.org). You will need rustc version [1.45.0](https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html) or higher. The recommended way to install Rust for development is from the [official download page](https://www.rust-lang.org/tools/install), using rustup. To build, download the source code and run: $ cargo build $ 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 --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`. `/usr/local/bin` is usually a good choice. - To compile and install the manual pages, you will need [pandoc](https://pandoc.org/). The `just man` command will compile the Markdown into manual pages, which it will place in the `target/man` directory. To use them, copy them into a directory that `man` will read. `/usr/local/share/man` is usually a good choice. ### Container image To build the container image of dog, you can use Docker or Kaniko. Here an example using Docker: $ docker build -t dog . You can then run it using the following command: $ docker run -it --rm dog To run dog directly, you can then define the following alias: $ alias dog="docker run -it --rm dog" ### End-to-end testing dog has an integration test suite written as [Specsheet](https://specsheet.software/) check documents. 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 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). ### Feature toggles dog has three Cargo features that can be switched off to remove functionality. While doing so makes dog less useful, it results in a smaller binary that takes less time to build. There are three feature toggles available, all of which are active by default: - `with_idna`, which enables [IDNA](https://en.wikipedia.org/wiki/Internationalized_domain_name) processing - `with_tls`, which enables DNS-over-TLS - `with_https`, which enables DNS-over-HTTPS (requires `with_tls`) Use `cargo` to build a binary that uses feature toggles. For example, to disable TLS and HTTPS support but keep IDNA support enabled, you can run: $ cargo build --no-default-features --features=with_idna The list of features that have been disabled can be checked at runtime as part of the `--version` string. --- ## Documentation For documentation on how to use dog, see the website: ## See also `mutt`, `tail`, `sleep`, `roff` ## Licence dog’s source code is licenced under the [European Union Public Licence](https://choosealicense.com/licenses/eupl-1.2/). ================================================ FILE: build.rs ================================================ //! This build script gets run during every build. Its purpose is to put //! together the files used for the `--help` and `--version`, which need to //! come in both coloured and non-coloured variants. The main usage text is //! contained in `src/usage.txt`; to make it easier to edit, backslashes (\) //! are used instead of the beginning of ANSI escape codes. //! //! The version string is quite complex: we want to show the version, //! current Git hash, and compilation date when building *debug* //! versions, but just the version for *release* versions. //! //! This script generates the string from the environment variables //! that Cargo adds (http://doc.crates.io/environment-variables.html) //! and runs `git` to get the SHA1 hash. It then writes the strings //! into files, which we can include during compilation. use std::env; use std::fs::File; use std::io::{self, Write}; use std::path::PathBuf; use datetime::{LocalDateTime, ISO}; /// The build script entry point. fn main() -> io::Result<()> { #![allow(clippy::write_with_newline)] let usage = include_str!("src/usage.txt"); let tagline = "dog \\1;32m●\\0m command-line DNS client"; let url = "https://dns.lookup.dog/"; let ver = if is_debug_build() { format!("{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), url) } else if is_development_version() { format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), git_hash(), build_date(), url) } else { format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url) }; // We need to create these files in the Cargo output directory. let out = PathBuf::from(env::var("OUT_DIR").unwrap()); // Pretty version text let mut f = File::create(&out.join("version.pretty.txt"))?; writeln!(f, "{}", convert_codes(&ver))?; // Bland version text let mut f = File::create(&out.join("version.bland.txt"))?; writeln!(f, "{}", strip_codes(&ver))?; // Pretty usage text let mut f = File::create(&out.join("usage.pretty.txt"))?; 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"))?; 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 { use std::process::Command; String::from_utf8_lossy( &Command::new("git") .args(&["rev-parse", "--short", "HEAD"]) .output().unwrap() .stdout).trim().to_string() } /// Whether we should show pre-release info in the version string. /// /// Both weekly releases and actual releases are --release releases, /// but actual releases will have a proper version number. 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() } /// Returns the version and build parameters string. fn version_string() -> String { let mut ver = cargo_version(); let feats = nonstandard_features_string(); if ! feats.is_empty() { ver.push_str(&format!(" [{}]", &feats)); } ver } /// Finds whether a feature is enabled by examining the Cargo variable. fn feature_enabled(name: &str) -> bool { env::var(&format!("CARGO_FEATURE_{}", name)) .map(|e| ! e.is_empty()) .unwrap_or(false) } /// A comma-separated list of non-standard feature choices. fn nonstandard_features_string() -> String { let mut s = Vec::new(); if ! feature_enabled("WITH_IDNA") { s.push("-idna"); } if ! feature_enabled("WITH_TLS") { s.push("-tls"); } if ! feature_enabled("WITH_HTTPS") { s.push("-https"); } s.join(", ") } /// Formats the current date as an ISO 8601 string. fn build_date() -> String { let now = LocalDateTime::now(); format!("{}", now.date().iso()) } ================================================ FILE: completions/dog.bash ================================================ _dog() { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} case "$prev" in -'?'|--help|-v|--version) return ;; -t|--type) COMPREPLY=( $( compgen -W 'A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT' -- "$cur" ) ) return ;; --edns) COMPREPLY=( $( compgen -W 'disable hide show' -- "$cur" ) ) return ;; -Z) COMPREPLY=( $( compgen -W 'aa ad bufsize= cd' -- "$cur" ) ) return ;; --class) COMPREPLY=( $( compgen -W 'IN CH HS' -- "$cur" ) ) return ;; --color|--colour) COMPREPLY=( $( compgen -W 'always automatic never' -- $cur ) ) return ;; esac case "$cur" in -*) COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) return ;; *) COMPREPLY=( $( compgen -W 'A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT' -- "$cur" ) ) ;; esac } && complete -o bashdefault -F _dog dog ================================================ FILE: completions/dog.fish ================================================ # Meta options complete -c dog -s 'v' -l 'version' -d "Show version of dog" complete -c dog -s '?' -l 'help' -d "Show list of command-line options" # Query options complete -c dog -x -a "(__fish_print_hostnames) A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT IN CH HS" complete -c dog -s 'q' -l 'query' -d "Host name or domain name to query" -x -a "(__fish_print_hostnames)" complete -c dog -s 't' -l 'type' -d "Type of the DNS record being queried" -x -a "A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT" complete -c dog -s 'n' -l 'nameserver' -d "Address of the nameserver to send packets to" -x -a "(__fish_print_hostnames)" complete -c dog -l 'class' -d "Network class of the DNS record being queried" -x -a "IN CH HS" # Sending options complete -c dog -l 'edns' -d "Whether to OPT in to EDNS" -x -a " disable\t'Do not send an OPT query' hide\t'Send an OPT query, but hide the result' show\t'Send an OPT query, and show the result' " complete -c dog -l 'txid' -d "Set the transaction ID to a specific value" -x complete -c dog -s 'Z' -d "Configure uncommon protocol-level tweaks" -x -a " aa\t'Set the AA (Authoritative Answers) query bit' ad\t'Set the AD (Authentic Data) query bit' bufsize=\t'Set the UDP payload size' cd\t'Set the CD (Checking Disabled) query bit' " # Protocol options complete -c dog -s 'U' -l 'udp' -d "Use the DNS protocol over UDP" complete -c dog -s 'T' -l 'tcp' -d "Use the DNS protocol over TCP" complete -c dog -s 'S' -l 'tls' -d "Use the DNS-over-TLS protocol" complete -c dog -s 'H' -l 'https' -d "Use the DNS-over-HTTPS protocol" # Output options complete -c dog -s '1' -l 'short' -d "Display nothing but the first result" complete -c dog -s 'J' -l 'json' -d "Display the output as JSON" complete -c dog -l 'color' -d "When to colorise the output" -x -a " always\t'Always use colors' automatic\t'Use colors when printing to a terminal' never\t'Never use colors' " complete -c dog -l 'colour' -d "When to colourise the output" -x -a " always\t'Always use colours' automatic\t'Use colours when printing to a terminal' never\t'Never use colours' " complete -c dog -l 'seconds' -d "Do not format durations, display them as seconds" complete -c dog -l 'time' -d "Print how long the response took to arrive" ================================================ FILE: completions/dog.ps1 ================================================ # Note: This works for both Windows PowerShell 5.1 and also PowerShell 7 (Core). # But beware that in Windows PowerShell 5.1, it has issues with completing args if they start with '-'. # For more information about the bug, see: https://github.com/PowerShell/PowerShell/issues/2912 # In PowerShell 7+, it should work correctly. Register-ArgumentCompleter -Native -CommandName 'dog' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) [string]$argsString = $commandAst.ToString() # skip the "dog", split the args afterwards as array [string[]]$argsArray = $argsString.Split([char[]]@(' ', '=')) | Select-Object -Skip 1 if ($argsArray -eq $null) { $argsArray = @() } # detect if starting a new arg (aka ending with space and asking for a completion) [bool]$isNewArg = $cursorPosition -gt $argsString.Length if ($isNewArg) { # if writing a new arg, add empty arg so that current and previous would be shifted $argsArray += '' } # get current arg (empty if starting new) [string]$currentArg = $argsArray[-1] if ([string]::IsNullOrEmpty($currentArg)) { $currentArg = '' } # get previous arg [string]$previousArg = $argsArray[-2] if ([string]::IsNullOrEmpty($previousArg)) { $previousArg = '' } [string[]]$dnsTypeValues = @('A', 'AAAA', 'CAA', 'CNAME', 'HINFO', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT') [string[]]$completions = @() [bool]$isOptionValue = $argsString.EndsWith('=') # complete option value switch -Regex ($previousArg) { '^(-q|--query)' { $isOptionValue = $true } '^(-t|--type)' { $isOptionValue = $true; $completions += $dnsTypeValues } '^(-n|--nameserver)' { $isOptionValue = $true } '^(--class)' { $isOptionValue = $true; $completions += @('IN', 'CH', 'HS') } '^(--edns)' { $isOptionValue = $true; $completions += @('disable', 'hide', 'show') } '^(--txid)' { $isOptionValue = $true } '^(-Z)' { $isOptionValue = $true; $completions += @('aa', 'ad', 'bufsize=', 'cd') } '^(--color|--colour)' { $isOptionValue = $true; $completions += @('always', 'automatic', 'never') } } # detect whether to complete option value if ($isOptionValue) { if (!$isNewArg) { # if using =, complete including the option name and = $completions = $completions | ForEach-Object { "$previousArg=$_" } } } else { # if not completing option value, offer DNS type values first $completions += $dnsTypeValues # complete option name [string[]]$allOptions = @( '-q', '--query', '-t', '--type', '-n', '--nameserver', '--class', '--edns', '--txid', '-Z', '-U', '--udp', '-T', '--tcp', '-S', '--tls', '-H', '--https', '-1', '--short', '-J', '--json', '--color', '--colour', '--seconds', '--time', '-?', '--help', '-v', '--version' ) | Sort-Object $completions += $allOptions } if ($completions.Count -gt 0) { # narrow down completions by like* matching return $completions -like "$currentArg*" } } ================================================ FILE: completions/dog.zsh ================================================ #compdef dog __dog() { _arguments \ "(- 1 *)"{-v,--version}"[Show version of dog]" \ "(- 1 *)"{-\?,--help}"[Show list of command-line options]" \ {-q,--query}"[Host name or domain name to query]::_hosts" \ {-t,--type}"[Type of the DNS record being queried]:(record type):(A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT)" \ {-n,--nameserver}"[Address of the nameserver to send packets to]::_hosts;" \ --class"[Network class of the DNS record being queried]:(network class):(IN CH HS)" \ --edns"[Whether to OPT in to EDNS]:(edns setting):(disable hide show)" \ --txid"[Set the transaction ID to a specific value]" \ -Z"[Configure uncommon protocol-level tweaks]:(protocol tweak):(aa ad bufsize= cd)" \ {-U,--udp}"[Use the DNS protocol over UDP]" \ {-T,--tcp}"[Use the DNS protocol over TCP]" \ {-S,--tls}"[Use the DNS-over-TLS protocol]" \ {-H,--https}"[Use the DNS-over-HTTPS protocol]" \ {-1,--short}"[Display nothing but the finst result]" \ {-J,--json}"[Display the output as JSON]" \ {--color,--colour}"[When to use terminal colours]:(setting):(always automatic never)" \ --seconds"[Do not format durations, display them as seconds]" \ --time"[Print how long the response took to arrive"] \ '*:filename:_hosts' } __dog ================================================ FILE: dns/Cargo.toml ================================================ [package] name = "dns" version = "0.2.0-pre" authors = ["Benjamin Sago "] edition = "2018" [lib] doctest = false [dependencies] # logging log = "0.4" # protocol parsing helper byteorder = "1.3" # printing of certain packets base64 = "0.13" # idna encoding unic-idna = { version = "0.9.0", optional = true } # mutation testing mutagen = { git = "https://github.com/llogiq/mutagen", optional = true } [dev-dependencies] pretty_assertions = "0.7" [features] default = [] # idna is enabled in the main dog crate with_idna = ["unic-idna"] with_mutagen = ["mutagen"] # needs nightly ================================================ FILE: dns/fuzz/.gitignore ================================================ target corpus artifacts ================================================ FILE: dns/fuzz/Cargo.toml ================================================ [package] name = "dns-fuzz" version = "0.0.1" authors = ["Automatically generated"] publish = false [package.metadata] cargo-fuzz = true [dependencies.dns] path = ".." [dependencies.libfuzzer-sys] version = "0.3.0" # Prevent this from interfering with workspaces [workspace] members = ["."] [[bin]] name = "fuzz_parsing" path = "fuzz_targets/fuzz_parsing.rs" ================================================ FILE: dns/fuzz/fuzz_targets/fuzz_parsing.rs ================================================ #![no_main] #[macro_use] extern crate libfuzzer_sys; extern crate dns; use dns::Response; fuzz_target!(|data: &[u8]| { let _ = Response::from_bytes(data); }); ================================================ FILE: dns/src/lib.rs ================================================ #![warn(deprecated_in_future)] #![warn(future_incompatible)] #![warn(missing_copy_implementations)] #![warn(missing_docs)] #![warn(nonstandard_style)] #![warn(rust_2018_compatibility)] #![warn(rust_2018_idioms)] #![warn(single_use_lifetimes)] #![warn(trivial_casts, trivial_numeric_casts)] #![warn(unused)] #![warn(clippy::all, clippy::pedantic)] #![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)] #![deny(clippy::cast_lossless)] #![deny(clippy::cast_possible_wrap)] #![deny(clippy::cast_sign_loss)] #![deny(unsafe_code)] //! The DNS crate is the ‘library’ part of dog. It implements the DNS //! protocol: creating and decoding packets from their byte structure. mod types; pub use self::types::*; mod strings; pub use self::strings::Labels; mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; pub mod record; ================================================ FILE: dns/src/record/a.rs ================================================ use std::net::Ipv4Addr; use log::*; use crate::wire::*; /// An **A** record type, which contains an `Ipv4Address`. /// /// # References /// /// - [RFC 1035 §3.4.1](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug, Copy, Clone)] pub struct A { /// The IPv4 address contained in the packet. pub address: Ipv4Addr, } impl Wire for A { const NAME: &'static str = "A"; const RR_TYPE: u16 = 1; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { if stated_length != 4 { warn!("Length is incorrect (record length {:?}, but should be four)", stated_length); 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 }) } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x7F, 0x00, 0x00, 0x01, // IPv4 address ]; assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), A { address: Ipv4Addr::new(127, 0, 0, 1) }); } #[test] fn record_too_short() { let buf = &[ 0x7F, 0x00, 0x00, // Too short IPv4 address ]; assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(4) })); } #[test] fn record_too_long() { let buf = &[ 0x7F, 0x00, 0x00, 0x00, // IPv4 address 0x01, // Unexpected extra byte ]; assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 5, mandated_length: MandatedLength::Exactly(4) })); } #[test] fn record_empty() { assert_eq!(A::read(0, &mut Cursor::new(&[])), Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(4) })); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x7F, 0x00, // Half an IPv4 address ]; assert_eq!(A::read(4, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/aaaa.rs ================================================ use std::net::Ipv6Addr; use log::*; use crate::wire::*; /// A **AAAA** record, which contains an `Ipv6Address`. /// /// # References /// /// - [RFC 3596](https://tools.ietf.org/html/rfc3596) — DNS Extensions to /// Support IP Version 6 (October 2003) #[derive(PartialEq, Debug, Copy, Clone)] pub struct AAAA { /// The IPv6 address contained in the packet. pub address: Ipv6Addr, } impl Wire for AAAA { const NAME: &'static str = "AAAA"; const RR_TYPE: u16 = 28; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { 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 }); } let mut buf = [0_u8; 16]; c.read_exact(&mut buf)?; let address = Ipv6Addr::from(buf); trace!("Parsed IPv6 address -> {:#x?}", address); Ok(Self { address }) } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IPv6 address ]; assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), AAAA { address: Ipv6Addr::new(0,0,0,0,0,0,0,0) }); } #[test] fn record_too_long() { let buf = &[ 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, // IPv6 address 0x09, // Unexpected extra byte ]; assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 17, mandated_length: MandatedLength::Exactly(16) })); } #[test] fn record_too_short() { let buf = &[ 0x05, 0x05, 0x05, 0x05, 0x05, // Five arbitrary bytes ]; assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 5, mandated_length: MandatedLength::Exactly(16) })); } #[test] fn record_empty() { assert_eq!(AAAA::read(0, &mut Cursor::new(&[])), Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(16) })); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x05, 0x05, 0x05, 0x05, 0x05, // Five arbitrary bytes ]; assert_eq!(AAAA::read(16, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/caa.rs ================================================ use log::*; use crate::wire::*; /// A **CAA** _(certification authority authorization)_ record. These allow /// domain names to specify which Certificate Authorities are allowed to issue /// certificates for the domain. /// /// # References /// /// - [RFC 6844](https://tools.ietf.org/html/rfc6844) — DNS Certification /// Authority Authorization Resource Record (January 2013) #[derive(PartialEq, Debug)] pub struct CAA { /// Whether this record is marked as “critical” or not. pub critical: bool, /// The “tag” part of the CAA record. pub tag: Box<[u8]>, /// The “value” part of the CAA record. pub value: Box<[u8]>, } impl Wire for CAA { const NAME: &'static str = "CAA"; const RR_TYPE: u16 = 257; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { // flags let flags = c.read_u8()?; trace!("Parsed flags -> {:#08b}", flags); let has_bit = |bit| { flags & bit == bit }; let critical = has_bit(0b_1000_0000); trace!("Parsed critical flag -> {:?}", critical); // tag let tag_length = c.read_u8()?; trace!("Parsed tag length -> {:?}", tag_length); let mut tag = vec![0_u8; usize::from(tag_length)].into_boxed_slice(); c.read_exact(&mut tag)?; trace!("Parsed tag -> {:?}", String::from_utf8_lossy(&tag)); // value let remaining_length = stated_length.saturating_sub(u16::from(tag_length)).saturating_sub(2); trace!("Remaining length -> {:?}", remaining_length); let mut value = vec![0_u8; usize::from(remaining_length)].into_boxed_slice(); c.read_exact(&mut value)?; trace!("Parsed value -> {:?}", String::from_utf8_lossy(&value)); Ok(Self { critical, tag, value }) } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses_non_critical() { let buf = &[ 0x00, // flags (all unset) 0x09, // tag length 0x69, 0x73, 0x73, 0x75, 0x65, 0x77, 0x69, 0x6c, 0x64, // tag 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x6e, 0x65, 0x74, // value ]; assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), CAA { critical: false, tag: Box::new(*b"issuewild"), value: Box::new(*b"entrust.net"), }); } #[test] fn parses_critical() { let buf = &[ 0x80, // flags (critical bit set) 0x09, // tag length 0x69, 0x73, 0x73, 0x75, 0x65, 0x77, 0x69, 0x6c, 0x64, // tag 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x6e, 0x65, 0x74, // value ]; assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), CAA { critical: true, tag: Box::new(*b"issuewild"), value: Box::new(*b"entrust.net"), }); } #[test] fn ignores_other_flags() { let buf = &[ 0x7F, // flags (all except critical bit set) 0x01, // tag length 0x65, // tag 0x45, // value ]; assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), CAA { critical: false, tag: Box::new(*b"e"), value: Box::new(*b"E"), }); } #[test] fn record_empty() { assert_eq!(CAA::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x00, // flags ]; assert_eq!(CAA::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/cname.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **CNAME** _(canonical name)_ record, which aliases one domain to another. /// /// # References /// /// - [RFC 1035 §3.3.1](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct CNAME { /// The domain name that this CNAME record is responding with. pub domain: Labels, } impl Wire for CNAME { const NAME: &'static str = "CNAME"; const RR_TYPE: u16 = 5; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let (domain, domain_length) = c.read_labels()?; trace!("Parsed domain -> {:?}", domain); if stated_length == domain_length { trace!("Length is correct"); Ok(Self { domain }) } else { warn!("Length is incorrect (stated length {:?}, domain length {:?})", stated_length, domain_length); Err(WireError::WrongLabelLength { stated_length, length_after_labels: domain_length }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, // domain 0x00, // domain terminator ]; assert_eq!(CNAME::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), CNAME { domain: Labels::encode("bsago.me").unwrap(), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x03, 0x65, 0x66, 0x67, // domain 0x00, // domain terminator ]; assert_eq!(CNAME::read(6, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 5 })); } #[test] fn record_empty() { assert_eq!(CNAME::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x05, 0x62, 0x73, // the stard of a string ]; assert_eq!(CNAME::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/eui48.rs ================================================ 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 { 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)?; trace!("Parsed 6-byte address -> {:#x?}", 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"); } } ================================================ FILE: dns/src/record/eui64.rs ================================================ 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 { 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)?; trace!("Parsed 8-byte address -> {:#x?}", 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"); } } ================================================ FILE: dns/src/record/hinfo.rs ================================================ use log::*; use crate::wire::*; /// A (an?) **HINFO** _(host information)_ record, which contains the CPU and /// OS information about a host. /// /// It also gets used as the response for an `ANY` query, if it is blocked. /// /// # References /// /// - [RFC 1035 §3.3.2](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) /// - [RFC 8482 §6](https://tools.ietf.org/html/rfc8482#section-6) — Providing /// Minimal-Sized Responses to DNS Queries That Have QTYPE=ANY (January 2019) #[derive(PartialEq, Debug)] pub struct HINFO { /// The CPU field, specifying the CPU type. pub cpu: Box<[u8]>, /// The OS field, specifying the operating system. pub os: Box<[u8]>, } impl Wire for HINFO { const NAME: &'static str = "HINFO"; const RR_TYPE: u16 = 13; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let cpu_length = c.read_u8()?; trace!("Parsed CPU length -> {:?}", cpu_length); let mut cpu = vec![0_u8; usize::from(cpu_length)].into_boxed_slice(); c.read_exact(&mut cpu)?; trace!("Parsed CPU -> {:?}", String::from_utf8_lossy(&cpu)); let os_length = c.read_u8()?; trace!("Parsed OS length -> {:?}", os_length); let mut os = vec![0_u8; usize::from(os_length)].into_boxed_slice(); c.read_exact(&mut os)?; trace!("Parsed OS -> {:?}", String::from_utf8_lossy(&os)); let length_after_labels = 1 + u16::from(cpu_length) + 1 + u16::from(os_length); if stated_length == length_after_labels { trace!("Length is correct"); Ok(Self { cpu, os }) } else { warn!("Length is incorrect (stated length {:?}, cpu plus length {:?}", stated_length, length_after_labels); Err(WireError::WrongLabelLength { stated_length, length_after_labels }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x0e, // cpu length 0x73, 0x6f, 0x6d, 0x65, 0x2d, 0x6b, 0x69, 0x6e, 0x64, 0x61, 0x2d, 0x63, 0x70, 0x75, // cpu 0x0d, // os length 0x73, 0x6f, 0x6d, 0x65, 0x2d, 0x6b, 0x69, 0x6e, 0x64, 0x61, 0x2d, 0x6f, 0x73, // os ]; assert_eq!(HINFO::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), HINFO { cpu: Box::new(*b"some-kinda-cpu"), os: Box::new(*b"some-kinda-os"), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x03, // cpu length 0x65, 0x66, 0x67, // cpu 0x03, // os length 0x68, 0x69, 0x70, // os ]; assert_eq!(HINFO::read(6, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 8 })); } #[test] fn record_empty() { assert_eq!(HINFO::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x14, 0x0A, 0x0B, 0x0C, // 32-bit CPU ]; assert_eq!(HINFO::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/loc.rs ================================================ use std::fmt; use log::*; use crate::wire::*; /// A **LOC** _(location)_ record, which points to a location on Earth using /// its latitude, longitude, and altitude. /// /// # References /// /// - [RFC 1876](https://tools.ietf.org/html/rfc1876) — A Means for Expressing /// Location Information in the Domain Name System (January 1996) #[derive(PartialEq, Debug, Copy, Clone)] pub struct LOC { /// The diameter of a sphere enclosing the entity at the location, as a /// measure of its size, measured in centimetres. pub size: Size, /// The diameter of the “circle of error” that this location could be in, /// measured in centimetres. pub horizontal_precision: u8, /// The amount of vertical space that this location could be in, measured /// in centimetres. pub vertical_precision: u8, /// The latitude of the centre of the sphere. If `None`, the packet /// parses, but the position is out of range. pub latitude: Option, /// The longitude of the centre of the sphere. If `None`, the packet /// parses, but the position is out of range. pub longitude: Option, /// The altitude of the centre of the sphere, measured in centimetres /// above a base of 100,000 metres below the GPS reference spheroid. pub altitude: Altitude, } /// A measure of size, in centimetres, represented by a base and an exponent. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Size { base: u8, power_of_ten: u8, } /// A position on one of the world’s axes. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Position { degrees: u32, arcminutes: u32, arcseconds: u32, milliarcseconds: u32, direction: Direction, } /// A position on the vertical axis. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Altitude { metres: i64, centimetres: i64, } /// One of the directions a position could be in, relative to the equator or /// prime meridian. #[derive(PartialEq, Debug, Copy, Clone)] pub enum Direction { North, East, South, West, } impl Wire for LOC { const NAME: &'static str = "LOC"; const RR_TYPE: u16 = 29; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let version = c.read_u8()?; trace!("Parsed version -> {:?}", version); if version != 0 { return Err(WireError::WrongVersion { stated_version: version, maximum_supported_version: 0, }); } if stated_length != 16 { let mandated_length = MandatedLength::Exactly(16); return Err(WireError::WrongRecordLength { stated_length, mandated_length }); } let size_bits = c.read_u8()?; let size = Size::from_u8(size_bits); trace!("Parsed size -> {:#08b} ({})", size_bits, size); let horizontal_precision = c.read_u8()?; trace!("Parsed horizontal precision -> {:?}", horizontal_precision); let vertical_precision = c.read_u8()?; trace!("Parsed vertical precision -> {:?}", vertical_precision); let latitude_num = c.read_u32::()?; let latitude = Position::from_u32(latitude_num, true); trace!("Parsed latitude -> {:?} ({:?})", latitude_num, latitude); let longitude_num = c.read_u32::()?; let longitude = Position::from_u32(longitude_num, false); trace!("Parsed longitude -> {:?} ({:?})", longitude_num, longitude); let altitude_num = c.read_u32::()?; let altitude = Altitude::from_u32(altitude_num); trace!("Parsed altitude -> {:?} ({:})", altitude_num, altitude); Ok(Self { size, horizontal_precision, vertical_precision, latitude, longitude, altitude, }) } } impl Size { /// Converts a number into the size it represents. To allow both small and /// large sizes, the input octet is split into two four-bit sizes, one the /// base, and one the power of ten exponent. fn from_u8(input: u8) -> Self { let base = input >> 4; let power_of_ten = input & 0b_0000_1111; Self { base, power_of_ten } } } impl Position { /// Converts a number into the position it represents. The input number is /// measured in thousandths of an arcsecond (milliarcseconds), with 2^31 /// as the equator or prime meridian. /// /// Returns `None` if the input is out of range, meaning it would wrap /// around to another half of the Earth once or more. fn from_u32(mut input: u32, vertical: bool) -> Option { let max_for_direction = if vertical { 90 } else { 180 }; let limit = 1000 * 60 * 60 * max_for_direction; if input < (0x_8000_0000 - limit) || input > (0x_8000_0000 + limit) { // Input is out of range None } else if input >= 0x_8000_0000 { // Input is north or east, so de-relativise it and divide into segments input -= 0x_8000_0000; let milliarcseconds = input % 1000; let total_arcseconds = input / 1000; let arcseconds = total_arcseconds % 60; let total_arcminutes = total_arcseconds / 60; let arcminutes = total_arcminutes % 60; let degrees = total_arcminutes / 60; let direction = if vertical { Direction::North } else { Direction::East }; Some(Self { degrees, arcminutes, arcseconds, milliarcseconds, direction }) } else { // Input is south or west, so do the calculations for let mut pos = Self::from_u32(input + (0x_8000_0000_u32 - input) * 2, vertical)?; pos.direction = if vertical { Direction::South } else { Direction::West }; Some(pos) } } } impl Altitude { fn from_u32(input: u32) -> Self { let mut input = i64::from(input); input -= 10_000_000; // 100,000m let metres = input / 100; let centimetres = input % 100; Self { metres, centimetres } } } impl fmt::Display for Size { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}e{}", self.base, self.power_of_ten) } } impl fmt::Display for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}°{}′{}", self.degrees, self.arcminutes, self.arcseconds, )?; if self.milliarcseconds != 0 { write!(f, ".{:03}", self.milliarcseconds)?; } write!(f, "″ {}", self.direction) } } impl fmt::Display for Direction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::North => write!(f, "N"), Self::East => write!(f, "E"), Self::South => write!(f, "S"), Self::West => write!(f, "W"), } } } impl fmt::Display for Altitude { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Usually there’s a space between the number and the unit, but // spaces are already used to delimit segments in the record summary if self.centimetres == 0 { write!(f, "{}m", self.metres) } else { write!(f, "{}.{:02}m", self.metres, self.centimetres) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x00, // version 0x32, // size, 0x00, // horizontal precision 0x00, // vertical precision 0x8b, 0x0d, 0x2c, 0x8c, // latitude 0x7f, 0xf8, 0xfc, 0xa5, // longitude 0x00, 0x98, 0x96, 0x80, // altitude ]; assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), LOC { size: Size { base: 3, power_of_ten: 2 }, horizontal_precision: 0, vertical_precision: 0, latitude: Position::from_u32(0x_8b_0d_2c_8c, true), longitude: Position::from_u32(0x_7f_f8_fc_a5, false), altitude: Altitude::from_u32(0x_00_98_96_80), }); } #[test] fn record_too_short() { let buf = &[ 0x00, // version 0x00, // size ]; assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 2, mandated_length: MandatedLength::Exactly(16) })); } #[test] fn record_too_long() { let buf = &[ 0x00, // version 0x32, // size, 0x00, // horizontal precision 0x00, // vertical precision 0x8b, 0x0d, 0x2c, 0x8c, // latitude 0x7f, 0xf8, 0xfc, 0xa5, // longitude 0x00, 0x98, 0x96, 0x80, // altitude 0x12, 0x34, 0x56, // some other stuff ]; assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 19, mandated_length: MandatedLength::Exactly(16) })); } #[test] fn more_recent_version() { let buf = &[ 0x80, // version 0x12, 0x34, 0x56, // some data in an unknown format ]; assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongVersion { stated_version: 128, maximum_supported_version: 0 })); } #[test] fn record_empty() { assert_eq!(LOC::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x00, // version ]; assert_eq!(LOC::read(16, &mut Cursor::new(buf)), Err(WireError::IO)); } } #[cfg(test)] mod size_test { use super::*; use pretty_assertions::assert_eq; #[test] fn zeroes() { assert_eq!(Size::from_u8(0b_0000_0000).to_string(), String::from("0e0")); } #[test] fn ones() { assert_eq!(Size::from_u8(0b_0001_0001).to_string(), String::from("1e1")); } #[test] fn schfourteen_teen() { assert_eq!(Size::from_u8(0b_1110_0011).to_string(), String::from("14e3")); } #[test] fn ones_but_bits_this_time() { assert_eq!(Size::from_u8(0b_1111_1111).to_string(), String::from("15e15")); } } #[cfg(test)] mod position_test { use super::*; use pretty_assertions::assert_eq; // centre line tests #[test] fn meridian() { assert_eq!(Position::from_u32(0x_8000_0000, false).unwrap().to_string(), String::from("0°0′0″ E")); } #[test] fn meridian_plus_one() { assert_eq!(Position::from_u32(0x_8000_0000 + 1, false).unwrap().to_string(), String::from("0°0′0.001″ E")); } #[test] fn meridian_minus_one() { assert_eq!(Position::from_u32(0x_8000_0000 - 1, false).unwrap().to_string(), String::from("0°0′0.001″ W")); } #[test] fn equator() { assert_eq!(Position::from_u32(0x_8000_0000, true).unwrap().to_string(), String::from("0°0′0″ N")); } #[test] fn equator_plus_one() { assert_eq!(Position::from_u32(0x_8000_0000 + 1, true).unwrap().to_string(), String::from("0°0′0.001″ N")); } #[test] fn equator_minus_one() { assert_eq!(Position::from_u32(0x_8000_0000 - 1, true).unwrap().to_string(), String::from("0°0′0.001″ S")); } // arbitrary value tests #[test] fn some_latitude() { assert_eq!(Position::from_u32(2332896396, true).unwrap().to_string(), String::from("51°30′12.748″ N")); } #[test] fn some_longitude() { assert_eq!(Position::from_u32(2147024037, false).unwrap().to_string(), String::from("0°7′39.611″ W")); } // limit tests #[test] fn the_north_pole() { assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 90), true).unwrap().to_string(), String::from("90°0′0″ N")); } #[test] fn the_north_pole_plus_one() { assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 90) + 1, true), None); } #[test] fn the_south_pole() { assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 90), true).unwrap().to_string(), String::from("90°0′0″ S")); } #[test] fn the_south_pole_minus_one() { assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 90) - 1, true), None); } #[test] fn the_far_east() { assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 180), false).unwrap().to_string(), String::from("180°0′0″ E")); } #[test] fn the_far_east_plus_one() { assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 180) + 1, false), None); } #[test] fn the_far_west() { assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 180), false).unwrap().to_string(), String::from("180°0′0″ W")); } #[test] fn the_far_west_minus_one() { assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 180) - 1, false), None); } } #[cfg(test)] mod altitude_test { use super::*; use pretty_assertions::assert_eq; #[test] fn base_level() { assert_eq!(Altitude::from_u32(10000000).to_string(), String::from("0m")); } #[test] fn up_high() { assert_eq!(Altitude::from_u32(20000000).to_string(), String::from("100000m")); } #[test] fn down_low() { assert_eq!(Altitude::from_u32(0).to_string(), String::from("-100000m")); } #[test] fn with_decimal() { assert_eq!(Altitude::from_u32(50505050).to_string(), String::from("405050.50m")); } } ================================================ FILE: dns/src/record/mod.rs ================================================ //! All the DNS record types, as well as how to parse each type. use crate::wire::*; mod a; pub use self::a::A; mod aaaa; pub use self::aaaa::AAAA; mod caa; 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; mod loc; pub use self::loc::LOC; mod mx; pub use self::mx::MX; mod naptr; 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; mod ptr; pub use self::ptr::PTR; mod sshfp; pub use self::sshfp::SSHFP; mod soa; pub use self::soa::SOA; mod srv; pub use self::srv::SRV; mod tlsa; 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; /// A record that’s been parsed from a byte buffer. #[derive(PartialEq, Debug)] #[allow(missing_docs)] pub enum Record { A(A), AAAA(AAAA), CAA(CAA), CNAME(CNAME), EUI48(EUI48), EUI64(EUI64), HINFO(HINFO), LOC(LOC), MX(MX), NAPTR(NAPTR), NS(NS), OPENPGPKEY(OPENPGPKEY), // OPT is not included here. PTR(PTR), SSHFP(SSHFP), SOA(SOA), SRV(SRV), TLSA(TLSA), TXT(TXT), URI(URI), /// A record with a type that we don’t recognise. Other { /// The number that’s meant to represent the record type. type_number: UnknownQtype, /// The undecodable bytes that were in this record. bytes: Vec, }, } /// The type of a record that may or may not be one of the known ones. Has no /// data associated with it other than what type of record it is. #[derive(PartialEq, Debug, Copy, Clone)] #[allow(missing_docs)] pub enum RecordType { A, AAAA, CAA, CNAME, EUI48, EUI64, HINFO, LOC, MX, NAPTR, NS, OPENPGPKEY, PTR, SSHFP, SOA, SRV, TLSA, TXT, URI, /// A record type we don’t recognise. Other(UnknownQtype), } impl From for RecordType { fn from(type_number: u16) -> Self { macro_rules! try_record { ($record:tt) => { if $record::RR_TYPE == type_number { return RecordType::$record; } } } try_record!(A); 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); try_record!(SOA); try_record!(SRV); try_record!(TLSA); try_record!(TXT); try_record!(URI); RecordType::Other(UnknownQtype::from(type_number)) } } impl RecordType { /// Determines the record type with a given name, or `None` if none is /// known. Matches names case-insensitively. pub fn from_type_name(type_name: &str) -> Option { macro_rules! try_record { ($record:tt) => { if $record::NAME.eq_ignore_ascii_case(type_name) { return Some(Self::$record); } } } try_record!(A); 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); try_record!(SOA); try_record!(SRV); try_record!(TLSA); try_record!(TXT); try_record!(URI); UnknownQtype::from_type_name(type_name).map(Self::Other) } /// Returns the record type number associated with this record type. pub fn type_number(self) -> u16 { match self { Self::A => A::RR_TYPE, Self::AAAA => AAAA::RR_TYPE, Self::CAA => CAA::RR_TYPE, Self::CNAME => CNAME::RR_TYPE, Self::EUI48 => EUI48::RR_TYPE, Self::EUI64 => EUI64::RR_TYPE, Self::HINFO => HINFO::RR_TYPE, Self::LOC => LOC::RR_TYPE, Self::MX => MX::RR_TYPE, Self::NAPTR => NAPTR::RR_TYPE, Self::NS => NS::RR_TYPE, Self::OPENPGPKEY => OPENPGPKEY::RR_TYPE, // Wherefore art thou, OPT Self::PTR => PTR::RR_TYPE, Self::SSHFP => SSHFP::RR_TYPE, Self::SOA => SOA::RR_TYPE, Self::SRV => SRV::RR_TYPE, Self::TLSA => TLSA::RR_TYPE, Self::TXT => TXT::RR_TYPE, Self::URI => URI::RR_TYPE, Self::Other(o) => o.type_number(), } } } // This code is really repetitive, I know, I know ================================================ FILE: dns/src/record/mx.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// An **MX** _(mail exchange)_ record, which contains the hostnames for mail /// servers that handle mail sent to the domain. /// /// # References /// /// - [RFC 1035 §3.3.9](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct MX { /// The preference that clients should give to this MX record amongst all /// that get returned. pub preference: u16, /// The domain name of the mail exchange server. pub exchange: Labels, } impl Wire for MX { const NAME: &'static str = "MX"; const RR_TYPE: u16 = 15; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let preference = c.read_u16::()?; trace!("Parsed preference -> {:?}", preference); let (exchange, exchange_length) = c.read_labels()?; trace!("Parsed exchange -> {:?}", exchange); let length_after_labels = 2 + exchange_length; if stated_length == length_after_labels { trace!("Length is correct"); Ok(Self { preference, exchange }) } else { warn!("Length is incorrect (stated length {:?}, preference plus exchange length {:?}", stated_length, length_after_labels); Err(WireError::WrongLabelLength { stated_length, length_after_labels }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x00, 0x0A, // preference 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, // exchange 0x00, // exchange terminator ]; assert_eq!(MX::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), MX { preference: 10, exchange: Labels::encode("bsago.me").unwrap(), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x00, 0x0A, // preference 0x03, 0x65, 0x66, 0x67, // domain 0x00, // domain terminator ]; assert_eq!(MX::read(6, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 7 })); } #[test] fn record_empty() { assert_eq!(MX::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x00, 0x0A, // half a preference ]; assert_eq!(MX::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/naptr.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **NAPTR** _(naming authority pointer)_ record, which holds a rule for /// the Dynamic Delegation Discovery System. /// /// # References /// /// - [RFC 3403](https://tools.ietf.org/html/rfc3403) — Dynamic Delegation /// Discovery System (DDDS) Part Three: The Domain Name System (DNS) Database /// (October 2002) #[derive(PartialEq, Debug)] pub struct NAPTR { /// The order in which NAPTR records must be processed. pub order: u16, /// The DDDS priority. pub preference: u16, /// A set of characters that control the rewriting and interpretation of /// the other fields. pub flags: Box<[u8]>, /// The service parameters applicable to this delegation path. pub service: Box<[u8]>, /// A regular expression that gets applied to a string in order to /// construct the next domain name to look up using the DDDS algorithm. pub regex: Box<[u8]>, /// The replacement domain name as part of the DDDS algorithm. pub replacement: Labels, } impl Wire for NAPTR { const NAME: &'static str = "NAPTR"; const RR_TYPE: u16 = 35; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let order = c.read_u16::()?; trace!("Parsed order -> {:?}", order); // preference let preference = c.read_u16::()?; trace!("Parsed preference -> {:?}", preference); // flags let flags_length = c.read_u8()?; trace!("Parsed flags length -> {:?}", flags_length); let mut flags = vec![0_u8; usize::from(flags_length)].into_boxed_slice(); c.read_exact(&mut flags)?; trace!("Parsed flags -> {:?}", String::from_utf8_lossy(&flags)); // service let service_length = c.read_u8()?; trace!("Parsed service length -> {:?}", service_length); let mut service = vec![0_u8; usize::from(service_length)].into_boxed_slice(); c.read_exact(&mut service)?; trace!("Parsed service -> {:?}", String::from_utf8_lossy(&service)); // regex let regex_length = c.read_u8()?; trace!("Parsed regex length -> {:?}", regex_length); let mut regex = vec![0_u8; usize::from(regex_length)].into_boxed_slice(); c.read_exact(&mut regex)?; trace!("Parsed regex -> {:?}", String::from_utf8_lossy(®ex)); // replacement let (replacement, replacement_length) = c.read_labels()?; trace!("Parsed replacement -> {:?}", replacement); let length_after_labels = 2 + 2 + 1 + u16::from(flags_length) + 1 + u16::from(service_length) + 1 + u16::from(regex_length) + replacement_length; if stated_length == length_after_labels { Ok(Self { order, preference, flags, service, regex, replacement }) } else { Err(WireError::WrongLabelLength { stated_length, length_after_labels }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x00, 0x05, // order 0x00, 0x0a, // preference 0x01, // flags length 0x73, // flags 0x03, // service length 0x53, 0x52, 0x56, // service 0x0e, // regex length 0x5c, 0x64, 0x5c, 0x64, 0x3a, 0x5c, 0x64, 0x5c, 0x64, 0x3a, 0x5c, 0x64, 0x5c, 0x64, // regex 0x0b, 0x73, 0x72, 0x76, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03, 0x64, 0x6f, 0x67, 0x00, // replacement ]; assert_eq!(NAPTR::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), NAPTR { order: 5, preference: 10, flags: Box::new(*b"s"), service: Box::new(*b"SRV"), regex: Box::new(*b"\\d\\d:\\d\\d:\\d\\d"), replacement: Labels::encode("srv-example.lookup.dog").unwrap(), }); } #[test] fn incorrect_length() { let buf = &[ 0x00, 0x05, // order 0x00, 0x0a, // preference 0x01, // flags length 0x73, // flags 0x03, // service length 0x53, 0x52, 0x56, // service 0x01, // regex length 0x64, // regex, 0x00, // replacement ]; assert_eq!(NAPTR::read(11, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 11, length_after_labels: 13 })); } #[test] fn record_empty() { assert_eq!(NAPTR::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x00, 0x0A, // order ]; assert_eq!(NAPTR::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/ns.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **NS** _(name server)_ record, which is used to point domains to name /// servers. /// /// # References /// /// - [RFC 1035 §3.3.11](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct NS { /// The address of a nameserver that provides this DNS response. pub nameserver: Labels, } impl Wire for NS { const NAME: &'static str = "NS"; const RR_TYPE: u16 = 2; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let (nameserver, nameserver_length) = c.read_labels()?; trace!("Parsed nameserver -> {:?}", nameserver); if stated_length == nameserver_length { trace!("Length is correct"); Ok(Self { nameserver }) } else { warn!("Length is incorrect (stated length {:?}, nameserver length {:?}", stated_length, nameserver_length); Err(WireError::WrongLabelLength { stated_length, length_after_labels: nameserver_length }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x01, 0x61, 0x0c, 0x67, 0x74, 0x6c, 0x64, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x03, 0x6e, 0x65, 0x74, // nameserver 0x00, // nameserver terminator ]; assert_eq!(NS::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), NS { nameserver: Labels::encode("a.gtld-servers.net").unwrap(), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x03, 0x65, 0x66, 0x67, // nameserver 0x00, // nameserver terminator ]; assert_eq!(NS::read(66, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 66, length_after_labels: 5 })); } #[test] fn record_empty() { assert_eq!(NS::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x01, // the first byte of a string ]; assert_eq!(NS::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/openpgpkey.rs ================================================ use log::*; 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, } 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 { 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)?; trace!("Parsed key -> {:#x?}", 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)); } } ================================================ FILE: dns/src/record/opt.rs ================================================ use std::convert::TryFrom; use std::io; use log::*; use crate::wire::*; /// A **OPT** _(options)_ pseudo-record, which is used to extend the DNS /// protocol with additional flags such as DNSSEC stuff. /// /// # Pseudo-record? /// /// Unlike all the other record types, which are used to return data about a /// domain name, the OPT record type is used to add more options to the /// request, including data about the client or the server. It can exist, with /// a payload, as a query or a response, though it’s usually encountered in /// the Additional section. Its purpose is to add more room to the DNS wire /// format, as backwards compatibility makes it impossible to simply add more /// flags to the header. /// /// The fact that this isn’t a standard record type is annoying for a DNS /// implementation. It re-purposes the ‘class’ and ‘TTL’ fields of the /// `Answer` struct, as they only have meaning when associated with a domain /// name. This means that the parser has to treat the OPT type specially, /// switching to `Opt::read` as soon as the rtype is detected. It also means /// the output has to deal with missing classes and TTLs. /// /// # References /// /// - [RFC 6891](https://tools.ietf.org/html/rfc6891) — Extension Mechanisms /// for DNS (April 2013) #[derive(PartialEq, Debug, Clone)] pub struct OPT { /// The maximum size of a UDP packet that the client supports. pub udp_payload_size: u16, /// The bits that form an extended rcode when non-zero. pub higher_bits: u8, /// The version number of the DNS extension mechanism. pub edns0_version: u8, /// Sixteen bits worth of flags. pub flags: u16, /// The payload of the OPT record. pub data: Vec, } impl OPT { /// The record type number associated with OPT. pub const RR_TYPE: u16 = 41; /// Reads from the given cursor to parse an OPT record. /// /// The buffer will have slightly more bytes to read for an OPT record /// than for a typical one: we will not have encountered the ‘class’ or /// ‘ttl’ fields, which have different meanings for this record type. /// 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(feature = "with_mutagen", ::mutagen::mutate)] pub fn read(c: &mut Cursor<&[u8]>) -> Result { let udp_payload_size = c.read_u16::()?; // replaces the class field trace!("Parsed UDP payload size -> {:?}", udp_payload_size); let higher_bits = c.read_u8()?; // replaces the ttl field... trace!("Parsed higher bits -> {:#08b}", higher_bits); let edns0_version = c.read_u8()?; // ...as does this... trace!("Parsed EDNS(0) version -> {:?}", edns0_version); let flags = c.read_u16::()?; // ...as does this trace!("Parsed flags -> {:#08b}", flags); let data_length = c.read_u16::()?; trace!("Parsed data length -> {:?}", data_length); let mut data = vec![0_u8; usize::from(data_length)]; c.read_exact(&mut data)?; trace!("Parsed data -> {:#x?}", data); Ok(Self { udp_payload_size, higher_bits, edns0_version, flags, data }) } /// Serialises this OPT record into a vector of bytes. /// /// This is necessary for OPT records to be sent in the Additional section /// of requests. pub fn to_bytes(&self) -> io::Result> { let mut bytes = Vec::with_capacity(32); bytes.write_u16::(self.udp_payload_size)?; bytes.write_u8(self.higher_bits)?; bytes.write_u8(self.edns0_version)?; bytes.write_u16::(self.flags)?; // We should not be sending any data at all in the request, really, // so sending too much data is downright nonsensical let data_len = u16::try_from(self.data.len()).expect("Sending too much data"); bytes.write_u16::(data_len)?; for b in &self.data { bytes.write_u8(*b)?; } Ok(bytes) } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses_no_data() { let buf = &[ 0x05, 0xAC, // UDP payload size 0x00, // higher bits 0x00, 0x00, // EDNS(0) version 0x00, 0x00, // flags 0x00, // data length (followed by no data) ]; assert_eq!(OPT::read(&mut Cursor::new(buf)).unwrap(), OPT { udp_payload_size: 1452, higher_bits: 0, edns0_version: 0, flags: 0, data: vec![], }); } #[test] fn parses_with_data() { let buf = &[ 0x05, 0xAC, // UDP payload size 0x00, // higher bits 0x00, 0x00, // EDNS(0) version 0x00, 0x00, // flags 0x04, // data length 0x01, 0x02, 0x03, 0x04, // data ]; assert_eq!(OPT::read(&mut Cursor::new(buf)).unwrap(), OPT { udp_payload_size: 1452, higher_bits: 0, edns0_version: 0, flags: 0, data: vec![1, 2, 3, 4], }); } #[test] fn record_empty() { assert_eq!(OPT::read(&mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x05, // half a UDP payload size ]; assert_eq!(OPT::read(&mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/others.rs ================================================ use std::fmt; /// A number representing a record type dog can’t deal with. #[derive(PartialEq, Debug, Copy, Clone)] pub enum UnknownQtype { /// An rtype number that dog is aware of, but does not know how to parse. HeardOf(&'static str, u16), /// A completely unknown rtype number. UnheardOf(u16), } impl UnknownQtype { /// Searches the list for an unknown type with the given name, returning a /// `HeardOf` variant if one is found, and `None` otherwise. pub fn from_type_name(type_name: &str) -> Option { let (name, num) = TYPES.iter().find(|t| t.0.eq_ignore_ascii_case(type_name))?; Some(Self::HeardOf(name, *num)) } /// Returns the type number behind this unknown type. pub fn type_number(self) -> u16 { match self { Self::HeardOf(_, num) | Self::UnheardOf(num) => num, } } } impl From for UnknownQtype { fn from(qtype: u16) -> Self { match TYPES.iter().find(|t| t.1 == qtype) { Some(tuple) => Self::HeardOf(tuple.0, qtype), None => Self::UnheardOf(qtype), } } } impl fmt::Display for UnknownQtype { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::HeardOf(name, _) => write!(f, "{}", name), Self::UnheardOf(num) => write!(f, "{}", num), } } } /// Mapping of record type names to their assigned numbers. static TYPES: &[(&str, u16)] = &[ ("AFSDB", 18), ("ANY", 255), ("APL", 42), ("AXFR", 252), ("CDNSKEY", 60), ("CDS", 59), ("CERT", 37), ("CSYNC", 62), ("DHCID", 49), ("DLV", 32769), ("DNAME", 39), ("DNSKEEYE", 48), ("DS", 43), ("HIP", 55), ("IPSECKEY", 45), ("IXFR", 251), ("KEY", 25), ("KX", 36), ("NSEC", 47), ("NSEC3", 50), ("NSEC3PARAM", 51), ("OPENPGPKEY", 61), ("RRSIG", 46), ("RP", 17), ("SIG", 24), ("SMIMEA", 53), ("TA", 32768), ("TKEY", 249), ("TSIG", 250), ("URI", 256), ]; #[cfg(test)] mod test { use super::*; #[test] fn known() { assert_eq!(UnknownQtype::from(46).to_string(), String::from("RRSIG")); } #[test] fn unknown() { assert_eq!(UnknownQtype::from(4444).to_string(), String::from("4444")); } } ================================================ FILE: dns/src/record/ptr.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **PTR** record, which holds a _pointer_ to a canonical name. This is /// most often used for reverse DNS lookups. /// /// # Encoding /// /// The text encoding is not specified, but this crate treats it as UTF-8. /// Invalid bytes are turned into the replacement character. /// /// # References /// /// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct PTR { /// The CNAME contained in the record. pub cname: Labels, } impl Wire for PTR { const NAME: &'static str = "PTR"; const RR_TYPE: u16 = 12; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let (cname, cname_length) = c.read_labels()?; trace!("Parsed cname -> {:?}", cname); if stated_length == cname_length { trace!("Length is correct"); Ok(Self { cname }) } else { warn!("Length is incorrect (stated length {:?}, cname length {:?}", stated_length, cname_length); Err(WireError::WrongLabelLength { stated_length, length_after_labels: cname_length }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x03, 0x64, 0x6e, 0x73, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, // cname 0x00, // cname terminator ]; assert_eq!(PTR::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), PTR { cname: Labels::encode("dns.google").unwrap(), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x03, 0x65, 0x66, 0x67, // cname 0x00, // cname terminator ]; assert_eq!(PTR::read(6, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 5 })); } #[test] fn record_empty() { assert_eq!(PTR::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x03, 0x64, // the start of a cname ]; assert_eq!(PTR::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/soa.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **SOA** _(start of authority)_ record, which contains administrative /// information about the zone the domain is in. These are returned when a /// server does not have a record for a domain. /// /// # References /// /// - [RFC 1035 §3.3.13](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct SOA { /// The primary master name for this server. pub mname: Labels, /// The e-mail address of the administrator responsible for this DNS zone. pub rname: Labels, /// A serial number for this DNS zone. pub serial: u32, /// Duration, in seconds, after which secondary nameservers should query /// the master for _its_ SOA record. pub refresh_interval: u32, /// Duration, in seconds, after which secondary nameservers should retry /// requesting the serial number from the master if it does not respond. /// It should be less than `refresh`. pub retry_interval: u32, /// Duration, in seconds, after which secondary nameservers should stop /// answering requests for this zone if the master does not respond. /// It should be greater than the sum of `refresh` and `retry`. pub expire_limit: u32, /// Duration, in seconds, of the minimum time-to-live. pub minimum_ttl: u32, } impl Wire for SOA { const NAME: &'static str = "SOA"; const RR_TYPE: u16 = 6; #[allow(clippy::similar_names)] #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let (mname, mname_length) = c.read_labels()?; trace!("Parsed mname -> {:?}", mname); let (rname, rname_length) = c.read_labels()?; trace!("Parsed rname -> {:?}", rname); let serial = c.read_u32::()?; trace!("Parsed serial -> {:?}", serial); let refresh_interval = c.read_u32::()?; trace!("Parsed refresh interval -> {:?}", refresh_interval); let retry_interval = c.read_u32::()?; trace!("Parsed retry interval -> {:?}", retry_interval); let expire_limit = c.read_u32::()?; trace!("Parsed expire limit -> {:?}", expire_limit); let minimum_ttl = c.read_u32::()?; trace!("Parsed minimum TTL -> {:?}", minimum_ttl); let length_after_labels = 4 * 5 + mname_length + rname_length; if stated_length == length_after_labels { trace!("Length is correct"); Ok(Self { mname, rname, serial, refresh_interval, retry_interval, expire_limit, minimum_ttl, }) } else { warn!("Length is incorrect (stated length {:?}, mname plus rname plus fields length {:?})", stated_length, length_after_labels); Err(WireError::WrongLabelLength { stated_length, length_after_labels }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, // mname 0x00, // mname terminator 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, // rname 0x00, // rname terminator 0x5d, 0x3c, 0xef, 0x02, // Serial 0x00, 0x01, 0x51, 0x80, // Refresh interval 0x00, 0x00, 0x1c, 0x20, // Retry interval 0x00, 0x09, 0x3a, 0x80, // Expire limit 0x00, 0x00, 0x01, 0x2c, // Minimum TTL ]; assert_eq!(SOA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), SOA { mname: Labels::encode("bsago.me").unwrap(), rname: Labels::encode("bsago.me").unwrap(), serial: 1564274434, refresh_interval: 86400, retry_interval: 7200, expire_limit: 604800, minimum_ttl: 300, }); } #[test] fn incorrect_record_length() { let buf = &[ 0x03, 0x65, 0x66, 0x67, // mname 0x00, // mname terminator 0x03, 0x65, 0x66, 0x67, // rname 0x00, // rname terminator 0x5d, 0x3c, 0xef, 0x02, // Serial 0x00, 0x01, 0x51, 0x80, // Refresh interval 0x00, 0x00, 0x1c, 0x20, // Retry interval 0x00, 0x09, 0x3a, 0x80, // Expire limit 0x00, 0x00, 0x01, 0x2c, // Minimum TTL ]; assert_eq!(SOA::read(89, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 89, length_after_labels: 30 })); } #[test] fn record_empty() { assert_eq!(SOA::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x05, 0x62, // the start of an mname ]; assert_eq!(SOA::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/srv.rs ================================================ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; /// A **SRV** record, which contains an IP address as well as a port number, /// for specifying the location of services more precisely. /// /// # References /// /// - [RFC 2782](https://tools.ietf.org/html/rfc2782) — A DNS RR for /// specifying the location of services (February 2000) #[derive(PartialEq, Debug)] pub struct SRV { /// The priority of this host among all that get returned. Lower values /// are higher priority. pub priority: u16, /// A weight to choose among results with the same priority. Higher values /// are higher priority. pub weight: u16, /// The port the service is serving on. pub port: u16, /// The hostname of the machine the service is running on. pub target: Labels, } impl Wire for SRV { const NAME: &'static str = "SRV"; const RR_TYPE: u16 = 33; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let priority = c.read_u16::()?; trace!("Parsed priority -> {:?}", priority); let weight = c.read_u16::()?; trace!("Parsed weight -> {:?}", weight); let port = c.read_u16::()?; trace!("Parsed port -> {:?}", port); let (target, target_length) = c.read_labels()?; trace!("Parsed target -> {:?}", target); let length_after_labels = 3 * 2 + target_length; if stated_length == length_after_labels { trace!("Length is correct"); Ok(Self { priority, weight, port, target }) } else { warn!("Length is incorrect (stated length {:?}, fields plus target length {:?})", stated_length, length_after_labels); Err(WireError::WrongLabelLength { stated_length, length_after_labels }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses() { let buf = &[ 0x00, 0x01, // priority 0x00, 0x01, // weight 0x92, 0x7c, // port 0x03, 0x61, 0x74, 0x61, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x03, 0x64, 0x63, 0x31, 0x06, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, // target 0x00, // target terminator ]; assert_eq!(SRV::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), SRV { priority: 1, weight: 1, port: 37500, target: Labels::encode("ata.local.node.dc1.consul").unwrap(), }); } #[test] fn incorrect_record_length() { let buf = &[ 0x00, 0x01, // priority 0x00, 0x01, // weight 0x92, 0x7c, // port 0x03, 0x61, 0x74, 0x61, // target 0x00, // target terminator ]; assert_eq!(SRV::read(16, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 16, length_after_labels: 11 })); } #[test] fn record_empty() { assert_eq!(SRV::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x00, // half a priority ]; assert_eq!(SRV::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/sshfp.rs ================================================ use log::*; use crate::wire::*; /// A **SSHFP** _(secure shell fingerprint)_ record, which contains the /// fingerprint of an SSH public key. /// /// # References /// /// - [RFC 4255](https://tools.ietf.org/html/rfc4255) — Using DNS to Securely /// Publish Secure Shell (SSH) Key Fingerprints (January 2006) #[derive(PartialEq, Debug)] pub struct SSHFP { /// The algorithm of the public key. This is a number with several defined /// mappings. pub algorithm: u8, /// The type of the fingerprint, which specifies the hashing algorithm /// used to derive the fingerprint. This is a number with several defined /// mappings. pub fingerprint_type: u8, /// The fingerprint of the public key. pub fingerprint: Vec, } impl Wire for SSHFP { const NAME: &'static str = "SSHFP"; const RR_TYPE: u16 = 44; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let algorithm = c.read_u8()?; trace!("Parsed algorithm -> {:?}", algorithm); let fingerprint_type = c.read_u8()?; trace!("Parsed fingerprint type -> {:?}", fingerprint_type); if stated_length <= 2 { let mandated_length = MandatedLength::AtLeast(3); return Err(WireError::WrongRecordLength { stated_length, mandated_length }); } let fingerprint_length = stated_length - 1 - 1; let mut fingerprint = vec![0_u8; usize::from(fingerprint_length)]; c.read_exact(&mut fingerprint)?; trace!("Parsed fingerprint -> {:#x?}", fingerprint); Ok(Self { algorithm, fingerprint_type, fingerprint }) } } impl SSHFP { /// Returns the hexadecimal representation of the fingerprint. pub fn hex_fingerprint(&self) -> String { self.fingerprint.iter() .map(|byte| format!("{:02x}", byte)) .collect() } } #[cfg(test)] mod test { use super::*; #[test] fn parses() { let buf = &[ 0x01, // algorithm 0x01, // fingerprint type 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, 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 ], }); } #[test] fn record_too_short() { let buf = &[ 0x01, // algorithm 0x01, // fingerprint type ]; assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 2, mandated_length: MandatedLength::AtLeast(3) })); } #[test] fn record_empty() { assert_eq!(SSHFP::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x01, // algorithm ]; assert_eq!(SSHFP::read(6, &mut Cursor::new(buf)), Err(WireError::IO)); } #[test] fn hex_rep() { let sshfp = SSHFP { algorithm: 1, fingerprint_type: 1, fingerprint: vec![ 0xf3, 0x48, 0xcd, 0xc9 ], }; assert_eq!(sshfp.hex_fingerprint(), String::from("f348cdc9")); } } ================================================ FILE: dns/src/record/tlsa.rs ================================================ use log::*; use crate::wire::*; /// A **TLSA** _(TLS authentication)_ record, which contains a TLS certificate /// (or a public key, or its hash), associating it with a domain. /// /// # References /// /// - [RFC 6698](https://tools.ietf.org/html/rfc6698) — The DNS-Based /// Authentication of Named Entities (DANE) Transport Layer Security /// Protocol: TLSA (August 2012) #[derive(PartialEq, Debug)] pub struct TLSA { /// A number representing the purpose of the certificate. pub certificate_usage: u8, /// A number representing which part of the certificate is returned in the /// data. This could be the full certificate, or just the public key. pub selector: u8, /// A number representing whether a certificate should be associated with /// the exact data, or with a hash of it. pub matching_type: u8, /// A series of bytes representing the certificate. pub certificate_data: Vec, } impl Wire for TLSA { const NAME: &'static str = "TLSA"; const RR_TYPE: u16 = 52; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let certificate_usage = c.read_u8()?; trace!("Parsed certificate_usage -> {:?}", certificate_usage); let selector = c.read_u8()?; trace!("Parsed selector -> {:?}", selector); let matching_type = c.read_u8()?; trace!("Parsed matching type -> {:?}", matching_type); if stated_length <= 3 { let mandated_length = MandatedLength::AtLeast(4); return Err(WireError::WrongRecordLength { stated_length, mandated_length }); } let certificate_data_length = stated_length - 1 - 1 - 1; let mut certificate_data = vec![0_u8; usize::from(certificate_data_length)]; c.read_exact(&mut certificate_data)?; trace!("Parsed fingerprint -> {:#x?}", certificate_data); Ok(Self { certificate_usage, selector, matching_type, certificate_data }) } } impl TLSA { /// Returns the hexadecimal representation of the fingerprint. pub fn hex_certificate_data(&self) -> String { self.certificate_data.iter() .map(|byte| format!("{:02x}", byte)) .collect() } } #[cfg(test)] mod test { use super::*; #[test] fn parses() { let buf = &[ 0x03, // certificate usage 0x01, // selector 0x01, // matching type 0x05, 0x95, 0x98, 0x11, 0x22, 0x33 // 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, 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 ], }); } #[test] fn record_too_short() { let buf = &[ 0x03, // certificate usage 0x01, // selector 0x01, // matching type ]; assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)), Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::AtLeast(4) })); } #[test] fn record_empty() { assert_eq!(TLSA::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x01, // certificate_usage ]; assert_eq!(TLSA::read(6, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/txt.rs ================================================ use log::*; use crate::wire::*; /// A **TXT** record, which holds arbitrary descriptive text. /// /// # Encoding /// /// The text encoding is not specified, but this crate treats it as UTF-8. /// Invalid bytes are turned into the replacement character. /// /// # References /// /// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) #[derive(PartialEq, Debug)] pub struct TXT { /// The messages contained in the record. pub messages: Vec>, } impl Wire for TXT { const NAME: &'static str = "TXT"; const RR_TYPE: u16 = 16; #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { let mut messages = Vec::new(); let mut total_length = 0_u16; loop { let mut buf = Vec::new(); loop { let next_length = c.read_u8()?; total_length += u16::from(next_length) + 1; trace!("Parsed slice length -> {:?} (total so far {:?})", next_length, total_length); for _ in 0 .. next_length { buf.push(c.read_u8()?); } if next_length < 255 { break; } else { trace!("Got length 255, so looping"); } } let message = buf.into_boxed_slice(); trace!("Parsed message -> {:?}", String::from_utf8_lossy(&message)); messages.push(message); if total_length >= stated_length { break; } } if stated_length == total_length { trace!("Length is correct"); Ok(Self { messages }) } else { warn!("Length is incorrect (stated length {:?}, messages length {:?})", stated_length, total_length); Err(WireError::WrongLabelLength { stated_length, length_after_labels: total_length }) } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn parses_one_iteration() { let buf = &[ 0x06, // message chunk length 0x74, 0x78, 0x74, 0x20, 0x6d, 0x65, // message chunk ]; assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), TXT { messages: vec![ Box::new(*b"txt me") ], }); } #[test] fn parses_two_iterations() { let buf = &[ 0xFF, // message chunk length 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, // exactly two hundred and fifty five ‘A’s (screaming) 0x04, // message chunk length 0x41, 0x41, 0x41, 0x41, // four more ‘A’s (the scream abruptly stops) ]; assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), TXT { messages: vec![ Box::new(*b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ AAAAAAAAAAAAAAAAAAAAAAAAAAA"), ], }); // did you know you can just _write_ code like this, and nobody will stop you? } #[test] fn right_at_the_limit() { let buf = &[ 0xFE, // message chunk length 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, // exactly two hundred and fifty four ‘B’s (a hive) ]; assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), TXT { messages: vec![ Box::new(*b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ BBBBBBBBBBBBBBBBBBBBBB"), ], }); } #[test] fn another_message() { let buf = &[ 0x06, // message chunk length 0x74, 0x78, 0x74, 0x20, 0x6d, 0x65, // message chunk 0x06, // message chunk length 0x79, 0x61, 0x20, 0x62, 0x65, 0x62, // message chunk ]; assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(), TXT { messages: vec![ Box::new(*b"txt me"), Box::new(*b"ya beb"), ], }); } #[test] fn length_too_short() { let buf = &[ 0x06, // message chunk length 0x74, 0x78, 0x74, 0x20, 0x6d, 0x65, // message chunk ]; assert_eq!(TXT::read(2, &mut Cursor::new(buf)), Err(WireError::WrongLabelLength { stated_length: 2, length_after_labels: 7 })); } #[test] fn record_empty() { assert_eq!(TXT::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); } #[test] fn buffer_ends_abruptly() { let buf = &[ 0x06, 0x74, // the start of a message ]; assert_eq!(TXT::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } ================================================ FILE: dns/src/record/uri.rs ================================================ 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: Box<[u8]>, } 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 { let priority = c.read_u16::()?; trace!("Parsed priority -> {:?}", priority); let weight = c.read_u16::()?; 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 target = vec![0_u8; usize::from(remaining_length)].into_boxed_slice(); c.read_exact(&mut target)?; trace!("Parsed target -> {:?}", String::from_utf8_lossy(&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: Box::new(*b"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: Box::new(*b"/"), }); } #[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)); } } ================================================ FILE: dns/src/strings.rs ================================================ //! Reading strings from the DNS wire protocol. use std::convert::TryFrom; use std::fmt; use std::io::{self, Write}; use byteorder::{ReadBytesExt, WriteBytesExt}; use log::*; use crate::wire::*; /// Domain names in the DNS protocol are encoded as **Labels**, which are /// segments of ASCII characters prefixed by their length. When written out, /// each segment is followed by a dot. /// /// The maximum length of a segment is 255 characters. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct Labels { segments: Vec<(u8, String)>, } #[cfg(feature = "with_idna")] fn label_to_ascii(label: &str) -> Result { let flags = unic_idna::Flags{use_std3_ascii_rules: false, transitional_processing: false, verify_dns_length: true}; unic_idna::to_ascii(label, flags) } #[cfg(not(feature = "with_idna"))] fn label_to_ascii(label: &str) -> Result { Ok(label.to_owned()) } impl Labels { /// Creates a new empty set of labels, which represent the root of the DNS /// as a domain with no name. pub fn root() -> Self { Self { segments: Vec::new() } } /// Encodes the given input string as labels. If any segment is too long, /// returns that segment as an error. pub fn encode(input: &str) -> Result { let mut segments = Vec::new(); for label in input.split('.') { if label.is_empty() { continue; } 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_idn)); } Err(e) => { warn!("Could not encode label {:?}: {}", label, e); return Err(label); } } } 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (_, segment) in &self.segments { write!(f, "{}.", segment)?; } Ok(()) } } /// An extension for `Cursor` that enables reading compressed domain names /// from DNS packets. pub(crate) trait ReadLabels { /// Read and expand a compressed domain name. fn read_labels(&mut self) -> Result<(Labels, u16), WireError>; } impl ReadLabels for Cursor<&[u8]> { fn read_labels(&mut self) -> Result<(Labels, u16), WireError> { let mut labels = Labels { segments: Vec::new() }; let bytes_read = read_string_recursive(&mut labels, self, &mut Vec::new())?; Ok((labels, bytes_read)) } } /// An extension for `Write` that enables writing domain names. pub(crate) trait WriteLabels { /// Write a domain name. /// /// The names being queried are written with one byte slice per /// domain segment, preceded by each segment’s length, with the /// whole thing ending with a segment of zero length. /// /// So “dns.lookup.dog” would be encoded as: /// “3, dns, 6, lookup, 3, dog, 0”. fn write_labels(&mut self, input: &Labels) -> io::Result<()>; } impl WriteLabels for W { fn write_labels(&mut self, input: &Labels) -> io::Result<()> { for (length, label) in &input.segments { self.write_u8(*length)?; for b in label.as_bytes() { self.write_u8(*b)?; } } self.write_u8(0)?; // terminate the string Ok(()) } } const RECURSION_LIMIT: usize = 8; /// Reads bytes from the given cursor into the given buffer, using the list of /// 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(feature = "with_mutagen", ::mutagen::mutate)] fn read_string_recursive(labels: &mut Labels, c: &mut Cursor<&[u8]>, recursions: &mut Vec) -> Result { let mut bytes_read = 0; loop { let byte = c.read_u8()?; bytes_read += 1; if byte == 0 { break; } else if byte >= 0b_1100_0000 { let name_one = byte - 0b1100_0000; let name_two = c.read_u8()?; bytes_read += 1; let offset = u16::from_be_bytes([name_one, name_two]); if recursions.contains(&offset) { warn!("Hit previous offset ({}) decoding string", offset); return Err(WireError::TooMuchRecursion(recursions.clone().into_boxed_slice())); } recursions.push(offset); if recursions.len() >= RECURSION_LIMIT { warn!("Hit recursion limit ({}) decoding string", RECURSION_LIMIT); return Err(WireError::TooMuchRecursion(recursions.clone().into_boxed_slice())); } trace!("Backtracking to offset {}", offset); let new_pos = c.position(); c.set_position(u64::from(offset)); read_string_recursive(labels, c, recursions)?; trace!("Coming back to {}", new_pos); c.set_position(new_pos); break; } // Otherwise, treat the byte as the length of a label, and read that // many characters. else { let mut name_buf = Vec::new(); for _ in 0 .. byte { let c = c.read_u8()?; bytes_read += 1; name_buf.push(c); } let string = String::from_utf8_lossy(&*name_buf).to_string(); labels.segments.push((byte, string)); } } Ok(bytes_read) } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; // The buffers used in these tests contain nothing but the labels we’re // decoding. In DNS packets found in the wild, the cursor will be able to // reach all the bytes of the packet, so the Answer section can reference // strings in the Query section. #[test] fn nothing() { let buf: &[u8] = &[ 0x00, // end reading ]; assert_eq!(Cursor::new(buf).read_labels(), Ok((Labels::root(), 1))); } #[test] fn one_label() { let buf: &[u8] = &[ 0x03, // label of length 3 b'o', b'n', b'e', // label 0x00, // end reading ]; assert_eq!(Cursor::new(buf).read_labels(), Ok((Labels::encode("one.").unwrap(), 5))); } #[test] fn two_labels() { let buf: &[u8] = &[ 0x03, // label of length 3 b'o', b'n', b'e', // label 0x03, // label of length 3 b't', b'w', b'o', // label 0x00, // end reading ]; assert_eq!(Cursor::new(buf).read_labels(), Ok((Labels::encode("one.two.").unwrap(), 9))); } #[test] fn label_followed_by_backtrack() { let buf: &[u8] = &[ 0x03, // label of length 3 b'o', b'n', b'e', // label 0xc0, 0x06, // skip to position 6 (the next byte) 0x03, // label of length 3 b't', b'w', b'o', // label 0x00, // end reading ]; assert_eq!(Cursor::new(buf).read_labels(), Ok((Labels::encode("one.two.").unwrap(), 6))); } #[test] fn extremely_long_label() { let mut buf: Vec = vec![ 0xbf, // label of length 191 ]; buf.extend(vec![0x65; 191]); // the rest of the label buf.push(0x00); // end reading assert_eq!(Cursor::new(&*buf).read_labels().unwrap().1, 193); } #[test] fn immediate_recursion() { let buf: &[u8] = &[ 0xc0, 0x00, // skip to position 0 ]; assert_eq!(Cursor::new(buf).read_labels(), Err(WireError::TooMuchRecursion(Box::new([ 0 ])))); } #[test] fn mutual_recursion() { let buf: &[u8] = &[ 0xc0, 0x02, // skip to position 2 0xc0, 0x00, // skip to position 0 ]; let mut cursor = Cursor::new(buf); assert_eq!(cursor.read_labels(), Err(WireError::TooMuchRecursion(Box::new([ 2, 0 ])))); } #[test] fn too_much_recursion() { let buf: &[u8] = &[ 0xc0, 0x02, // skip to position 2 0xc0, 0x04, // skip to position 4 0xc0, 0x06, // skip to position 6 0xc0, 0x08, // skip to position 8 0xc0, 0x0A, // skip to position 10 0xc0, 0x0C, // skip to position 12 0xc0, 0x0E, // skip to position 14 0xc0, 0x10, // skip to position 16 0x00, // no label ]; let mut cursor = Cursor::new(buf); assert_eq!(cursor.read_labels(), Err(WireError::TooMuchRecursion(Box::new([ 2, 4, 6, 8, 10, 12, 14, 16 ])))); } } ================================================ FILE: dns/src/types.rs ================================================ //! DNS packets are traditionally implemented with both the request and //! response packets at the same type. After all, both follow the same format, //! with the request packet having zero answer fields, and the response packet //! having at least one record in its answer fields. use crate::record::{Record, RecordType, OPT}; use crate::strings::Labels; /// A request that gets sent out over a transport. #[derive(PartialEq, Debug)] pub struct Request { /// The transaction ID of this request. This is used to make sure /// different DNS packets don’t answer each other’s questions. pub transaction_id: u16, /// The flags that accompany every DNS packet. pub flags: Flags, /// The query that this request is making. Only one query is allowed per /// request, as traditionally, DNS servers only respond to the first query /// in a packet. pub query: Query, /// An additional record that may be sent as part of the query. pub additional: Option, } /// A response obtained from a DNS server. #[derive(PartialEq, Debug)] pub struct Response { /// The transaction ID, which should match the ID of the request. pub transaction_id: u16, /// The flags that accompany every DNS packet. pub flags: Flags, /// The queries section. pub queries: Vec, /// The answers section. pub answers: Vec, /// The authoritative nameservers section. pub authorities: Vec, /// The additional records section. pub additionals: Vec, } /// A DNS query section. #[derive(PartialEq, Debug)] pub struct Query { /// The domain name being queried, in human-readable dotted notation. pub qname: Labels, /// The class number. pub qclass: QClass, /// The type number. pub qtype: RecordType, } /// A DNS answer section. #[derive(PartialEq, Debug)] pub enum Answer { /// This is a standard answer with every field. Standard { /// The domain name being answered for. qname: Labels, /// This answer’s class. qclass: QClass, /// The time-to-live duration, in seconds. ttl: u32, /// The record contained in this answer. record: Record, }, /// This is a pseudo-record answer, so some of the fields (class and TTL) /// have different meaning. Pseudo { /// The domain name being answered for. qname: Labels, /// The OPT record contained in this answer. opt: OPT, }, } /// A DNS record class. Of these, the only one that’s in regular use anymore /// is the Internet class. #[derive(PartialEq, Debug, Copy, Clone)] pub enum QClass { /// The **Internet** class. IN, /// The **Chaosnet** class. CH, /// The **Hesiod** class. HS, /// A class number that does not map to any known class. Other(u16), } /// The flags that accompany every DNS packet. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Flags { /// Whether this packet is a response packet. pub response: bool, /// The operation being performed. pub opcode: Opcode, /// In a response, whether the server is providing authoritative DNS responses. pub authoritative: bool, /// In a response, whether this message has been truncated by the transport. pub truncated: bool, /// In a query, whether the server may query other nameservers recursively. /// It is up to the server whether it will actually do this. pub recursion_desired: bool, /// In a response, whether the server allows recursive query support. pub recursion_available: bool, /// In a response, whether the server is marking this data as authentic. pub authentic_data: bool, /// In a request, whether the server should disable its authenticity /// checking for the request’s queries. pub checking_disabled: bool, /// In a response, a code indicating an error if one occurred. pub error_code: Option, } /// A number representing the operation being performed. #[derive(PartialEq, Debug, Copy, Clone)] pub enum Opcode { /// This request is a standard query, or this response is answering a /// standard query. Query, /// Any other opcode. This can be from 1 to 15, as the opcode field is /// four bits wide, and 0 is taken. Other(u8), } /// A code indicating an error. /// /// # References /// /// - [RFC 6895 §2.3](https://tools.ietf.org/html/rfc6895#section-2.3) — Domain /// Name System (DNS) IANA Considerations (April 2013) #[derive(PartialEq, Debug, Copy, Clone)] pub enum ErrorCode { /// `FormErr` — The server was unable to interpret the query. FormatError, /// `ServFail` — There was a problem with the server. ServerFailure, /// `NXDomain` — The domain name referenced in the query does not exist. NXDomain, /// `NotImp` — The server does not support one of the requested features. NotImplemented, /// `Refused` — The server was able to interpret the query, but refused to /// fulfil it. QueryRefused, /// `BADVERS` and `BADSIG` — The server did not accept the EDNS version, /// or failed to verify a signature. The same code is used for both. BadVersion, /// An error code with no currently-defined meaning. Other(u16), /// An error code within the ‘Reserved for Private Use’ range. Private(u16) } impl Answer { /// Whether this Answer holds a standard record, not a pseudo record. pub fn is_standard(&self) -> bool { matches!(self, Self::Standard { .. }) } } ================================================ FILE: dns/src/wire.rs ================================================ //! Parsing the DNS wire protocol. pub(crate) use std::io::{Cursor, Read}; pub(crate) use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io; use log::*; use crate::record::{Record, RecordType, OPT}; use crate::strings::{Labels, ReadLabels, WriteLabels}; use crate::types::*; impl Request { /// Converts this request to a vector of bytes. pub fn to_bytes(&self) -> io::Result> { let mut bytes = Vec::with_capacity(32); bytes.write_u16::(self.transaction_id)?; bytes.write_u16::(self.flags.to_u16())?; bytes.write_u16::(1)?; // query count bytes.write_u16::(0)?; // answer count bytes.write_u16::(0)?; // authority RR count bytes.write_u16::(if self.additional.is_some() { 1 } else { 0 })?; // additional RR count bytes.write_labels(&self.query.qname)?; bytes.write_u16::(self.query.qtype.type_number())?; bytes.write_u16::(self.query.qclass.to_u16())?; if let Some(opt) = &self.additional { bytes.write_u8(0)?; // usually a name bytes.write_u16::(OPT::RR_TYPE)?; bytes.extend(opt.to_bytes()?); } Ok(bytes) } /// Returns the OPT record to be sent as part of requests. pub fn additional_record() -> OPT { OPT { udp_payload_size: 512, higher_bits: 0, edns0_version: 0, flags: 0, data: Vec::new(), } } } impl Response { /// Reads bytes off of the given slice, parsing them into a response. #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] pub fn from_bytes(bytes: &[u8]) -> Result { info!("Parsing response"); trace!("Bytes -> {:?}", bytes); let mut c = Cursor::new(bytes); let transaction_id = c.read_u16::()?; trace!("Read txid -> {:?}", transaction_id); let flags = Flags::from_u16(c.read_u16::()?); trace!("Read flags -> {:#?}", flags); let query_count = c.read_u16::()?; let answer_count = c.read_u16::()?; let authority_count = c.read_u16::()?; let additional_count = c.read_u16::()?; // We can pre-allocate these vectors by giving them an initial // capacity based on the count fields. But because the count fields // are user-controlled (with a maximum of 2^16 - 1) we cannot trust // them _entirely_, so cap the pre-allocation if the count looks // arbitrarily large (9 seems about right). let mut queries = Vec::with_capacity(usize::from(query_count.min(9))); debug!("Reading {}x query from response", query_count); for _ in 0 .. query_count { let (qname, _) = c.read_labels()?; queries.push(Query::from_bytes(qname, &mut c)?); } let mut answers = Vec::with_capacity(usize::from(answer_count.min(9))); debug!("Reading {}x answer from response", answer_count); for _ in 0 .. answer_count { let (qname, _) = c.read_labels()?; answers.push(Answer::from_bytes(qname, &mut c)?); } let mut authorities = Vec::with_capacity(usize::from(authority_count.min(9))); debug!("Reading {}x authority from response", authority_count); for _ in 0 .. authority_count { let (qname, _) = c.read_labels()?; authorities.push(Answer::from_bytes(qname, &mut c)?); } let mut additionals = Vec::with_capacity(usize::from(additional_count.min(9))); debug!("Reading {}x additional answer from response", additional_count); for _ in 0 .. additional_count { let (qname, _) = c.read_labels()?; additionals.push(Answer::from_bytes(qname, &mut c)?); } Ok(Self { transaction_id, flags, queries, answers, authorities, additionals }) } } impl Query { /// Reads bytes from the given cursor, and parses them into a query with /// the given domain name. #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result { let qtype_number = c.read_u16::()?; trace!("Read qtype number -> {:?}", qtype_number ); let qtype = RecordType::from(qtype_number); trace!("Found qtype -> {:?}", qtype ); let qclass = QClass::from_u16(c.read_u16::()?); trace!("Read qclass -> {:?}", qtype); Ok(Self { qtype, qclass, qname }) } } impl Answer { /// Reads bytes from the given cursor, and parses them into an answer with /// the given domain name. #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result { let qtype_number = c.read_u16::()?; trace!("Read qtype number -> {:?}", qtype_number ); if qtype_number == OPT::RR_TYPE { let opt = OPT::read(c)?; Ok(Self::Pseudo { qname, opt }) } else { let qtype = RecordType::from(qtype_number); trace!("Found qtype -> {:?}", qtype ); let qclass = QClass::from_u16(c.read_u16::()?); trace!("Read qclass -> {:?}", qtype); let ttl = c.read_u32::()?; trace!("Read TTL -> {:?}", ttl); let record_length = c.read_u16::()?; trace!("Read record length -> {:?}", record_length); let record = Record::from_bytes(qtype, record_length, c)?; Ok(Self::Standard { qclass, qname, record, ttl }) } } } 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(feature = "with_mutagen", ::mutagen::mutate)] fn from_bytes(record_type: RecordType, len: u16, c: &mut Cursor<&[u8]>) -> Result { if cfg!(feature = "with_mutagen") { warn!("Mutation is enabled!"); } macro_rules! read_record { ($record:tt) => { { info!("Parsing {} record (type {}, len {})", crate::record::$record::NAME, record_type.type_number(), len); Wire::read(len, c).map(Self::$record) } } } match record_type { RecordType::A => read_record!(A), RecordType::AAAA => read_record!(AAAA), RecordType::CAA => read_record!(CAA), RecordType::CNAME => read_record!(CNAME), RecordType::EUI48 => read_record!(EUI48), RecordType::EUI64 => read_record!(EUI64), RecordType::HINFO => read_record!(HINFO), RecordType::LOC => read_record!(LOC), RecordType::MX => read_record!(MX), RecordType::NAPTR => read_record!(NAPTR), RecordType::NS => read_record!(NS), RecordType::OPENPGPKEY => read_record!(OPENPGPKEY), RecordType::PTR => read_record!(PTR), RecordType::SSHFP => read_record!(SSHFP), RecordType::SOA => read_record!(SOA), RecordType::SRV => read_record!(SRV), RecordType::TLSA => read_record!(TLSA), RecordType::TXT => read_record!(TXT), RecordType::URI => read_record!(URI), RecordType::Other(type_number) => { let mut bytes = Vec::new(); for _ in 0 .. len { bytes.push(c.read_u8()?); } Ok(Self::Other { type_number, bytes }) } } } } impl QClass { fn from_u16(uu: u16) -> Self { match uu { 0x0001 => Self::IN, 0x0003 => Self::CH, 0x0004 => Self::HS, _ => Self::Other(uu), } } fn to_u16(self) -> u16 { match self { Self::IN => 0x0001, Self::CH => 0x0003, Self::HS => 0x0004, Self::Other(uu) => uu, } } } impl Flags { /// The set of flags that represents a query packet. pub fn query() -> Self { Self::from_u16(0b_0000_0001_0000_0000) } /// The set of flags that represents a successful response. pub fn standard_response() -> Self { Self::from_u16(0b_1000_0001_1000_0000) } /// 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; } match self.opcode { 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; } // (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; } bits } /// Extracts the flags from the given two-byte number. pub fn from_u16(bits: u16) -> Self { let has_bit = |bit| { bits & bit == bit }; Self { response: has_bit(0b_1000_0000_0000_0000), opcode: Opcode::from_bits((bits.to_be_bytes()[0] & 0b_0111_1000) >> 3), authoritative: has_bit(0b_0000_0100_0000_0000), truncated: has_bit(0b_0000_0010_0000_0000), recursion_desired: has_bit(0b_0000_0001_0000_0000), recursion_available: has_bit(0b_0000_0000_1000_0000), authentic_data: has_bit(0b_0000_0000_0010_0000), checking_disabled: has_bit(0b_0000_0000_0001_0000), error_code: ErrorCode::from_bits(bits & 0b_1111), } } } impl Opcode { /// Extracts the opcode from this four-bit number, which should have been /// extracted from the packet and shifted to be in the range 0–15. fn from_bits(bits: u8) -> Self { if bits == 0 { Self::Query } else { assert!(bits <= 15, "bits {:#08b} out of range", bits); Self::Other(bits) } } } impl ErrorCode { /// Extracts the rcode from the last four bits of the flags field. fn from_bits(bits: u16) -> Option { if (0x0F01 .. 0x0FFF).contains(&bits) { return Some(Self::Private(bits)); } match bits { 0 => None, 1 => Some(Self::FormatError), 2 => Some(Self::ServerFailure), 3 => Some(Self::NXDomain), 4 => Some(Self::NotImplemented), 5 => Some(Self::QueryRefused), 16 => Some(Self::BadVersion), n => Some(Self::Other(n)), } } } /// Trait for decoding DNS record structures from bytes read over the wire. pub trait Wire: Sized { /// This record’s type as a string, such as `"A"` or `"CNAME"`. const NAME: &'static str; /// The number signifying that a record is of this type. /// See const RR_TYPE: u16; /// Read at most `len` bytes from the given `Cursor`. This cursor travels /// throughout the complete data — by this point, we have read the entire /// response into a buffer. fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result; } /// Something that can go wrong deciphering a record. #[derive(PartialEq, Debug)] pub enum WireError { /// There was an IO error reading from the cursor. /// Almost all the time, this means that the buffer was too short. IO, // (io::Error is not PartialEq so we don’t propagate it) /// When the DNS standard requires records of this type to have a certain /// fixed length, but the response specified a different length. /// /// This error should be returned regardless of the _content_ of the /// record, whatever it is. WrongRecordLength { /// The length of the record’s data, as specified in the packet. stated_length: u16, /// The length of the record that the DNS specification mandates. mandated_length: MandatedLength, }, /// When the length of this record as specified in the packet differs from /// the computed length, as determined by reading labels. /// /// There are two ways, in general, to read arbitrary-length data from a /// stream of bytes: length-prefixed (read the length, then read that many /// bytes) or sentinel-terminated (keep reading bytes until you read a /// certain value, usually zero). The DNS protocol uses both: each /// record’s size is specified up-front in the packet, but inside the /// record, there exist arbitrary-length strings that must be read until a /// zero is read, indicating there is no more string. /// /// Consider the case of a packet, with a specified length, containing a /// string of arbitrary length (such as the CNAME or TXT records). A DNS /// client has to deal with this in one of two ways: /// /// 1. Read exactly the specified length of bytes from the record, raising /// an error if the contents are too short or a string keeps going past /// the length (assume the length is correct but the contents are wrong). /// /// 2. Read as many bytes from the record as the string requests, raising /// an error if the number of bytes read at the end differs from the /// expected length of the record (assume the length is wrong but the /// contents are correct). /// /// Note that no matter which way is picked, the record will still be /// incorrect — it only impacts the parsing of records that occur after it /// in the packet. Knowing which method should be used requires knowing /// what caused the DNS packet to be erroneous, which we cannot know. /// /// dog picks the second way. If a record ends up reading more or fewer /// bytes than it is ‘supposed’ to, it will raise this error, but _after_ /// having read a different number of bytes than the specified length. WrongLabelLength { /// The length of the record’s data, as specified in the packet. stated_length: u16, /// The computed length of the record’s data, based on the number of /// bytes consumed by reading labels from the packet. length_after_labels: u16, }, /// When the data contained a string containing a cycle of pointers. /// Contains the vector of indexes that was being checked. TooMuchRecursion(Box<[u16]>), /// When the data contained a string with a pointer to an index outside of /// the packet. Contains the invalid index. OutOfBounds(u16), /// When a record in the packet contained a version field that specifies /// the format of its remaining fields, but this version is too recent to /// be supported, so we cannot parse it. WrongVersion { /// The version of the record layout, as specified in the packet stated_version: u8, /// The maximum version that this version of dog supports. maximum_supported_version: u8, } } /// The rule for how long a record in a packet should be. #[derive(PartialEq, Debug, Copy, Clone)] pub enum MandatedLength { /// The record should be exactly this many bytes in length. Exactly(u16), /// The record should be _at least_ this many bytes in length. AtLeast(u16), } impl From for WireError { fn from(ioe: io::Error) -> Self { error!("IO error -> {:?}", ioe); Self::IO } } ================================================ FILE: dns/tests/wire_building_tests.rs ================================================ use dns::{Request, Flags, Query, Labels, QClass}; use dns::record::RecordType; 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: RecordType::from(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); } ================================================ FILE: dns/tests/wire_parsing_tests.rs ================================================ use std::net::Ipv4Addr; use dns::{Response, Query, Answer, Labels, Flags, Opcode, QClass}; use dns::record::{Record, A, CNAME, OPT, SOA, UnknownQtype, RecordType}; use pretty_assertions::assert_eq; #[test] fn parse_nothing() { assert!(Response::from_bytes(&[]).is_err()); } #[test] fn parse_response_standard() { let buf = &[ 0x0d, 0xcd, // transaction ID 0x81, 0x80, // flags (standard query, response, no error) 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // counts (1, 1, 0, 1) // the query: 0x03, 0x64, 0x6e, 0x73, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03, 0x64, 0x6f, 0x67, 0x00, // "dns.lookup.dog." 0x00, 0x01, // type A 0x00, 0x01, // class IN // the answer: 0xc0, 0x0c, // to find the name, backtrack to position 0x0c (12) 0x00, 0x01, // type A 0x00, 0x01, // class IN 0x00, 0x00, 0x03, 0xa5, // TTL (933 seconds) 0x00, 0x04, // record data length 4 0x8a, 0x44, 0x75, 0x5e, // record date (138.68.117.94) // the additional: 0x00, // no name 0x00, 0x29, // type OPT 0x02, 0x00, // UDP payload size (512) 0x00, 0x00, // higher bits (all 0) 0x00, // EDNS version 0x00, 0x00, // extra bits (DO bit unset) 0x00, // data length 0 ]; let response = Response { transaction_id: 0x0dcd, flags: Flags { response: true, opcode: Opcode::Query, authoritative: false, truncated: false, recursion_desired: true, recursion_available: true, authentic_data: false, checking_disabled: false, error_code: None, }, queries: vec![ Query { qname: Labels::encode("dns.lookup.dog").unwrap(), qclass: QClass::IN, qtype: RecordType::A, }, ], answers: vec![ Answer::Standard { qname: Labels::encode("dns.lookup.dog").unwrap(), qclass: QClass::IN, ttl: 933, record: Record::A(A { address: Ipv4Addr::new(138, 68, 117, 94), }), } ], authorities: vec![], additionals: vec![ 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)); } #[test] fn parse_response_with_mixed_string() { let buf = &[ 0x06, 0x9f, // transaction ID 0x81, 0x80, // flags (standard query, response, no error) 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // counts (1, 1, 0, 0) // the query: 0x0d, 0x63, 0x6e, 0x61, 0x6d, 0x65, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03, 0x64, 0x6f, 0x67, 0x00, // "cname-example.lookup.dog" 0x00, 0x05, // type CNAME 0x00, 0x01, // class IN // the answer: 0xc0, 0x0c, // to find the name, backtrack to position 0x0c (12) 0x00, 0x05, // type CNAME 0x00, 0x01, // class IN 0x00, 0x00, 0x03, 0x69, // TTL (873 seconds) 0x00, 0x06, // record data length 6 0x03, 0x64, 0x6e, 0x73, 0xc0, 0x1a, // "dns.lookup.dog.", which is "dns." + backtrack to position 0x1a (28) ]; let response = Response { transaction_id: 0x069f, flags: Flags { response: true, opcode: Opcode::Query, authoritative: false, truncated: false, recursion_desired: true, recursion_available: true, authentic_data: false, checking_disabled: false, error_code: None, }, queries: vec![ Query { qname: Labels::encode("cname-example.lookup.dog").unwrap(), qclass: QClass::IN, qtype: RecordType::CNAME, }, ], answers: vec![ Answer::Standard { qname: Labels::encode("cname-example.lookup.dog").unwrap(), qclass: QClass::IN, ttl: 873, record: Record::CNAME(CNAME { domain: Labels::encode("dns.lookup.dog").unwrap(), }), } ], authorities: vec![], additionals: vec![], }; 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: RecordType::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)); } ================================================ FILE: dns-transport/Cargo.toml ================================================ [package] name = "dns-transport" version = "0.2.0-pre" authors = ["Benjamin Sago "] edition = "2018" [lib] doctest = false test = false [dependencies] # dns wire protocol dns = { path = "../dns" } # logging log = "0.4" # tls networking native-tls = { version = "0.2", optional = true } # http response parsing httparse = { version = "1.3", optional = true } rustls = { version = "0.19", optional = true } webpki = { version = "0.21.0", optional = true } webpki-roots = { version = "0.21.0", optional = true } cfg-if = "1" [features] default = [] # these are enabled in the main dog crate with_tls = [] with_https = ["httparse"] with_nativetls = ["native-tls"] with_nativetls_vendored = ["native-tls", "native-tls/vendored"] with_rustls = ["rustls", "webpki-roots", "webpki"] ================================================ FILE: dns-transport/src/auto.rs ================================================ use log::*; use dns::{Request, Response}; 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. /// /// This is the default behaviour for many DNS clients. pub struct AutoTransport { addr: String, } impl AutoTransport { /// Creates a new automatic transport that connects to the given host. pub fn new(addr: String) -> Self { Self { addr } } } impl Transport for AutoTransport { fn send(&self, request: &Request) -> Result { let udp_transport = UdpTransport::new(self.addr.clone()); let udp_response = udp_transport.send(&request)?; if ! udp_response.flags.truncated { return Ok(udp_response); } debug!("Truncated flag set, so switching to TCP"); let tcp_transport = TcpTransport::new(self.addr.clone()); let tcp_response = tcp_transport.send(&request)?; Ok(tcp_response) } } ================================================ FILE: dns-transport/src/error.rs ================================================ /// Something that can go wrong making a DNS request. #[derive(Debug)] pub enum Error { /// The data in the response did not parse correctly from the DNS wire /// protocol format. WireError(dns::WireError), /// There was a problem with the network making a TCP or UDP request. NetworkError(std::io::Error), /// Not enough information was received from the server before a `read` /// call returned zero bytes. TruncatedResponse, /// There was a problem making a TLS request. #[cfg(feature = "with_nativetls")] TlsError(native_tls::Error), /// There was a problem _establishing_ a TLS request. #[cfg(feature = "with_nativetls")] TlsHandshakeError(native_tls::HandshakeError), /// Provided dns name is not valid #[cfg(feature = "with_rustls")] RustlsInvalidDnsNameError(webpki::InvalidDNSNameError), /// There was a problem decoding the response HTTP headers or body. #[cfg(feature = "with_https")] HttpError(httparse::Error), /// The HTTP response code was something other than 200 OK, along with the /// response code text, if present. #[cfg(feature = "with_https")] WrongHttpStatus(u16, Option), } // From impls impl From for Error { fn from(inner: dns::WireError) -> Self { Self::WireError(inner) } } impl From for Error { fn from(inner: std::io::Error) -> Self { Self::NetworkError(inner) } } #[cfg(feature = "with_nativetls")] impl From for Error { fn from(inner: native_tls::Error) -> Self { Self::TlsError(inner) } } #[cfg(feature = "with_nativetls")] impl From> for Error { fn from(inner: native_tls::HandshakeError) -> Self { Self::TlsHandshakeError(inner) } } #[cfg(feature = "with_rustls")] impl From for Error { fn from(inner: webpki::InvalidDNSNameError) -> Self { Self::RustlsInvalidDnsNameError(inner) } } #[cfg(feature = "with_https")] impl From for Error { fn from(inner: httparse::Error) -> Self { Self::HttpError(inner) } } ================================================ FILE: dns-transport/src/https.rs ================================================ #![cfg_attr(not(feature = "https"), allow(unused))] use std::io::{Read, Write}; use std::net::TcpStream; use log::*; use dns::{Request, Response, WireError}; use super::{Transport, Error}; use super::tls_stream; /// The **HTTPS transport**, which sends DNS wire data inside HTTP packets /// encrypted with TLS, using TCP. pub struct HttpsTransport { url: String, } impl HttpsTransport { /// Creates a new HTTPS transport that connects to the given URL. pub fn new(url: String) -> Self { Self { url } } } fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { haystack.windows(needle.len()).position(|window| window == needle) } fn contains_header(buf: &[u8]) -> bool { let header_end: [u8; 4] = [ 13, 10, 13, 10 ]; find_subsequence(buf, &header_end).is_some() } use tls_stream::TlsStream; impl Transport for HttpsTransport { #[cfg(any(feature = "with_https"))] fn send(&self, request: &Request) -> Result { let (domain, path) = self.split_domain().expect("Invalid HTTPS nameserver"); info!("Opening TLS socket to {:?}", domain); let mut stream = Self::stream(&domain, 443)?; debug!("Connected"); let request_bytes = request.to_bytes().expect("failed to serialise request"); let mut bytes_to_send = format!("\ POST {} HTTP/1.1\r\n\ Host: {}\r\n\ Content-Type: application/dns-message\r\n\ Accept: application/dns-message\r\n\ User-Agent: {}\r\n\ Content-Length: {}\r\n\r\n", path, domain, USER_AGENT, request_bytes.len()).into_bytes(); bytes_to_send.extend(request_bytes); info!("Sending {} bytes of data to {:?} over HTTPS", bytes_to_send.len(), self.url); stream.write_all(&bytes_to_send)?; debug!("Wrote all bytes"); info!("Waiting to receive..."); let mut buf = [0; 4096]; let mut read_len = stream.read(&mut buf)?; while !contains_header(&buf[0..read_len]) { if read_len == buf.len() { return Err(Error::WireError(WireError::IO)); } read_len += stream.read(&mut buf[read_len..])?; } let mut expected_len = read_len; info!("Received {} bytes of data", read_len); let mut headers = [httparse::EMPTY_HEADER; 16]; let mut response = httparse::Response::new(&mut headers); let index: usize = response.parse(&buf)?.unwrap(); if response.code != Some(200) { let reason = response.reason.map(str::to_owned); return Err(Error::WrongHttpStatus(response.code.unwrap(), reason)); } for header in response.headers { let str_value = String::from_utf8_lossy(header.value); debug!("Header {:?} -> {:?}", header.name, str_value); if header.name == "Content-Length" { let content_length: usize = str_value.parse().unwrap(); expected_len = index + content_length; } } while read_len < expected_len { if read_len == buf.len() { return Err(Error::WireError(WireError::IO)); } read_len += stream.read(&mut buf[read_len..])?; } let body = &buf[index .. read_len]; debug!("HTTP body has {} bytes", body.len()); let response = Response::from_bytes(&body)?; Ok(response) } #[cfg(not(feature = "with_https"))] fn send(&self, request: &Request) -> Result { unreachable!("HTTPS feature disabled") } } impl HttpsTransport { fn split_domain(&self) -> Option<(&str, &str)> { if let Some(sp) = self.url.strip_prefix("https://") { if let Some(colon_index) = sp.find('/') { return Some((&sp[.. colon_index], &sp[colon_index ..])); } } None } } /// The User-Agent header sent with HTTPS requests. static USER_AGENT: &str = concat!("dog/", env!("CARGO_PKG_VERSION")); ================================================ FILE: dns-transport/src/lib.rs ================================================ //! All the DNS transport types. #![warn(deprecated_in_future)] #![warn(future_incompatible)] #![warn(missing_copy_implementations)] #![warn(missing_docs)] #![warn(nonstandard_style)] #![warn(rust_2018_compatibility)] #![warn(rust_2018_idioms)] #![warn(single_use_lifetimes)] #![warn(trivial_casts, trivial_numeric_casts)] #![warn(unused)] #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::must_use_candidate)] #![allow(clippy::option_if_let_else)] #![allow(clippy::pub_enum_variant_names)] #![allow(clippy::wildcard_imports)] #![deny(clippy::cast_possible_truncation)] #![deny(clippy::cast_lossless)] #![deny(clippy::cast_possible_wrap)] #![deny(clippy::cast_sign_loss)] #![deny(unsafe_code)] mod auto; pub use self::auto::AutoTransport; mod udp; pub use self::udp::UdpTransport; mod tcp; pub use self::tcp::TcpTransport; mod tls; pub use self::tls::TlsTransport; mod https; pub use self::https::HttpsTransport; mod error; mod tls_stream; pub use self::error::Error; /// The trait implemented by all transport types. pub trait Transport { /// Convert the request to bytes, send it over the network, wait for a /// response, deserialise it from bytes, and return it, asynchronously. /// /// # Errors /// /// Returns an `Error` error if there’s an I/O error sending or /// receiving data, or the DNS packet in the response contained invalid /// bytes and failed to parse, or if there was a protocol-level error for /// the TLS and HTTPS transports. fn send(&self, request: &dns::Request) -> Result; } ================================================ FILE: dns-transport/src/tcp.rs ================================================ use std::convert::TryFrom; use std::net::TcpStream; use std::io::{Read, Write}; use log::*; use dns::{Request, Response}; use super::{Transport, Error}; /// The **TCP transport**, which sends DNS wire data over a TCP stream. /// /// # References /// /// - [RFC 1035 §4.2.2](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) /// - [RFC 7766](https://tools.ietf.org/html/rfc1035) — DNS Transport over /// TCP, Implementation Requirements (March 2016) pub struct TcpTransport { addr: String, } impl TcpTransport { /// Creates a new TCP transport that connects to the given host. pub fn new(addr: String) -> Self { Self { addr } } } impl Transport for TcpTransport { fn send(&self, request: &Request) -> Result { info!("Opening TCP stream"); let mut stream = if self.addr.contains(':') { TcpStream::connect(&*self.addr)? } else { TcpStream::connect((&*self.addr, 53))? }; debug!("Opened"); // The message is prepended with the length when sent over TCP, // so the server knows how long it is (RFC 1035 §4.2.2) let mut bytes_to_send = request.to_bytes().expect("failed to serialise request"); Self::prefix_with_length(&mut bytes_to_send); info!("Sending {} bytes of data to {:?} over TCP", bytes_to_send.len(), self.addr); let written_len = stream.write(&bytes_to_send)?; debug!("Wrote {} bytes", written_len); let read_bytes = Self::length_prefixed_read(&mut stream)?; let response = Response::from_bytes(&read_bytes)?; Ok(response) } } impl TcpTransport { /// Mutate the given byte buffer, prefixing it with its own length as a /// big-endian `u16`. pub(crate) fn prefix_with_length(bytes: &mut Vec) { let len_bytes = u16::try_from(bytes.len()) .expect("request too long") .to_be_bytes(); bytes.insert(0, len_bytes[0]); bytes.insert(1, len_bytes[1]); } /// Reads from the given I/O source as many times as necessary to read a /// length-prefixed stream of bytes. The first two bytes are taken as a /// big-endian `u16` to determine the length. Then, that many bytes are /// read from the source. /// /// # Errors /// /// Returns an error if there’s a network error during reading, or not /// enough bytes have been sent. pub(crate) fn length_prefixed_read(stream: &mut impl Read) -> Result, Error> { info!("Waiting to receive..."); let mut buf = [0; 4096]; let mut read_len = stream.read(&mut buf[..])?; if read_len == 0 { warn!("Received no bytes!"); return Err(Error::TruncatedResponse); } else if read_len == 1 { info!("Received one byte of data"); let second_read_len = stream.read(&mut buf[1..])?; if second_read_len == 0 { warn!("Received no bytes the second time!"); return Err(Error::TruncatedResponse); } read_len += second_read_len; } else { info!("Received {} bytes of data", read_len); } let total_len = u16::from_be_bytes([buf[0], buf[1]]); if read_len - 2 == usize::from(total_len) { debug!("We have enough bytes"); return Ok(buf[2..read_len].to_vec()); } debug!("We need to read {} bytes total", total_len); let mut combined_buffer = buf[2..read_len].to_vec(); while combined_buffer.len() < usize::from(total_len) { let mut extend_buf = [0; 4096]; let extend_len = stream.read(&mut extend_buf[..])?; info!("Received further {} bytes of data (of {})", extend_len, total_len); if read_len == 0 { warn!("Read zero bytes!"); return Err(Error::TruncatedResponse); } combined_buffer.extend(&extend_buf[0 .. extend_len]); } Ok(combined_buffer) } } ================================================ FILE: dns-transport/src/tls.rs ================================================ #![cfg_attr(not(feature = "tls"), allow(unused))] use std::net::TcpStream; use std::io::Write; use log::*; use dns::{Request, Response}; use super::{Transport, Error, TcpTransport}; use super::tls_stream::TlsStream; /// The **TLS transport**, which sends DNS wire data using TCP through an /// encrypted TLS connection. pub struct TlsTransport { addr: String, } impl TlsTransport { /// Creates a new TLS transport that connects to the given host. pub fn new(addr: String) -> Self { Self { addr } } } impl Transport for TlsTransport { #[cfg(feature = "with_tls")] fn send(&self, request: &Request) -> Result { info!("Opening TLS socket"); let domain = self.sni_domain(); info!("Connecting using domain {:?}", domain); let mut stream = if self.addr.contains(':') { let mut parts = self.addr.split(":"); let domain = parts.nth(0).unwrap(); let port = parts.last().unwrap().parse::().expect("Invalid port number"); Self::stream(domain, port)? } else { Self::stream(&*self.addr, 853)? }; debug!("Connected"); // The message is prepended with the length when sent over TCP, // so the server knows how long it is (RFC 1035 §4.2.2) let mut bytes_to_send = request.to_bytes().expect("failed to serialise request"); TcpTransport::prefix_with_length(&mut bytes_to_send); info!("Sending {} bytes of data to {} over TLS", bytes_to_send.len(), self.addr); stream.write_all(&bytes_to_send)?; debug!("Wrote all bytes"); let read_bytes = TcpTransport::length_prefixed_read(&mut stream)?; let response = Response::from_bytes(&read_bytes)?; Ok(response) } #[cfg(not(feature = "with_tls"))] fn send(&self, request: &Request) -> Result { unreachable!("TLS feature disabled") } } impl TlsTransport { fn sni_domain(&self) -> &str { if let Some(colon_index) = self.addr.find(':') { &self.addr[.. colon_index] } else { &self.addr[..] } } } ================================================ FILE: dns-transport/src/tls_stream.rs ================================================ use std::net::TcpStream; use super::Error; use super::HttpsTransport; use super::TlsTransport; #[cfg(any(feature = "with_nativetls", feature = "with_nativetls_vendored"))] fn stream_nativetls(domain: &str, port: u16) -> Result, Error> { let connector = native_tls::TlsConnector::new()?; let stream = TcpStream::connect((domain, port))?; Ok(connector.connect(domain, stream)?) } #[cfg(feature = "with_rustls")] fn stream_rustls(domain: &str, port: u16) -> Result, Error> { use std::sync::Arc; let mut config = rustls::ClientConfig::new(); config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); let dns_name = webpki::DNSNameRef::try_from_ascii_str(domain)?; let conn = rustls::ClientSession::new(&Arc::new(config), dns_name); let sock = TcpStream::connect((domain, port))?; let tls = rustls::StreamOwned::new(conn, sock); Ok(tls) } pub trait TlsStream { fn stream(domain: &str, port: u16) -> Result; } #[cfg(any(feature = "with_tls", feature = "with_https"))] cfg_if::cfg_if! { if #[cfg(any(feature = "with_nativetls", feature = "with_nativetls_vendored"))] { impl TlsStream> for HttpsTransport { fn stream(domain: &str, port: u16) -> Result, Error> { stream_nativetls(domain, port) } } impl TlsStream> for TlsTransport { fn stream(domain: &str, port: u16) -> Result, Error> { stream_nativetls(domain, port) } } } else if #[cfg(feature = "with_rustls")] { impl TlsStream> for HttpsTransport { fn stream(domain: &str, port: u16) -> Result, Error> { stream_rustls(domain, port) } } impl TlsStream> for TlsTransport { fn stream(domain: &str, port: u16) -> Result, Error> { stream_rustls(domain, port) } } } else { unreachable!("tls/https enabled but no tls implementation provided") } } ================================================ FILE: dns-transport/src/udp.rs ================================================ use std::net::{Ipv4Addr, UdpSocket}; use log::*; use dns::{Request, Response}; use super::{Transport, Error}; /// The **UDP transport**, which sends DNS wire data inside a UDP datagram. /// /// # References /// /// - [RFC 1035 §4.2.1](https://tools.ietf.org/html/rfc1035) — Domain Names, /// Implementation and Specification (November 1987) pub struct UdpTransport { addr: String, } impl UdpTransport { /// Creates a new UDP transport that connects to the given host. pub fn new(addr: String) -> Self { Self { addr } } } impl Transport for UdpTransport { fn send(&self, request: &Request) -> Result { info!("Opening UDP socket"); // TODO: This will need to be changed for IPv6 support. let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?; if self.addr.contains(':') { socket.connect(&*self.addr)?; } else { socket.connect((&*self.addr, 53))?; } debug!("Opened"); let bytes_to_send = request.to_bytes().expect("failed to serialise request"); info!("Sending {} bytes of data to {} over UDP", bytes_to_send.len(), self.addr); let written_len = socket.send(&bytes_to_send)?; debug!("Wrote {} bytes", written_len); info!("Waiting to receive..."); let mut buf = vec![0; 4096]; let received_len = socket.recv(&mut buf)?; info!("Received {} bytes of data", received_len); let response = Response::from_bytes(&buf[.. received_len])?; Ok(response) } } ================================================ FILE: man/dog.1.md ================================================ % dog(1) v0.1.0 NAME ==== dog — a command-line DNS client SYNOPSIS ======== `dog [options] [domains...]` **dog** is a command-line DNS client. It has colourful output, supports the DNS-over-TLS and DNS-over-HTTPS protocols, and can emit JSON. EXAMPLES ======== `dog example.net` : Query the `A` record of a domain using default settings `dog example.net MX` : ...looking up `MX` records instead `dog example.net MX @1.1.1.1` : ...using a specific nameserver instead `dog example.net MX @1.1.1.1 -T` : ...using TCP rather than UDP `dog -q example.net -t MX -n 1.1.1.1 -T` : As above, but using explicit arguments QUERY OPTIONS ============= `-q`, `--query=HOST` : Host name or domain name to query. `-t`, `--type=TYPE` : Type of the DNS record being queried (`A`, `MX`, `NS`...) `-n`, `--nameserver=ADDR` : Address of the nameserver to send packets to. `--class=CLASS` : Network class of the DNS record being queried (`IN`, `CH`, `HS`) By default, dog will request A records using the system default resolver. At least one domain name must be passed — dog will not automatically query the root nameservers. Query options passed in using a command-line option, such as ‘`--query lookup.dog`’ or ‘`--type MX`’, or as plain arguments, such as ‘`lookup.dog`’ or ‘`MX`’. dog will make an intelligent guess as to what plain arguments mean (`MX` is quite clearly a type), which makes it easier to compose ad-hoc queries quickly. If precision is desired, use the long-form options. If more than one domain, type, nameserver, or class is specified, dog will perform one query for each combination, and display the combined results in a table. For example, passing three type arguments and two domain name arguments will send six requests. DNS traditionally uses port 53 for both TCP and UDP. To use a resolver with a different port, include the port number after a colon (`:`) in the nameserver address. SENDING OPTIONS =============== `--edns=SETTING` : Whether to opt in to DNS. This can be ‘`disable`’, ‘`hide`’, or ‘`show`’. `--txid=NUMBER` : Set the transaction ID to a specific value. `-Z=TWEAKS` : Set uncommon protocol-level tweaks. TRANSPORT OPTIONS ================= `-U`, `--udp` : Use the DNS protocol over UDP. `-T`, `--tcp` : Use the DNS protocol over TCP. `-S`, `--tls` : Use the DNS-over-TLS protocol. `-H`, `--https` : Use the DNS-over-HTTPS protocol. By default, dog will use the UDP protocol, automatically re-sending the request using TCP if the response indicates that the message is too large for UDP. Passing `--udp` will only use UDP and will fail in this case; passing `--tcp` will use TCP by default. The DNS-over-TLS (DoT) and DNS-over-HTTPS (DoH) protocols are available with the `--tls` and `--https` options. Bear in mind that the system default resolver is unlikely to respond to requests using these protocols. Note that if a hostname or domain name is given as a nameserver, rather than an IP address, the resolution of that host is performed by the operating system, _not_ by dog. Unlike the others, the HTTPS transport type requires an entire URL, complete with protocol, domain name, and path. OUTPUT OPTIONS ============== `-1`, `--short` : Short mode: display nothing but the first result. `-J`, `--json` : Display the output as JSON. `--color`, `--colour=WHEN` : When to colourise the output. This can be ‘`always`’, ‘`automatic`’, or ‘`never`’. `--seconds` : Do not format durations as hours and minutes; instead, display them as seconds. `--time` : Print how long the response took to arrive. META OPTIONS ============ `--help` : Displays an overview of the command-line options. `--version` : Displays the version of dog being invoked. ENVIRONMENT VARIABLES ===================== dog responds to the following environment variables: ## `DOG_DEBUG` Set this to any non-empty value to have dog emit debugging information to standard error. For more in-depth output, set this to the exact string ‘`trace`’. RECORD TYPES ============ dog understands and can interpret the following record types: `A` : IPv4 addresses `AAAA` : IPv6 addresses `CAA` : permitted certificate authorities `CNAME` : canonical domain aliases `HINFO` : system information and, sometimes, forbidden request explanations `LOC` : location information `MX` : e-mail server addresses `NAPTR` : DDDS rules `NS` : domain name servers `OPT` : extensions to the DNS protocol `PTR` : pointers to canonical names, usually for reverse lookups `SOA` : administrative information about zones `SRV` : IP addresses with port numbers `SSHFP` : SSH key fingerprints `TLSA` : TLS certificates, public keys, and hashes `TXT` : arbitrary textual information When a response DNS packet contains a record of one of these known types, dog will display it in a table containing the type name and a human-readable summary of its contents. Records with a type number that does not map to any known record type will still be displayed. As they cannot be interpreted, their contents will be displayed as a series of numbers instead. dog also contains a list of record type names that it knows the type number of, but is not able to interpret, such as `IXFR` or `ANY` or `AFSDB`. These are acceptable as command-line arguments, meaning you can send an AFSDB request with ‘`dog AFSDB`’. However, their response contents will still be displayed as numbers. They may be supported in future versions of dog. PROTOCOL TWEAKS =============== The `-Z` command-line argument can be used one or more times to set some protocol-level options in the DNS queries that get sent. It accepts the following values: `aa` : Sets the `AA` (Authoritative Answers) bit in the query. `ad` : Sets the `AD` (Authentic Data) bit in the query. `bufsize=NUM` : Sets the UDP payload size field in the OPT field in the query. This has no effect if EDNS is diabled. `cd` : Sets the `CD` (Checking Disabled) bit in the query. EXIT STATUSES ============= 0 : If everything goes OK. 1 : If there was a network, I/O, or TLS error during operation. 2 : If there is no result from the server when running in short mode. This can be any received server error, not just NXDOMAIN. 3 : If there was a problem with the command-line arguments. 4 : If there was a problem obtaining the system nameserver information. AUTHOR ====== dog is maintained by Benjamin ‘ogham’ Sago. **Website:** `https://dns.lookup.dog/` \ **Source code:** `https://github.com/ogham/dog` ================================================ FILE: src/colours.rs ================================================ //! Colours, colour schemes, and terminal styling. use ansi_term::Style; use ansi_term::Color::*; /// The **colours** are used to paint the input. #[derive(Debug, Default)] pub struct Colours { pub qname: Style, pub answer: Style, pub authority: Style, pub additional: Style, pub a: Style, 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, pub soa: Style, pub srv: Style, pub tlsa: Style, pub txt: Style, pub uri: Style, pub unknown: Style, } impl Colours { /// Create a new colour palette that has a variety of different styles /// defined. This is used by default. pub fn pretty() -> Self { Self { qname: Blue.bold(), answer: Style::default(), authority: Cyan.normal(), additional: Green.normal(), a: Green.bold(), 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(), soa: Purple.normal(), srv: Cyan.normal(), tlsa: Yellow.normal(), txt: Yellow.normal(), uri: Yellow.normal(), unknown: White.on(Red), } } /// Create a new colour palette where no styles are defined, causing /// output to be rendered as plain text without any formatting. /// This is used when output is not to a terminal. pub fn plain() -> Self { Self::default() } } ================================================ FILE: src/connect.rs ================================================ //! Creating DNS transports based on the user’s input arguments. use dns_transport::*; /// A **transport type** creates a `Transport` that determines which protocols /// should be used to send and receive DNS wire data over the network. #[derive(PartialEq, Debug, Copy, Clone)] pub enum TransportType { /// Send packets over UDP or TCP. /// UDP is used by default. If the request packet would be too large, send /// a TCP packet instead; if a UDP _response_ packet is truncated, try /// again with TCP. Automatic, /// Send packets over UDP only. /// If the request packet is too large or the response packet is /// truncated, fail with an error. UDP, /// Send packets over TCP only. TCP, /// Send encrypted DNS-over-TLS packets. TLS, /// Send encrypted DNS-over-HTTPS packets. HTTPS, } impl TransportType { /// Creates a boxed `Transport` depending on the transport type. The /// parameter will be a URL for the HTTPS transport type, and a /// stringified address for the others. pub fn make_transport(self, param: String) -> Box { match self { Self::Automatic => Box::new(AutoTransport::new(param)), Self::UDP => Box::new(UdpTransport::new(param)), Self::TCP => Box::new(TcpTransport::new(param)), Self::TLS => Box::new(TlsTransport::new(param)), Self::HTTPS => Box::new(HttpsTransport::new(param)), } } } ================================================ FILE: src/hints.rs ================================================ //! Hints to the user made before a query is sent, in case the answer that //! comes back isn’t what they expect. use std::collections::BTreeSet; use std::fs::File; use std::io; use log::*; /// The set of hostnames that are configured to point to a specific host in /// the hosts file on the local machine. This gets queried before a request is /// made: because the running OS will consult the hosts file before looking up /// a hostname, but dog will not, it’s possible for dog to output one address /// while the OS is using another. dog displays a warning when this is the /// case, to prevent confusion. #[derive(Default)] pub struct LocalHosts { hostnames: BTreeSet, } impl LocalHosts { /// Loads the set of hostnames from the hosts file path on Unix. #[cfg(unix)] pub fn load() -> io::Result { debug!("Reading hints from /etc/hosts"); Self::load_from_file(File::open("/etc/hosts")?) } /// Loads the set of hostnames from the hosts file path on Windows. #[cfg(windows)] pub fn load() -> io::Result { debug!("Reading hints from /etc/hosts equivalent"); Self::load_from_file(File::open("C:\\Windows\\system32\\drivers\\etc\\hosts")?) } /// On other machines, load an empty set of hostnames that match nothing. #[cfg(all(not(windows), not(unix)))] pub fn load() -> io::Result { Ok(Self::default()) } /// Reads hostnames from the given file and returns them as a `LocalHosts` /// struct, or an I/O error if one occurs. The file should be in the /// standard `/etc/hosts` format, with one entry per line, separated by /// whitespace, where the first field is the address and the remaining /// fields are hostname aliases, and `#` signifies a comment. fn load_from_file(file: File) -> io::Result { use std::io::{BufRead, BufReader}; if cfg!(test) { panic!("load_from_file() called from test code"); } let reader = BufReader::new(file); let mut hostnames = BTreeSet::new(); for line in reader.lines() { let mut line = line?; if let Some(hash_index) = line.find('#') { line.truncate(hash_index); } for hostname in line.split_ascii_whitespace().skip(1) { match dns::Labels::encode(hostname) { Ok(hn) => { hostnames.insert(hn); } Err(e) => { warn!("Failed to encode local host hint {:?}: {}", hostname, e); } } } } trace!("{} hostname hints loaded OK.", hostnames.len()); Ok(Self { hostnames }) } /// Queries this set of hostnames to see if the given name, which is about /// to be queried for, exists within the file. pub fn contains(&self, hostname_in_query: &dns::Labels) -> bool { self.hostnames.contains(hostname_in_query) } } ================================================ FILE: src/logger.rs ================================================ //! Debug error logging. use std::ffi::OsStr; use ansi_term::{Colour, ANSIString}; /// Sets the internal logger, changing the log level based on the value of an /// environment variable. pub fn configure>(ev: Option) { let ev = match ev { Some(v) => v, None => return, }; let env_var = ev.as_ref(); if env_var.is_empty() { return; } if env_var == "trace" { log::set_max_level(log::LevelFilter::Trace); } else { log::set_max_level(log::LevelFilter::Debug); } let result = log::set_logger(GLOBAL_LOGGER); if let Err(e) = result { eprintln!("Failed to initialise logger: {}", e); } } #[derive(Debug)] struct Logger; const GLOBAL_LOGGER: &Logger = &Logger; impl log::Log for Logger { fn enabled(&self, _: &log::Metadata<'_>) -> bool { true // no need to filter after using ‘set_max_level’. } fn log(&self, record: &log::Record<'_>) { let open = Colour::Fixed(243).paint("["); let level = level(record.level()); let close = Colour::Fixed(243).paint("]"); eprintln!("{}{} {}{} {}", open, level, record.target(), close, record.args()); } fn flush(&self) { // no need to flush with ‘eprintln!’. } } fn level(level: log::Level) -> ANSIString<'static> { match level { log::Level::Error => Colour::Red.paint("ERROR"), log::Level::Warn => Colour::Yellow.paint("WARN"), log::Level::Info => Colour::Cyan.paint("INFO"), log::Level::Debug => Colour::Blue.paint("DEBUG"), log::Level::Trace => Colour::Fixed(245).paint("TRACE"), } } ================================================ FILE: src/main.rs ================================================ //! dog, the command-line DNS client. #![warn(deprecated_in_future)] #![warn(future_incompatible)] #![warn(missing_copy_implementations)] #![warn(missing_docs)] #![warn(nonstandard_style)] #![warn(rust_2018_compatibility)] #![warn(rust_2018_idioms)] #![warn(single_use_lifetimes)] #![warn(trivial_casts, trivial_numeric_casts)] #![warn(unused)] #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::enum_glob_use)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::option_if_let_else)] #![allow(clippy::too_many_lines)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::wildcard_imports)] #![deny(unsafe_code)] use log::*; mod colours; mod connect; mod hints; mod logger; mod output; mod requests; mod resolve; mod table; mod txid; mod options; use self::options::*; /// Configures logging, parses the command-line options, and handles any /// errors before passing control over to the Dog type. fn main() { use std::env; use std::process::exit; logger::configure(env::var_os("DOG_DEBUG")); #[cfg(windows)] if let Err(e) = ansi_term::enable_ansi_support() { warn!("Failed to enable ANSI support: {}", e); } match Options::getopts(env::args_os().skip(1)) { OptionsResult::Ok(options) => { info!("Running with options -> {:#?}", options); disabled_feature_check(&options); exit(run(options)); } OptionsResult::Help(help_reason, use_colours) => { if use_colours.should_use_colours() { print!("{}", include_str!(concat!(env!("OUT_DIR"), "/usage.pretty.txt"))); } else { print!("{}", include_str!(concat!(env!("OUT_DIR"), "/usage.bland.txt"))); } if help_reason == HelpReason::NoDomains { exit(exits::OPTIONS_ERROR); } else { exit(exits::SUCCESS); } } OptionsResult::Version(use_colours) => { if use_colours.should_use_colours() { print!("{}", include_str!(concat!(env!("OUT_DIR"), "/version.pretty.txt"))); } else { print!("{}", include_str!(concat!(env!("OUT_DIR"), "/version.bland.txt"))); } exit(exits::SUCCESS); } OptionsResult::InvalidOptionsFormat(oe) => { eprintln!("dog: Invalid options: {}", oe); exit(exits::OPTIONS_ERROR); } OptionsResult::InvalidOptions(why) => { eprintln!("dog: Invalid options: {}", why); exit(exits::OPTIONS_ERROR); } } } /// Runs dog with some options, returning the status to exit with. fn run(Options { requests, format, measure_time }: Options) -> i32 { use std::time::Instant; let should_show_opt = requests.edns.should_show(); let mut responses = Vec::new(); let timer = if measure_time { Some(Instant::now()) } else { None }; let mut errored = false; let local_host_hints = match hints::LocalHosts::load() { Ok(lh) => lh, Err(e) => { warn!("Error loading local host hints: {}", e); hints::LocalHosts::default() } }; for hostname_in_query in &requests.inputs.domains { if local_host_hints.contains(hostname_in_query) { eprintln!("warning: domain '{}' also exists in hosts file", hostname_in_query); } } let request_tuples = match requests.generate() { Ok(rt) => rt, Err(e) => { eprintln!("Unable to obtain resolver: {}", e); return exits::SYSTEM_ERROR; } }; for (transport, request_list) in request_tuples { 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; } } } } let duration = timer.map(|t| t.elapsed()); if format.print(responses, duration) { if errored { exits::NETWORK_ERROR } else { exits::SUCCESS } } else { exits::NO_SHORT_RESULTS } } /// Checks whether the options contain parameters that will cause dog to fail /// because the feature is disabled by exiting if so. #[allow(unused)] fn disabled_feature_check(options: &Options) { use std::process::exit; use crate::connect::TransportType; #[cfg(all(not(feature = "with_tls"), not(feature = "with_rustls_tls")))] if options.requests.inputs.transport_types.contains(&TransportType::TLS) { eprintln!("dog: Cannot use '--tls': This version of dog has been compiled without TLS support"); exit(exits::OPTIONS_ERROR); } #[cfg(all(not(feature = "with_https"), not(feature = "with_rustls_https")))] if options.requests.inputs.transport_types.contains(&TransportType::HTTPS) { eprintln!("dog: Cannot use '--https': This version of dog has been compiled without HTTPS support"); exit(exits::OPTIONS_ERROR); } } /// The possible status numbers dog can exit with. mod exits { /// Exit code for when everything turns out OK. pub const SUCCESS: i32 = 0; /// Exit code for when there was at least one network error during execution. pub const NETWORK_ERROR: i32 = 1; /// Exit code for when there is no result from the server when running in /// short mode. This can be any received server error, not just `NXDOMAIN`. pub const NO_SHORT_RESULTS: i32 = 2; /// Exit code for when the command-line options are invalid. pub const OPTIONS_ERROR: i32 = 3; /// Exit code for when the system network configuration could not be determined. pub const SYSTEM_ERROR: i32 = 4; } ================================================ FILE: src/options.rs ================================================ //! Command-line option parsing. use std::ffi::OsStr; use std::fmt; use log::*; use dns::{QClass, Labels}; use dns::record::RecordType; use crate::connect::TransportType; use crate::output::{OutputFormat, UseColours, TextFormat}; use crate::requests::{RequestGenerator, Inputs, ProtocolTweaks, UseEDNS}; use crate::resolve::ResolverType; use crate::txid::TxidGenerator; /// The command-line options used when running dog. #[derive(PartialEq, Debug)] pub struct Options { /// The requests to make and how they should be generated. pub requests: RequestGenerator, /// Whether to display the time taken after every query. pub measure_time: bool, /// How to format the output data. pub format: OutputFormat, } impl Options { /// Parses and interprets a set of options from the user’s command-line /// arguments. /// /// This returns an `Ok` set of options if successful and running /// normally, a `Help` or `Version` variant if one of those options is /// specified, or an error variant if there’s an invalid option or /// inconsistency within the options after they were parsed. #[allow(unused_results)] pub fn getopts(args: C) -> OptionsResult where C: IntoIterator, C::Item: AsRef, { let mut opts = getopts::Options::new(); // Query options opts.optmulti("q", "query", "Host name or domain name to query", "HOST"); opts.optmulti("t", "type", "Type of the DNS record being queried (A, MX, NS...)", "TYPE"); opts.optmulti("n", "nameserver", "Address of the nameserver to send packets to", "ADDR"); opts.optmulti("", "class", "Network class of the DNS record being queried (IN, CH, HS)", "CLASS"); // Sending options opts.optopt ("", "edns", "Whether to OPT in to EDNS (disable, hide, show)", "SETTING"); opts.optopt ("", "txid", "Set the transaction ID to a specific value", "NUMBER"); opts.optmulti("Z", "", "Set uncommon protocol tweaks", "TWEAKS"); // Protocol options opts.optflag ("U", "udp", "Use the DNS protocol over UDP"); opts.optflag ("T", "tcp", "Use the DNS protocol over TCP"); opts.optflag ("S", "tls", "Use the DNS-over-TLS protocol"); opts.optflag ("H", "https", "Use the DNS-over-HTTPS protocol"); // Output options opts.optopt ("", "color", "When to use terminal colors", "WHEN"); opts.optopt ("", "colour", "When to use terminal colours", "WHEN"); opts.optflag ("J", "json", "Display the output as JSON"); opts.optflag ("", "seconds", "Do not format durations, display them as seconds"); opts.optflag ("1", "short", "Short mode: display nothing but the first result"); opts.optflag ("", "time", "Print how long the response took to arrive"); // Meta options opts.optflag ("v", "version", "Print version information"); opts.optflag ("?", "help", "Print list of command-line options"); let matches = match opts.parse(args) { Ok(m) => m, Err(e) => return OptionsResult::InvalidOptionsFormat(e), }; let uc = UseColours::deduce(&matches); if matches.opt_present("version") { OptionsResult::Version(uc) } else if matches.opt_present("help") { OptionsResult::Help(HelpReason::Flag, uc) } else { match Self::deduce(matches) { Ok(opts) => { if opts.requests.inputs.domains.is_empty() { OptionsResult::Help(HelpReason::NoDomains, uc) } else { OptionsResult::Ok(opts) } } Err(e) => { OptionsResult::InvalidOptions(e) } } } } fn deduce(matches: getopts::Matches) -> Result { let measure_time = matches.opt_present("time"); let format = OutputFormat::deduce(&matches); let requests = RequestGenerator::deduce(matches)?; Ok(Self { requests, measure_time, format }) } } impl RequestGenerator { fn deduce(matches: getopts::Matches) -> Result { let edns = UseEDNS::deduce(&matches)?; let txid_generator = TxidGenerator::deduce(&matches)?; let protocol_tweaks = ProtocolTweaks::deduce(&matches)?; let inputs = Inputs::deduce(matches)?; Ok(Self { inputs, txid_generator, edns, protocol_tweaks }) } } impl Inputs { fn deduce(matches: getopts::Matches) -> Result { let mut inputs = Self::default(); inputs.load_transport_types(&matches); inputs.load_named_args(&matches)?; inputs.load_free_args(matches)?; inputs.check_for_missing_nameserver()?; inputs.load_fallbacks(); Ok(inputs) } fn load_transport_types(&mut self, matches: &getopts::Matches) { if matches.opt_present("https") { self.transport_types.push(TransportType::HTTPS); } if matches.opt_present("tls") { self.transport_types.push(TransportType::TLS); } if matches.opt_present("tcp") { self.transport_types.push(TransportType::TCP); } if matches.opt_present("udp") { self.transport_types.push(TransportType::UDP); } } fn load_named_args(&mut self, matches: &getopts::Matches) -> Result<(), OptionsError> { for domain in matches.opt_strs("query") { self.add_domain(&domain)?; } for record_name in matches.opt_strs("type") { if record_name.eq_ignore_ascii_case("OPT") { return Err(OptionsError::QueryTypeOPT); } else if let Some(record_type) = RecordType::from_type_name(&record_name) { self.add_type(record_type); } else if let Ok(type_number) = record_name.parse::() { self.record_types.push(RecordType::from(type_number)); } else { return Err(OptionsError::InvalidQueryType(record_name)); } } for ns in matches.opt_strs("nameserver") { self.add_nameserver(&ns); } for class_name in matches.opt_strs("class") { if let Some(class) = parse_class_name(&class_name) { self.add_class(class); } else if let Ok(class_number) = class_name.parse() { self.add_class(QClass::Other(class_number)); } else { return Err(OptionsError::InvalidQueryClass(class_name)); } } Ok(()) } fn load_free_args(&mut self, matches: getopts::Matches) -> Result<(), OptionsError> { for argument in matches.free { if let Some(nameserver) = argument.strip_prefix('@') { trace!("Got nameserver -> {:?}", nameserver); self.add_nameserver(nameserver); } else if is_constant_name(&argument) { if argument.eq_ignore_ascii_case("OPT") { return Err(OptionsError::QueryTypeOPT); } else if let Some(class) = parse_class_name(&argument) { trace!("Got qclass -> {:?}", &argument); self.add_class(class); } else if let Some(record_type) = RecordType::from_type_name(&argument) { trace!("Got qtype -> {:?}", &argument); self.add_type(record_type); } else { trace!("Got single-word domain -> {:?}", &argument); self.add_domain(&argument)?; } } else { trace!("Got domain -> {:?}", &argument); self.add_domain(&argument)?; } } Ok(()) } fn check_for_missing_nameserver(&self) -> Result<(), OptionsError> { if self.resolver_types.is_empty() && self.transport_types == [TransportType::HTTPS] { Err(OptionsError::MissingHttpsUrl) } else { Ok(()) } } fn load_fallbacks(&mut self) { if self.record_types.is_empty() { self.record_types.push(RecordType::A); } if self.classes.is_empty() { self.classes.push(QClass::IN); } if self.resolver_types.is_empty() { self.resolver_types.push(ResolverType::SystemDefault); } if self.transport_types.is_empty() { self.transport_types.push(TransportType::Automatic); } } fn add_domain(&mut self, input: &str) -> Result<(), OptionsError> { if let Ok(domain) = Labels::encode(input) { self.domains.push(domain); Ok(()) } else { Err(OptionsError::InvalidDomain(input.into())) } } fn add_type(&mut self, rt: RecordType) { self.record_types.push(rt); } fn add_nameserver(&mut self, input: &str) { self.resolver_types.push(ResolverType::Specific(input.into())); } fn add_class(&mut self, class: QClass) { self.classes.push(class); } } fn is_constant_name(argument: &str) -> bool { let first_char = match argument.chars().next() { Some(c) => c, None => return false, }; if ! first_char.is_ascii_alphabetic() { return false; } argument.chars().all(|c| c.is_ascii_alphanumeric()) } fn parse_class_name(input: &str) -> Option { if input.eq_ignore_ascii_case("IN") { Some(QClass::IN) } else if input.eq_ignore_ascii_case("CH") { Some(QClass::CH) } else if input.eq_ignore_ascii_case("HS") { Some(QClass::HS) } else { None } } impl TxidGenerator { fn deduce(matches: &getopts::Matches) -> Result { if let Some(starting_txid) = matches.opt_str("txid") { if let Some(start) = parse_dec_or_hex(&starting_txid) { Ok(Self::Sequence(start)) } else { Err(OptionsError::InvalidTxid(starting_txid)) } } else { Ok(Self::Random) } } } fn parse_dec_or_hex(input: &str) -> Option { if let Some(hex_str) = input.strip_prefix("0x") { match u16::from_str_radix(hex_str, 16) { Ok(num) => { Some(num) } Err(e) => { warn!("Error parsing hex number: {}", e); None } } } else { match input.parse() { Ok(num) => { Some(num) } Err(e) => { warn!("Error parsing number: {}", e); None } } } } impl OutputFormat { fn deduce(matches: &getopts::Matches) -> Self { if matches.opt_present("short") { let summary_format = TextFormat::deduce(matches); Self::Short(summary_format) } else if matches.opt_present("json") { Self::JSON } else { let use_colours = UseColours::deduce(matches); let summary_format = TextFormat::deduce(matches); Self::Text(use_colours, summary_format) } } } impl UseColours { fn deduce(matches: &getopts::Matches) -> Self { match matches.opt_str("color").or_else(|| matches.opt_str("colour")).unwrap_or_default().as_str() { "automatic" | "auto" | "" => Self::Automatic, "always" | "yes" => Self::Always, "never" | "no" => Self::Never, otherwise => { warn!("Unknown colour setting {:?}", otherwise); Self::Automatic }, } } } impl TextFormat { fn deduce(matches: &getopts::Matches) -> Self { let format_durations = ! matches.opt_present("seconds"); Self { format_durations } } } impl UseEDNS { fn deduce(matches: &getopts::Matches) -> Result { if let Some(edns) = matches.opt_str("edns") { match edns.as_str() { "disable" | "off" => Ok(Self::Disable), "hide" => Ok(Self::SendAndHide), "show" => Ok(Self::SendAndShow), oh => Err(OptionsError::InvalidEDNS(oh.into())), } } else { Ok(Self::SendAndHide) } } } impl ProtocolTweaks { fn deduce(matches: &getopts::Matches) -> Result { let mut tweaks = Self::default(); for tweak_str in matches.opt_strs("Z") { match &*tweak_str { "aa" | "authoritative" => { tweaks.set_authoritative_flag = true; } "ad" | "authentic" => { tweaks.set_authentic_flag = true; } "cd" | "checking-disabled" => { tweaks.set_checking_disabled_flag = true; } otherwise => { if let Some(remaining_num) = tweak_str.strip_prefix("bufsize=") { match remaining_num.parse() { Ok(parsed_bufsize) => { tweaks.udp_payload_size = Some(parsed_bufsize); continue; } Err(e) => { warn!("Failed to parse buffer size: {}", e); } } } return Err(OptionsError::InvalidTweak(otherwise.into())); } } } Ok(tweaks) } } /// The result of the `Options::getopts` function. #[derive(PartialEq, Debug)] pub enum OptionsResult { /// The options were parsed successfully. Ok(Options), /// There was an error (from `getopts`) parsing the arguments. InvalidOptionsFormat(getopts::Fail), /// There was an error with the combination of options the user selected. InvalidOptions(OptionsError), /// Can’t run any checks because there’s help to display! Help(HelpReason, UseColours), /// One of the arguments was `--version`, to display the version number. Version(UseColours), } /// The reason that help is being displayed. If it’s for the `--help` flag, /// then we shouldn’t return an error exit status. #[derive(PartialEq, Debug, Copy, Clone)] pub enum HelpReason { /// Help was requested with the `--help` flag. Flag, /// There were no domains being queried, so display help instead. /// Unlike `dig`, we don’t implicitly search for the root domain. NoDomains, } /// Something wrong with the combination of options the user has picked. #[derive(PartialEq, Debug)] pub enum OptionsError { InvalidDomain(String), InvalidEDNS(String), InvalidQueryType(String), InvalidQueryClass(String), InvalidTxid(String), InvalidTweak(String), QueryTypeOPT, MissingHttpsUrl, } impl fmt::Display for OptionsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidDomain(domain) => write!(f, "Invalid domain {:?}", domain), Self::InvalidEDNS(edns) => write!(f, "Invalid EDNS setting {:?}", edns), Self::InvalidQueryType(qt) => write!(f, "Invalid query type {:?}", qt), Self::InvalidQueryClass(qc) => write!(f, "Invalid query class {:?}", qc), Self::InvalidTxid(txid) => write!(f, "Invalid transaction ID {:?}", txid), Self::InvalidTweak(tweak) => write!(f, "Invalid protocol tweak {:?}", tweak), Self::QueryTypeOPT => write!(f, "OPT request is sent by default (see -Z flag)"), Self::MissingHttpsUrl => write!(f, "You must pass a URL as a nameserver when using --https"), } } } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; use dns::record::UnknownQtype; impl Inputs { fn fallbacks() -> Self { Inputs { domains: vec![ /* No domains by default */ ], record_types: vec![ RecordType::A ], classes: vec![ QClass::IN ], resolver_types: vec![ ResolverType::SystemDefault ], transport_types: vec![ TransportType::Automatic ], } } } impl OptionsResult { fn unwrap(self) -> Options { match self { Self::Ok(o) => o, _ => panic!("{:?}", self), } } } // help tests #[test] fn help() { assert_eq!(Options::getopts(&[ "--help" ]), OptionsResult::Help(HelpReason::Flag, UseColours::Automatic)); } #[test] fn help_no_colour() { assert_eq!(Options::getopts(&[ "--help", "--colour=never" ]), OptionsResult::Help(HelpReason::Flag, UseColours::Never)); } #[test] fn version() { assert_eq!(Options::getopts(&[ "--version" ]), OptionsResult::Version(UseColours::Automatic)); } #[test] fn version_yes_color() { assert_eq!(Options::getopts(&[ "--version", "--color", "always" ]), OptionsResult::Version(UseColours::Always)); } #[test] fn fail() { assert_eq!(Options::getopts(&[ "--pear" ]), OptionsResult::InvalidOptionsFormat(getopts::Fail::UnrecognizedOption("pear".into()))); } #[test] fn empty() { let nothing: Vec<&str> = vec![]; assert_eq!(Options::getopts(nothing), OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic)); } #[test] fn an_unrelated_argument() { assert_eq!(Options::getopts(&[ "--time" ]), OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic)); } // query tests #[test] fn just_domain() { let options = Options::getopts(&[ "lookup.dog" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], .. Inputs::fallbacks() }); } #[test] fn just_named_domain() { let options = Options::getopts(&[ "-q", "lookup.dog" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], .. Inputs::fallbacks() }); } #[test] fn domain_and_type() { let options = Options::getopts(&[ "lookup.dog", "SOA" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], record_types: vec![ RecordType::SOA ], .. Inputs::fallbacks() }); } #[test] fn domain_and_type_lowercase() { let options = Options::getopts(&[ "lookup.dog", "soa" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], record_types: vec![ RecordType::SOA ], .. Inputs::fallbacks() }); } #[test] fn domain_and_other_type() { let options = Options::getopts(&[ "lookup.dog", "any" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], record_types: vec![ RecordType::Other(UnknownQtype::from_type_name("ANY").unwrap()) ], .. Inputs::fallbacks() }); } #[test] fn domain_and_single_domain() { let options = Options::getopts(&[ "lookup.dog", "mixes" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap(), Labels::encode("mixes").unwrap() ], .. Inputs::fallbacks() }); } #[test] fn domain_and_nameserver() { let options = Options::getopts(&[ "lookup.dog", "@1.1.1.1" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()) ], .. Inputs::fallbacks() }); } #[test] fn domain_and_class() { let options = Options::getopts(&[ "lookup.dog", "CH" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], .. Inputs::fallbacks() }); } #[test] fn domain_and_class_lowercase() { let options = Options::getopts(&[ "lookup.dog", "ch" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], .. Inputs::fallbacks() }); } #[test] fn all_free() { let options = Options::getopts(&[ "lookup.dog", "CH", "NS", "@1.1.1.1" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], record_types: vec![ RecordType::NS ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()) ], .. Inputs::fallbacks() }); } #[test] fn all_parameters() { let options = Options::getopts(&[ "-q", "lookup.dog", "--class", "CH", "--type", "SOA", "--nameserver", "1.1.1.1" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], record_types: vec![ RecordType::SOA ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()) ], .. Inputs::fallbacks() }); } #[test] fn all_parameters_lowercase() { let options = Options::getopts(&[ "-q", "lookup.dog", "--class", "ch", "--type", "soa", "--nameserver", "1.1.1.1" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], record_types: vec![ RecordType::SOA ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()) ], .. Inputs::fallbacks() }); } #[test] fn two_types() { let options = Options::getopts(&[ "-q", "lookup.dog", "--type", "SRV", "--type", "AAAA" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], record_types: vec![ RecordType::SRV, RecordType::AAAA ], .. Inputs::fallbacks() }); } #[test] fn two_classes() { let options = Options::getopts(&[ "-q", "lookup.dog", "--class", "IN", "--class", "CH" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::IN, QClass::CH ], .. Inputs::fallbacks() }); } #[test] fn all_mixed_1() { let options = Options::getopts(&[ "lookup.dog", "--class", "CH", "SOA", "--nameserver", "1.1.1.1" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::CH ], record_types: vec![ RecordType::SOA ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()) ], .. Inputs::fallbacks() }); } #[test] fn all_mixed_2() { let options = Options::getopts(&[ "CH", "SOA", "MX", "IN", "-q", "lookup.dog", "--class", "HS" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("lookup.dog").unwrap() ], classes: vec![ QClass::HS, QClass::CH, QClass::IN ], record_types: vec![ RecordType::SOA, RecordType::MX ], .. Inputs::fallbacks() }); } #[test] fn all_mixed_3() { 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() ], resolver_types: vec![ ResolverType::Specific("1.1.1.1".into()), ResolverType::Specific("1.0.0.1".into()), ], .. Inputs::fallbacks() }); } #[test] fn explicit_numerics() { let options = Options::getopts(&[ "11", "--class", "22", "--type", "33" ]).unwrap(); assert_eq!(options.requests.inputs, Inputs { domains: vec![ Labels::encode("11").unwrap() ], classes: vec![ QClass::Other(22) ], record_types: vec![ RecordType::from(33) ], .. Inputs::fallbacks() }); } #[test] fn edns_and_tweaks() { let options = Options::getopts(&[ "dom.ain", "--edns", "show", "-Z", "authentic" ]).unwrap(); assert_eq!(options.requests.edns, UseEDNS::SendAndShow); assert_eq!(options.requests.protocol_tweaks.set_authentic_flag, true); } #[test] fn two_more_tweaks() { let options = Options::getopts(&[ "dom.ain", "-Z", "aa", "-Z", "cd" ]).unwrap(); assert_eq!(options.requests.protocol_tweaks.set_authoritative_flag, true); assert_eq!(options.requests.protocol_tweaks.set_checking_disabled_flag, true); } #[test] fn udp_size() { let options = Options::getopts(&[ "dom.ain", "-Z", "bufsize=4096" ]).unwrap(); assert_eq!(options.requests.protocol_tweaks.udp_payload_size, Some(4096)); } #[test] fn short_mode() { let tf = TextFormat { format_durations: true }; let options = Options::getopts(&[ "dom.ain", "--short" ]).unwrap(); assert_eq!(options.format, OutputFormat::Short(tf)); } #[test] fn short_mode_seconds() { let tf = TextFormat { format_durations: false }; let options = Options::getopts(&[ "dom.ain", "--short", "--seconds" ]).unwrap(); assert_eq!(options.format, OutputFormat::Short(tf)); } #[test] fn json_output() { let options = Options::getopts(&[ "dom.ain", "--json" ]).unwrap(); assert_eq!(options.format, OutputFormat::JSON); } #[test] fn specific_txid() { let options = Options::getopts(&[ "dom.ain", "--txid", "1234" ]).unwrap(); assert_eq!(options.requests.txid_generator, TxidGenerator::Sequence(1234)); } #[test] fn all_transport_types() { use crate::connect::TransportType::*; let options = Options::getopts(&[ "dom.ain", "--https", "--tls", "--tcp", "--udp" ]).unwrap(); assert_eq!(options.requests.inputs.transport_types, vec![ HTTPS, TLS, TCP, UDP ]); } // invalid options tests #[test] fn invalid_named_class() { assert_eq!(Options::getopts(&[ "lookup.dog", "--class", "tubes" ]), OptionsResult::InvalidOptions(OptionsError::InvalidQueryClass("tubes".into()))); } #[test] fn invalid_named_class_too_big() { assert_eq!(Options::getopts(&[ "lookup.dog", "--class", "999999" ]), OptionsResult::InvalidOptions(OptionsError::InvalidQueryClass("999999".into()))); } #[test] fn invalid_named_type() { assert_eq!(Options::getopts(&[ "lookup.dog", "--type", "tubes" ]), OptionsResult::InvalidOptions(OptionsError::InvalidQueryType("tubes".into()))); } #[test] fn invalid_named_type_too_big() { assert_eq!(Options::getopts(&[ "lookup.dog", "--type", "999999" ]), OptionsResult::InvalidOptions(OptionsError::InvalidQueryType("999999".into()))); } #[test] fn invalid_txid() { assert_eq!(Options::getopts(&[ "lookup.dog", "--txid=0x10000" ]), OptionsResult::InvalidOptions(OptionsError::InvalidTxid("0x10000".into()))); } #[test] fn invalid_edns() { assert_eq!(Options::getopts(&[ "--edns=yep" ]), OptionsResult::InvalidOptions(OptionsError::InvalidEDNS("yep".into()))); } #[test] fn invalid_tweaks() { assert_eq!(Options::getopts(&[ "-Zsleep" ]), OptionsResult::InvalidOptions(OptionsError::InvalidTweak("sleep".into()))); } #[test] fn invalid_udp_size() { assert_eq!(Options::getopts(&[ "-Z", "bufsize=null" ]), OptionsResult::InvalidOptions(OptionsError::InvalidTweak("bufsize=null".into()))); } #[test] fn invalid_udp_size_size() { assert_eq!(Options::getopts(&[ "-Z", "bufsize=999999999" ]), OptionsResult::InvalidOptions(OptionsError::InvalidTweak("bufsize=999999999".into()))); } #[test] fn invalid_udp_size_missing() { assert_eq!(Options::getopts(&[ "-Z", "bufsize=" ]), OptionsResult::InvalidOptions(OptionsError::InvalidTweak("bufsize=".into()))); } #[test] fn missing_https_url() { assert_eq!(Options::getopts(&[ "--https", "lookup.dog" ]), OptionsResult::InvalidOptions(OptionsError::MissingHttpsUrl)); } // opt tests #[test] fn opt() { assert_eq!(Options::getopts(&[ "OPT", "lookup.dog" ]), OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT)); } #[test] fn opt_lowercase() { assert_eq!(Options::getopts(&[ "opt", "lookup.dog" ]), OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT)); } #[test] fn opt_arg() { assert_eq!(Options::getopts(&[ "-t", "OPT", "lookup.dog" ]), OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT)); } #[test] fn opt_arg_lowercase() { assert_eq!(Options::getopts(&[ "-t", "opt", "lookup.dog" ]), OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT)); } // txid tests #[test] fn number_parsing() { assert_eq!(parse_dec_or_hex("1234"), Some(1234)); assert_eq!(parse_dec_or_hex("0x1234"), Some(0x1234)); assert_eq!(parse_dec_or_hex("0xABcd"), Some(0xABcd)); assert_eq!(parse_dec_or_hex("65536"), None); assert_eq!(parse_dec_or_hex("0x65536"), None); assert_eq!(parse_dec_or_hex(""), None); assert_eq!(parse_dec_or_hex("0x"), None); } } ================================================ FILE: src/output.rs ================================================ //! Text and JSON output. use std::fmt; use std::time::Duration; use std::env; use dns::{Response, Query, Answer, QClass, ErrorCode, WireError, MandatedLength}; use dns::record::{Record, RecordType, UnknownQtype, OPT}; use dns_transport::Error as TransportError; use json::{object, JsonValue}; use crate::colours::Colours; use crate::table::{Table, Section}; /// How to format the output data. #[derive(PartialEq, Debug, Copy, Clone)] pub enum OutputFormat { /// Format the output as plain text, optionally adding ANSI colours. Text(UseColours, TextFormat), /// Format the output as one line of plain text. Short(TextFormat), /// Format the entries as JSON. JSON, } /// When to use colours in the output. #[derive(PartialEq, Debug, Copy, Clone)] pub enum UseColours { /// Always use colours. Always, /// Use colours if output is to a terminal; otherwise, do not. Automatic, /// Never use colours. Never, } /// Options that govern how text should be rendered in record summaries. #[derive(PartialEq, Debug, Copy, Clone)] pub struct TextFormat { /// Whether to format TTLs as hours, minutes, and seconds. pub format_durations: bool, } impl UseColours { /// Whether we should use colours or not. This checks whether the user has /// overridden the colour setting, and if not, whether output is to a /// terminal. pub fn should_use_colours(self) -> bool { self == Self::Always || (atty::is(atty::Stream::Stdout) && env::var("NO_COLOR").is_err() && self != Self::Never) } /// Creates a palette of colours depending on the user’s wishes or whether /// output is to a terminal. pub fn palette(self) -> Colours { if self.should_use_colours() { Colours::pretty() } else { Colours::plain() } } } impl OutputFormat { /// Prints the entirety of the output, formatted according to the /// settings. If the duration has been measured, it should also be /// printed. Returns `false` if there were no results to print, and `true` /// otherwise. pub fn print(self, responses: Vec, duration: Option) -> bool { match self { Self::Short(tf) => { let all_answers = responses.into_iter().flat_map(|r| r.answers).collect::>(); if all_answers.is_empty() { eprintln!("No results"); return false; } for answer in all_answers { match answer { Answer::Standard { record, .. } => { println!("{}", tf.record_payload_summary(record)) } Answer::Pseudo { opt, .. } => { println!("{}", tf.pseudo_record_payload_summary(opt)) } } } } Self::JSON => { let mut rs = Vec::new(); for response in responses { let json = object! { "queries": json_queries(response.queries), "answers": json_answers(response.answers), "authorities": json_answers(response.authorities), "additionals": json_answers(response.additionals), }; rs.push(json); } if let Some(duration) = duration { let object = object! { "responses": rs, "duration": { "secs": duration.as_secs(), "millis": duration.subsec_millis(), }, }; println!("{}", object); } else { let object = object! { "responses": rs, }; println!("{}", object); } } Self::Text(uc, tf) => { let mut table = Table::new(uc.palette(), tf); for response in responses { if let Some(rcode) = response.flags.error_code { print_error_code(rcode); } for a in response.answers { table.add_row(a, Section::Answer); } for a in response.authorities { table.add_row(a, Section::Authority); } for a in response.additionals { table.add_row(a, Section::Additional); } } table.print(duration); } } true } /// Print an error that’s ocurred while sending or receiving DNS packets /// to standard error. pub fn print_error(self, error: TransportError) { match self { Self::Short(..) | Self::Text(..) => { eprintln!("Error [{}]: {}", erroneous_phase(&error), error_message(error)); } Self::JSON => { let object = object! { "error": true, "error_phase": erroneous_phase(&error), "error_message": error_message(error), }; eprintln!("{}", object); } } } } impl TextFormat { /// Formats a summary of a record in a received DNS response. Each record /// type contains wildly different data, so the format of the summary /// depends on what record it’s for. pub fn record_payload_summary(self, record: Record) -> String { match record { Record::A(a) => { format!("{}", a.address) } Record::AAAA(aaaa) => { format!("{}", aaaa.address) } Record::CAA(caa) => { if caa.critical { format!("{} {} (critical)", Ascii(&caa.tag), Ascii(&caa.value)) } else { format!("{} {} (non-critical)", Ascii(&caa.tag), Ascii(&caa.value)) } } Record::CNAME(cname) => { format!("{:?}", cname.domain.to_string()) } Record::EUI48(eui48) => { format!("{:?}", eui48.formatted_address()) } Record::EUI64(eui64) => { format!("{:?}", eui64.formatted_address()) } Record::HINFO(hinfo) => { format!("{} {}", Ascii(&hinfo.cpu), Ascii(&hinfo.os)) } Record::LOC(loc) => { format!("{} ({}, {}) ({}, {}, {})", loc.size, loc.horizontal_precision, loc.vertical_precision, loc.latitude .map_or_else(|| "Out of range".into(), |e| e.to_string()), loc.longitude.map_or_else(|| "Out of range".into(), |e| e.to_string()), loc.altitude, ) } Record::MX(mx) => { format!("{} {:?}", mx.preference, mx.exchange.to_string()) } Record::NAPTR(naptr) => { format!("{} {} {} {} {} {:?}", naptr.order, naptr.preference, Ascii(&naptr.flags), Ascii(&naptr.service), Ascii(&naptr.regex), naptr.replacement.to_string(), ) } Record::NS(ns) => { format!("{:?}", ns.nameserver.to_string()) } Record::OPENPGPKEY(opgp) => { format!("{:?}", opgp.base64_key()) } Record::PTR(ptr) => { format!("{:?}", ptr.cname.to_string()) } Record::SSHFP(sshfp) => { format!("{} {} {}", sshfp.algorithm, sshfp.fingerprint_type, sshfp.hex_fingerprint(), ) } Record::SOA(soa) => { format!("{:?} {:?} {} {} {} {} {}", soa.mname.to_string(), soa.rname.to_string(), soa.serial, self.format_duration(soa.refresh_interval), self.format_duration(soa.retry_interval), self.format_duration(soa.expire_limit), self.format_duration(soa.minimum_ttl), ) } Record::SRV(srv) => { format!("{} {} {:?}:{}", srv.priority, srv.weight, srv.target.to_string(), srv.port) } Record::TLSA(tlsa) => { format!("{} {} {} {:?}", tlsa.certificate_usage, tlsa.selector, tlsa.matching_type, tlsa.hex_certificate_data(), ) } Record::TXT(txt) => { let messages = txt.messages.iter().map(|t| Ascii(t).to_string()).collect::>(); messages.join(", ") } Record::URI(uri) => { format!("{} {} {}", uri.priority, uri.weight, Ascii(&uri.target)) } Record::Other { bytes, .. } => { format!("{:?}", bytes) } } } /// Formats a summary of an OPT pseudo-record. Pseudo-records have a different /// structure than standard ones. pub fn pseudo_record_payload_summary(self, opt: OPT) -> String { format!("{} {} {} {} {:?}", opt.udp_payload_size, opt.higher_bits, opt.edns0_version, opt.flags, opt.data) } /// Formats a duration depending on whether it should be displayed as /// seconds, or as computed units. pub fn format_duration(self, seconds: u32) -> String { if self.format_durations { format_duration_hms(seconds) } else { format!("{}", seconds) } } } /// Formats a duration as days, hours, minutes, and seconds, skipping leading /// zero units. fn format_duration_hms(seconds: u32) -> String { if seconds < 60 { format!("{}s", seconds) } else if seconds < 60 * 60 { format!("{}m{:02}s", seconds / 60, seconds % 60) } else if seconds < 60 * 60 * 24 { format!("{}h{:02}m{:02}s", seconds / 3600, (seconds % 3600) / 60, seconds % 60) } else { format!("{}d{}h{:02}m{:02}s", seconds / 86400, (seconds % 86400) / 3600, (seconds % 3600) / 60, seconds % 60) } } /// Serialises multiple DNS queries as a JSON value. fn json_queries(queries: Vec) -> JsonValue { let queries = queries.iter().map(|q| { object! { "name": q.qname.to_string(), "class": json_class(q.qclass), "type": json_record_type_name(q.qtype), } }).collect::>(); queries.into() } /// Serialises multiple received DNS answers as a JSON value. fn json_answers(answers: Vec) -> JsonValue { let answers = answers.into_iter().map(|a| { match a { Answer::Standard { qname, qclass, ttl, record } => { object! { "name": qname.to_string(), "class": json_class(qclass), "ttl": ttl, "type": json_record_name(&record), "data": json_record_data(record), } } Answer::Pseudo { qname, opt } => { object! { "name": qname.to_string(), "type": "OPT", "data": { "version": opt.edns0_version, "data": opt.data, }, } } } }).collect::>(); answers.into() } fn json_class(class: QClass) -> JsonValue { match class { QClass::IN => "IN".into(), QClass::CH => "CH".into(), QClass::HS => "HS".into(), QClass::Other(n) => n.into(), } } /// Serialises a DNS record type name. fn json_record_type_name(record: RecordType) -> JsonValue { match record { RecordType::A => "A".into(), RecordType::AAAA => "AAAA".into(), RecordType::CAA => "CAA".into(), RecordType::CNAME => "CNAME".into(), RecordType::EUI48 => "EUI48".into(), RecordType::EUI64 => "EUI64".into(), RecordType::HINFO => "HINFO".into(), RecordType::LOC => "LOC".into(), RecordType::MX => "MX".into(), RecordType::NAPTR => "NAPTR".into(), RecordType::NS => "NS".into(), RecordType::OPENPGPKEY => "OPENPGPKEY".into(), RecordType::PTR => "PTR".into(), RecordType::SOA => "SOA".into(), RecordType::SRV => "SRV".into(), RecordType::SSHFP => "SSHFP".into(), RecordType::TLSA => "TLSA".into(), RecordType::TXT => "TXT".into(), RecordType::URI => "URI".into(), RecordType::Other(unknown) => { match unknown { UnknownQtype::HeardOf(name, _) => (*name).into(), UnknownQtype::UnheardOf(num) => (num).into(), } } } } /// Serialises a DNS record type name. fn json_record_name(record: &Record) -> JsonValue { match record { Record::A(_) => "A".into(), Record::AAAA(_) => "AAAA".into(), Record::CAA(_) => "CAA".into(), Record::CNAME(_) => "CNAME".into(), Record::EUI48(_) => "EUI48".into(), Record::EUI64(_) => "EUI64".into(), Record::HINFO(_) => "HINFO".into(), Record::LOC(_) => "LOC".into(), Record::MX(_) => "MX".into(), Record::NAPTR(_) => "NAPTR".into(), Record::NS(_) => "NS".into(), Record::OPENPGPKEY(_) => "OPENPGPKEY".into(), Record::PTR(_) => "PTR".into(), Record::SOA(_) => "SOA".into(), Record::SRV(_) => "SRV".into(), Record::SSHFP(_) => "SSHFP".into(), Record::TLSA(_) => "TLSA".into(), Record::TXT(_) => "TXT".into(), Record::URI(_) => "URI".into(), Record::Other { type_number, .. } => { match type_number { UnknownQtype::HeardOf(name, _) => (*name).into(), UnknownQtype::UnheardOf(num) => (*num).into(), } } } } /// Serialises a received DNS record as a JSON value. /// Even though DNS doesn’t specify a character encoding, strings are still /// converted from UTF-8, because JSON specifies UTF-8. fn json_record_data(record: Record) -> JsonValue { match record { Record::A(a) => { object! { "address": a.address.to_string(), } } Record::AAAA(aaaa) => { object! { "address": aaaa.address.to_string(), } } Record::CAA(caa) => { object! { "critical": caa.critical, "tag": String::from_utf8_lossy(&caa.tag).to_string(), "value": String::from_utf8_lossy(&caa.value).to_string(), } } Record::CNAME(cname) => { object! { "domain": cname.domain.to_string(), } } Record::EUI48(eui48) => { object! { "identifier": eui48.formatted_address(), } } Record::EUI64(eui64) => { object! { "identifier": eui64.formatted_address(), } } Record::HINFO(hinfo) => { object! { "cpu": String::from_utf8_lossy(&hinfo.cpu).to_string(), "os": String::from_utf8_lossy(&hinfo.os).to_string(), } } Record::LOC(loc) => { object! { "size": loc.size.to_string(), "precision": { "horizontal": loc.horizontal_precision, "vertical": loc.vertical_precision, }, "point": { "latitude": loc.latitude.map(|e| e.to_string()), "longitude": loc.longitude.map(|e| e.to_string()), "altitude": loc.altitude.to_string(), }, } } Record::MX(mx) => { object! { "preference": mx.preference, "exchange": mx.exchange.to_string(), } } Record::NAPTR(naptr) => { object! { "order": naptr.order, "flags": String::from_utf8_lossy(&naptr.flags).to_string(), "service": String::from_utf8_lossy(&naptr.service).to_string(), "regex": String::from_utf8_lossy(&naptr.regex).to_string(), "replacement": naptr.replacement.to_string(), } } Record::NS(ns) => { object! { "nameserver": ns.nameserver.to_string(), } } Record::OPENPGPKEY(opgp) => { object! { "key": opgp.base64_key(), } } Record::PTR(ptr) => { object! { "cname": ptr.cname.to_string(), } } Record::SSHFP(sshfp) => { object! { "algorithm": sshfp.algorithm, "fingerprint_type": sshfp.fingerprint_type, "fingerprint": sshfp.hex_fingerprint(), } } Record::SOA(soa) => { object! { "mname": soa.mname.to_string(), } } Record::SRV(srv) => { object! { "priority": srv.priority, "weight": srv.weight, "port": srv.port, "target": srv.target.to_string(), } } Record::TLSA(tlsa) => { object! { "certificate_usage": tlsa.certificate_usage, "selector": tlsa.selector, "matching_type": tlsa.matching_type, "certificate_data": tlsa.hex_certificate_data(), } } Record::TXT(txt) => { let ms = txt.messages.into_iter() .map(|txt| String::from_utf8_lossy(&txt).to_string()) .collect::>(); object! { "messages": ms, } } Record::URI(uri) => { object! { "priority": uri.priority, "weight": uri.weight, "target": String::from_utf8_lossy(&uri.target).to_string(), } } Record::Other { bytes, .. } => { object! { "bytes": bytes, } } } } /// A wrapper around displaying characters that escapes quotes and /// backslashes, and writes control and upper-bit bytes as their number rather /// than their character. This is needed because even though such characters /// are not allowed in domain names, packets can contain anything, and we need /// a way to display the response, whatever it is. struct Ascii<'a>(&'a [u8]); impl fmt::Display for Ascii<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\"")?; for byte in self.0.iter().copied() { if byte < 32 || byte >= 128 { write!(f, "\\{}", byte)?; } else if byte == b'"' { write!(f, "\\\"")?; } else if byte == b'\\' { write!(f, "\\\\")?; } else { write!(f, "{}", byte as char)?; } } write!(f, "\"") } } /// Prints a message describing the “error code” field of a DNS packet. This /// happens when the packet was received correctly, but the server indicated /// an error. pub fn print_error_code(rcode: ErrorCode) { match rcode { ErrorCode::FormatError => println!("Status: Format Error"), ErrorCode::ServerFailure => println!("Status: Server Failure"), ErrorCode::NXDomain => println!("Status: NXDomain"), ErrorCode::NotImplemented => println!("Status: Not Implemented"), ErrorCode::QueryRefused => println!("Status: Query Refused"), ErrorCode::BadVersion => println!("Status: Bad Version"), ErrorCode::Private(num) => println!("Status: Private Reason ({})", num), ErrorCode::Other(num) => println!("Status: Other Failure ({})", num), } } /// Returns the “phase” of operation where an error occurred. This gets shown /// to the user so they can debug what went wrong. fn erroneous_phase(error: &TransportError) -> &'static str { match error { TransportError::WireError(_) => "protocol", TransportError::TruncatedResponse | TransportError::NetworkError(_) => "network", #[cfg(feature = "with_nativetls")] TransportError::TlsError(_) | TransportError::TlsHandshakeError(_) => "tls", #[cfg(feature = "with_rustls")] TransportError::RustlsInvalidDnsNameError(_) => "tls", // TODO: Actually wrong, could be https #[cfg(feature = "with_https")] TransportError::HttpError(_) | TransportError::WrongHttpStatus(_,_) => "http", } } /// Formats an error into its human-readable message. fn error_message(error: TransportError) -> String { match error { TransportError::WireError(e) => wire_error_message(e), TransportError::TruncatedResponse => "Truncated response".into(), TransportError::NetworkError(e) => e.to_string(), #[cfg(feature = "with_nativetls")] TransportError::TlsError(e) => e.to_string(), #[cfg(feature = "with_nativetls")] TransportError::TlsHandshakeError(e) => e.to_string(), #[cfg(any(feature = "with_rustls"))] TransportError::RustlsInvalidDnsNameError(e) => e.to_string(), #[cfg(feature = "with_https")] TransportError::HttpError(e) => e.to_string(), #[cfg(feature = "with_https")] TransportError::WrongHttpStatus(t,r) => format!("Nameserver returned HTTP {} ({})", t, r.unwrap_or_else(|| "No reason".into())) } } /// Formats a wire error into its human-readable message, describing what was /// wrong with the packet we received. fn wire_error_message(error: WireError) -> String { match error { WireError::IO => { "Malformed packet: insufficient data".into() } WireError::WrongRecordLength { stated_length, mandated_length: MandatedLength::Exactly(len) } => { format!("Malformed packet: record length should be {}, got {}", len, stated_length ) } WireError::WrongRecordLength { stated_length, mandated_length: MandatedLength::AtLeast(len) } => { format!("Malformed packet: record length should be at least {}, got {}", len, stated_length ) } WireError::WrongLabelLength { stated_length, length_after_labels } => { format!("Malformed packet: length {} was specified, but read {} bytes", stated_length, length_after_labels) } WireError::TooMuchRecursion(indices) => { format!("Malformed packet: too much recursion: {:?}", indices) } WireError::OutOfBounds(index) => { format!("Malformed packet: out of bounds ({})", index) } WireError::WrongVersion { stated_version, maximum_supported_version } => { format!("Malformed packet: record specifies version {}, expected up to {}", stated_version, maximum_supported_version) } } } #[cfg(test)] mod test { use super::*; #[test] fn escape_quotes() { assert_eq!(Ascii(b"Mallard \"The Duck\" Fillmore").to_string(), "\"Mallard \\\"The Duck\\\" Fillmore\""); } #[test] fn escape_backslashes() { assert_eq!(Ascii(b"\\").to_string(), "\"\\\\\""); } #[test] fn escape_lows() { assert_eq!(Ascii(b"\n\r\t").to_string(), "\"\\10\\13\\9\""); } #[test] fn escape_highs() { assert_eq!(Ascii("pâté".as_bytes()).to_string(), "\"p\\195\\162t\\195\\169\""); } } ================================================ FILE: src/requests.rs ================================================ //! Request generation based on the user’s input arguments. use crate::connect::TransportType; use crate::resolve::{ResolverType, ResolverLookupError}; use crate::txid::TxidGenerator; /// All the information necessary to generate requests for one or more /// queries, nameservers, or transport types. #[derive(PartialEq, Debug)] pub struct RequestGenerator { /// The input parameter matrix. pub inputs: Inputs, /// How to generate transaction IDs. pub txid_generator: TxidGenerator, /// Whether to OPT in to DNS extensions. pub edns: UseEDNS, /// Other weird protocol options. pub protocol_tweaks: ProtocolTweaks, } /// Which things the user has specified they want queried. #[derive(PartialEq, Debug, Default)] pub struct Inputs { /// The list of domain names to query. pub domains: Vec, /// The list of DNS record types to query for. pub record_types: Vec, /// The list of DNS classes to query for. pub classes: Vec, /// The list of resolvers to send queries to. pub resolver_types: Vec, /// The list of transport types to send queries over. pub transport_types: Vec, } /// Weird protocol options that are allowed by the spec but are not common. #[derive(PartialEq, Debug, Default, Copy, Clone)] pub struct ProtocolTweaks { /// Set the `AA` (Authoritative Answer) flag in the header of each request. pub set_authoritative_flag: bool, /// Set the `AD` (Authentic Data) flag in the header of each request. pub set_authentic_flag: bool, /// Set the `CD` (Checking Disabled) flag in the header of each request. pub set_checking_disabled_flag: bool, /// Set the buffer size field in the OPT record of each request. pub udp_payload_size: Option, } /// Whether to send or display OPT packets. #[derive(PartialEq, Debug, Copy, Clone)] pub enum UseEDNS { /// Do not send an OPT query in requests, and do not display them. Disable, /// Send an OPT query in requests, but hide the result. This is the /// default, because the information is usually not useful to the user. SendAndHide, /// Send an OPT query in requests, _and_ display any OPT records in the /// response we receive. SendAndShow, } /// The entry type for `RequestGenerator`: a transport to send a request, and /// a list of one or more DNS queries to send over it, as determined by the /// search path in the resolver. pub type RequestSet = (Box, Vec); impl RequestGenerator { /// 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) -> Result, ResolverLookupError> { let mut requests = Vec::new(); let resolvers = self.inputs.resolver_types.into_iter() .map(ResolverType::obtain) .collect::, _>>()?; for domain in &self.inputs.domains { for qtype in self.inputs.record_types.iter().copied() { for qclass in self.inputs.classes.iter().copied() { for resolver in &resolvers { for transport_type in &self.inputs.transport_types { let mut flags = dns::Flags::query(); self.protocol_tweaks.set_request_flags(&mut flags); let mut additional = None; if self.edns.should_send() { let mut opt = dns::Request::additional_record(); self.protocol_tweaks.set_request_opt_fields(&mut opt); additional = Some(opt); } let nameserver = resolver.nameserver(); let transport = transport_type.make_transport(nameserver); 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((transport, request_list)); } } } } } Ok(requests) } } impl UseEDNS { /// Whether the user wants to send OPT records. pub fn should_send(self) -> bool { self != Self::Disable } /// Whether the user wants to display sent OPT records. pub fn should_show(self) -> bool { self == Self::SendAndShow } } impl ProtocolTweaks { /// Sets fields in the DNS flags based on the user’s requested tweaks. pub fn set_request_flags(self, flags: &mut dns::Flags) { if self.set_authoritative_flag { flags.authoritative = true; } if self.set_authentic_flag { flags.authentic_data = true; } if self.set_checking_disabled_flag { flags.checking_disabled = true; } } /// Set the payload size field in the outgoing OPT record, if the user has /// requested to do so. pub fn set_request_opt_fields(self, opt: &mut dns::record::OPT) { if let Some(bufsize) = self.udp_payload_size { opt.udp_payload_size = bufsize; } } } ================================================ FILE: src/resolve.rs ================================================ //! Specifying the address of the DNS server to send requests to. use std::fmt; use std::io; use log::*; use dns::Labels; /// A **resolver type** is the source of a `Resolver`. #[derive(PartialEq, Debug)] pub enum ResolverType { /// Obtain a resolver by consulting the system in order to find a /// nameserver and a search list. SystemDefault, /// Obtain a resolver by using the given user-submitted string. Specific(String), } impl ResolverType { /// Obtains a resolver by the means specified in this type. Returns an /// error if there was a problem looking up system information, or if /// there is no suitable nameserver available. pub fn obtain(self) -> Result { match self { Self::SystemDefault => { system_nameservers() } Self::Specific(nameserver) => { let search_list = Vec::new(); Ok(Resolver { nameserver, search_list }) } } } } /// A **resolver** knows the address of the server we should /// send DNS requests to, and the search list for name lookup. #[derive(Debug)] pub struct Resolver { /// The address of the nameserver. pub nameserver: String, /// The search list for name lookup. pub search_list: Vec, } impl Resolver { /// 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 /// the search list. pub fn name_list(&self, name: &Labels) -> Vec { 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(_) => warn!("Invalid search list: {}", search), } } list.push(name.clone()); list } } /// Looks up the system default nameserver on Unix, by querying /// `/etc/resolv.conf` and using the first line that specifies one. /// 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() -> Result { use std::fs::File; use std::io::{BufRead, BufReader}; if cfg!(test) { panic!("system_nameservers() called from test code"); } let f = File::open("/etc/resolv.conf")?; let reader = BufReader::new(f); let mut nameservers = Vec::new(); let mut search_list = Vec::new(); for line in reader.lines() { let line = line?; if let Some(nameserver_str) = line.strip_prefix("nameserver ") { let ip: Result = nameserver_str.parse(); // TODO: This will need to be changed for IPv6 support. match ip { Ok(_ip) => nameservers.push(nameserver_str.into()), 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())); } } if let Some(nameserver) = nameservers.into_iter().next() { Ok(Resolver { nameserver, search_list }) } else { Err(ResolverLookupError::NoNameserver) } } /// 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)] #[allow(unused)] // todo: Remove this when the time is right fn system_nameservers() -> Result { use std::net::{IpAddr, UdpSocket}; if cfg!(test) { panic!("system_nameservers() called from test code"); } // According to the specification, prefer ipv6 by default. // TODO: add control flag to select an ip family. #[derive(Debug, PartialEq)] enum ForceIPFamily { V4, V6, None, } // get the IP of the Network adapter that is used to access the Internet // https://stackoverflow.com/questions/24661022/getting-ip-adress-associated-to-real-hardware-ethernet-controller-in-windows-c fn get_ipv4() -> io::Result { let s = UdpSocket::bind("0.0.0.0:0")?; s.connect("8.8.8.8:53")?; let addr = s.local_addr()?; Ok(addr.ip()) } fn get_ipv6() -> io::Result { let s = UdpSocket::bind("[::1]:0")?; s.connect("[2001:4860:4860::8888]:53")?; let addr = s.local_addr()?; Ok(addr.ip()) } let force_ip_family: ForceIPFamily = ForceIPFamily::None; let ip = match force_ip_family { ForceIPFamily::V4 => get_ipv4().ok(), ForceIPFamily::V6 => get_ipv6().ok(), ForceIPFamily::None => get_ipv6().or(get_ipv4()).ok(), }; let search_list = Vec::new(); // todo: implement this let adapters = ipconfig::get_adapters()?; let active_adapters = adapters.iter().filter(|a| { a.oper_status() == ipconfig::OperStatus::IfOperStatusUp && !a.gateways().is_empty() }); if let Some(dns_server) = active_adapters .clone() .find(|a| ip.map(|ip| a.ip_addresses().contains(&ip)).unwrap_or(false)) .map(|a| a.dns_servers().first()) .flatten() { debug!("Found first nameserver {:?}", dns_server); let nameserver = dns_server.to_string(); Ok(Resolver { nameserver, search_list }) } // Fallback else if let Some(dns_server) = active_adapters .flat_map(|a| a.dns_servers()) .find(|d| (d.is_ipv4() && force_ip_family != ForceIPFamily::V6) || d.is_ipv6()) { debug!("Found first fallback nameserver {:?}", dns_server); let nameserver = dns_server.to_string(); Ok(Resolver { nameserver, search_list }) } else { Err(ResolverLookupError::NoNameserver) } } /// 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() -> Result { warn!("Unable to fetch default nameservers on this platform."); Err(ResolverLookupError::UnsupportedPlatform) } /// Something that can go wrong while obtaining a `Resolver`. pub enum ResolverLookupError { /// The system information was successfully read, but there was no adapter /// suitable to use. NoNameserver, /// There was an error accessing the network configuration. IO(io::Error), /// There was an error accessing the network configuration (extra errors /// that can only happen on Windows). #[cfg(windows)] Windows(ipconfig::error::Error), /// dog is running on a platform where it doesn’t know how to get the /// network configuration, so the user must supply one instead. #[cfg(all(not(unix), not(windows)))] UnsupportedPlatform, } impl From for ResolverLookupError { fn from(error: io::Error) -> ResolverLookupError { Self::IO(error) } } #[cfg(windows)] impl From for ResolverLookupError { fn from(error: ipconfig::error::Error) -> ResolverLookupError { Self::Windows(error) } } impl fmt::Display for ResolverLookupError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NoNameserver => { write!(f, "No nameserver found") } Self::IO(ioe) => { write!(f, "Error reading network configuration: {}", ioe) } #[cfg(windows)] Self::Windows(ipe) => { write!(f, "Error reading network configuration: {}", ipe) } #[cfg(all(not(unix), not(windows)))] Self::UnsupportedPlatform => { write!(f, "dog cannot automatically detect nameservers on this platform; you will have to provide one explicitly") } } } } ================================================ FILE: src/table.rs ================================================ //! Rendering tables of DNS response results. use std::time::Duration; use ansi_term::ANSIString; use dns::Answer; use dns::record::Record; use crate::colours::Colours; use crate::output::TextFormat; /// A **table** is built up from all the response records present in a DNS /// packet. It then gets displayed to the user. #[derive(Debug)] pub struct Table { colours: Colours, text_format: TextFormat, rows: Vec, } /// A row of the table. This contains all the fields #[derive(Debug)] pub struct Row { qtype: ANSIString<'static>, qname: String, ttl: Option, section: Section, summary: String, } /// The section of the DNS response that a record was read from. #[derive(PartialEq, Debug, Copy, Clone)] pub enum Section { /// This record was found in the **Answer** section. Answer, /// This record was found in the **Authority** section. Authority, /// This record was found in the **Additional** section. Additional, } impl Table { /// Create a new table with no rows. pub fn new(colours: Colours, text_format: TextFormat) -> Self { Self { colours, text_format, rows: Vec::new() } } /// Adds a row to the table, containing the data in the given answer in /// the right section. pub fn add_row(&mut self, answer: Answer, section: Section) { match answer { Answer::Standard { record, qname, ttl, .. } => { let qtype = self.coloured_record_type(&record); let qname = qname.to_string(); let summary = self.text_format.record_payload_summary(record); let ttl = Some(self.text_format.format_duration(ttl)); self.rows.push(Row { qtype, qname, ttl, summary, section }); } Answer::Pseudo { qname, opt } => { let qtype = self.colours.opt.paint("OPT"); let qname = qname.to_string(); let summary = self.text_format.pseudo_record_payload_summary(opt); self.rows.push(Row { qtype, qname, ttl: None, summary, section }); } } } /// Prints the formatted table to stdout. pub fn print(self, duration: Option) { if ! self.rows.is_empty() { let qtype_len = self.max_qtype_len(); let qname_len = self.max_qname_len(); let ttl_len = self.max_ttl_len(); for r in &self.rows { for _ in 0 .. qtype_len - r.qtype.len() { print!(" "); } print!("{} {} ", r.qtype, self.colours.qname.paint(&r.qname)); for _ in 0 .. qname_len - r.qname.len() { print!(" "); } if let Some(ttl) = &r.ttl { for _ in 0 .. ttl_len - ttl.len() { print!(" "); } print!("{}", ttl); } else { for _ in 0 .. ttl_len { print!(" "); } } println!(" {} {}", self.format_section(r.section), r.summary); } } if let Some(dur) = duration { println!("Ran in {}ms", dur.as_millis()); } } 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::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()), } } fn max_qtype_len(&self) -> usize { self.rows.iter().map(|r| r.qtype.len()).max().unwrap() } fn max_qname_len(&self) -> usize { self.rows.iter().map(|r| r.qname.len()).max().unwrap() } fn max_ttl_len(&self) -> usize { self.rows.iter().map(|r| r.ttl.as_ref().map_or(0, String::len)).max().unwrap() } fn format_section(&self, section: Section) -> ANSIString<'static> { match section { Section::Answer => self.colours.answer.paint(" "), Section::Authority => self.colours.authority.paint("A"), Section::Additional => self.colours.additional.paint("+"), } } } ================================================ FILE: src/txid.rs ================================================ //! Transaction ID generation. /// A **transaction ID generator** is used to create unique ID numbers to /// identify each packet, as part of the DNS protocol. #[derive(PartialEq, Debug, Copy, Clone)] pub enum TxidGenerator { /// Generate random transaction IDs each time. Random, /// Generate transaction IDs in a sequence, starting from the given value, /// wrapping around. Sequence(u16), } impl TxidGenerator { pub fn generate(self) -> u16 { match self { Self::Random => rand::random(), Self::Sequence(start) => start, // todo } } } ================================================ FILE: src/usage.txt ================================================ \4mUsage:\0m \1mdog\0m \1;33m[OPTIONS]\0m [--] \32m\0m \4mExamples:\0m \1mdog\0m \32mexample.net\0m Query a domain using default settings \1mdog\0m \32mexample.net MX\0m ...looking up MX records instead \1mdog\0m \32mexample.net MX @1.1.1.1\0m ...using a specific nameserver instead \1mdog\0m \32mexample.net MX @1.1.1.1\0m \1;33m-T\0m ...using TCP rather than UDP \1mdog\0m \1;33m-q\0m \33mexample.net\0m \1;33m-t\0m \33mMX\0m \1;33m-n\0m \33m1.1.1.1\0m \1;33m-T\0m As above, but using explicit arguments \4mQuery options:\0m \32m\0m Human-readable host names, nameservers, types, or classes \1;33m-q\0m, \1;33m--query\0m=\33mHOST\0m Host name or domain name to query \1;33m-t\0m, \1;33m--type\0m=\33mTYPE\0m Type of the DNS record being queried (A, MX, NS...) \1;33m-n\0m, \1;33m--nameserver\0m=\33mADDR\0m Address of the nameserver to send packets to \1;33m--class\0m=\33mCLASS\0m Network class of the DNS record being queried (IN, CH, HS) \4mSending options:\0m \1;33m--edns\0m=\33mSETTING\0m Whether to OPT in to EDNS (disable, hide, show) \1;33m--txid\0m=\33mNUMBER\0m Set the transaction ID to a specific value \1;33m-Z\0m=\33mTWEAKS\0m Set uncommon protocol-level tweaks \4mProtocol options:\0m \1;33m-U\0m, \1;33m--udp\0m Use the DNS protocol over UDP \1;33m-T\0m, \1;33m--tcp\0m Use the DNS protocol over TCP \1;33m-S\0m, \1;33m--tls\0m Use the DNS-over-TLS protocol \1;33m-H\0m, \1;33m--https\0m Use the DNS-over-HTTPS protocol \4mOutput options:\0m \1;33m-1\0m, \1;33m--short\0m Short mode: display nothing but the first result \1;33m-J\0m, \1;33m--json\0m Display the output as JSON \1;33m--color\0m, \1;33m--colour\0m=\33mWHEN\0m When to colourise the output (always, automatic, never) \1;33m--seconds\0m Do not format durations, display them as seconds \1;33m--time\0m Print how long the response took to arrive \4mMeta options:\0m \1;33m-?\0m, \1;33m--help\0m Print list of command-line options \1;33m-v\0m, \1;33m--version\0m Print version information ================================================ FILE: xtests/README.md ================================================ # dog › xtests 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. The checks are written as [Specsheet] documents, which you’ll need to have installed. For the JSON tests, you’ll also need [jq]. 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 layout The tests have been divided into four 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 between you and those nameservers 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. 4. **features**, which checks dog does the right thing when certain features have been enabled or disabled at compile-time. These tests also should not make network requests when behaving correctly. All four 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: - `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/ [jq]: https://stedolan.github.io/jq/ [public Cloudflare DNS resolver]: https://developers.cloudflare.com/1.1.1.1/ [madns resolver]: https://madns.binarystar.systems/ ================================================ FILE: xtests/features/none.toml ================================================ # These tests are meant to be run against a dog binary compiled with # `--no-default-features`. They will fail otherwise. [[cmd]] name = "The missing features are documented in the version" shell = "dog --version" stdout = { string = "[-idna, -tls, -https]" } stderr = { empty = true } status = 0 tags = [ 'features' ] [[cmd]] name = "The ‘--tls’ option is not accepted when the feature is disabled" shell = "dog --tls a.b.c.d" stdout = { empty = true } stderr = { file = "outputs/disabled_tls.txt" } status = 3 tags = [ 'features' ] [[cmd]] name = "The ‘--https option is not accepted when the feature is disabled" shell = "dog --https a.b.c.d @name.server" stdout = { empty = true } stderr = { file = "outputs/disabled_https.txt" } status = 3 tags = [ 'features' ] ================================================ FILE: xtests/features/outputs/disabled_https.txt ================================================ dog: Cannot use '--https': This version of dog has been compiled without HTTPS support ================================================ FILE: xtests/features/outputs/disabled_tls.txt ================================================ dog: Cannot use '--tls': This version of dog has been compiled without TLS support ================================================ FILE: xtests/live/badssl.toml ================================================ # Untrusted certificates [[cmd]] name = "Using a DNS-over-HTTPS server with an expired certificate" shell = "dog --https @https://expired.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server with the wrong host in the certificate" shell = "dog --https @https://wrong.host.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server with a self-signed certificate" shell = "dog --https @https://self-signed.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server with an untrusted root certificate" shell = "dog --https @https://untrusted-root.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server with a revoked certificate" shell = "dog --https @https://revoked.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server with a known bad certificate" shell = "dog --https @https://superfish.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: The certificate was not trusted." } status = 1 tags = [ 'live', 'badssl', 'https' ] # Handshake failures [[cmd]] name = "Using a DNS-over-HTTPS server that accepts the null cipher" shell = "dog --https @https://null.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: handshake failure" } status = 1 tags = [ 'live', 'badssl', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server that accepts the rc4-md5 cipher" shell = "dog --https @https://rc4-md5.badssl.com/ lookup.dog" stdout = { empty = true } stderr = { string = "Error [tls]: handshake failure" } status = 1 tags = [ 'live', 'badssl', 'https' ] ================================================ FILE: xtests/live/basics.toml ================================================ # Colour output [[cmd]] name = "Running dog with ‘--colour=always’ produces colourful output" shell = "dog dns.google --colour=always" stdout = { string = "\u001B[1;32mA\u001B[0m \u001B[1;34mdns.google.\u001B[0m" } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] [[cmd]] name = "Running dog produces an A record by default" shell = "dog dns.google" stdout = { string = "A dns.google." } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] [[cmd]] name = "Running dog with ‘--colour=never’ produces plain output" shell = "dog dns.google --colour=never" stdout = { string = "A dns.google." } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] # Default record type and transport [[cmd]] name = "Running dog with ‘-U’ produces no errors" shell = "dog dns.google -U" stdout = { string = "A dns.google." } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] [[cmd]] name = "Running dog with ‘A’ produces no errors" shell = "dog A dns.google" stdout = { string = "A dns.google." } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] [[cmd]] name = "Running dog with ‘--time’ outputs a duration" shell = "dog A dns.google --time" stdout = { string = "Ran in" } stderr = { empty = true } status = 0 tags = [ "live", "isp" ] # Network errors [[cmd]] name = "Using a DNS server that does not exist on the network" shell = "dog A dns.google @non.exist --time" stdout = { string = "Ran in" } stderr = { string = "Error [network]" } status = 1 tags = [ "live", "isp" ] ================================================ FILE: xtests/live/bins.toml ================================================ # HTTPS [[cmd]] name = "Using a DNS-over-HTTPS server that returns status 500" shell = "dog --https @https://eu.httpbin.org/status/500 lookup.dog" stdout = { empty = true } stderr = { string = "Error [http]: Nameserver returned HTTP 500 (INTERNAL SERVER ERROR)" } status = 1 tags = [ 'live', 'httpbin', 'https' ] [[cmd]] name = "Using a DNS-over-HTTPS server that returns no content" shell = "dog --https @https://eu.httpbin.org/status/200 lookup.dog" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ 'live', 'httpbin', 'https' ] # TCP # [[cmd]] # name = "Using a TCP server that returns an empty message" # shell = "dog --tcp @52.20.16.20:30000 lookup.dog" # stdout = { empty = true } # stderr = { string = "Error [network]: Truncated response" } # status = 1 # tags = [ 'live', 'tcpbin', 'tcp' ] # The above test is flaky. It works correctly the first time, but produces a # different error message on subsequent runs. # # The ‘other’ tcpbin can be used to test the truncated response error # handling, but it requires waiting 60 seconds for their server to give up and # send us a FIN: # # - dog --tcp bsago.me @tcpbin.com:4242 # - dog --tls bsago.me @tcpbin.com:4243 ================================================ FILE: xtests/live/https.toml ================================================ # A records [[cmd]] name = "Look up an existing A record using HTTPS" shell = "dog a-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '10.20.30.40' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "a" ] [[cmd]] name = "Look up a missing A record using HTTPS" shell = "dog non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "a" ] # AAAA records [[cmd]] name = "Look up an existing AAAA record using HTTPS" shell = "dog AAAA aaaa-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '::1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "aaaa" ] [[cmd]] name = "Look up a missing AAAA record using HTTPS" shell = "dog AAAA non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "aaaa" ] # CAA records [[cmd]] name = "Look up an existing CAA record using HTTPS" shell = "dog CAA caa-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '"issue" "some.certificate.authority" (non-critical)' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "caa" ] [[cmd]] name = "Look up a missing CAA record using HTTPS" shell = "dog CAA non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "caa" ] # CNAME records [[cmd]] name = "Look up an existing CNAME record using HTTPS" shell = "dog CNAME cname-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '"dns.lookup.dog."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "cname" ] [[cmd]] name = "Look up a missing CNAME record using HTTPS" shell = "dog CNAME non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "cname" ] # HINFO records [[cmd]] name = "Look up an existing HINFO record using HTTPS" shell = "dog HINFO hinfo-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '"some-kinda-os"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "hinfo" ] [[cmd]] name = "Look up a missing HINFO record using HTTPS" shell = "dog HINFO non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "hinfo" ] # MX records [[cmd]] name = "Look up an existing MX record using HTTPS" shell = "dog MX mx-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '10 "some.mail.server."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "mx" ] [[cmd]] name = "Look up a missing MX record using HTTPS" shell = "dog MX non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "mx" ] # NS records [[cmd]] name = "Look up an existing NS record using HTTPS" shell = "dog NS lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "ns" ] [[cmd]] name = "Look up a missing NS record using HTTPS" shell = "dog NS non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "ns" ] # SOA records [[cmd]] name = "Look up an existing SOA record using HTTPS" shell = "dog SOA lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "soa" ] [[cmd]] name = "Look up a missing SOA record using HTTPS" shell = "dog MX non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "soa" ] # SRV records [[cmd]] name = "Look up an existing SRV record using HTTPS" shell = "dog SRV srv-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '20 "dns.lookup.dog.":5000' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "srv" ] [[cmd]] name = "Look up a missing SRV record using HTTPS" shell = "dog SRV non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "srv" ] # TXT records [[cmd]] name = "Look up an existing TXT record using HTTPS" shell = "dog TXT txt-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https" stdout = { string = '"Cache Invalidation and Naming Things"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "https", "txt" ] [[cmd]] name = "Look up a missing TXT record using HTTPS" shell = "dog TXT non.existent @https://cloudflare-dns.com/dns-query --short --https" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "https", "txt" ] ================================================ FILE: xtests/live/json.toml ================================================ # A records [[cmd]] name = "Look up an existing A record formatted as JSON" shell = "dog a-example.lookup.dog @1.1.1.1 --json" stdout = { string = '10.20.30.40' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "a" ] [[cmd]] name = "Look up a missing A record formatted as JSON" shell = "dog non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "a" ] # AAAA records [[cmd]] name = "Look up an existing AAAA record formatted as JSON" shell = "dog AAAA aaaa-example.lookup.dog @1.1.1.1 --json" stdout = { string = '::1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "aaaa" ] [[cmd]] name = "Look up a missing AAAA record formatted as JSON" shell = "dog AAAA non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "aaaa" ] # CAA records [[cmd]] name = "Look up an existing CAA record formatted as JSON" shell = "dog CAA caa-example.lookup.dog @1.1.1.1 --json" stdout = { string = '"some.certificate.authority"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "caa" ] [[cmd]] name = "Look up a missing CAA record formatted as JSON" shell = "dog CAA non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "caa" ] # CNAME records [[cmd]] name = "Look up an existing CNAME record formatted as JSON" shell = "dog CNAME cname-example.lookup.dog @1.1.1.1 --json" stdout = { string = '"dns.lookup.dog."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "cname" ] [[cmd]] name = "Look up a missing CNAME record formatted as JSON" shell = "dog CNAME non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "cname" ] # HINFO records [[cmd]] name = "Look up an existing HINFO record formatted as JSON" shell = "dog HINFO hinfo-example.lookup.dog @1.1.1.1 --json" stdout = { string = '"some-kinda-os"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "hinfo" ] [[cmd]] name = "Look up a missing HINFO record formatted as JSON" shell = "dog HINFO non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "hinfo" ] # MX records [[cmd]] name = "Look up an existing MX record formatted as JSON" shell = "dog MX mx-example.lookup.dog @1.1.1.1 --json" stdout = { string = 'some.mail.server.' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "mx" ] [[cmd]] name = "Look up a missing MX record formatted as JSON" shell = "dog MX non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "mx" ] # NS records [[cmd]] name = "Look up an existing NS record formatted as JSON" shell = "dog NS lookup.dog @1.1.1.1 --json" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "ns" ] [[cmd]] name = "Look up a missing NS record formatted as JSON" shell = "dog NS non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "ns" ] # SOA records [[cmd]] name = "Look up an existing SOA record formatted as JSON" shell = "dog SOA lookup.dog @1.1.1.1 --json" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "soa" ] [[cmd]] name = "Look up a missing SOA record formatted as JSON" shell = "dog MX non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "soa" ] # SRV records [[cmd]] name = "Look up an existing SRV record formatted as JSON" shell = "dog SRV srv-example.lookup.dog @1.1.1.1 --json" stdout = { string = 'dns.lookup.dog.' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "srv" ] [[cmd]] name = "Look up a missing SRV record formatted as JSON" shell = "dog SRV non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "srv" ] # TXT records [[cmd]] name = "Look up an existing TXT record formatted as JSON" shell = "dog TXT txt-example.lookup.dog @1.1.1.1 --json" stdout = { string = '"Cache Invalidation and Naming Things"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "txt" ] [[cmd]] name = "Look up a missing TXT record formatted as JSON" shell = "dog TXT non.existent @1.1.1.1 --json" stdout = { string = '[]' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "json", "txt" ] ================================================ FILE: xtests/live/tcp.toml ================================================ # A records [[cmd]] name = "Look up an existing A record using TCP" shell = "dog a-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '10.20.30.40' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "a" ] [[cmd]] name = "Look up a missing A record using TCP" shell = "dog non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "a" ] # AAAA records [[cmd]] name = "Look up an existing AAAA record using TCP" shell = "dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '::1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "aaaa" ] [[cmd]] name = "Look up a missing AAAA record using TCP" shell = "dog AAAA non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "aaaa" ] # CAA records [[cmd]] name = "Look up an existing CAA record using TCP" shell = "dog CAA caa-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '"issue" "some.certificate.authority" (non-critical)' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "caa" ] [[cmd]] name = "Look up a missing CAA record using TCP" shell = "dog CAA non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "caa" ] # CNAME records [[cmd]] name = "Look up an existing CNAME record using TCP" shell = "dog CNAME cname-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '"dns.lookup.dog."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "cname" ] [[cmd]] name = "Look up a missing CNAME record using TCP" shell = "dog CNAME non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "cname" ] # HINFO records [[cmd]] name = "Look up an existing HINFO record using TCP" shell = "dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '"some-kinda-cpu"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "hinfo" ] [[cmd]] name = "Look up a missing HINFO record using TCP" shell = "dog HINFO non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "hinfo" ] # MX records [[cmd]] name = "Look up an existing MX record using TCP" shell = "dog MX mx-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '10 "some.mail.server."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "mx" ] [[cmd]] name = "Look up a missing MX record using TCP" shell = "dog MX non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "mx" ] # NS records [[cmd]] name = "Look up an existing NS record using TCP" shell = "dog NS lookup.dog @1.1.1.1 --short --tcp" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "ns" ] [[cmd]] name = "Look up a missing NS record using TCP" shell = "dog NS non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "ns" ] # SOA records [[cmd]] name = "Look up an existing SOA record using TCP" shell = "dog SOA lookup.dog @1.1.1.1 --short --tcp" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "soa" ] [[cmd]] name = "Look up a missing SOA record using TCP" shell = "dog MX non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "soa" ] # SRV records [[cmd]] name = "Look up an existing SRV record using TCP" shell = "dog SRV srv-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '20 "dns.lookup.dog.":5000' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "srv" ] [[cmd]] name = "Look up a missing SRV record using TCP" shell = "dog SRV non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "srv" ] # TXT records [[cmd]] name = "Look up an existing TXT record using TCP" shell = "dog TXT txt-example.lookup.dog @1.1.1.1 --short --tcp" stdout = { string = '"Cache Invalidation and Naming Things"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tcp", "txt" ] [[cmd]] name = "Look up a missing TXT record using TCP" shell = "dog TXT non.existent @1.1.1.1 --short --tcp" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tcp", "txt" ] ================================================ FILE: xtests/live/tls.toml ================================================ # A records [[cmd]] name = "Look up an existing A record using TLS" shell = "dog a-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '10.20.30.40' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "a" ] [[cmd]] name = "Look up a missing A record using TLS" shell = "dog non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "a" ] # AAAA records [[cmd]] name = "Look up an existing AAAA record using TLS" shell = "dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '::1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "aaaa" ] [[cmd]] name = "Look up a missing AAAA record using TLS" shell = "dog AAAA non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "aaaa" ] # CAA records [[cmd]] name = "Look up an existing CAA record using TLS" shell = "dog CAA caa-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '"issue" "some.certificate.authority" (non-critical)' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "caa" ] [[cmd]] name = "Look up a missing CAA record using TLS" shell = "dog CAA non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "caa" ] # CNAME records [[cmd]] name = "Look up an existing CNAME record using TLS" shell = "dog CNAME cname-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '"dns.lookup.dog."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "cname" ] [[cmd]] name = "Look up a missing CNAME record using TLS" shell = "dog CNAME non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "cname" ] # CNAME records [[cmd]] name = "Look up an existing HINFO record using TLS" shell = "dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '"some-kinda-os"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "hinfo" ] [[cmd]] name = "Look up a missing HINFO record using TLS" shell = "dog HINFO non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "hinfo" ] # MX records [[cmd]] name = "Look up an existing MX record using TLS" shell = "dog MX mx-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '10 "some.mail.server."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "mx" ] [[cmd]] name = "Look up a missing MX record using TLS" shell = "dog MX non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "mx" ] # NS records [[cmd]] name = "Look up an existing NS record using TLS" shell = "dog NS lookup.dog @1.1.1.1 --short --tls" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "ns" ] [[cmd]] name = "Look up a missing NS record using TLS" shell = "dog NS non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "ns" ] # SOA records [[cmd]] name = "Look up an existing SOA record using TLS" shell = "dog SOA lookup.dog @1.1.1.1 --short --tls" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "soa" ] [[cmd]] name = "Look up a missing SOA record using TLS" shell = "dog MX non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "soa" ] # SRV records [[cmd]] name = "Look up an existing SRV record using TLS" shell = "dog SRV srv-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '20 "dns.lookup.dog.":5000' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "srv" ] [[cmd]] name = "Look up a missing SRV record using TLS" shell = "dog SRV non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "srv" ] # TXT records [[cmd]] name = "Look up an existing TXT record using TLS" shell = "dog TXT txt-example.lookup.dog @1.1.1.1 --short --tls" stdout = { string = '"Cache Invalidation and Naming Things"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "tls", "txt" ] [[cmd]] name = "Look up a missing TXT record using TLS" shell = "dog TXT non.existent @1.1.1.1 --short --tls" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "tls", "txt" ] ================================================ FILE: xtests/live/udp.toml ================================================ # A records [[cmd]] name = "Look up an existing A record using UDP" shell = "dog a-example.lookup.dog @1.1.1.1 --short" stdout = { string = '10.20.30.40' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "a" ] [[cmd]] name = "Look up a missing A record using UDP" shell = "dog non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "a" ] # AAAA records [[cmd]] name = "Look up an existing AAAA record using UDP" shell = "dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short" stdout = { string = '::1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "aaaa" ] [[cmd]] name = "Look up a missing AAAA record using UDP" shell = "dog AAAA non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "aaaa" ] # CAA records [[cmd]] name = "Look up an existing CAA record using UDP" shell = "dog CAA caa-example.lookup.dog @1.1.1.1 --short" stdout = { string = '"issue" "some.certificate.authority" (non-critical)' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "caa" ] [[cmd]] name = "Look up a missing CAA record using UDP" shell = "dog CAA non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "caa" ] # CNAME records [[cmd]] name = "Look up an existing CNAME record using UDP" shell = "dog CNAME cname-example.lookup.dog @1.1.1.1 --short" stdout = { string = '"dns.lookup.dog."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "cname" ] [[cmd]] name = "Look up a missing CNAME record using UDP" shell = "dog CNAME non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "cname" ] # CNAME records [[cmd]] name = "Look up an existing HINFO record using UDP" shell = "dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short" stdout = { string = '"some-kinda-cpu"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "hinfo" ] [[cmd]] name = "Look up a missing HINFO record using UDP" shell = "dog HINFO non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "hinfo" ] # MX records [[cmd]] name = "Look up an existing MX record using UDP" shell = "dog MX mx-example.lookup.dog @1.1.1.1 --short" stdout = { string = '10 "some.mail.server."' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "mx" ] [[cmd]] name = "Look up a missing MX record using UDP" shell = "dog MX non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "mx" ] # NS records [[cmd]] name = "Look up an existing NS record using UDP" shell = "dog NS lookup.dog @1.1.1.1 --short" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "ns" ] [[cmd]] name = "Look up a missing NS record using UDP" shell = "dog NS non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "ns" ] # SOA records [[cmd]] name = "Look up an existing SOA record using UDP" shell = "dog SOA lookup.dog @1.1.1.1 --short" stdout = { string = 'ns1' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "soa" ] [[cmd]] name = "Look up a missing SOA record using UDP" shell = "dog MX non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "soa" ] # SRV records [[cmd]] name = "Look up an existing SRV record using UDP" shell = "dog SRV srv-example.lookup.dog @1.1.1.1 --short" stdout = { string = '20 "dns.lookup.dog.":5000' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "srv" ] [[cmd]] name = "Look up a missing SRV record using UDP" shell = "dog SRV non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "srv" ] # TXT records [[cmd]] name = "Look up an existing TXT record using UDP" shell = "dog TXT txt-example.lookup.dog @1.1.1.1 --short" stdout = { string = '"Cache Invalidation and Naming Things"' } stderr = { empty = true } status = 0 tags = [ "live", "cloudflare", "udp", "txt" ] [[cmd]] name = "Look up a missing TXT record using UDP" shell = "dog TXT non.existent @1.1.1.1 --short" stdout = { empty = true } stderr = { string = "No results" } status = 2 tags = [ "live", "cloudflare", "udp", "txt" ] ================================================ FILE: xtests/madns/a-records.toml ================================================ # A record successes [[cmd]] name = "Running with ‘a.example’ prints the correct A record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A a.example" stdout = { file = "outputs/a.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "a", "madns" ] # A record successes (JSON) [[cmd]] name = "Running with ‘a.example --json’ prints the correct A record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A a.example --json | jq" stdout = { file = "outputs/a.example.json" } stderr = { empty = true } status = 0 tags = [ "a", "madns", "json" ] # A record invalid packets [[cmd]] name = "Running with ‘too-long.a.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A incomplete.a.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "a", "madns" ] ================================================ FILE: xtests/madns/aaaa-records.toml ================================================ # AAAA record successes [[cmd]] name = "Running with ‘aaaa.example’ prints the correct AAAA record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA aaaa.example" stdout = { file = "outputs/aaaa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "aaaa", "madns" ] # AAAA record successes (JSON) [[cmd]] name = "Running with ‘aaaa.example --json’ prints the correct AAAA record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA aaaa.example --json | jq" stdout = { file = "outputs/aaaa.example.json" } stderr = { empty = true } status = 0 tags = [ "aaaa", "madns", "json" ] # AAAA record invalid packets [[cmd]] name = "Running with ‘too-long.aaaa.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA incomplete.aaaa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "aaaa", "madns" ] ================================================ FILE: xtests/madns/caa-records.toml ================================================ # CAA record successes [[cmd]] name = "Running with ‘caa.example’ prints the correct CAA record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA others.caa.example" stdout = { file = "outputs/others.caa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "caa", "madns" ] [[cmd]] name = "Running with ‘utf8.caa.example’ escapes characters in the fields" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA utf8.caa.example" stdout = { file = "outputs/utf8.caa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "chars" ] [[cmd]] name = "Running with ‘bad-utf8.caa.example’ escapes characters in the fields and does not crash" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA bad-utf8.caa.example" stdout = { file = "outputs/bad-utf8.caa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "chars" ] # CAA record successes (JSON) [[cmd]] name = "Running with ‘caa.example --json’ prints the correct CAA record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA caa.example --json | jq" stdout = { file = "outputs/caa.example.json" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "json" ] [[cmd]] name = "Running with ‘critical.caa.example --json’ prints the correct CAA record structurewith the flag" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA critical.caa.example --json | jq" stdout = { file = "outputs/critical.caa.example.json" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "json" ] [[cmd]] name = "Running with ‘others.caa.example --json’ prints the correct CAA record structure and ignores the flags" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA others.caa.example --json | jq" stdout = { file = "outputs/others.caa.example.json" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "json" ] [[cmd]] name = "Running with ‘utf8.caa.example --json’ interprets the response as UTF-8" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA utf8.caa.example --json | jq" stdout = { file = "outputs/utf8.caa.example.json" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "chars", "json" ] [[cmd]] name = "Running with ‘bad-utf8.caa.example --json’ uses UTF-8 replacement characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA bad-utf8.caa.example --json | jq" stdout = { file = "outputs/bad-utf8.caa.example.json" } stderr = { empty = true } status = 0 tags = [ "caa", "madns", "chars", "json" ] # CAA record invalid packets [[cmd]] name = "Running with ‘empty.caa.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA incomplete.caa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "caa", "madns" ] ================================================ FILE: xtests/madns/cname-records.toml ================================================ # CNAME record successes [[cmd]] name = "Running with ‘cname.example’ prints the correct CNAME record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME cname.example" stdout = { file = "outputs/cname.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "cname", "madns" ] # CNAME record successes (JSON) [[cmd]] name = "Running with ‘cname.example --json’ prints the correct CNAME record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME cname.example --json | jq" stdout = { file = "outputs/cname.example.json" } stderr = { empty = true } status = 0 tags = [ "cname", "madns", "json" ] # CNAME record invalid packets [[cmd]] name = "Running with ‘empty.cname.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME incomplete.cname.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "cname", "madns" ] ================================================ FILE: xtests/madns/eui48-records.toml ================================================ # EUI48 record successes [[cmd]] name = "Running with ‘eui48.example’ prints the correct EUI48 record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 eui48.example" stdout = { file = "outputs/eui48.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "eui48", "madns" ] # EUI48 record successes (JSON) [[cmd]] name = "Running with ‘eui48.example --json’ prints the correct EUI48 record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 eui48.example --json | jq" stdout = { file = "outputs/eui48.example.json" } stderr = { empty = true } status = 0 tags = [ "eui48", "madns", "json" ] # EUI48 record invalid packets [[cmd]] name = "Running with ‘too-long.eui48.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 incomplete.eui48.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "eui48", "madns" ] ================================================ FILE: xtests/madns/eui64-records.toml ================================================ # EUI64 record successes [[cmd]] name = "Running with ‘eui64.example’ prints the correct EUI64 record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 eui64.example" stdout = { file = "outputs/eui64.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "eui64", "madns" ] # EUI64 record successes (JSON) [[cmd]] name = "Running with ‘eui64.example --json’ prints the correct EUI64 record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 eui64.example --json | jq" stdout = { file = "outputs/eui64.example.json" } stderr = { empty = true } status = 0 tags = [ "eui64", "madns", "json" ] # EUI64 record invalid packets [[cmd]] name = "Running with ‘too-long.eui64.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 incomplete.eui64.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "eui64", "madns" ] ================================================ FILE: xtests/madns/hinfo-records.toml ================================================ # HINFO record successes [[cmd]] name = "Running with ‘hinfo.example’ prints the correct HINFO record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO hinfo.example" stdout = { file = "outputs/hinfo.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns" ] [[cmd]] name = "Running with ‘utf8.hinfo.example’ escapes characters in the fields" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO utf8.hinfo.example" stdout = { file = "outputs/utf8.hinfo.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns", "chars" ] [[cmd]] name = "Running with ‘bad-utf8.hinfo.example’ escapes characters in the fields and does not crash" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO bad-utf8.hinfo.example" stdout = { file = "outputs/bad-utf8.hinfo.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns", "chars" ] # HINFO record successes (JSON) [[cmd]] name = "Running with ‘hinfo.example --json’ prints the correct HINFO record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO hinfo.example --json | jq" stdout = { file = "outputs/hinfo.example.json" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns", "json" ] [[cmd]] name = "Running with ‘utf8.hinfo.example --json’ interprets the response as UTF-8" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO utf8.hinfo.example --json | jq" stdout = { file = "outputs/utf8.hinfo.example.json" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns", "chars", "json" ] [[cmd]] name = "Running with ‘bad-utf8.hinfo.example --json’ uses UTF-8 replacement characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO bad-utf8.hinfo.example --json | jq" stdout = { file = "outputs/bad-utf8.hinfo.example.json" } stderr = { empty = true } status = 0 tags = [ "hinfo", "madns", "chars", "json" ] # HINFO record invalid packets [[cmd]] name = "Running with ‘empty.hinfo.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO incomplete.hinfo.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "hinfo", "madns" ] ================================================ FILE: xtests/madns/loc-records.toml ================================================ # LOC record successes [[cmd]] name = "Running with ‘loc.example’ prints the correct LOC record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC loc.example" stdout = { file = "outputs/loc.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "loc", "madns" ] # LOC record successes (JSON) [[cmd]] name = "Running with ‘loc.example --json’ prints the correct LOC record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC loc.example --json | jq" stdout = { file = "outputs/loc.example.json" } stderr = { empty = true } status = 0 tags = [ "loc", "madns", "json" ] # 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 out-of-range positions (JSON) [[cmd]] name = "Running with ‘far-negative-longitude.loc.invalid’ displays a record structure with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-longitude.loc.invalid --json | jq" stdout = { file = "outputs/far-negative-longitude.loc.invalid.json" } stderr = { empty = true } status = 0 tags = [ "loc", "madns", "json" ] [[cmd]] name = "Running with ‘far-positive-longitude.loc.invalid’ displays a record structure with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-longitude.loc.invalid --json | jq" stdout = { file = "outputs/far-positive-longitude.loc.invalid.json" } stderr = { empty = true } status = 0 tags = [ "loc", "madns", "json" ] [[cmd]] name = "Running with ‘far-negative-latitude.loc.invalid’ displays a record structure with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-latitude.loc.invalid --json | jq" stdout = { file = "outputs/far-negative-latitude.loc.invalid.json" } stderr = { empty = true } status = 0 tags = [ "loc", "madns", "json" ] [[cmd]] name = "Running with ‘far-positive-latitude.loc.invalid’ displays a record structure with an out-of-range field" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-latitude.loc.invalid --json | jq" stdout = { file = "outputs/far-positive-latitude.loc.invalid.json" } stderr = { empty = true } status = 0 tags = [ "loc", "madns", "json" ] # LOC record version 1 [[cmd]] name = "Running with ‘v1-conform.loc.invalid’ displays a version error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC incomplete.loc.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "loc", "madns" ] ================================================ FILE: xtests/madns/mx-records.toml ================================================ # MX record successes [[cmd]] name = "Running with ‘mx.example’ prints the correct MX record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX mx.example" stdout = { file = "outputs/mx.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "mx", "madns" ] # MX record successes (JSON) [[cmd]] name = "Running with ‘mx.example --json’ prints the correct MX record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX mx.example --json | jq" stdout = { file = "outputs/mx.example.json" } stderr = { empty = true } status = 0 tags = [ "mx", "madns", "json" ] # MX record invalid packets [[cmd]] name = "Running with ‘empty.mx.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX incomplete.mx.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "mx", "madns" ] ================================================ FILE: xtests/madns/naptr-records.toml ================================================ # NAPTR record successes [[cmd]] name = "Running with ‘naptr.example’ prints the correct NAPTR record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-regex.naptr.example" stdout = { file = "outputs/bad-regex.naptr.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns" ] [[cmd]] name = "Running with ‘utf8.naptr.example’ escapes characters in the NAPTR" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR utf8.naptr.invalid" stdout = { file = "outputs/utf8.naptr.invalid.ansitxt" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns", "chars" ] [[cmd]] name = "Running with ‘bad-utf8.naptr.example’ escapes characters in the NAPTR and does not crash" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-utf8.naptr.invalid" stdout = { file = "outputs/bad-utf8.naptr.invalid.ansitxt" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns", "chars" ] # NAPTR record successes (JSON) [[cmd]] name = "Running with ‘naptr.example --json’ prints the correct NAPTR record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR naptr.example --json | jq" stdout = { file = "outputs/naptr.example.json" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns", "json" ] [[cmd]] name = "Running with ‘utf8.naptr.example --json’ interprets the response as UTF-8" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR utf8.naptr.invalid --json | jq" stdout = { file = "outputs/utf8.naptr.invalid.json" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns", "chars", "json" ] [[cmd]] name = "Running with ‘bad-utf8.naptr.example --json’ uses UTF-8 replacement characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-utf8.naptr.invalid --json | jq" stdout = { file = "outputs/bad-utf8.naptr.invalid.json" } stderr = { empty = true } status = 0 tags = [ "naptr", "madns", "chars", "json" ] # NAPTR record invalid packets [[cmd]] name = "Running with ‘empty.naptr.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR incomplete.naptr.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "naptr", "madns" ] ================================================ FILE: xtests/madns/ns-records.toml ================================================ # NS record successes [[cmd]] name = "Running with ‘ns.example’ prints the correct NS record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS ns.example" stdout = { file = "outputs/ns.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "ns", "madns" ] # NS record successes (JSON) [[cmd]] name = "Running with ‘ns.example --json’ prints the correct NS record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS ns.example --json | jq" stdout = { file = "outputs/ns.example.json" } stderr = { empty = true } status = 0 tags = [ "ns", "madns", "json" ] # NS record invalid packets [[cmd]] name = "Running with ‘empty.ns.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS incomplete.ns.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "ns", "madns" ] ================================================ FILE: xtests/madns/openpgpkey-records.toml ================================================ # OPENPGPKEY record successes [[cmd]] name = "Running with ‘openpgpkey.example’ prints the correct OPENPGPKEY record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY openpgpkey.example" stdout = { file = "outputs/openpgpkey.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "openpgpkey", "madns" ] # OPENPGPKEY record successes (JSON) [[cmd]] name = "Running with ‘openpgpkey.example --json’ prints the correct OPENPGPKEY record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY openpgpkey.example --json | jq" stdout = { file = "outputs/openpgpkey.example.json" } stderr = { empty = true } status = 0 tags = [ "openpgpkey", "madns", "json" ] # OPENPGPKEY record invalid packets [[cmd]] name = "Running with ‘empty.openpgpkey.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY incomplete.openpgpkey.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "openpgpkey", "madns" ] ================================================ FILE: xtests/madns/opt-records.toml ================================================ # OPT record successes [[cmd]] name = "Running with ‘opt.example’ prints the correct OPT record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} 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 ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A named.opt.invalid --edns=show" stdout = { file = "outputs/named.opt.invalid.ansitxt" } stderr = { empty = true } status = 0 tags = [ "opt", "madns" ] # OPT record successes (JSON) [[cmd]] name = "Running with ‘opt.example --json’ prints the correct OPT record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A opt.example --edns=show --json | jq" stdout = { file = "outputs/opt.example.json" } stderr = { empty = true } status = 0 tags = [ "opt", "madns", "json" ] [[cmd]] name = "Running with ‘do-flag.opt.example --json’ prints the correct OPT record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A do-flag.opt.example --edns=show --json | jq" stdout = { file = "outputs/do-flag.opt.example.json" } stderr = { empty = true } status = 0 tags = [ "opt", "madns", "json" ] [[cmd]] name = "Running with ‘other-flags.opt.example --json’ prints the correct OPT record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A other-flags.opt.example --edns=show --json | jq" stdout = { file = "outputs/other-flags.opt.example.json" } stderr = { empty = true } status = 0 tags = [ "opt", "madns", "json" ] [[cmd]] name = "Running with ‘named.opt.invalid --json’ prints the correct OPT record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A named.opt.invalid --edns=show --json | jq" stdout = { file = "outputs/named.opt.invalid.json" } stderr = { empty = true } status = 0 tags = [ "opt", "madns", "json" ] # OPT record invalid packets [[cmd]] name = "Running with ‘incomplete.opt.invalid’ displays a record length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A incomplete.opt.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "opt", "madns" ] ================================================ FILE: xtests/madns/outputs/a.example.ansitxt ================================================ A a.example. 10m00s 127.0.0.1 ================================================ FILE: xtests/madns/outputs/a.example.json ================================================ { "responses": [ { "queries": [ { "name": "a.example.", "class": "IN", "type": "A" } ], "answers": [ { "name": "a.example.", "class": "IN", "ttl": 600, "type": "A", "data": { "address": "127.0.0.1" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/aaaa.example.ansitxt ================================================ AAAA aaaa.example. 10m00s ::1 ================================================ FILE: xtests/madns/outputs/aaaa.example.json ================================================ { "responses": [ { "queries": [ { "name": "aaaa.example.", "class": "IN", "type": "AAAA" } ], "answers": [ { "name": "aaaa.example.", "class": "IN", "ttl": 600, "type": "AAAA", "data": { "address": "::1" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/ansi.str.example.ansitxt ================================================ CNAME ansi.str.example. 10m00s "\u{1b}[32mgreen.\u{1b}[34mblue.\u{1b}[31mred.\u{1b}[0m." ================================================ FILE: xtests/madns/outputs/ansi.str.example.json ================================================ { "responses": [ { "queries": [ { "name": "ansi.str.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "ansi.str.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "\u001b[32mgreen.\u001b[34mblue.\u001b[31mred.\u001b[0m." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/bad-regex.naptr.example.ansitxt ================================================ NAPTR bad-regex.naptr.example. 10m00s 5 10 "s" "SRV" "(((((((((((((((((((((((((" "srv.example." ================================================ FILE: xtests/madns/outputs/bad-utf8.caa.example.ansitxt ================================================ CAA bad-utf8.caa.example. 10m00s "issuewild" "\208\208\160\255" (non-critical) ================================================ FILE: xtests/madns/outputs/bad-utf8.caa.example.json ================================================ { "responses": [ { "queries": [ { "name": "bad-utf8.caa.example.", "class": "IN", "type": "CAA" } ], "answers": [ { "name": "bad-utf8.caa.example.", "class": "IN", "ttl": 600, "type": "CAA", "data": { "critical": false, "tag": "issuewild", "value": "�Р�" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/bad-utf8.hinfo.example.ansitxt ================================================ HINFO bad-utf8.hinfo.example. 10m00s "\208\208\160\255" "\208\208\160\255" ================================================ FILE: xtests/madns/outputs/bad-utf8.hinfo.example.json ================================================ { "responses": [ { "queries": [ { "name": "bad-utf8.hinfo.example.", "class": "IN", "type": "HINFO" } ], "answers": [ { "name": "bad-utf8.hinfo.example.", "class": "IN", "ttl": 600, "type": "HINFO", "data": { "cpu": "�Р�", "os": "�Р�" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/bad-utf8.naptr.invalid.ansitxt ================================================ NAPTR bad-utf8.naptr.invalid. 10m00s 5 10 "\208\208\160\255" "\208\208\160\255" "\208\208\160\255" "�Р�." ================================================ FILE: xtests/madns/outputs/bad-utf8.naptr.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "bad-utf8.naptr.invalid.", "class": "IN", "type": "NAPTR" } ], "answers": [ { "name": "bad-utf8.naptr.invalid.", "class": "IN", "ttl": 600, "type": "NAPTR", "data": { "order": 5, "flags": "�Р�", "service": "�Р�", "regex": "�Р�", "replacement": "�Р�." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/bad-utf8.txt.example.ansitxt ================================================ TXT bad-utf8.txt.example. 10m00s "\208\208\160\255" ================================================ FILE: xtests/madns/outputs/bad-utf8.txt.example.json ================================================ { "responses": [ { "queries": [ { "name": "bad-utf8.txt.example.", "class": "IN", "type": "TXT" } ], "answers": [ { "name": "bad-utf8.txt.example.", "class": "IN", "ttl": 600, "type": "TXT", "data": { "messages": [ "�Р�" ] } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/bad-utf8.uri.example.ansitxt ================================================ URI bad-utf8.uri.example. 10m00s 10 16 "\208\208\160\255" ================================================ FILE: xtests/madns/outputs/bad-utf8.uri.example.json ================================================ { "responses": [ { "queries": [ { "name": "bad-utf8.uri.example.", "class": "IN", "type": "URI" } ], "answers": [ { "name": "bad-utf8.uri.example.", "class": "IN", "ttl": 600, "type": "URI", "data": { "priority": 10, "weight": 16, "target": "�Р�" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/caa.example.ansitxt ================================================ CAA caa.example. 10m00s "issuewild" "trustworthy.example" (non-critical) ================================================ FILE: xtests/madns/outputs/caa.example.json ================================================ { "responses": [ { "queries": [ { "name": "caa.example.", "class": "IN", "type": "CAA" } ], "answers": [ { "name": "caa.example.", "class": "IN", "ttl": 600, "type": "CAA", "data": { "critical": false, "tag": "issuewild", "value": "trustworthy.example" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/cname.example.ansitxt ================================================ CNAME cname.example. 10m00s "dns.lookup.dog." ================================================ FILE: xtests/madns/outputs/cname.example.json ================================================ { "responses": [ { "queries": [ { "name": "cname.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "cname.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "dns.lookup.dog." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/critical.caa.example.ansitxt ================================================ CAA critical.caa.example. 10m00s "issuewild" "trustworthy.example" (critical) ================================================ FILE: xtests/madns/outputs/critical.caa.example.json ================================================ { "responses": [ { "queries": [ { "name": "critical.caa.example.", "class": "IN", "type": "CAA" } ], "answers": [ { "name": "critical.caa.example.", "class": "IN", "ttl": 600, "type": "CAA", "data": { "critical": true, "tag": "issuewild", "value": "trustworthy.example" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/do-flag.opt.example.ansitxt ================================================ A do-flag.opt.example. 10m00s 127.0.0.1 OPT  + 1452 0 0 32768 [] ================================================ FILE: xtests/madns/outputs/do-flag.opt.example.json ================================================ { "responses": [ { "queries": [ { "name": "do-flag.opt.example.", "class": "IN", "type": "A" } ], "answers": [ { "name": "do-flag.opt.example.", "class": "IN", "ttl": 600, "type": "A", "data": { "address": "127.0.0.1" } } ], "authorities": [], "additionals": [ { "name": "", "type": "OPT", "data": { "version": 0, "data": [] } } ] } ] } ================================================ FILE: xtests/madns/outputs/eui48.example.ansitxt ================================================ EUI48 eui48.example. 10m00s "12-34-56-78-90-ab" ================================================ FILE: xtests/madns/outputs/eui48.example.json ================================================ { "responses": [ { "queries": [ { "name": "eui48.example.", "class": "IN", "type": "EUI48" } ], "answers": [ { "name": "eui48.example.", "class": "IN", "ttl": 600, "type": "EUI48", "data": { "identifier": "12-34-56-78-90-ab" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/eui64.example.ansitxt ================================================ EUI64 eui64.example. 10m00s "12-34-56-ff-fe-78-90-ab" ================================================ FILE: xtests/madns/outputs/eui64.example.json ================================================ { "responses": [ { "queries": [ { "name": "eui64.example.", "class": "IN", "type": "EUI64" } ], "answers": [ { "name": "eui64.example.", "class": "IN", "ttl": 600, "type": "EUI64", "data": { "identifier": "12-34-56-ff-fe-78-90-ab" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/far-negative-latitude.loc.invalid.ansitxt ================================================ LOC far-negative-latitude.loc.invalid. 10m00s 3e2 (0, 0) (Out of range, 0°0′0″ E, 0m) ================================================ FILE: xtests/madns/outputs/far-negative-latitude.loc.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "far-negative-latitude.loc.invalid.", "class": "IN", "type": "LOC" } ], "answers": [ { "name": "far-negative-latitude.loc.invalid.", "class": "IN", "ttl": 600, "type": "LOC", "data": { "size": "3e2", "precision": { "horizontal": 0, "vertical": 0 }, "point": { "latitude": null, "longitude": "0°0′0″ E", "altitude": "0m" } } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/far-negative-longitude.loc.invalid.ansitxt ================================================ LOC far-negative-longitude.loc.invalid. 10m00s 3e2 (0, 0) (0°0′0″ N, Out of range, 0m) ================================================ FILE: xtests/madns/outputs/far-negative-longitude.loc.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "far-negative-longitude.loc.invalid.", "class": "IN", "type": "LOC" } ], "answers": [ { "name": "far-negative-longitude.loc.invalid.", "class": "IN", "ttl": 600, "type": "LOC", "data": { "size": "3e2", "precision": { "horizontal": 0, "vertical": 0 }, "point": { "latitude": "0°0′0″ N", "longitude": null, "altitude": "0m" } } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/far-positive-latitude.loc.invalid.ansitxt ================================================ LOC far-positive-latitude.loc.invalid. 10m00s 3e2 (0, 0) (Out of range, 0°0′0″ E, 0m) ================================================ FILE: xtests/madns/outputs/far-positive-latitude.loc.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "far-positive-latitude.loc.invalid.", "class": "IN", "type": "LOC" } ], "answers": [ { "name": "far-positive-latitude.loc.invalid.", "class": "IN", "ttl": 600, "type": "LOC", "data": { "size": "3e2", "precision": { "horizontal": 0, "vertical": 0 }, "point": { "latitude": null, "longitude": "0°0′0″ E", "altitude": "0m" } } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/far-positive-longitude.loc.invalid.ansitxt ================================================ LOC far-positive-longitude.loc.invalid. 10m00s 3e2 (0, 0) (0°0′0″ N, Out of range, 0m) ================================================ FILE: xtests/madns/outputs/far-positive-longitude.loc.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "far-positive-longitude.loc.invalid.", "class": "IN", "type": "LOC" } ], "answers": [ { "name": "far-positive-longitude.loc.invalid.", "class": "IN", "ttl": 600, "type": "LOC", "data": { "size": "3e2", "precision": { "horizontal": 0, "vertical": 0 }, "point": { "latitude": "0°0′0″ N", "longitude": null, "altitude": "0m" } } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/hinfo.example.ansitxt ================================================ HINFO hinfo.example. 10m00s "some-kinda-cpu" "some-kinda-os" ================================================ FILE: xtests/madns/outputs/hinfo.example.json ================================================ { "responses": [ { "queries": [ { "name": "hinfo.example.", "class": "IN", "type": "HINFO" } ], "answers": [ { "name": "hinfo.example.", "class": "IN", "ttl": 600, "type": "HINFO", "data": { "cpu": "some-kinda-cpu", "os": "some-kinda-os" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/loc.example.ansitxt ================================================ LOC loc.example. 10m00s 3e2 (0, 0) (51°30′12.748″ N, 0°7′39.611″ W, 0m) ================================================ FILE: xtests/madns/outputs/loc.example.json ================================================ { "responses": [ { "queries": [ { "name": "loc.example.", "class": "IN", "type": "LOC" } ], "answers": [ { "name": "loc.example.", "class": "IN", "ttl": 600, "type": "LOC", "data": { "size": "3e2", "precision": { "horizontal": 0, "vertical": 0 }, "point": { "latitude": "51°30′12.748″ N", "longitude": "0°7′39.611″ W", "altitude": "0m" } } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/mx.example.ansitxt ================================================ MX mx.example. 10m00s 10 "exchange.example." ================================================ FILE: xtests/madns/outputs/mx.example.json ================================================ { "responses": [ { "queries": [ { "name": "mx.example.", "class": "IN", "type": "MX" } ], "answers": [ { "name": "mx.example.", "class": "IN", "ttl": 600, "type": "MX", "data": { "preference": 10, "exchange": "exchange.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/named.opt.invalid.ansitxt ================================================ A named.opt.invalid. 10m00s 127.0.0.1 OPT bingle.bongle.dingle.dangle. + 1452 0 0 0 [] ================================================ FILE: xtests/madns/outputs/named.opt.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "named.opt.invalid.", "class": "IN", "type": "A" } ], "answers": [ { "name": "named.opt.invalid.", "class": "IN", "ttl": 600, "type": "A", "data": { "address": "127.0.0.1" } } ], "authorities": [], "additionals": [ { "name": "bingle.bongle.dingle.dangle.", "type": "OPT", "data": { "version": 0, "data": [] } } ] } ] } ================================================ FILE: xtests/madns/outputs/naptr.example.ansitxt ================================================ NAPTR naptr.example. 10m00s 5 10 "s" "SRV" "\\d\\d:\\d\\d:\\d\\d" "srv.example." ================================================ FILE: xtests/madns/outputs/naptr.example.json ================================================ { "responses": [ { "queries": [ { "name": "naptr.example.", "class": "IN", "type": "NAPTR" } ], "answers": [ { "name": "naptr.example.", "class": "IN", "ttl": 600, "type": "NAPTR", "data": { "order": 5, "flags": "s", "service": "SRV", "regex": "\\d\\d:\\d\\d:\\d\\d", "replacement": "srv.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/newline.str.example.ansitxt ================================================ CNAME newline.str.example. 10m00s "some\nnew\r\nlines\n.example." ================================================ FILE: xtests/madns/outputs/newline.str.example.json ================================================ { "responses": [ { "queries": [ { "name": "newline.str.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "newline.str.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "some\nnew\r\nlines\n.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/ns.example.ansitxt ================================================ NS ns.example. 10m00s "a.gtld-servers.net." ================================================ FILE: xtests/madns/outputs/ns.example.json ================================================ { "responses": [ { "queries": [ { "name": "ns.example.", "class": "IN", "type": "NS" } ], "answers": [ { "name": "ns.example.", "class": "IN", "ttl": 600, "type": "NS", "data": { "nameserver": "a.gtld-servers.net." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/null.str.example.ansitxt ================================================ CNAME null.str.example. 10m00s "some\u{0}null\u{0}\u{0}chars\u{0}.example." ================================================ FILE: xtests/madns/outputs/null.str.example.json ================================================ { "responses": [ { "queries": [ { "name": "null.str.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "null.str.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "some\u0000null\u0000\u0000chars\u0000.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/openpgpkey.example.ansitxt ================================================ OPENPGPKEY openpgpkey.example. 10m00s "EjRWeA==" ================================================ FILE: xtests/madns/outputs/openpgpkey.example.json ================================================ { "responses": [ { "queries": [ { "name": "openpgpkey.example.", "class": "IN", "type": "OPENPGPKEY" } ], "answers": [ { "name": "openpgpkey.example.", "class": "IN", "ttl": 600, "type": "OPENPGPKEY", "data": { "key": "EjRWeA==" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/opt.example.ansitxt ================================================ A opt.example. 10m00s 127.0.0.1 OPT  + 1452 0 0 0 [] ================================================ FILE: xtests/madns/outputs/opt.example.json ================================================ { "responses": [ { "queries": [ { "name": "opt.example.", "class": "IN", "type": "A" } ], "answers": [ { "name": "opt.example.", "class": "IN", "ttl": 600, "type": "A", "data": { "address": "127.0.0.1" } } ], "authorities": [], "additionals": [ { "name": "", "type": "OPT", "data": { "version": 0, "data": [] } } ] } ] } ================================================ FILE: xtests/madns/outputs/other-flags.opt.example.ansitxt ================================================ A other-flags.opt.example. 10m00s 127.0.0.1 OPT  + 1452 0 0 32767 [] ================================================ FILE: xtests/madns/outputs/other-flags.opt.example.json ================================================ { "responses": [ { "queries": [ { "name": "other-flags.opt.example.", "class": "IN", "type": "A" } ], "answers": [ { "name": "other-flags.opt.example.", "class": "IN", "ttl": 600, "type": "A", "data": { "address": "127.0.0.1" } } ], "authorities": [], "additionals": [ { "name": "", "type": "OPT", "data": { "version": 0, "data": [] } } ] } ] } ================================================ FILE: xtests/madns/outputs/others.caa.example.ansitxt ================================================ CAA caa.example. 10m00s "issuewild" "trustworthy.example" (non-critical) ================================================ FILE: xtests/madns/outputs/others.caa.example.json ================================================ { "responses": [ { "queries": [ { "name": "caa.example.", "class": "IN", "type": "CAA" } ], "answers": [ { "name": "caa.example.", "class": "IN", "ttl": 600, "type": "CAA", "data": { "critical": false, "tag": "issuewild", "value": "trustworthy.example" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/ptr.example.ansitxt ================================================ PTR ptr.example. 10m00s "dns.example." ================================================ FILE: xtests/madns/outputs/ptr.example.json ================================================ { "responses": [ { "queries": [ { "name": "ptr.example.", "class": "IN", "type": "PTR" } ], "answers": [ { "name": "ptr.example.", "class": "IN", "ttl": 600, "type": "PTR", "data": { "cname": "dns.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/slash.uri.example.ansitxt ================================================ URI slash.uri.example. 10m00s 10 1 "/" ================================================ FILE: xtests/madns/outputs/soa.example.ansitxt ================================================ SOA soa.example. 10m00s "mname.example." "rname.example." 1564274434 1d0h00m00s 2h00m00s 7d0h00m00s 5m00s ================================================ FILE: xtests/madns/outputs/soa.example.json ================================================ { "responses": [ { "queries": [ { "name": "soa.example.", "class": "IN", "type": "SOA" } ], "answers": [ { "name": "soa.example.", "class": "IN", "ttl": 600, "type": "SOA", "data": { "mname": "mname.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/srv.example.ansitxt ================================================ SRV srv.example. 10m00s 1 1 "service.example.":37500 ================================================ FILE: xtests/madns/outputs/srv.example.json ================================================ { "responses": [ { "queries": [ { "name": "srv.example.", "class": "IN", "type": "SRV" } ], "answers": [ { "name": "srv.example.", "class": "IN", "ttl": 600, "type": "SRV", "data": { "priority": 1, "weight": 1, "port": 37500, "target": "service.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/sshfp.example.ansitxt ================================================ SSHFP sshfp.example. 10m00s 1 1 212223242526 ================================================ FILE: xtests/madns/outputs/sshfp.example.json ================================================ { "responses": [ { "queries": [ { "name": "sshfp.example.", "class": "IN", "type": "SSHFP" } ], "answers": [ { "name": "sshfp.example.", "class": "IN", "ttl": 600, "type": "SSHFP", "data": { "algorithm": 1, "fingerprint_type": 1, "fingerprint": "212223242526" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/tab.str.example.ansitxt ================================================ CNAME tab.str.example. 10m00s "some\ttab\t\tchars\t.example." ================================================ FILE: xtests/madns/outputs/tab.str.example.json ================================================ { "responses": [ { "queries": [ { "name": "tab.str.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "tab.str.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "some\ttab\t\tchars\t.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/tlsa.example.ansitxt ================================================ TLSA tlsa.example. 10m00s 3 1 1 "112233445566" ================================================ FILE: xtests/madns/outputs/tlsa.example.json ================================================ { "responses": [ { "queries": [ { "name": "tlsa.example.", "class": "IN", "type": "TLSA" } ], "answers": [ { "name": "tlsa.example.", "class": "IN", "ttl": 600, "type": "TLSA", "data": { "certificate_usage": 3, "selector": 1, "matching_type": 1, "certificate_data": "112233445566" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/txt.example.ansitxt ================================================ TXT txt.example. 10m00s "Cache Invalidation and Naming Things" ================================================ FILE: xtests/madns/outputs/txt.example.json ================================================ { "responses": [ { "queries": [ { "name": "txt.example.", "class": "IN", "type": "TXT" } ], "answers": [ { "name": "txt.example.", "class": "IN", "ttl": 600, "type": "TXT", "data": { "messages": [ "Cache Invalidation and Naming Things" ] } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/upperbit.str.example.ansitxt ================================================ CNAME upperbit.str.example. 10m00s "\u{7f}�����.example." ================================================ FILE: xtests/madns/outputs/upperbit.str.example.json ================================================ { "responses": [ { "queries": [ { "name": "upperbit.str.example.", "class": "IN", "type": "CNAME" } ], "answers": [ { "name": "upperbit.str.example.", "class": "IN", "ttl": 600, "type": "CNAME", "data": { "domain": "\u007f�����.example." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/uri.example.ansitxt ================================================ URI uri.example. 10m00s 10 16 "https://rfcs.io/" ================================================ FILE: xtests/madns/outputs/uri.example.json ================================================ { "responses": [ { "queries": [ { "name": "uri.example.", "class": "IN", "type": "URI" } ], "answers": [ { "name": "uri.example.", "class": "IN", "ttl": 600, "type": "URI", "data": { "priority": 10, "weight": 16, "target": "https://rfcs.io/" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/utf8.caa.example.ansitxt ================================================ CAA utf8.caa.example. 10m00s "issuewild" "trustworthy\240\159\140\180example" (non-critical) ================================================ FILE: xtests/madns/outputs/utf8.caa.example.json ================================================ { "responses": [ { "queries": [ { "name": "utf8.caa.example.", "class": "IN", "type": "CAA" } ], "answers": [ { "name": "utf8.caa.example.", "class": "IN", "ttl": 600, "type": "CAA", "data": { "critical": false, "tag": "issuewild", "value": "trustworthy🌴example" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/utf8.hinfo.example.ansitxt ================================================ HINFO utf8.hinfo.example. 10m00s "some\240\159\140\180kinda\240\159\140\180cpu" "some\240\159\140\180kinda\240\159\140\180os" ================================================ FILE: xtests/madns/outputs/utf8.hinfo.example.json ================================================ { "responses": [ { "queries": [ { "name": "utf8.hinfo.example.", "class": "IN", "type": "HINFO" } ], "answers": [ { "name": "utf8.hinfo.example.", "class": "IN", "ttl": 600, "type": "HINFO", "data": { "cpu": "some🌴kinda🌴cpu", "os": "some🌴kinda🌴os" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/utf8.naptr.invalid.ansitxt ================================================ NAPTR utf8.naptr.invalid. 10m00s 5 10 "\240\159\140\180" "\240\159\140\180" "\240\159\140\180" "🌴." ================================================ FILE: xtests/madns/outputs/utf8.naptr.invalid.json ================================================ { "responses": [ { "queries": [ { "name": "utf8.naptr.invalid.", "class": "IN", "type": "NAPTR" } ], "answers": [ { "name": "utf8.naptr.invalid.", "class": "IN", "ttl": 600, "type": "NAPTR", "data": { "order": 5, "flags": "🌴", "service": "🌴", "regex": "🌴", "replacement": "🌴." } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/utf8.txt.example.ansitxt ================================================ TXT utf8.txt.example. 10m00s "\240\159\146\176Cache \240\159\153\133\226\128\141\239\184\143Invalidation \226\133\139and \240\159\147\155Naming \240\159\142\179Things" ================================================ FILE: xtests/madns/outputs/utf8.txt.example.json ================================================ { "responses": [ { "queries": [ { "name": "utf8.txt.example.", "class": "IN", "type": "TXT" } ], "answers": [ { "name": "utf8.txt.example.", "class": "IN", "ttl": 600, "type": "TXT", "data": { "messages": [ "💰Cache 🙅‍️Invalidation ⅋and 📛Naming 🎳Things" ] } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/outputs/utf8.uri.example.ansitxt ================================================ URI utf8.uri.example. 10m00s 10 16 "https://\240\159\146\169.la/" ================================================ FILE: xtests/madns/outputs/utf8.uri.example.json ================================================ { "responses": [ { "queries": [ { "name": "utf8.uri.example.", "class": "IN", "type": "URI" } ], "answers": [ { "name": "utf8.uri.example.", "class": "IN", "ttl": 600, "type": "URI", "data": { "priority": 10, "weight": 16, "target": "https://💩.la/" } } ], "authorities": [], "additionals": [] } ] } ================================================ FILE: xtests/madns/protocol-chars.toml ================================================ # Character escaping [[cmd]] name = "Running with ‘ansi.str.example’ properly escapes the codes" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME ansi.str.example" stdout = { file = "outputs/ansi.str.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘newline.str.example’ properly escapes the newlines" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME newline.str.example" stdout = { file = "outputs/newline.str.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘null.str.example’ properly handles the null bytes" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME null.str.example" stdout = { file = "outputs/null.str.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘tab.str.example’ properly escapes the tabs" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME tab.str.example" stdout = { file = "outputs/tab.str.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘upperbit.str.example’ properly escapes the upper-bit characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME upperbit.str.example" stdout = { file = "outputs/upperbit.str.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] # Character escaping (JSON) [[cmd]] name = "Running with ‘ansi.str.example --json’ properly escapes the codes in the JSON string" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME ansi.str.example --json | jq" stdout = { file = "outputs/ansi.str.example.json" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns", "json" ] [[cmd]] name = "Running with ‘newline.str.example --json’ properly escapes the newlines in the JSON string" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME newline.str.example --json | jq" stdout = { file = "outputs/newline.str.example.json" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns", "json" ] [[cmd]] name = "Running with ‘null.str.example --json’ properly handles the null bytes in the JSON string" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME null.str.example --json | jq" stdout = { file = "outputs/null.str.example.json" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns", "json" ] [[cmd]] name = "Running with ‘tab.str.example --json’ properly escapes the tabs in the JSON string" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME tab.str.example --json | jq" stdout = { file = "outputs/tab.str.example.json" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns", "json" ] [[cmd]] name = "Running with ‘upperbit.str.example --json’ properly escapes the upper-bit characters in the JSON string" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME upperbit.str.example --json | jq" stdout = { file = "outputs/upperbit.str.example.json" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns", "json" ] ================================================ FILE: xtests/madns/protocol-compression.toml ================================================ [[cmd]] name = "Running with ‘out-of-range.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A out-of-range.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘recursive-1.invalid’ displays a recursion error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A recursive-1.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: too much recursion: [37]" } status = 1 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘recursive-2.invalid’ displays a recursion error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A recursive-2.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: too much recursion: [53, 37]" } status = 1 tags = [ "protocol", "madns" ] ================================================ FILE: xtests/madns/protocol-error-codes.toml ================================================ [[cmd]] name = "Running with ‘formerr.invalid’ displays the error code" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A formerr.invalid" stdout = { string = "Status: Format Error" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘servfail.invalid’ displays the error code" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A servfail.invalid" stdout = { string = "Status: Server Failure" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘nxdomain.invalid’ displays the error code" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A nxdomain.invalid" stdout = { string = "Status: NXDomain" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘notimp.invalid’ displays the error code" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A notimp.invalid" stdout = { string = "Status: Not Implemented" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] [[cmd]] name = "Running with ‘refused.invalid’ displays the error code" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A refused.invalid" stdout = { string = "Status: Query Refused" } stderr = { empty = true } status = 0 tags = [ "protocol", "madns" ] ================================================ FILE: xtests/madns/ptr-records.toml ================================================ # PTR record successes [[cmd]] name = "Running with ‘ptr.example’ prints the correct PTR record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR ptr.example" stdout = { file = "outputs/ptr.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "ptr", "madns" ] # PTR record successes (JSON) [[cmd]] name = "Running with ‘ptr.example --json’ prints the correct PTR record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR ptr.example --json | jq" stdout = { file = "outputs/ptr.example.json" } stderr = { empty = true } status = 0 tags = [ "ptr", "madns", "json" ] # PTR record invalid packets [[cmd]] name = "Running with ‘empty.ptr.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR empty.ptr.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "ptr", "madns" ] [[cmd]] name = "Running with ‘incomplete.ptr.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR incomplete.ptr.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "ptr", "madns" ] ================================================ FILE: xtests/madns/soa-records.toml ================================================ # SOA record successes [[cmd]] name = "Running with ‘soa.example’ prints the correct SOA record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA soa.example" stdout = { file = "outputs/soa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "soa", "madns" ] # SOA record successes (JSON) [[cmd]] name = "Running with ‘soa.example --json’ prints the correct SOA record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA soa.example --json | jq" stdout = { file = "outputs/soa.example.json" } stderr = { empty = true } status = 0 tags = [ "soa", "madns", "json" ] # SOA record invalid packets [[cmd]] name = "Running with ‘empty.soa.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA empty.soa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "soa", "madns" ] [[cmd]] name = "Running with ‘incomplete.soa.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA incomplete.soa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "soa", "madns" ] ================================================ FILE: xtests/madns/srv-records.toml ================================================ # SRV record successes [[cmd]] name = "Running with ‘srv.example’ prints the correct SRV record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV srv.example" stdout = { file = "outputs/srv.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "soa", "madns" ] # SRV record successes (JSON) [[cmd]] name = "Running with ‘srv.example --json’ prints the correct SRV record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV srv.example --json | jq" stdout = { file = "outputs/srv.example.json" } stderr = { empty = true } status = 0 tags = [ "soa", "madns", "json" ] # SRV record invalid packets [[cmd]] name = "Running with ‘empty.srv.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV empty.srv.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "soa", "madns" ] [[cmd]] name = "Running with ‘incomplete.srv.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV incomplete.srv.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "soa", "madns" ] ================================================ FILE: xtests/madns/sshfp-records.toml ================================================ # SSHFP record successes [[cmd]] name = "Running with ‘sshfp.example’ prints the correct SSHFP record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP sshfp.example" stdout = { file = "outputs/sshfp.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "sshfp", "madns" ] # SSHFP record successes (JSON) [[cmd]] name = "Running with ‘sshfp.example --json’ prints the correct SSHFP record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP sshfp.example --json | jq" stdout = { file = "outputs/sshfp.example.json" } stderr = { empty = true } status = 0 tags = [ "sshfp", "madns", "json" ] # SSHFP record invalid packets [[cmd]] name = "Running with ‘empty.sshfp.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP empty.sshfp.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "sshfp", "madns" ] [[cmd]] name = "Running with ‘incomplete.sshfp.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP incomplete.sshfp.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "sshfp", "madns" ] ================================================ FILE: xtests/madns/tlsa-records.toml ================================================ # TLSA record successes [[cmd]] name = "Running with ‘tlsa.example’ prints the correct TLSA record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA tlsa.example" stdout = { file = "outputs/tlsa.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "tlsa", "madns" ] # TLSA record successes (JSON) [[cmd]] name = "Running with ‘tlsa.example --json’ prints the correct TLSA record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA tlsa.example --json | jq" stdout = { file = "outputs/tlsa.example.json" } stderr = { empty = true } status = 0 tags = [ "tlsa", "madns", "json" ] # TLSA record invalid packets [[cmd]] name = "Running with ‘empty.tlsa.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA empty.tlsa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "tlsa", "madns" ] [[cmd]] name = "Running with ‘incomplete.tlsa.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA incomplete.tlsa.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "tlsa", "madns" ] ================================================ FILE: xtests/madns/txt-records.toml ================================================ # TXT record successes [[cmd]] name = "Running with ‘txt.example’ prints the correct TXT record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT txt.example" stdout = { file = "outputs/txt.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "txt", "madns" ] [[cmd]] name = "Running with ‘utf8.txt.example’ escapes characters in the message" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT utf8.txt.example" stdout = { file = "outputs/utf8.txt.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "txt", "madns", "chars" ] [[cmd]] name = "Running with ‘bad-utf8.txt.example’ escapes characters in the message and does not crash" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT bad-utf8.txt.example" stdout = { file = "outputs/bad-utf8.txt.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "txt", "madns", "chars" ] # TXT record successes (JSON) [[cmd]] name = "Running with ‘txt.example --json’ prints the correct TXT record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT txt.example --json | jq" stdout = { file = "outputs/txt.example.json" } stderr = { empty = true } status = 0 tags = [ "txt", "madns", "json" ] [[cmd]] name = "Running with ‘utf8.txt.example --json’ interprets the response as UTF-8" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT utf8.txt.example --json | jq" stdout = { file = "outputs/utf8.txt.example.json" } stderr = { empty = true } status = 0 tags = [ "txt", "madns", "chars", "json" ] [[cmd]] name = "Running with ‘bad-utf8.txt.example --json’ uses UTF-8 replacement characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT bad-utf8.txt.example --json | jq" stdout = { file = "outputs/bad-utf8.txt.example.json" } stderr = { empty = true } status = 0 tags = [ "txt", "madns", "chars", "json" ] # TXT record invalid packets [[cmd]] name = "Running with ‘empty.txt.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT empty.txt.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "txt", "madns" ] [[cmd]] name = "Running with ‘incomplete.txt.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT incomplete.txt.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "txt", "madns" ] ================================================ FILE: xtests/madns/uri-records.toml ================================================ # URI record successes [[cmd]] name = "Running with ‘uri.example’ prints the correct URI record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI uri.example" stdout = { file = "outputs/uri.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "uri", "madns" ] [[cmd]] name = "Running with ‘slash.uri.example’ still prints the correct URI record" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI slash.uri.example" stdout = { file = "outputs/slash.uri.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "uri", "madns" ] [[cmd]] name = "Running with ‘utf8.uri.example’ escapes characters in the URI" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI utf8.uri.example" stdout = { file = "outputs/utf8.uri.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "uri", "madns", "chars" ] [[cmd]] name = "Running with ‘bad-utf8.uri.example’ escapes characters in the URI and does not crash" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI bad-utf8.uri.example" stdout = { file = "outputs/bad-utf8.uri.example.ansitxt" } stderr = { empty = true } status = 0 tags = [ "uri", "madns", "chars" ] # URI record successes (JSON) [[cmd]] name = "Running with ‘uri.example --json’ prints the correct URI record structure" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI uri.example --json | jq" stdout = { file = "outputs/uri.example.json" } stderr = { empty = true } status = 0 tags = [ "uri", "madns", "json" ] [[cmd]] name = "Running with ‘utf8.uri.example --json’ interprets the response as UTF-8" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI utf8.uri.example --json | jq" stdout = { file = "outputs/utf8.uri.example.json" } stderr = { empty = true } status = 0 tags = [ "uri", "madns", "chars", "json" ] [[cmd]] name = "Running with ‘bad-utf8.uri.example --json’ uses UTF-8 replacement characters" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI bad-utf8.uri.example --json | jq" stdout = { file = "outputs/bad-utf8.uri.example.json" } stderr = { empty = true } status = 0 tags = [ "uri", "madns", "chars", "json" ] # URI record invalid packets [[cmd]] name = "Running with ‘missing-data.uri.invalid’ displays a packet length error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI missing-data.uri.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: record length should be at least 5, got 4" } status = 1 tags = [ "uri", "madns" ] [[cmd]] name = "Running with ‘empty.uri.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI empty.uri.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "uri", "madns" ] [[cmd]] name = "Running with ‘incomplete.uri.invalid’ displays a protocol error" shell = "dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI incomplete.uri.invalid" stdout = { empty = true } stderr = { string = "Error [protocol]: Malformed packet: insufficient data" } status = 1 tags = [ "uri", "madns" ] ================================================ FILE: xtests/options/errors.toml ================================================ [[cmd]] name = "Running dog with ‘--wibble’ warns about the invalid argument" shell = "dog --wibble" stdout = { empty = true } stderr = { file = "outputs/invalid-argument.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--class’ warns about the missing argument parameter" shell = "dog --class" stdout = { empty = true } stderr = { file = "outputs/missing-parameter.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--type XYZZY’ warns about the invalid record type" shell = "dog --type XYZZY dns.google" stdout = { empty = true } stderr = { file = "outputs/invalid-query-type.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--class XYZZY’ warns about the invalid class" shell = "dog --class XYZZY dns.google" stdout = { empty = true } stderr = { file = "outputs/invalid-query-class.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘-Z aoeu’ warns about the invalid protocol tweak" shell = "dog -Z aoeu dns.google" stdout = { empty = true } stderr = { file = "outputs/invalid-protocol-tweak.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘OPT’ warns that OPT requests are sent by default" shell = "dog OPT dns.google" stdout = { empty = true } stderr = { file = "outputs/opt-query.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘opt’ also warns that OPT requests are sent by default" shell = "dog opt dns.google" stdout = { empty = true } stderr = { file = "outputs/opt-query.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--type OPT’ warns that OPT requests are sent by default" shell = "dog --type OPT dns.google" stdout = { empty = true } stderr = { file = "outputs/opt-query.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--type opt’ also warns that OPT requests are sent by default" shell = "dog --type opt dns.google" stdout = { empty = true } stderr = { file = "outputs/opt-query.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with a domain longer than 255 bytes warns about it being too long" shell = "dog 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" stdout = { empty = true } stderr = { file = "outputs/huge-domain.txt" } status = 3 tags = [ 'options' ] [[cmd]] name = "Running dog with ‘--https’ and no nameserver warns that one is missing" shell = "dog --https dns.google" stdout = { empty = true } stderr = { file = "outputs/missing-nameserver.txt" } status = 3 tags = [ 'options' ] ================================================ FILE: xtests/options/help.toml ================================================ # help [[cmd]] name = "Running ‘dog --help’ shows help" shell = "dog --help" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --help --color=automatic’ not to a terminal shows help without colour" shell = "dog --help --color=automatic" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --help --colour=always’ shows help with colour" shell = "dog --help --colour=always" stdout = { string = "dog \u001B[1;32m●\u001B[0m" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --help --color=never’ shows without colour" shell = "dog --help --color=never" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog’ with no arguments shows help" shell = "dog" stdout = { string = "dog ●" } stderr = { empty = true } status = 3 tags = [ 'options' ] # versions [[cmd]] name = "Running ‘dog --version’ shows version information" shell = "dog --version" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --version --colour=automatic’ not to a terminal shows version information without colour" shell = "dog --version --colour=automatic" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --version --color=always’ shows version information with colour" shell = "dog --version --color=always" stdout = { string = "dog \u001B[1;32m●\u001B[0m" } stderr = { empty = true } status = 0 tags = [ 'options' ] [[cmd]] name = "Running ‘dog --version --colour=never’ shows version information without colour" shell = "dog --version --colour=never" stdout = { string = "dog ●" } stderr = { empty = true } status = 0 tags = [ 'options' ] ================================================ FILE: xtests/options/outputs/huge-domain.txt ================================================ dog: Invalid options: Invalid domain "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" ================================================ FILE: xtests/options/outputs/invalid-argument.txt ================================================ dog: Invalid options: Unrecognized option: 'wibble' ================================================ FILE: xtests/options/outputs/invalid-protocol-tweak.txt ================================================ dog: Invalid options: Invalid protocol tweak "aoeu" ================================================ FILE: xtests/options/outputs/invalid-query-class.txt ================================================ dog: Invalid options: Invalid query class "XYZZY" ================================================ FILE: xtests/options/outputs/invalid-query-type.txt ================================================ dog: Invalid options: Invalid query type "XYZZY" ================================================ FILE: xtests/options/outputs/missing-nameserver.txt ================================================ dog: Invalid options: You must pass a URL as a nameserver when using --https ================================================ FILE: xtests/options/outputs/missing-parameter.txt ================================================ dog: Invalid options: Argument to option 'class' missing ================================================ FILE: xtests/options/outputs/opt-query.txt ================================================ dog: Invalid options: OPT request is sent by default (see -Z flag)