├── .github ├── actions │ ├── setup-cargo-post │ │ └── action.yml │ └── setup-cargo-pre │ │ └── action.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── pre-release ├── src ├── dns │ ├── client.rs │ ├── commands │ │ ├── mod.rs │ │ └── query.rs │ ├── config.rs │ ├── constants.rs │ ├── mod.rs │ ├── serde.rs │ └── util.rs ├── lib.rs └── main.rs └── tests ├── fixtures └── zones │ └── nushell.sh.zone ├── integration.rs └── query ├── expected.rs └── mod.rs /.github/actions/setup-cargo-post/action.yml: -------------------------------------------------------------------------------- 1 | name: setup-cargo-post 2 | description: post run cargo steps 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: save cargo cache 8 | id: cache-cargo-save 9 | uses: actions/cache/save@main 10 | with: 11 | path: | 12 | ${{ github.workspace }}/.cache 13 | ${{ github.workspace }}/target 14 | key: >- 15 | cargo-${{ 16 | hashFiles( 17 | 'Cargo.toml', 18 | 'Cargo.lock', 19 | '.cache/cargo/.crates2.json', 20 | 'target/**/.fingerprint/*/*.json' 21 | ) 22 | }} 23 | -------------------------------------------------------------------------------- /.github/actions/setup-cargo-pre/action.yml: -------------------------------------------------------------------------------- 1 | name: setup-cargo-pre 2 | description: set up caching for cargo and tooling 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: add CARGO_HOME to path 8 | id: setup-cargo-path 9 | shell: bash 10 | run: 'echo "PATH=$PATH:$CARGO_HOME/bin" >> $GITHUB_ENV' 11 | 12 | - name: restore cargo cache 13 | id: cache-cargo-restore 14 | uses: actions/cache/restore@main 15 | with: 16 | path: | 17 | ${{ github.workspace }}/.cache 18 | ${{ github.workspace }}/target 19 | key: >- 20 | cargo-${{ 21 | hashFiles( 22 | 'Cargo.toml', 23 | 'Cargo.lock', 24 | '.cache/cargo/.crates2.json', 25 | 'target/**/.fingerprint/*/*.json' 26 | ) 27 | }} 28 | restore-keys: | 29 | cargo- 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | # missing build dependencies on windows? 18 | # os: [ubuntu-latest, macos-latest, windows-latest] 19 | os: [ubuntu-latest, macos-latest] 20 | 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@main 24 | 25 | - name: setup-cargo-pre 26 | uses: ./.github/actions/setup-cargo-pre 27 | 28 | - name: cargo test 29 | run: cargo test --workspace 30 | 31 | - name: setup-cargo-post 32 | uses: ./.github/actions/setup-cargo-post 33 | if: always() 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | # push: 4 | # branches: 5 | # - hickory-* 6 | workflow_dispatch: 7 | inputs: 8 | execute: 9 | description: --execute 10 | type: boolean 11 | default: false 12 | 13 | publish: 14 | description: publish 15 | type: boolean 16 | default: true 17 | 18 | release_level: 19 | description: level to release 20 | type: choice 21 | options: 22 | - none 23 | - major 24 | - minor 25 | - patch 26 | - release 27 | - rc 28 | - beta 29 | - alpha 30 | default: release 31 | 32 | dev_level: 33 | description: level to bump after release 34 | type: choice 35 | options: 36 | - none 37 | - rc 38 | - beta 39 | - alpha 40 | default: alpha 41 | 42 | jobs: 43 | release: 44 | runs-on: ubuntu-latest 45 | 46 | permissions: 47 | id-token: write 48 | contents: write 49 | 50 | env: 51 | CARGO_HOME: ${{ github.workspace }}/.cache/cargo 52 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} 53 | NU_LOG_LEVEL: info 54 | EXECUTE: "${{ inputs.execute && '--execute' || '' }}" 55 | 56 | steps: 57 | - name: checkout 58 | uses: actions/checkout@main 59 | 60 | - name: setup-cargo-pre 61 | uses: ./.github/actions/setup-cargo-pre 62 | 63 | - name: set up git config 64 | env: 65 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | run: | 67 | git config user.name ${{ secrets.GIT_USER_NAME }} 68 | git config user.email ${{ secrets.GIT_EMAIL }} 69 | 70 | - name: install cargo-release 71 | id: install-cargo-release 72 | run: cargo install cargo-release --locked 73 | env: 74 | CARGO_TARGET_DIR: ${{ github.workspace }}/.cache/tools/cargo-release 75 | 76 | - name: install nushell 77 | id: install-nu 78 | run: cargo install nu --locked 79 | env: 80 | CARGO_TARGET_DIR: ${{ github.workspace }}/.cache/tools/nu 81 | 82 | - name: release version 83 | if: ${{ inputs.release_level != 'none' }} 84 | run: | 85 | cargo release version --no-confirm ${{ inputs.release_level }} ${{ env.EXECUTE }} 86 | cargo release hook 87 | 88 | - name: release version tag and push 89 | if: ${{ inputs.release_level != 'none' && inputs.execute }} 90 | run: | 91 | cargo release commit --no-confirm ${{ env.EXECUTE }} 92 | cargo release tag --no-confirm ${{ env.EXECUTE }} 93 | cargo release push --no-confirm ${{ env.EXECUTE }} 94 | 95 | - name: publish 96 | if: ${{ inputs.publish }} 97 | run: | 98 | cargo release publish --no-confirm ${{ env.EXECUTE }} 99 | 100 | - name: bump version 101 | if: ${{ inputs.dev_level != 'none' }} 102 | run: | 103 | cargo release version --no-confirm ${{ inputs.dev_level }} ${{ env.EXECUTE }} 104 | 105 | - name: bump version tag and push 106 | if: ${{ inputs.dev_level != 'none' && inputs.execute }} 107 | run: | 108 | cargo release commit --no-confirm ${{ env.EXECUTE }} 109 | cargo release push --no-tag --no-confirm ${{ env.EXECUTE }} 110 | 111 | - name: setup-cargo-post 112 | uses: ./.github/actions/setup-cargo-post 113 | if: always() 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /target/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [4.0.1] - 2025-04-24 11 | 12 | * Upgrade nushell to 0.104.0 + other deps 13 | 14 | ## [4.0.0] - 2025-04-24 15 | 16 | ### Changed 17 | 18 | #### **BREAKING** Upgrade hickory to 0.25.1 19 | [hickory 0.25](https://github.com/hickory-dns/hickory-dns/releases/tag/v0.25.0) 20 | introduced massive breaking changes. The output of this plugin was updated to 21 | reflect those changes. 22 | 23 | * Many breaking changes were made to the data structures of the crate, so 24 | without listing all the details, the record types which have changes 25 | in their structure are: 26 | 27 | * `CDNSKEY` 28 | * `CDS` 29 | * `DNSKEY` 30 | * `DS` 31 | * `KEY` 32 | * `TLSA` 33 | 34 | The `edns` record had its `dnssec_ok` column moved into a nested `flags` 35 | record. 36 | * The DNSSEC mode of `strict` has been removed, since hickory now does negative 37 | validation. Now in the default mode of `--dnssec opportunistic`, if a record 38 | has no DNSSEC signatures, this is cryptographically validated from upstream 39 | resolvers, and an error is returned if this validation fails. 40 | * A new column `proof` has been added to the `answer` table which represents the 41 | record's DNSSEC proof status. See 42 | [here](https://docs.rs/hickory-proto/0.25.1/hickory_proto/dnssec/proof/enum.Proof.html) 43 | for details. 44 | 45 | ### Other 46 | * Upgrade nushell crates to 0.103.0 47 | 48 | ## [3.0.7] - 2025-02-14 49 | 50 | * Upgrade nushell crates to 0.102.0 51 | 52 | ## [3.0.6] - 2024-12-08 53 | 54 | * Upgrade nushell crates to 0.100.0 55 | 56 | ## [3.0.5] - 2024-10-17 57 | 58 | * Upgrade nushell crates to 0.99 59 | 60 | ## [3.0.4] - 2024-09-19 61 | 62 | * Upgrade nushell crates to 0.98 63 | 64 | ## [3.0.3] - 2024-08-28 65 | 66 | * Upgrade nushell crates to 0.97.1 67 | 68 | ## [3.0.2] - 2024-07-28 69 | 70 | * Upgrade nushell crates to 0.96 71 | 72 | ## [3.0.0] - 2024-05-05 73 | 74 | This release fixes compatibility with Nushell 0.93, which introduced major 75 | breaking changes to the plugin API, including the ability to stream output 76 | results. Accordingly, some breaking changes to this plugin's output format 77 | accompanies these fixes. 78 | 79 | 80 | ### Fixes 81 | 82 | * Compatibility with Nushell 0.93 83 | 84 | ### Added 85 | 86 | * Output is now streamed, so `dns query` can now be used in the middle of a 87 | pipeline and is able to remain resource efficient. 88 | * To accompany the above, queries are done concurrently, though output is 89 | returned in the same order queries are given. The level of concurrency can be 90 | tuned with the new `--tasks` flag. (Please exercise caution. Don't DOS your 91 | nameserver!) 92 | * A new CLI flag `--timeout` is added that allows controlling how long to wait 93 | before timing out a request 94 | * CLI flags can now be given through the plug-in configuration in the main 95 | `config.nu`. See the README for an example. 96 | 97 | ### Changed 98 | 99 | * The output format was a record that included at the top level a field for 100 | the nameserver which was queried and one for the messages in the response. 101 | This top level record is now omitted. The output is a table of the response 102 | messages. The output is now equivalent to if one had done `dns query | get 103 | messages` before. If you wish to confirm which nameserver you are querying, 104 | you can set the `RUST_LOG` environment variable to `info`. 105 | 106 | ## [2.0.0] - 2024-03-12 107 | 108 | * Fix typo: `recusion_desired` was fixed to `recursion_desired`. This is 109 | technically a **breaking change** if you use this in any scripts. 110 | * Upgrade nu to 0.91.0 111 | 112 | ## [1.0.5] - 2024-02-08 113 | 114 | * Upgrade dependencies. Fixes breakage in nu 0.90.1 115 | 116 | ## [1.0.4] - 2024-01-29 117 | 118 | * Upgrade dependencies 119 | 120 | ## [1.0.3] - 2023-10-21 121 | 122 | * `trust-dns` has been rebranded as `hickory`. Change all the crates and upgrade 123 | to 0.24 124 | 125 | ## [1.0.2] - 2023-10-20 126 | 127 | * Upgrade dependencies 128 | 129 | ## [1.0.1] - 2023-10-19 130 | 131 | * Upgrade to nu 0.86.0 132 | * Upgrade all other dependencies 133 | * Added logging. You can see logs by setting the `RUST_LOG` environment variable 134 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nu_plugin_dns" 3 | version = "4.0.2-alpha.1" 4 | authors = ["Skyler Hawthorne "] 5 | description = "A DNS utility for nushell" 6 | 7 | repository = "https://github.com/dead10ck/nu_plugin_dns" 8 | edition = "2021" 9 | license = "MPL-2.0" 10 | readme = "README.md" 11 | keywords = ["dns", "dig", "nu", "nushell", "plugin"] 12 | categories = ["command-line-utilities"] 13 | exclude = [ ".github/" ] 14 | 15 | [package.metadata.release] 16 | allow-branch = [ "main" ] 17 | pre-release-hook = [ "./pre-release" ] 18 | 19 | [[bin]] 20 | name = "nu_plugin_dns" 21 | bench = false 22 | 23 | [lib] 24 | bench = false 25 | 26 | [profile.release] 27 | codegen-units = 1 28 | lto = "thin" 29 | 30 | [dependencies] 31 | chrono = { version = "0.4", features = [ "std" ], default-features = false } 32 | futures-util = "0.3.31" 33 | nu-plugin = "0.104.0" 34 | nu-protocol = "0.104.0" 35 | 36 | tokio = "1.45.0" 37 | tracing = "0.1" 38 | tracing-subscriber = { version = "0.3", features = [ "env-filter" ] } 39 | 40 | # rustls and webpki must keep in lockstep with hickory 41 | rustls = "0.23.27" 42 | webpki-roots = "1.0.0" 43 | tokio-util = { version = "0.7.15", features = ["rt"] } 44 | 45 | [dependencies.hickory-resolver] 46 | version = "0.25.2" 47 | features = [ 48 | "dnssec-ring", 49 | "tls-ring", 50 | "https-ring", 51 | "quic-ring", 52 | "h3-ring", 53 | ] 54 | 55 | [dependencies.hickory-proto] 56 | version = "0.25.2" 57 | features = [ 58 | "dnssec-ring", 59 | "tls-ring", 60 | "https-ring", 61 | "quic-ring", 62 | "h3-ring", 63 | ] 64 | 65 | [dependencies.hickory-client] 66 | version = "0.25.2" 67 | features = [ 68 | "dnssec-ring", 69 | "tls-ring", 70 | "https-ring", 71 | "quic-ring", 72 | "h3-ring", 73 | ] 74 | 75 | [dev-dependencies] 76 | nu-plugin-test-support = "0.104.0" 77 | nu-command = "0.104.0" 78 | # calamine 0.26.1 was published with a compile error, and nu 0.103.0 depends on 79 | # it, so we must upgrade it ourselves. Once nu upgrades this, we can remove it. 80 | calamine = "0.27.0" 81 | tokio = { version = "1.45.0", features = ["fs"] } 82 | 83 | [dev-dependencies.hickory-server] 84 | version = "0.25.2" 85 | features = [ 86 | "dnssec-ring", 87 | "tls-ring", 88 | "https-ring", 89 | "quic-ring", 90 | "h3-ring", 91 | ] 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `nu_plugin_dns` 2 | 3 | [Nushell](http://www.nushell.sh/) plugin that does DNS queries and parses 4 | results into meaningful types. Intended to be a native replacement for 5 | [`dig`](https://en.m.wikipedia.org/wiki/Dig_(command)). Uses the excellent 6 | [`hickory`](https://github.com/hickory-dns/hickory-dns) crates. 7 | 8 | ## Usage 9 | 10 | * All queries by default attempt to validate records with DNSSEC. If records 11 | do not have DNSSEC or the nameserver does not support it, then by default, it 12 | falls back to plain queries. This behavior can be tuned with the `--dnssec` 13 | flag. 14 | * Supported protocols are UDP, TCP, TLS, HTTPS, and QUIC 15 | * If no nameserver address is specified, the system's DNS config is used, or if 16 | none is available, falls back to Google. 17 | 18 | ### Examples 19 | 20 | ``` 21 | simple query for A / AAAA records 22 | > dns query amazon.com 23 | 24 | ╭─#─┬───────────────header───────────────┬────────question─────────┬───────────────────────────────────answer────────────────────────────────────┬───────────────────────────────────────────────authority───────────────────────────────────────────────┬───additional───┬──────────────────edns──────────────────┬─size──╮ 25 | │ 0 │ ╭─────────────────────┬──────────╮ │ ╭───────┬─────────────╮ │ [list 0 items] │ ╭─#─┬────name─────┬─type─┬─class─┬──ttl──┬─────────────────────rdata─────────────────────┬──proof───╮ │ [list 0 items] │ ╭─────────────┬──────────────────────╮ │ 106 B │ 26 | │ │ │ id │ 64823 │ │ │ name │ amazon.com. │ │ │ │ 0 │ amazon.com. │ SOA │ IN │ 15min │ ╭─────────┬─────────────────────────────────╮ │ insecure │ │ │ │ rcode_high │ 0 │ │ │ 27 | │ │ │ message_type │ RESPONSE │ │ │ type │ AAAA │ │ │ │ │ │ │ │ │ │ mname │ dns-external-master.amazon.com. │ │ │ │ │ │ version │ 0 │ │ │ 28 | │ │ │ op_code │ QUERY │ │ │ class │ IN │ │ │ │ │ │ │ │ │ │ rname │ hostmaster.amazon.com. │ │ │ │ │ │ │ ╭───────────┬──────╮ │ │ │ 29 | │ │ │ authoritative │ false │ │ ╰───────┴─────────────╯ │ │ │ │ │ │ │ │ │ serial │ 2010194083 │ │ │ │ │ │ flags │ │ dnssec_ok │ true │ │ │ │ 30 | │ │ │ truncated │ false │ │ │ │ │ │ │ │ │ │ │ refresh │ 3min │ │ │ │ │ │ │ ╰───────────┴──────╯ │ │ │ 31 | │ │ │ recursion_desired │ true │ │ │ │ │ │ │ │ │ │ │ retry │ 1min │ │ │ │ │ │ max_payload │ 1.2 kB │ │ │ 32 | │ │ │ recursion_available │ true │ │ │ │ │ │ │ │ │ │ │ expire │ 1wk │ │ │ │ │ │ opts │ {record 0 fields} │ │ │ 33 | │ │ │ authentic_data │ false │ │ │ │ │ │ │ │ │ │ │ minimum │ 15min │ │ │ │ │ ╰─────────────┴──────────────────────╯ │ │ 34 | │ │ │ response_code │ No Error │ │ │ │ │ │ │ │ │ │ ╰─────────┴─────────────────────────────────╯ │ │ │ │ │ │ 35 | │ │ │ query_count │ 1 │ │ │ │ ╰───┴─────────────┴──────┴───────┴───────┴───────────────────────────────────────────────┴──────────╯ │ │ │ │ 36 | │ │ │ answer_count │ 0 │ │ │ │ │ │ │ │ 37 | │ │ │ name_server_count │ 1 │ │ │ │ │ │ │ │ 38 | │ │ │ additional_count │ 1 │ │ │ │ │ │ │ │ 39 | │ │ ╰─────────────────────┴──────────╯ │ │ │ │ │ │ │ 40 | │ 1 │ ╭─────────────────────┬──────────╮ │ ╭───────┬─────────────╮ │ ╭─#─┬────name─────┬─type─┬─class─┬────ttl────┬──────rdata──────┬──proof───╮ │ [list 0 items] │ [list 0 items] │ ╭─────────────┬──────────────────────╮ │ 87 B │ 41 | │ │ │ id │ 28567 │ │ │ name │ amazon.com. │ │ │ 0 │ amazon.com. │ A │ IN │ 9min 2sec │ 205.251.242.103 │ insecure │ │ │ │ │ rcode_high │ 0 │ │ │ 42 | │ │ │ message_type │ RESPONSE │ │ │ type │ A │ │ │ 1 │ amazon.com. │ A │ IN │ 9min 2sec │ 54.239.28.85 │ insecure │ │ │ │ │ version │ 0 │ │ │ 43 | │ │ │ op_code │ QUERY │ │ │ class │ IN │ │ │ 2 │ amazon.com. │ A │ IN │ 9min 2sec │ 52.94.236.248 │ insecure │ │ │ │ │ │ ╭───────────┬──────╮ │ │ │ 44 | │ │ │ authoritative │ false │ │ ╰───────┴─────────────╯ │ ╰───┴─────────────┴──────┴───────┴───────────┴─────────────────┴──────────╯ │ │ │ │ flags │ │ dnssec_ok │ true │ │ │ │ 45 | │ │ │ truncated │ false │ │ │ │ │ │ │ │ ╰───────────┴──────╯ │ │ │ 46 | │ │ │ recursion_desired │ true │ │ │ │ │ │ │ max_payload │ 1.2 kB │ │ │ 47 | │ │ │ recursion_available │ true │ │ │ │ │ │ │ opts │ {record 0 fields} │ │ │ 48 | │ │ │ authentic_data │ false │ │ │ │ │ │ ╰─────────────┴──────────────────────╯ │ │ 49 | │ │ │ response_code │ No Error │ │ │ │ │ │ │ │ 50 | │ │ │ query_count │ 1 │ │ │ │ │ │ │ │ 51 | │ │ │ answer_count │ 3 │ │ │ │ │ │ │ │ 52 | │ │ │ name_server_count │ 0 │ │ │ │ │ │ │ │ 53 | │ │ │ additional_count │ 1 │ │ │ │ │ │ │ │ 54 | │ │ ╰─────────────────────┴──────────╯ │ │ │ │ │ │ │ 55 | ╰───┴────────────────────────────────────┴─────────────────────────┴─────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────┴────────────────────────────────────────┴───────╯ 56 | ``` 57 | 58 | ``` 59 | specify query type 60 | > dns query --type CNAME en.wikipedia.org 61 | 62 | ╭─#─┬───────────────header───────────────┬───────────question────────────┬──────────────────────────────────────answer───────────────────────────────────────┬───authority────┬───additional───┬──────────────────edns──────────────────┬─size─╮ 63 | │ 0 │ ╭─────────────────────┬──────────╮ │ ╭───────┬───────────────────╮ │ ╭─#─┬───────name────────┬─type──┬─class─┬─ttl──┬────────rdata────────┬──proof───╮ │ [list 0 items] │ [list 0 items] │ ╭─────────────┬──────────────────────╮ │ 74 B │ 64 | │ │ │ id │ 55408 │ │ │ name │ en.wikipedia.org. │ │ │ 0 │ en.wikipedia.org. │ CNAME │ IN │ 1day │ dyna.wikimedia.org. │ insecure │ │ │ │ │ rcode_high │ 0 │ │ │ 65 | │ │ │ message_type │ RESPONSE │ │ │ type │ CNAME │ │ ╰───┴───────────────────┴───────┴───────┴──────┴─────────────────────┴──────────╯ │ │ │ │ version │ 0 │ │ │ 66 | │ │ │ op_code │ QUERY │ │ │ class │ IN │ │ │ │ │ │ │ ╭───────────┬──────╮ │ │ │ 67 | │ │ │ authoritative │ false │ │ ╰───────┴───────────────────╯ │ │ │ │ │ flags │ │ dnssec_ok │ true │ │ │ │ 68 | │ │ │ truncated │ false │ │ │ │ │ │ │ │ ╰───────────┴──────╯ │ │ │ 69 | │ │ │ recursion_desired │ true │ │ │ │ │ │ │ max_payload │ 1.2 kB │ │ │ 70 | │ │ │ recursion_available │ true │ │ │ │ │ │ │ opts │ {record 0 fields} │ │ │ 71 | │ │ │ authentic_data │ false │ │ │ │ │ │ ╰─────────────┴──────────────────────╯ │ │ 72 | │ │ │ response_code │ No Error │ │ │ │ │ │ │ │ 73 | │ │ │ query_count │ 1 │ │ │ │ │ │ │ │ 74 | │ │ │ answer_count │ 1 │ │ │ │ │ │ │ │ 75 | │ │ │ name_server_count │ 0 │ │ │ │ │ │ │ │ 76 | │ │ │ additional_count │ 1 │ │ │ │ │ │ │ │ 77 | │ │ ╰─────────────────────┴──────────╯ │ │ │ │ │ │ │ 78 | ╰───┴────────────────────────────────────┴───────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────┴────────────────┴────────────────┴────────────────────────────────────────┴──────╯ 79 | ``` 80 | 81 | ``` 82 | specify query types by numeric ID, and get numeric IDs in output 83 | > dns query --type [5, 15] -c google.com 84 | 85 | ╭─#─┬────────────────────header─────────────────────┬───────────question───────────┬──────────────────────────────────────────────────answer───────────────────────────────────────────────────┬──────────────────────────────────────────────────authority───────────────────────────────────────────────────┬───additional───┬──────────────────edns──────────────────┬─size─╮ 86 | │ 0 │ ╭─────────────────────┬─────────────────────╮ │ ╭───────┬──────────────────╮ │ [list 0 items] │ ╭─#─┬────name─────┬──────type──────┬─────class─────┬─ttl──┬────────────────rdata────────────────┬──proof───╮ │ [list 0 items] │ ╭─────────────┬──────────────────────╮ │ 89 B │ 87 | │ │ │ id │ 38468 │ │ │ name │ google.com. │ │ │ │ 0 │ google.com. │ ╭──────┬─────╮ │ ╭──────┬────╮ │ 1min │ ╭─────────┬───────────────────────╮ │ insecure │ │ │ │ rcode_high │ 0 │ │ │ 88 | │ │ │ │ ╭──────┬──────────╮ │ │ │ │ ╭──────┬───────╮ │ │ │ │ │ │ │ name │ SOA │ │ │ name │ IN │ │ │ │ mname │ ns1.google.com. │ │ │ │ │ │ version │ 0 │ │ │ 89 | │ │ │ message_type │ │ name │ RESPONSE │ │ │ │ type │ │ name │ CNAME │ │ │ │ │ │ │ │ code │ 6 │ │ │ code │ 1 │ │ │ │ rname │ dns-admin.google.com. │ │ │ │ │ │ │ ╭───────────┬──────╮ │ │ │ 90 | │ │ │ │ │ code │ 1 │ │ │ │ │ │ code │ 5 │ │ │ │ │ │ │ ╰──────┴─────╯ │ ╰──────┴────╯ │ │ │ serial │ 750526362 │ │ │ │ │ │ flags │ │ dnssec_ok │ true │ │ │ │ 91 | │ │ │ │ ╰──────┴──────────╯ │ │ │ │ ╰──────┴───────╯ │ │ │ │ │ │ │ │ │ │ refresh │ 15min │ │ │ │ │ │ │ ╰───────────┴──────╯ │ │ │ 92 | │ │ │ │ ╭──────┬───────╮ │ │ │ │ ╭──────┬────╮ │ │ │ │ │ │ │ │ │ │ retry │ 15min │ │ │ │ │ │ max_payload │ 1.2 kB │ │ │ 93 | │ │ │ op_code │ │ name │ QUERY │ │ │ │ class │ │ name │ IN │ │ │ │ │ │ │ │ │ │ │ expire │ 30min │ │ │ │ │ │ opts │ {record 0 fields} │ │ │ 94 | │ │ │ │ │ code │ 0 │ │ │ │ │ │ code │ 1 │ │ │ │ │ │ │ │ │ │ │ minimum │ 1min │ │ │ │ │ ╰─────────────┴──────────────────────╯ │ │ 95 | │ │ │ │ ╰──────┴───────╯ │ │ │ │ ╰──────┴────╯ │ │ │ │ │ │ │ │ │ ╰─────────┴───────────────────────╯ │ │ │ │ │ │ 96 | │ │ │ authoritative │ false │ │ ╰───────┴──────────────────╯ │ │ ╰───┴─────────────┴────────────────┴───────────────┴──────┴─────────────────────────────────────┴──────────╯ │ │ │ │ 97 | │ │ │ truncated │ false │ │ │ │ │ │ │ │ 98 | │ │ │ recursion_desired │ true │ │ │ │ │ │ │ │ 99 | │ │ │ recursion_available │ true │ │ │ │ │ │ │ │ 100 | │ │ │ authentic_data │ false │ │ │ │ │ │ │ │ 101 | │ │ │ │ ╭──────┬──────────╮ │ │ │ │ │ │ │ │ 102 | │ │ │ response_code │ │ name │ No Error │ │ │ │ │ │ │ │ │ 103 | │ │ │ │ │ code │ 0 │ │ │ │ │ │ │ │ │ 104 | │ │ │ │ ╰──────┴──────────╯ │ │ │ │ │ │ │ │ 105 | │ │ │ query_count │ 1 │ │ │ │ │ │ │ │ 106 | │ │ │ answer_count │ 0 │ │ │ │ │ │ │ │ 107 | │ │ │ name_server_count │ 1 │ │ │ │ │ │ │ │ 108 | │ │ │ additional_count │ 1 │ │ │ │ │ │ │ │ 109 | │ │ ╰─────────────────────┴─────────────────────╯ │ │ │ │ │ │ │ 110 | │ 1 │ ╭─────────────────────┬─────────────────────╮ │ ╭───────┬───────────────╮ │ ╭─#─┬────name─────┬─────type──────┬─────class─────┬─ttl──┬───────────────rdata───────────────┬──proof───╮ │ [list 0 items] │ [list 0 items] │ ╭─────────────┬──────────────────────╮ │ 60 B │ 111 | │ │ │ id │ 24790 │ │ │ name │ google.com. │ │ │ 0 │ google.com. │ ╭──────┬────╮ │ ╭──────┬────╮ │ 5min │ ╭────────────┬──────────────────╮ │ insecure │ │ │ │ │ rcode_high │ 0 │ │ │ 112 | │ │ │ │ ╭──────┬──────────╮ │ │ │ │ ╭──────┬────╮ │ │ │ │ │ │ name │ MX │ │ │ name │ IN │ │ │ │ preference │ 10 │ │ │ │ │ │ │ version │ 0 │ │ │ 113 | │ │ │ message_type │ │ name │ RESPONSE │ │ │ │ type │ │ name │ MX │ │ │ │ │ │ │ code │ 15 │ │ │ code │ 1 │ │ │ │ exchange │ smtp.google.com. │ │ │ │ │ │ │ │ ╭───────────┬──────╮ │ │ │ 114 | │ │ │ │ │ code │ 1 │ │ │ │ │ │ code │ 15 │ │ │ │ │ │ ╰──────┴────╯ │ ╰──────┴────╯ │ │ ╰────────────┴──────────────────╯ │ │ │ │ │ │ flags │ │ dnssec_ok │ true │ │ │ │ 115 | │ │ │ │ ╰──────┴──────────╯ │ │ │ │ ╰──────┴────╯ │ │ ╰───┴─────────────┴───────────────┴───────────────┴──────┴───────────────────────────────────┴──────────╯ │ │ │ │ │ ╰───────────┴──────╯ │ │ │ 116 | │ │ │ │ ╭──────┬───────╮ │ │ │ │ ╭──────┬────╮ │ │ │ │ │ │ max_payload │ 1.2 kB │ │ │ 117 | │ │ │ op_code │ │ name │ QUERY │ │ │ │ class │ │ name │ IN │ │ │ │ │ │ │ opts │ {record 0 fields} │ │ │ 118 | │ │ │ │ │ code │ 0 │ │ │ │ │ │ code │ 1 │ │ │ │ │ │ ╰─────────────┴──────────────────────╯ │ │ 119 | │ │ │ │ ╰──────┴───────╯ │ │ │ │ ╰──────┴────╯ │ │ │ │ │ │ │ 120 | │ │ │ authoritative │ false │ │ ╰───────┴───────────────╯ │ │ │ │ │ │ 121 | │ │ │ truncated │ false │ │ │ │ │ │ │ │ 122 | │ │ │ recursion_desired │ true │ │ │ │ │ │ │ │ 123 | │ │ │ recursion_available │ true │ │ │ │ │ │ │ │ 124 | │ │ │ authentic_data │ false │ │ │ │ │ │ │ │ 125 | │ │ │ │ ╭──────┬──────────╮ │ │ │ │ │ │ │ │ 126 | │ │ │ response_code │ │ name │ No Error │ │ │ │ │ │ │ │ │ 127 | │ │ │ │ │ code │ 0 │ │ │ │ │ │ │ │ │ 128 | │ │ │ │ ╰──────┴──────────╯ │ │ │ │ │ │ │ │ 129 | │ │ │ query_count │ 1 │ │ │ │ │ │ │ │ 130 | │ │ │ answer_count │ 1 │ │ │ │ │ │ │ │ 131 | │ │ │ name_server_count │ 0 │ │ │ │ │ │ │ │ 132 | │ │ │ additional_count │ 1 │ │ │ │ │ │ │ │ 133 | │ │ ╰─────────────────────┴─────────────────────╯ │ │ │ │ │ │ │ 134 | ╰───┴───────────────────────────────────────────────┴──────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────┴────────────────────────────────────────┴──────╯ 135 | ``` 136 | 137 | ``` 138 | pipe name into command 139 | > 'google.com' | dns query 140 | ``` 141 | 142 | ``` 143 | pipe lists of names into command 144 | > ['google.com', 'amazon.com'] | dns query 145 | ``` 146 | 147 | ``` 148 | query record name that has labels with non-renderable bytes 149 | > [ $"ding(char -u '07')-ds", "metric", "gstatic", "com" ] | each { into binary } | collect { $in } | dns query 150 | ``` 151 | 152 | ``` 153 | pipe table of queries into command (ignores --type flag) 154 | > [{name: 'google.com', type: 'A'}, {name: 'amazon.com', type: 'A'}] | dns query 155 | ``` 156 | 157 | ``` 158 | choose a different protocol and/or port 159 | > dns query -p tls -n dns.google -s 8.8.8.8 en.wikipedia.org 160 | > dns query -p https -n cloudflare-dns.com -s 1.1.1.1 en.wikipedia.org 161 | > dns query -p quic -n dns.adguard-dns.com -s 94.140.15.15:853 en.wikipedia.org 162 | ``` 163 | 164 | ## Configuration 165 | 166 | You can specify any of the command line flags in your `config.nu` to make them 167 | permanent. If an option is specified in both the `config.nu` and the CLI, the 168 | CLI takes precedence. 169 | 170 | ```nu 171 | $env.config.plugins.dns = { 172 | server: "94.140.15.15" 173 | protocol: https 174 | dns-name: dns.adguard-dns.com 175 | dnssec-mode: none 176 | tasks: 16 177 | timeout: 30sec 178 | } 179 | ``` 180 | 181 | ## Install 182 | 183 | ```nu 184 | cargo install nu_plugin_dns 185 | plugin add $"($env.CARGO_HOME)/bin/nu_plugin_dns" 186 | plugin use dns 187 | ``` 188 | -------------------------------------------------------------------------------- /pre-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nu 2 | 3 | use std/log 4 | 5 | log info $"pre-release: NEW_VERSION: ($env.NEW_VERSION)" 6 | 7 | if $env.NEW_VERSION =~ '-' { 8 | log info "pre-release: dev version, no checks" 9 | exit 0 10 | } 11 | 12 | let found = open CHANGELOG.md | find $"# [($env.NEW_VERSION)]" | length 13 | 14 | if $found == 0 { 15 | log info $"pre-release: missing change log entry for version ($env.NEW_VERSION)" 16 | exit 1 17 | } 18 | 19 | print "pre-release: checks passed" 20 | -------------------------------------------------------------------------------- /src/dns/client.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::{pin::Pin, sync::Arc, time::Duration}; 3 | 4 | use futures_util::Stream; 5 | use hickory_client::client::{Client, DnssecClient}; 6 | use hickory_proto::xfer::{DnsRequestSender, Protocol}; 7 | use hickory_proto::{ 8 | h2::HttpsClientStreamBuilder, quic::QuicClientStream, runtime::RuntimeProvider, 9 | tcp::TcpClientStream, udp::UdpClientStream, xfer::DnsResponse, DnsHandle, DnsMultiplexer, 10 | ProtoError, 11 | }; 12 | use nu_protocol::{LabeledError, Span}; 13 | use rustls::{pki_types::TrustAnchor, RootCertStore}; 14 | use tokio::task::JoinHandle; 15 | 16 | use super::{config::Config, serde::DnssecMode}; 17 | 18 | type DnsHandleResponse = 19 | Pin> + Send + 'static)>>; 20 | pub(crate) type BgHandle = JoinHandle>; 21 | 22 | #[derive(Clone)] 23 | pub struct DnsClient { 24 | client: HickoryDnsClient, 25 | } 26 | 27 | #[derive(Clone)] 28 | pub enum HickoryDnsClient { 29 | Standard(Client), 30 | Dnssec(DnssecClient), 31 | } 32 | 33 | impl DnsClient { 34 | pub async fn connect(config: &Config, conn: F) -> Result<(Self, BgHandle), LabeledError> 35 | where 36 | S: DnsRequestSender, 37 | F: Future> + 'static + Send + Unpin, 38 | { 39 | let connect_err = |err| { 40 | LabeledError::new("connection error").with_label( 41 | format!("Error creating client connection: {}", err), 42 | Span::unknown(), 43 | ) 44 | }; 45 | 46 | let (client, bg) = match config.dnssec_mode.item { 47 | DnssecMode::None => { 48 | let (client, bg) = Client::connect(conn).await.map_err(connect_err)?; 49 | (HickoryDnsClient::Standard(client), bg) 50 | } 51 | DnssecMode::Opportunistic => { 52 | let (client, bg) = DnssecClient::connect(conn).await.map_err(connect_err)?; 53 | (HickoryDnsClient::Dnssec(client), bg) 54 | } 55 | }; 56 | 57 | Ok((DnsClient { client }, tokio::spawn(bg))) 58 | } 59 | 60 | pub async fn new( 61 | config: &Config, 62 | provider: impl RuntimeProvider, 63 | ) -> Result<(Self, BgHandle), LabeledError> { 64 | let (client, bg) = match config.protocol.item { 65 | Protocol::Udp => { 66 | DnsClient::connect( 67 | config, 68 | UdpClientStream::builder(config.server.item, provider.clone()) 69 | .with_timeout( 70 | // can't set a timeout on HTTPS client, so work 71 | // around by setting the client internal timeout 72 | // very long for all the others so we can set 73 | // our own instead 74 | Some(Duration::from_secs(60 * 60 * 24 * 365)), 75 | ) 76 | .build(), 77 | ) 78 | .await? 79 | } 80 | Protocol::Tcp => { 81 | DnsClient::connect(config, { 82 | let (stream, sender) = TcpClientStream::new( 83 | config.server.item, 84 | None, 85 | // can't set a timeout on HTTPS client, so work around 86 | // by setting the client internal timeout very long for 87 | // all the others so we can set our own instead 88 | Some(Duration::from_secs(60 * 60 * 24 * 365)), 89 | provider.clone(), 90 | ); 91 | 92 | DnsMultiplexer::<_>::new(stream, sender, None) 93 | }) 94 | .await? 95 | } 96 | proto @ (Protocol::Https | Protocol::Tls | Protocol::Quic) => { 97 | let root_store = RootCertStore::from_iter( 98 | webpki_roots::TLS_SERVER_ROOTS 99 | .iter() 100 | .map(TrustAnchor::to_owned), 101 | ); 102 | 103 | let client_config = rustls::ClientConfig::builder() 104 | .with_root_certificates(root_store) 105 | .with_no_client_auth(); 106 | 107 | match proto { 108 | Protocol::Tls => { 109 | let client_config = Arc::new(client_config); 110 | DnsClient::connect(config, { 111 | let (stream, sender) = hickory_proto::rustls::tls_client_connect( 112 | config.server.item, 113 | // safe to unwrap because having a DNS name 114 | // is enforced when constructing the config 115 | config.dns_name.as_ref().unwrap().clone().item, 116 | client_config.clone(), 117 | provider.clone(), 118 | ); 119 | DnsMultiplexer::<_>::with_timeout( 120 | stream, 121 | sender, 122 | // can't set a timeout on HTTPS client, so work 123 | // around by setting the client internal timeout 124 | // very long for all the others so we can set 125 | // our own instead 126 | Duration::from_secs(60 * 60 * 24 * 365), 127 | None, 128 | ) 129 | }) 130 | .await? 131 | } 132 | Protocol::Https => { 133 | let client_config = Arc::new(client_config); 134 | DnsClient::connect(config, { 135 | HttpsClientStreamBuilder::with_client_config( 136 | client_config.clone(), 137 | provider.clone(), 138 | ) 139 | .build( 140 | config.server.item, 141 | config.dns_name.as_ref().unwrap().clone().item, 142 | // FIXME: Add a config option 143 | String::from("/dns-query"), 144 | ) 145 | }) 146 | .await? 147 | } 148 | Protocol::Quic => { 149 | DnsClient::connect(config, { 150 | let mut builder = QuicClientStream::builder(); 151 | builder.crypto_config(client_config.clone()); 152 | builder.build( 153 | config.server.item, 154 | config.dns_name.as_ref().unwrap().clone().item, 155 | ) 156 | }) 157 | .await? 158 | } 159 | _ => unreachable!(), 160 | } 161 | } 162 | proto => { 163 | return Err(LabeledError::new("unknown protocol") 164 | .with_label(format!("Unknown protocol: {}", proto), config.protocol.span)) 165 | } 166 | }; 167 | 168 | Ok((client, bg)) 169 | } 170 | } 171 | 172 | impl DnsHandle for DnsClient { 173 | type Response = DnsHandleResponse; 174 | 175 | fn send(&self, request: R) -> Self::Response 176 | where 177 | R: Into + Unpin + Send + 'static, 178 | { 179 | let request = request.into(); 180 | 181 | match &self.client { 182 | HickoryDnsClient::Standard(client) => Box::pin(client.send(request)), 183 | HickoryDnsClient::Dnssec(client) => Box::pin(client.send(request)), 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/dns/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod query; 2 | -------------------------------------------------------------------------------- /src/dns/commands/query.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | sync::{atomic::AtomicBool, Arc}, 4 | time::Duration, 5 | }; 6 | 7 | use futures_util::{stream::FuturesOrdered, Future, FutureExt, StreamExt, TryStreamExt}; 8 | use hickory_client::client::ClientHandle; 9 | use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; 10 | use nu_protocol::{ 11 | Example, IntoValue, LabeledError, ListStream, PipelineData, Signals, Signature, Span, 12 | SyntaxShape, Value, 13 | }; 14 | use tokio::sync::mpsc; 15 | use tokio_util::sync::CancellationToken; 16 | use tracing_subscriber::prelude::*; 17 | 18 | use crate::{ 19 | dns::{ 20 | client::{BgHandle, DnsClient}, 21 | config::Config, 22 | constants, 23 | serde::{self, Query}, 24 | }, 25 | Dns, 26 | }; 27 | 28 | pub type DnsQueryResult = 29 | FuturesOrdered> + Send>>>; 30 | pub type DnsQueryPluginClient = Arc>>; 31 | 32 | #[derive(Debug)] 33 | pub struct DnsQuery; 34 | 35 | impl DnsQuery { 36 | pub(crate) async fn run_impl( 37 | &self, 38 | plugin: &Dns, 39 | engine: &EngineInterface, 40 | call: &EvaluatedCall, 41 | input: PipelineData, 42 | ) -> Result { 43 | let _ = tracing_subscriber::registry() 44 | .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) 45 | .with(tracing_subscriber::EnvFilter::from_default_env()) 46 | .try_init(); 47 | 48 | let config = Config::from_nu(engine.get_plugin_config()?, call)?; 49 | let arg_inputs: Value = call.nth(0).unwrap_or(Value::nothing(call.head)); 50 | 51 | let input: PipelineData = match input { 52 | PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _) => { 53 | PipelineData::Value(arg_inputs, None) 54 | } 55 | val => { 56 | if !arg_inputs.is_empty() { 57 | return Err(LabeledError::new("ambiguous input").with_label( 58 | "Input should either be positional args or piped, but not both", 59 | val.span().unwrap_or(Span::unknown()), 60 | )); 61 | } 62 | 63 | val 64 | } 65 | }; 66 | 67 | let client = tokio::time::timeout(config.timeout.item, plugin.dns_client(&config)) 68 | .await 69 | .map_err(|_| { 70 | LabeledError::new("timed out").with_label( 71 | format!("connecting to {} timed out", config.server.item), 72 | config.server.span, 73 | ) 74 | })??; 75 | 76 | let config = Arc::new(config); 77 | 78 | match input { 79 | PipelineData::Value(val, _) => { 80 | if tracing::enabled!(tracing::Level::TRACE) { 81 | tracing::trace!(phase = "input", data.kind = "value", ?val); 82 | } else { 83 | tracing::debug!(phase = "input", data.kind = "value"); 84 | } 85 | 86 | let values = Self::query(config, val, client.clone()) 87 | .await 88 | .try_collect::>() 89 | .await?; 90 | 91 | let val = PipelineData::Value(Value::list(values, Span::unknown()), None); 92 | 93 | tracing::trace!(phase = "return", ?val); 94 | 95 | Ok(val) 96 | } 97 | PipelineData::ListStream(stream, _) => { 98 | tracing::debug!(phase = "input", data.kind = "stream"); 99 | 100 | let span = stream.span(); 101 | let ctrlc = Signals::new(Arc::new(AtomicBool::new(false))); 102 | let (request_tx, request_rx) = mpsc::channel(config.tasks.item); 103 | let (resp_tx, mut resp_rx) = mpsc::channel(config.tasks.item); 104 | 105 | plugin.spawn(watch_sigterm( 106 | ctrlc.clone(), 107 | plugin.cancel.clone(), 108 | plugin.client.clone(), 109 | )); 110 | 111 | plugin.spawn(coordinate_queries( 112 | config, 113 | client, 114 | request_rx, 115 | resp_tx, 116 | plugin.cancel.clone(), 117 | )); 118 | 119 | plugin 120 | .spawn_blocking({ 121 | let cancel = plugin.cancel.clone(); 122 | move || stream_requests(stream, cancel, request_tx) 123 | }) 124 | .await; 125 | 126 | Ok(PipelineData::ListStream( 127 | ListStream::new( 128 | std::iter::from_fn(move || { 129 | tokio::task::block_in_place(|| { 130 | resp_rx.blocking_recv().map(|resp| { 131 | resp.unwrap_or_else(|err| { 132 | Value::error(err.into(), Span::unknown()) 133 | }) 134 | }) 135 | }) 136 | }) 137 | .inspect(|val| log_response_val(val, "return")), 138 | span, 139 | ctrlc, 140 | ), 141 | None, 142 | )) 143 | } 144 | data => Err(LabeledError::new("invalid input").with_label( 145 | "Only values can be passed as input", 146 | data.span().unwrap_or(Span::unknown()), 147 | )), 148 | } 149 | } 150 | 151 | pub(crate) async fn query( 152 | config: Arc, 153 | input: Value, 154 | client: DnsClient, 155 | ) -> DnsQueryResult { 156 | let in_span = input.span(); 157 | let queries = match Query::try_from_value(&input, &config) { 158 | Ok(queries) => queries, 159 | Err(err) => { 160 | return vec![ 161 | Box::pin(std::future::ready(Ok(Value::error(err.into(), in_span)))) 162 | as Pin + Send>>, 163 | ] 164 | .into_iter() 165 | .collect() 166 | } 167 | }; 168 | 169 | tracing::debug!(request.queries = ?queries); 170 | 171 | let mut responses = FuturesOrdered::new(); 172 | 173 | for query in queries { 174 | let mut client = client.clone(); 175 | let config = config.clone(); 176 | 177 | let resp = async move { 178 | let parts = query.0; 179 | 180 | tracing::info!(query.phase = "start", query.parts = ?parts); 181 | 182 | let request = tokio::time::timeout( 183 | config.timeout.item, 184 | client.query(parts.name.clone(), parts.query_class, parts.query_type), 185 | ); 186 | 187 | request 188 | .await 189 | .map_err(|_| { 190 | LabeledError::new("timed out").with_label( 191 | format!("request to {} timed out", config.server.item), 192 | config.server.span, 193 | ) 194 | })? 195 | .map_err(|err| { 196 | LabeledError::new("DNS error") 197 | .with_label(format!("Error in DNS response: {:?}", err), in_span) 198 | }) 199 | .and_then(|resp: hickory_proto::xfer::DnsResponse| { 200 | let resp = serde::Response::new(resp); 201 | 202 | if tracing::enabled!(tracing::Level::DEBUG) { 203 | tracing::debug!(query.phase = "finish", query.parts = ?parts, query.resp = ?resp); 204 | } else { 205 | tracing::info!(query.phase = "finish", query.parts = ?parts); 206 | } 207 | 208 | resp.into_value(config.code.item) 209 | }) 210 | .inspect_err( 211 | |err| tracing::debug!(query.phase = "finish", query.error = ?err), 212 | ) 213 | .inspect(|resp| { 214 | log_response_val(resp, "finish"); 215 | }) 216 | }; 217 | 218 | // apparently you cannot just collect this into a `FuturesOrdered` 219 | // because doing so causes each future to be polled in serial, 220 | // completely defeating the point 221 | responses.push_back(Box::pin(resp) as Pin + Send>>); 222 | } 223 | 224 | responses 225 | } 226 | } 227 | 228 | async fn watch_sigterm( 229 | ctrlc: Signals, 230 | cancel: CancellationToken, 231 | client: DnsQueryPluginClient, 232 | ) -> Result<(), LabeledError> { 233 | while !ctrlc.interrupted() 234 | && client 235 | .write() 236 | .await 237 | .as_mut() 238 | .is_some_and(|bg| (&mut bg.1).now_or_never().is_none()) 239 | { 240 | tokio::time::sleep(Duration::from_millis(500)).await; 241 | } 242 | 243 | cancel.cancel(); 244 | Ok(()) 245 | } 246 | 247 | fn stream_requests( 248 | stream: ListStream, 249 | cancel: CancellationToken, 250 | request_tx: mpsc::Sender, 251 | ) -> Result<(), LabeledError> { 252 | tracing::trace!(task.sender.phase = "start"); 253 | 254 | let result = stream.into_iter().try_for_each(|val| { 255 | tracing::trace!(query = ?val, query.phase = "send"); 256 | 257 | if cancel.is_cancelled() { 258 | return Err(LabeledError::new("canceled")); 259 | } 260 | 261 | request_tx.blocking_send(val).map_err(|send_err| { 262 | LabeledError::new("internal error").with_label( 263 | format!("failed to send dns query result: {}", send_err), 264 | Span::unknown(), 265 | ) 266 | }) 267 | }); 268 | 269 | tracing::trace!(task.sender.phase = "exit", task.sender.result = ?result); 270 | 271 | result 272 | } 273 | 274 | async fn coordinate_queries( 275 | config: Arc, 276 | client: DnsClient, 277 | mut request_rx: mpsc::Receiver, 278 | resp_tx: mpsc::Sender>, 279 | cancel: CancellationToken, 280 | ) -> Result<(), LabeledError> { 281 | tracing::trace!(task.query_coordinator.phase = "start"); 282 | let mut buf = Vec::with_capacity(config.tasks.item); 283 | 284 | while request_rx.recv_many(&mut buf, config.tasks.item).await > 0 { 285 | tracing::trace!(query.phase = "batch received", query.batchsize = buf.len()); 286 | 287 | let config = config.clone(); 288 | let client = client.clone(); 289 | let cancel = cancel.clone(); 290 | 291 | let val = std::mem::replace(&mut buf, Vec::with_capacity(config.tasks.item)) 292 | .into_value(Span::unknown()); 293 | 294 | tracing::trace!(task.query_exec.phase = "start"); 295 | 296 | let mut result = tokio::select! { 297 | _ = cancel.cancelled() => vec![Box::pin(std::future::ready(Err(LabeledError::new("canceled")))) as Pin + Send>>].into_iter().collect(), 298 | resp = DnsQuery::query(config, val, client) => resp, 299 | }; 300 | 301 | tracing::trace!( 302 | task.query_exec.phase = "end", 303 | task.query_exec.result = ?result 304 | ); 305 | 306 | while let Some(resp) = StreamExt::next(&mut result).await { 307 | resp_tx.send(resp).await.map_err(|send_err| { 308 | LabeledError::new("internal error").with_label( 309 | format!("failed to send dns query result: {}", send_err), 310 | Span::unknown(), 311 | ) 312 | })?; 313 | } 314 | } 315 | 316 | tracing::trace!(task.query_coordinator.phase = "exit"); 317 | 318 | Ok(()) 319 | } 320 | 321 | pub(crate) fn log_response_val(resp: &Value, phase: &str) { 322 | if tracing::enabled!(tracing::Level::TRACE) { 323 | tracing::trace!(query.phase = phase, query.response = ?resp) 324 | } else { 325 | let question = resp.get_data_by_key("question"); 326 | let answer = resp.get_data_by_key("answer"); 327 | tracing::debug!( 328 | query.phase = phase, 329 | query.response.question = ?question, 330 | query.response.answer = ?answer 331 | ); 332 | } 333 | } 334 | 335 | impl PluginCommand for DnsQuery { 336 | type Plugin = Dns; 337 | 338 | fn run( 339 | &self, 340 | plugin: &Self::Plugin, 341 | engine: &EngineInterface, 342 | call: &EvaluatedCall, 343 | input: PipelineData, 344 | ) -> Result { 345 | plugin 346 | .main_runtime 347 | .block_on(self.run_impl(plugin, engine, call, input)) 348 | } 349 | 350 | fn name(&self) -> &str { 351 | constants::commands::QUERY 352 | } 353 | 354 | fn description(&self) -> &str { 355 | "Perform a DNS query" 356 | } 357 | 358 | fn signature(&self) -> nu_protocol::Signature { 359 | Signature::build(self.name()) 360 | .rest( 361 | constants::flags::NAME, 362 | 363 | // [NOTE] this does not work 364 | // SyntaxShape::OneOf(vec![ 365 | // SyntaxShape::String, 366 | // SyntaxShape::List(Box::new(SyntaxShape::OneOf(vec![ 367 | // SyntaxShape::String, 368 | // SyntaxShape::Binary, 369 | // SyntaxShape::Int, 370 | // SyntaxShape::Boolean, 371 | // ]))), 372 | // ]), 373 | SyntaxShape::Any, 374 | 375 | "DNS record name", 376 | ) 377 | .named( 378 | constants::flags::SERVER, 379 | SyntaxShape::String, 380 | "Nameserver to query (defaults to system config or 8.8.8.8)", 381 | Some('s'), 382 | ) 383 | .named( 384 | constants::flags::PROTOCOL, 385 | SyntaxShape::String, 386 | "Protocol to use to connect to the nameserver: UDP, TCP. (default: UDP)", 387 | Some('p'), 388 | ) 389 | .named(constants::flags::TYPE, SyntaxShape::Any, "Query type", Some('t')) 390 | .named(constants::flags::CLASS, SyntaxShape::Any, "Query class", None) 391 | .switch( 392 | constants::flags::CODE, 393 | "Return code fields with both string and numeric representations", 394 | Some('c'), 395 | ) 396 | .named( 397 | constants::flags::DNSSEC, 398 | SyntaxShape::String, 399 | "Perform DNSSEC validation on records. Choices are: \"none\", \"opportunistic\" (validate if RRSIGs present, otherwise no validation; default)", 400 | Some('d'), 401 | ) 402 | .named( 403 | constants::flags::DNS_NAME, 404 | SyntaxShape::String, 405 | "DNS name of the TLS certificate in use by the nameserver (for TLS and HTTPS only)", 406 | Some('n'), 407 | ) 408 | .named( 409 | constants::flags::TASKS, 410 | SyntaxShape::Int, 411 | format!("Number of concurrent tasks to execute queries. Please be mindful not to overwhelm your nameserver! Default: {}", constants::config::default::TASKS), 412 | Some('j'), 413 | ) 414 | .named( 415 | constants::flags::TIMEOUT, 416 | SyntaxShape::Duration, 417 | format!("How long a request can take before timing out. Be aware the concurrency level can affect this. Default: {}sec", constants::config::default::TIMEOUT.as_secs()), 418 | None, 419 | ) 420 | } 421 | 422 | fn examples(&self) -> Vec { 423 | vec![ 424 | Example { 425 | example: "dns query google.com", 426 | description: "simple query for A / AAAA records", 427 | result: None, 428 | }, 429 | Example { 430 | example: "dns query --type CNAME google.com", 431 | description: "specify query type", 432 | result: None, 433 | }, 434 | Example { 435 | example: "dns query --type [cname, mx] -c google.com", 436 | description: "specify multiple query types", 437 | result: None, 438 | }, 439 | Example { 440 | example: "dns query --type [5, 15] -c google.com", 441 | description: "specify query types by numeric ID, and get numeric IDs in output", 442 | result: None, 443 | }, 444 | Example { 445 | example: "'google.com' | dns query", 446 | description: "pipe name to command", 447 | result: None, 448 | }, 449 | Example { 450 | example: "['google.com', 'amazon.com'] | dns query", 451 | description: "pipe lists of names to command", 452 | result: None, 453 | }, 454 | Example { 455 | example: "[ $\"ding(char -u '07')-ds\", \"metric\", \"gstatic\", \"com\" ] | each { into binary } | collect { $in } | dns query", 456 | description: "query record name that has labels with non-renderable bytes", 457 | result: None, 458 | }, 459 | Example { 460 | example: "[{{name: 'google.com', type: 'A'}}, {{name: 'amazon.com', type: 'A'}}] | dns query", 461 | description: "pipe table of queries to command (ignores --type flag)", 462 | result: None, 463 | }, 464 | ] 465 | } 466 | 467 | fn search_terms(&self) -> Vec<&str> { 468 | vec!["dns", "network", "dig"] 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/dns/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, SocketAddr}, 3 | str::FromStr, 4 | time::Duration, 5 | }; 6 | 7 | use hickory_proto::{ 8 | rr::{DNSClass, RecordType}, 9 | xfer::Protocol, 10 | }; 11 | use hickory_resolver::config::ResolverConfig; 12 | use nu_plugin::EvaluatedCall; 13 | use nu_protocol::{record, LabeledError, Span, Spanned, Value}; 14 | 15 | use crate::spanned; 16 | 17 | use super::{ 18 | constants::{self, flags}, 19 | serde::{self, DnssecMode, RType}, 20 | }; 21 | 22 | #[derive(Debug)] 23 | pub struct Config { 24 | pub protocol: Spanned, 25 | pub server: Spanned, 26 | 27 | pub qtypes: Spanned>>, 28 | pub class: Spanned, 29 | 30 | pub code: Spanned, 31 | pub dnssec_mode: Spanned, 32 | pub dns_name: Option>, 33 | 34 | pub tasks: Spanned, 35 | pub timeout: Spanned, 36 | } 37 | 38 | impl TryFrom for Config { 39 | type Error = LabeledError; 40 | 41 | fn try_from(value: Value) -> Result { 42 | let mut record = value.into_record()?; 43 | Config::from_values(|name| record.remove(name)) 44 | } 45 | } 46 | 47 | impl TryFrom<&EvaluatedCall> for Config { 48 | type Error = LabeledError; 49 | 50 | fn try_from(call: &EvaluatedCall) -> Result { 51 | Config::from_values(|name| call.get_flag_value(name)) 52 | } 53 | } 54 | 55 | impl Config { 56 | pub fn from_nu( 57 | plugin_config: Option, 58 | call: &EvaluatedCall, 59 | ) -> Result { 60 | tracing::debug!(?plugin_config, ?call); 61 | 62 | let plugin_config = match plugin_config { 63 | None => Value::record(record!(), Span::unknown()), 64 | Some(cfg) => cfg, 65 | }; 66 | 67 | Config::from_values(|name| { 68 | let cfg_val = plugin_config.get_data_by_key(name); 69 | let call_val = match (call.has_flag(name), call.get_flag_value(name)) { 70 | (Ok(true), None) => Some(Value::bool(true, Span::unknown())), 71 | (_, val) => val, 72 | }; 73 | 74 | match (cfg_val, call_val) { 75 | (None, None) => None, 76 | (None, val @ Some(_)) => val, 77 | (val @ Some(_), None) => val, 78 | 79 | // CLI flags take precedence over config 80 | (Some(_), callv @ Some(_)) => callv, 81 | } 82 | }) 83 | } 84 | 85 | pub fn from_values(mut get_value: F) -> Result 86 | where 87 | F: FnMut(&str) -> Option, 88 | { 89 | let protocol = match get_value(flags::PROTOCOL) { 90 | None => None, 91 | Some(val) => { 92 | let span = val.span(); 93 | Some( 94 | serde::Protocol::try_from(val) 95 | .map(|serde::Protocol(proto)| spanned!(proto, span))?, 96 | ) 97 | } 98 | }; 99 | 100 | let needs_dns_name = matches!( 101 | protocol, 102 | Some(Spanned { 103 | item: Protocol::Tls | Protocol::Https | Protocol::Quic, 104 | .. 105 | }) 106 | ); 107 | 108 | let dns_name = match get_value(constants::flags::DNS_NAME) { 109 | None => None, 110 | Some(val) => { 111 | let span = val.span(); 112 | 113 | if !needs_dns_name { 114 | return Err(LabeledError::new("invalid config combination").with_label( 115 | "DNS name only makes sense for TLS, HTTPS, or QUIC", 116 | val.span(), 117 | )); 118 | } 119 | 120 | Some(spanned!(val.into_string()?, span)) 121 | } 122 | }; 123 | 124 | let (addr, protocol) = match get_value(flags::SERVER) { 125 | Some(ref value @ Value::String { .. }) => { 126 | let protocol = protocol.unwrap_or(spanned!(Protocol::Udp, Span::unknown())); 127 | 128 | let addr = SocketAddr::from_str(value.as_str().unwrap()) 129 | .or_else(|_| { 130 | IpAddr::from_str(value.as_str().unwrap()).map(|ip| { 131 | SocketAddr::new(ip, constants::config::default_port(protocol.item)) 132 | }) 133 | }) 134 | .map_err(|err| { 135 | LabeledError::new("invalid server") 136 | .with_label(err.to_string(), value.clone().span()) 137 | })?; 138 | 139 | let addr = spanned!(addr, value.span()); 140 | 141 | (addr, protocol) 142 | } 143 | None => { 144 | let (config, _) = 145 | hickory_resolver::system_conf::read_system_conf().unwrap_or_default(); 146 | tracing::debug!(?config); 147 | match config.name_servers() { 148 | [ns, ..] => ( 149 | spanned!(ns.socket_addr, Span::unknown()), 150 | spanned!(ns.protocol, Span::unknown()), 151 | ), 152 | [] => { 153 | let config = ResolverConfig::default(); 154 | let ns = config.name_servers().first().unwrap(); 155 | 156 | // if protocol is explicitly configured, it should take 157 | // precedence over the system config 158 | ( 159 | spanned!(ns.socket_addr, Span::unknown()), 160 | protocol.unwrap_or(spanned!(ns.protocol, Span::unknown())), 161 | ) 162 | } 163 | } 164 | } 165 | Some(val) => { 166 | return Err(LabeledError::new("invalid server address") 167 | .with_label("server address should be a string", val.span())); 168 | } 169 | }; 170 | 171 | if needs_dns_name && dns_name.is_none() { 172 | return Err(LabeledError::new("need DNS name").with_label( 173 | "protocol needs to be accompanied by --dns-name", 174 | protocol.span, 175 | )); 176 | } 177 | 178 | let qtypes: Spanned>> = match get_value(constants::flags::TYPE) { 179 | Some(list @ Value::List { .. }) => { 180 | let span = list.span(); 181 | let vals = list.as_list()?; 182 | 183 | spanned!( 184 | vals.iter() 185 | .map(|val| { 186 | let span = val.span(); 187 | Result::<_, LabeledError>::Ok(spanned!(RType::try_from(val)?.0, span)) 188 | }) 189 | .collect::, _>>()? 190 | .into_iter() 191 | .collect(), 192 | span 193 | ) 194 | } 195 | Some(ref val) => spanned!( 196 | vec![spanned!(RType::try_from(val)?.0, val.span())], 197 | val.span() 198 | ), 199 | None => spanned!( 200 | vec![ 201 | spanned!(RecordType::AAAA, Span::unknown()), 202 | spanned!(RecordType::A, Span::unknown()), 203 | ], 204 | Span::unknown() 205 | ), 206 | }; 207 | 208 | let class = match get_value(constants::flags::CLASS) { 209 | Some(val) => { 210 | let span = val.span(); 211 | spanned!(crate::dns::serde::DNSClass::try_from(val)?.0, span) 212 | } 213 | None => spanned!(hickory_proto::rr::DNSClass::IN, Span::unknown()), 214 | }; 215 | 216 | let code = match get_value(constants::flags::CODE) { 217 | Some(val @ Value::Bool { .. }) => { 218 | spanned!(val.as_bool().unwrap(), val.span()) 219 | } 220 | _ => spanned!(false, Span::unknown()), 221 | }; 222 | 223 | let dnssec_mode = match get_value(constants::flags::DNSSEC) { 224 | Some(val) => { 225 | let span = val.span(); 226 | spanned!(serde::DnssecMode::try_from(val)?, span) 227 | } 228 | None => spanned!(serde::DnssecMode::Opportunistic, Span::unknown()), 229 | }; 230 | 231 | let tasks = match get_value(constants::flags::TASKS) { 232 | Some(val @ Value::Int { .. }) => { 233 | let span = val.span(); 234 | spanned!( 235 | val.as_int()? 236 | .try_into() 237 | .map_err(|err| LabeledError::new("invalid input") 238 | .with_label(format!("should be positive int: {err}"), val.span()))?, 239 | span 240 | ) 241 | } 242 | None => spanned!(constants::config::default::TASKS, Span::unknown()), 243 | 244 | Some(val) => { 245 | return Err(LabeledError::new("should be int") 246 | .with_label("number of tasks should be an int", val.span())) 247 | } 248 | }; 249 | 250 | let timeout = match get_value(constants::flags::TIMEOUT) { 251 | Some(val @ Value::Duration { .. }) => { 252 | let span = val.span(); 253 | spanned!( 254 | Duration::from_nanos(val.as_duration()?.try_into().map_err(|err| { 255 | LabeledError::new("invalid duration") 256 | .with_label(format!("should be positive duration: {err}"), val.span()) 257 | })?), 258 | span 259 | ) 260 | } 261 | None => spanned!(constants::config::default::TIMEOUT, Span::unknown()), 262 | 263 | Some(val) => { 264 | return Err(LabeledError::new("should be duration") 265 | .with_label("timeout should be a positive duration", val.span())) 266 | } 267 | }; 268 | 269 | Ok(Self { 270 | protocol, 271 | server: addr, 272 | qtypes, 273 | code, 274 | class, 275 | dnssec_mode, 276 | dns_name, 277 | tasks, 278 | timeout, 279 | }) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/dns/constants.rs: -------------------------------------------------------------------------------- 1 | pub mod commands { 2 | pub const QUERY: &str = "dns query"; 3 | } 4 | 5 | pub mod flags { 6 | pub const DNS_NAME: &str = "dns-name"; 7 | pub const NAME: &str = "name"; 8 | pub const SERVER: &str = "server"; 9 | pub const PROTOCOL: &str = "protocol"; 10 | pub const TYPE: &str = "type"; 11 | pub const CLASS: &str = "class"; 12 | pub const DNSSEC: &str = "dnssec"; 13 | pub const CODE: &str = "code"; 14 | pub const TASKS: &str = "tasks"; 15 | pub const TIMEOUT: &str = "timeout"; 16 | } 17 | 18 | pub mod config { 19 | use hickory_proto::xfer::Protocol; 20 | 21 | pub mod default { 22 | use std::time::Duration; 23 | 24 | pub const TASKS: usize = 8; 25 | pub const TIMEOUT: Duration = Duration::from_secs(30); 26 | } 27 | 28 | pub fn default_port(protocol: Protocol) -> u16 { 29 | match protocol { 30 | Protocol::Udp | Protocol::Tcp => 53, 31 | Protocol::Tls | Protocol::Quic => 853, 32 | Protocol::Https => 443, 33 | _ => 53, 34 | } 35 | } 36 | } 37 | 38 | pub mod columns { 39 | pub mod message { 40 | pub const HEADER: &str = "header"; 41 | pub const QUESTION: &str = "question"; 42 | pub const ANSWER: &str = "answer"; 43 | pub const AUTHORITY: &str = "authority"; 44 | pub const ADDITIONAL: &str = "additional"; 45 | pub const EDNS: &str = "edns"; 46 | pub const SIZE: &str = "size"; 47 | 48 | pub const COLS: &[&str] = &[HEADER, QUESTION, ANSWER, AUTHORITY, ADDITIONAL, EDNS, SIZE]; 49 | 50 | pub mod header { 51 | pub const ID: &str = "id"; 52 | pub const MESSAGE_TYPE: &str = "message_type"; 53 | pub const OP_CODE: &str = "op_code"; 54 | pub const AUTHORITATIVE: &str = "authoritative"; 55 | pub const TRUNCATED: &str = "truncated"; 56 | pub const RECURSION_DESIRED: &str = "recursion_desired"; 57 | pub const RECURSION_AVAILABLE: &str = "recursion_available"; 58 | pub const AUTHENTIC_DATA: &str = "authentic_data"; 59 | pub const RESPONSE_CODE: &str = "response_code"; 60 | pub const QUERY_COUNT: &str = "query_count"; 61 | pub const ANSWER_COUNT: &str = "answer_count"; 62 | pub const NAME_SERVER_COUNT: &str = "name_server_count"; 63 | pub const ADDITIONAL_COUNT: &str = "additional_count"; 64 | 65 | pub const COLS: &[&str] = &[ 66 | ID, 67 | MESSAGE_TYPE, 68 | OP_CODE, 69 | AUTHORITATIVE, 70 | TRUNCATED, 71 | RECURSION_DESIRED, 72 | RECURSION_AVAILABLE, 73 | AUTHENTIC_DATA, 74 | RESPONSE_CODE, 75 | QUERY_COUNT, 76 | ANSWER_COUNT, 77 | NAME_SERVER_COUNT, 78 | ADDITIONAL_COUNT, 79 | ]; 80 | } 81 | 82 | pub mod query { 83 | pub const COLS: &[&str] = &[ 84 | super::super::rr::NAME, 85 | super::super::rr::TYPE, 86 | super::super::rr::CLASS, 87 | ]; 88 | } 89 | } 90 | 91 | pub mod rr { 92 | pub const NAME: &str = "name"; 93 | pub const TYPE: &str = "type"; 94 | pub const CLASS: &str = "class"; 95 | pub const TTL: &str = "ttl"; 96 | pub const RDATA: &str = "rdata"; 97 | pub const PROOF: &str = "proof"; 98 | 99 | pub const COLS: &[&str] = &[NAME, TYPE, CLASS, TTL, RDATA, PROOF]; 100 | 101 | pub mod code { 102 | pub const CODE: &str = "code"; 103 | 104 | pub const COLS: &[&str] = &[super::NAME, CODE]; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures_util::Future; 4 | use hickory_proto::runtime::TokioRuntimeProvider; 5 | use nu_plugin::{Plugin, PluginCommand}; 6 | use nu_protocol::LabeledError; 7 | use tokio::task::JoinHandle; 8 | use tokio_util::{sync::CancellationToken, task::TaskTracker}; 9 | 10 | use self::{client::DnsClient, commands::query::DnsQueryPluginClient}; 11 | pub use config::Config; 12 | 13 | pub mod client; 14 | pub mod commands; 15 | pub mod config; 16 | pub mod constants; 17 | pub mod serde; 18 | #[macro_use] 19 | pub mod util; 20 | 21 | pub struct Dns { 22 | main_runtime: tokio::runtime::Runtime, 23 | runtime_provider: TokioRuntimeProvider, 24 | tasks: TaskTracker, 25 | cancel: CancellationToken, 26 | client: DnsQueryPluginClient, 27 | } 28 | 29 | impl Plugin for Dns { 30 | fn commands(&self) -> Vec>> { 31 | vec![Box::new(commands::query::DnsQuery)] 32 | } 33 | 34 | fn version(&self) -> String { 35 | env!("CARGO_PKG_VERSION").into() 36 | } 37 | } 38 | 39 | impl Dns { 40 | pub const PLUGIN_NAME: &str = "dns"; 41 | 42 | pub fn new() -> Self { 43 | Self { 44 | main_runtime: tokio::runtime::Runtime::new().unwrap(), 45 | runtime_provider: TokioRuntimeProvider::new(), 46 | tasks: TaskTracker::new(), 47 | cancel: CancellationToken::new(), 48 | client: Arc::new(tokio::sync::RwLock::new(None)), 49 | } 50 | } 51 | 52 | pub async fn dns_client(&self, config: &Config) -> Result { 53 | // Since the plug-in binary is left running in the background by the 54 | // nushell engine between invocations, we leave a handle to it attached 55 | // to the plug-in object instance so we can reuse it across invocations. 56 | // If there is one already, use it. 57 | // 58 | // We could use OnceLock once get_or_try_init is stable 59 | if let Some((client, _)) = &*self.client.read().await { 60 | return Ok(client.clone()); 61 | } 62 | 63 | let mut client_guard = self.client.write().await; 64 | 65 | // it is cheap to clone and hand back an owned client because underneath 66 | // it is just a mpsc::Sender 67 | match &mut *client_guard { 68 | Some((client, _)) => Ok(client.clone()), 69 | None => { 70 | let (client, client_bg) = self.make_dns_client(config).await?; 71 | *client_guard = Some((client.clone(), client_bg)); 72 | Ok(client) 73 | } 74 | } 75 | } 76 | 77 | async fn make_dns_client( 78 | &self, 79 | config: &Config, 80 | ) -> Result<(DnsClient, JoinHandle>), LabeledError> { 81 | let (client, bg) = DnsClient::new(config, self.runtime_provider.clone()).await?; 82 | tracing::info!(client.addr = ?config.server, client.protocol = ?config.protocol); 83 | Ok((client, bg)) 84 | } 85 | 86 | pub fn spawn(&self, future: F) 87 | where 88 | F: Future> + Send + 'static, 89 | { 90 | self.tasks.spawn(future); 91 | } 92 | 93 | pub async fn spawn_blocking(&self, future: F) 94 | where 95 | F: FnOnce() -> Result<(), LabeledError> + Send + 'static, 96 | { 97 | self.tasks.spawn_blocking(future); 98 | } 99 | 100 | pub async fn close(&self) { 101 | self.tasks.close(); 102 | self.tasks.wait().await; 103 | } 104 | } 105 | 106 | impl Default for Dns { 107 | fn default() -> Self { 108 | Self::new() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/dns/serde.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use hickory_proto::{ 6 | dnssec::{ 7 | self, 8 | rdata::{ 9 | key::{KeyTrust, KeyUsage}, 10 | DNSSECRData, 11 | }, 12 | PublicKey, 13 | }, 14 | rr::{ 15 | domain, 16 | rdata::{ 17 | opt::{EdnsCode, EdnsOption}, 18 | sshfp, 19 | svcb::{EchConfigList, IpHint, SvcParamValue, Unknown}, 20 | tlsa, 21 | }, 22 | RecordType, 23 | }, 24 | ProtoError, 25 | }; 26 | use nu_protocol::{record, FromValue, LabeledError, Span, Value}; 27 | 28 | use super::config::Config; 29 | use super::constants; 30 | 31 | fn code_to_record_u16(bytes: C, code: bool) -> Value 32 | where 33 | C: Display + Into, 34 | { 35 | let code_string = Value::string(bytes.to_string(), Span::unknown()); 36 | 37 | if code { 38 | Value::record( 39 | nu_protocol::Record::from_iter(std::iter::zip( 40 | Vec::from_iter( 41 | constants::columns::rr::code::COLS 42 | .iter() 43 | .map(|s| String::from(*s)), 44 | ), 45 | vec![ 46 | code_string, 47 | Value::int(Into::::into(bytes) as i64, Span::unknown()), 48 | ], 49 | )), 50 | Span::unknown(), 51 | ) 52 | } else { 53 | code_string 54 | } 55 | } 56 | 57 | fn code_to_record_u8(bytes: C, code: bool) -> Value 58 | where 59 | C: Display + Into, 60 | { 61 | let code_string = Value::string(bytes.to_string(), Span::unknown()); 62 | 63 | if code { 64 | Value::record( 65 | nu_protocol::Record::from_iter(std::iter::zip( 66 | Vec::from_iter( 67 | constants::columns::rr::code::COLS 68 | .iter() 69 | .map(|s| String::from(*s)), 70 | ), 71 | vec![ 72 | code_string, 73 | Value::int(Into::::into(bytes) as i64, Span::unknown()), 74 | ], 75 | )), 76 | Span::unknown(), 77 | ) 78 | } else { 79 | code_string 80 | } 81 | } 82 | 83 | #[derive(Debug)] 84 | pub struct Response(hickory_proto::xfer::DnsResponse); 85 | 86 | impl Response { 87 | pub fn new(msg: hickory_proto::xfer::DnsResponse) -> Self { 88 | Self(msg) 89 | } 90 | 91 | pub fn into_inner(self) -> hickory_proto::xfer::DnsResponse { 92 | self.0 93 | } 94 | 95 | pub fn size(&self) -> usize { 96 | self.0.as_buffer().len() 97 | } 98 | 99 | pub fn into_value(self, code: bool) -> Result { 100 | let size = Value::filesize(self.size() as i64, Span::unknown()); 101 | let message = self.into_inner().into_message(); 102 | let header = Header(message.header()).into_value(code); 103 | let mut parts = message.into_parts(); 104 | 105 | let question = parts.queries.pop().map_or_else( 106 | || Value::record(record!(), Span::unknown()), 107 | |q| Query(q).into_value(code), 108 | ); 109 | 110 | let parse_records = 111 | |records: Vec| -> Result { 112 | Ok(Value::list( 113 | records 114 | .into_iter() 115 | .map(|record| Record(record).into_value(code)) 116 | .collect::>()?, 117 | Span::unknown(), 118 | )) 119 | }; 120 | 121 | let answer = parse_records(parts.answers)?; 122 | let authority = parse_records(parts.name_servers)?; 123 | let additional = parse_records(parts.additionals)?; 124 | let edns = parts 125 | .edns 126 | .map(|edns| Edns(edns).into_value()) 127 | .unwrap_or(Value::nothing(Span::unknown())); 128 | 129 | Ok(Value::record( 130 | nu_protocol::Record::from_iter(std::iter::zip( 131 | Vec::from_iter( 132 | constants::columns::message::COLS 133 | .iter() 134 | .map(|s| (*s).into()), 135 | ), 136 | vec![header, question, answer, authority, additional, edns, size], 137 | )), 138 | Span::unknown(), 139 | )) 140 | } 141 | } 142 | 143 | pub struct Header<'r>(pub &'r hickory_proto::op::Header); 144 | 145 | impl Header<'_> { 146 | pub fn into_value(self, code: bool) -> Value { 147 | let Header(header) = self; 148 | 149 | let id = Value::int(header.id().into(), Span::unknown()); 150 | 151 | let message_type_string = Value::string(header.message_type().to_string(), Span::unknown()); 152 | let message_type = if code { 153 | Value::record( 154 | nu_protocol::Record::from_iter(std::iter::zip( 155 | Vec::from_iter( 156 | constants::columns::rr::code::COLS 157 | .iter() 158 | .map(|s| String::from(*s)), 159 | ), 160 | vec![ 161 | message_type_string, 162 | Value::int(header.message_type() as i64, Span::unknown()), 163 | ], 164 | )), 165 | Span::unknown(), 166 | ) 167 | } else { 168 | message_type_string 169 | }; 170 | 171 | let op_code = code_to_record_u8(header.op_code(), code); 172 | let authoritative = Value::bool(header.authoritative(), Span::unknown()); 173 | let truncated = Value::bool(header.truncated(), Span::unknown()); 174 | let recursion_desired = Value::bool(header.recursion_desired(), Span::unknown()); 175 | let recursion_available = Value::bool(header.recursion_available(), Span::unknown()); 176 | let authentic_data = Value::bool(header.authentic_data(), Span::unknown()); 177 | let response_code = code_to_record_u16(header.response_code(), code); 178 | let query_count = Value::int(header.query_count().into(), Span::unknown()); 179 | let answer_count = Value::int(header.answer_count().into(), Span::unknown()); 180 | let name_server_count = Value::int(header.name_server_count().into(), Span::unknown()); 181 | let additional_count = Value::int(header.additional_count().into(), Span::unknown()); 182 | 183 | Value::record( 184 | nu_protocol::Record::from_iter(std::iter::zip( 185 | Vec::from_iter( 186 | constants::columns::message::header::COLS 187 | .iter() 188 | .map(|s| (*s).into()), 189 | ), 190 | vec![ 191 | id, 192 | message_type, 193 | op_code, 194 | authoritative, 195 | truncated, 196 | recursion_desired, 197 | recursion_available, 198 | authentic_data, 199 | response_code, 200 | query_count, 201 | answer_count, 202 | name_server_count, 203 | additional_count, 204 | ], 205 | )), 206 | Span::unknown(), 207 | ) 208 | } 209 | } 210 | 211 | #[derive(Debug)] 212 | pub struct Query(pub hickory_proto::op::query::Query); 213 | 214 | impl Query { 215 | pub fn into_value(self, code: bool) -> Value { 216 | let Query(query) = self; 217 | 218 | let name = Value::string(query.name().to_utf8(), Span::unknown()); 219 | let qtype = code_to_record_u16(query.query_type(), code); 220 | let class = code_to_record_u16(query.query_class(), code); 221 | 222 | Value::record( 223 | nu_protocol::Record::from_iter(std::iter::zip( 224 | Vec::from_iter( 225 | constants::columns::message::query::COLS 226 | .iter() 227 | .map(|s| (*s).into()), 228 | ), 229 | vec![name, qtype, class], 230 | )), 231 | Span::unknown(), 232 | ) 233 | } 234 | } 235 | 236 | impl Query { 237 | pub fn try_from_value(value: &Value, config: &Config) -> Result, LabeledError> { 238 | tracing::debug!(?value); 239 | 240 | match value { 241 | // If a record is given, it must have at least a name and qtype and 242 | // will be used as is, overriding any command line arguments. 243 | rec @ Value::Record { .. } => { 244 | let span = rec.span(); 245 | 246 | let must_have_col_err = |col| { 247 | LabeledError::new("invalid input") 248 | .with_label(format!("Record must have a column named '{}'", col), span) 249 | }; 250 | 251 | let name = domain::Name::from_utf8( 252 | String::from_value( 253 | rec.get_data_by_key(constants::columns::rr::NAME) 254 | .ok_or_else(|| must_have_col_err(constants::columns::rr::NAME))?, 255 | ) 256 | .map_err(|err| { 257 | LabeledError::new("invalid value") 258 | .with_label(format!("Could not convert value to String: {}", err), span) 259 | })?, 260 | ) 261 | .map_err(|err| { 262 | LabeledError::new("invalid name") 263 | .with_label(format!("Could not convert string to name: {}", err), span) 264 | })?; 265 | 266 | let qtype = RType::try_from( 267 | &rec.get_data_by_key(constants::columns::rr::TYPE) 268 | .ok_or_else(|| must_have_col_err(constants::columns::rr::TYPE))?, 269 | )?; 270 | 271 | let class = rec 272 | .get_data_by_key(constants::columns::rr::CLASS) 273 | .map(DNSClass::try_from) 274 | .unwrap_or(Ok(DNSClass(hickory_proto::rr::DNSClass::IN)))? 275 | .0; 276 | 277 | let mut query = hickory_proto::op::Query::query(name, qtype.0); 278 | query.set_query_class(class); 279 | 280 | Ok(vec![Query(query)]) 281 | } 282 | 283 | // If any other input type is given, the CLI flags fill in the type 284 | // and class. 285 | str_val @ Value::String { val, .. } => { 286 | let span = str_val.span(); 287 | 288 | let name = domain::Name::from_utf8(val).map_err(|err| { 289 | LabeledError::new("invalid name") 290 | .with_label(format!("Error parsing name: {}", err), span) 291 | })?; 292 | 293 | tracing::debug!(?name); 294 | 295 | let queries = config 296 | .qtypes 297 | .item 298 | .iter() 299 | .map(|qtype| { 300 | let mut query = hickory_proto::op::Query::query(name.clone(), qtype.item); 301 | query.set_query_class(config.class.item); 302 | Query(query) 303 | }) 304 | .collect(); 305 | 306 | Ok(queries) 307 | } 308 | list @ Value::List { vals, .. } => { 309 | if !vals.iter().all(|val| { 310 | matches!( 311 | val, 312 | Value::Binary { .. } 313 | | Value::Int { .. } 314 | | Value::Bool { .. } 315 | | Value::Nothing { .. } 316 | ) 317 | }) { 318 | return Ok(vals 319 | .iter() 320 | .map(|val| Query::try_from_value(val, config)) 321 | .collect::, _>>()? 322 | .into_iter() 323 | .flatten() 324 | .collect()); 325 | } 326 | 327 | let span = list.span(); 328 | 329 | let name = domain::Name::from_labels( 330 | vals.iter() 331 | .map(|val| match val { 332 | Value::Binary { val: bin_val, .. } => Ok(bin_val.clone()), 333 | Value::Int { val, .. } => { 334 | let bytes = val.to_ne_bytes(); 335 | let non0 = bytes 336 | .iter() 337 | .position(|n| *n != 0) 338 | .unwrap_or(bytes.len() - 1); 339 | 340 | Ok(Vec::from(&bytes[non0..])) 341 | } 342 | Value::Bool { val, .. } => Ok(vec![*val as u8]), 343 | Value::Nothing { .. } => Ok(vec![0]), 344 | 345 | _ => Err(LabeledError::new("invalid name") 346 | .with_label("Invalid input type for name", val.span())), 347 | }) 348 | .collect::, _>>()?, 349 | ) 350 | .map_err(|err| { 351 | LabeledError::new("invalid name") 352 | .with_label(format!("Error parsing into name: {}", err), span) 353 | })?; 354 | 355 | let queries = config 356 | .qtypes 357 | .item 358 | .iter() 359 | .map(|qtype| { 360 | let mut query = hickory_proto::op::Query::query(name.clone(), qtype.item); 361 | query.set_query_class(config.class.item); 362 | Query(query) 363 | }) 364 | .collect(); 365 | 366 | Ok(queries) 367 | } 368 | val => Err(LabeledError::new("invalid input type").with_label( 369 | format!("could not convert input to a DNS record name: {:?}", val), 370 | val.span(), 371 | )), 372 | } 373 | } 374 | } 375 | 376 | pub struct Record(pub hickory_proto::rr::resource::Record); 377 | 378 | impl Record { 379 | pub fn into_value(self, code: bool) -> Result { 380 | let Record(record) = self; 381 | let parts = record.into_parts(); 382 | 383 | let name = Value::string(parts.name_labels.to_utf8(), Span::unknown()); 384 | let rtype = code_to_record_u16(parts.rdata.record_type(), code); 385 | let class = code_to_record_u16(parts.dns_class, code); 386 | let ttl = util::sec_to_duration(parts.ttl); 387 | let rdata = RData(parts.rdata).into_value()?; 388 | let proof = Value::string(parts.proof.to_string().to_lowercase(), Span::unknown()); 389 | 390 | Ok(Value::record( 391 | nu_protocol::Record::from_iter(std::iter::zip( 392 | Vec::from_iter(constants::columns::rr::COLS.iter().map(|s| (*s).into())), 393 | vec![name, rtype, class, ttl, rdata, proof], 394 | )), 395 | Span::unknown(), 396 | )) 397 | } 398 | } 399 | 400 | pub struct RData(pub hickory_proto::rr::RData); 401 | 402 | impl RData { 403 | pub fn into_value(self) -> Result { 404 | let val = match self.0 { 405 | hickory_proto::rr::RData::CAA(caa) => { 406 | let issuer_ctitical = Value::bool(caa.issuer_critical(), Span::unknown()); 407 | let tag = Value::string(caa.tag().as_str(), Span::unknown()); 408 | let value = match caa.value() { 409 | hickory_proto::rr::rdata::caa::Value::Issuer(issuer_name, key_values) => { 410 | let issuer_name = issuer_name 411 | .as_ref() 412 | .map(|name| Value::string(name.to_string(), Span::unknown())) 413 | .unwrap_or(Value::nothing(Span::unknown())); 414 | 415 | let parameters: HashMap = key_values 416 | .iter() 417 | .map(|key_val| { 418 | ( 419 | key_val.key().into(), 420 | Value::string(key_val.value(), Span::unknown()), 421 | ) 422 | }) 423 | .collect(); 424 | 425 | Value::record( 426 | nu_protocol::Record::from_iter(std::iter::zip( 427 | vec!["issuer_name".into(), "parameters".into()], 428 | vec![ 429 | issuer_name, 430 | Value::record( 431 | nu_protocol::Record::from_iter(parameters), 432 | Span::unknown(), 433 | ), 434 | ], 435 | )), 436 | Span::unknown(), 437 | ) 438 | } 439 | hickory_proto::rr::rdata::caa::Value::Url(url) => { 440 | Value::string(url.to_string(), Span::unknown()) 441 | } 442 | hickory_proto::rr::rdata::caa::Value::Unknown(data) => { 443 | Value::binary(data.clone(), Span::unknown()) 444 | } 445 | }; 446 | 447 | Value::record( 448 | record![ 449 | "issuer_critical" => issuer_ctitical, 450 | "tag" => tag, 451 | "value" => value, 452 | ], 453 | Span::unknown(), 454 | ) 455 | } 456 | // CSYNC seems to be missing some accessors in the trust-dns lib, 457 | // which oddly enough actually are serialized in the `Display` impl, 458 | // so just use that 459 | // hickory_proto::rr::RData::CSYNC(_) => todo!(), 460 | hickory_proto::rr::RData::HINFO(hinfo) => { 461 | let cpu = util::string_or_binary(hinfo.cpu()); 462 | let os = util::string_or_binary(hinfo.os()); 463 | 464 | Value::record( 465 | record!( 466 | "cpu" => cpu, 467 | "os" => os, 468 | ), 469 | Span::unknown(), 470 | ) 471 | } 472 | 473 | hickory_proto::rr::RData::HTTPS(hickory_proto::rr::rdata::HTTPS(svcb)) 474 | | hickory_proto::rr::RData::SVCB(svcb) => { 475 | let svc_priority = Value::int(svcb.svc_priority() as i64, Span::unknown()); 476 | let target_name = Value::string(svcb.target_name().to_string(), Span::unknown()); 477 | let svc_params = svcb.svc_params().iter().map(|(key, value)| { 478 | let value = match value { 479 | SvcParamValue::Mandatory(param_keys) => Value::list( 480 | param_keys 481 | .0 482 | .iter() 483 | .map(|key| Value::string(key.to_string(), Span::unknown())) 484 | .collect(), 485 | Span::unknown(), 486 | ), 487 | SvcParamValue::Alpn(alpn) => Value::list( 488 | alpn.0 489 | .iter() 490 | .map(|alpn| Value::string(alpn, Span::unknown())) 491 | .collect(), 492 | Span::unknown(), 493 | ), 494 | nda @ SvcParamValue::NoDefaultAlpn => { 495 | Value::string(nda.to_string(), Span::unknown()) 496 | } 497 | SvcParamValue::Port(port) => Value::int(*port as i64, Span::unknown()), 498 | SvcParamValue::Ipv4Hint(IpHint(ipv4s)) => Value::list( 499 | ipv4s 500 | .iter() 501 | .map(|ip| Value::string(ip.to_string(), Span::unknown())) 502 | .collect(), 503 | Span::unknown(), 504 | ), 505 | SvcParamValue::EchConfigList(EchConfigList(config)) => { 506 | Value::binary(config.clone(), Span::unknown()) 507 | } 508 | SvcParamValue::Ipv6Hint(IpHint(ipv6s)) => Value::list( 509 | ipv6s 510 | .iter() 511 | .map(|ip| Value::string(ip.to_string(), Span::unknown())) 512 | .collect(), 513 | Span::unknown(), 514 | ), 515 | SvcParamValue::Unknown(Unknown(bytes)) => { 516 | Value::binary(bytes.clone(), Span::unknown()) 517 | } 518 | }; 519 | 520 | (key.to_string(), value) 521 | }); 522 | 523 | let svc_params = 524 | Value::record(nu_protocol::Record::from_iter(svc_params), Span::unknown()); 525 | 526 | Value::record( 527 | record!( 528 | "svc_priority" => svc_priority, 529 | "target_name" => target_name, 530 | "svc_params" => svc_params, 531 | ), 532 | Span::unknown(), 533 | ) 534 | } 535 | 536 | hickory_proto::rr::RData::MX(mx) => { 537 | let preference = Value::int(mx.preference() as i64, Span::unknown()); 538 | let exchange = Value::string(mx.exchange().to_string(), Span::unknown()); 539 | 540 | Value::record( 541 | record![ 542 | "preference" => preference, 543 | "exchange" => exchange 544 | ], 545 | Span::unknown(), 546 | ) 547 | } 548 | 549 | hickory_proto::rr::RData::NAPTR(naptr) => { 550 | let order = Value::int(naptr.order() as i64, Span::unknown()); 551 | let preference = Value::int(naptr.preference() as i64, Span::unknown()); 552 | let flags = util::string_or_binary(naptr.flags()); 553 | let services = util::string_or_binary(naptr.services()); 554 | let regexp = util::string_or_binary(naptr.regexp()); 555 | let replacement = Value::string(naptr.replacement().to_string(), Span::unknown()); 556 | 557 | Value::record( 558 | record![ 559 | "order" => order, 560 | "preference" => preference, 561 | "flags" => flags, 562 | "services" => services, 563 | "regexp" => regexp, 564 | "replacement" => replacement, 565 | ], 566 | Span::unknown(), 567 | ) 568 | } 569 | 570 | hickory_proto::rr::RData::NULL(null) => util::string_or_binary(null.anything()), 571 | hickory_proto::rr::RData::NS(ns) => Value::string(ns.to_string(), Span::unknown()), 572 | hickory_proto::rr::RData::OPENPGPKEY(key) => { 573 | Value::binary(key.public_key(), Span::unknown()) 574 | } 575 | hickory_proto::rr::RData::OPT(opt) => Opt(&opt).into_value(), 576 | hickory_proto::rr::RData::PTR(name) => Value::string(name.to_string(), Span::unknown()), 577 | 578 | hickory_proto::rr::RData::SOA(soa) => { 579 | let mname = Value::string(soa.mname().to_string(), Span::unknown()); 580 | let rname = Value::string(soa.rname().to_string(), Span::unknown()); 581 | let serial = Value::int(soa.serial() as i64, Span::unknown()); 582 | let refresh = util::sec_to_duration(soa.refresh() as u64); 583 | let retry = util::sec_to_duration(soa.retry() as u64); 584 | let expire = util::sec_to_duration(soa.expire() as u64); 585 | let minimum = util::sec_to_duration(soa.minimum() as u64); 586 | 587 | Value::record( 588 | record![ 589 | "mname" => mname, 590 | "rname" => rname, 591 | "serial" => serial, 592 | "refresh" => refresh, 593 | "retry" => retry, 594 | "expire" => expire, 595 | "minimum" => minimum, 596 | ], 597 | Span::unknown(), 598 | ) 599 | } 600 | 601 | hickory_proto::rr::RData::SRV(srv) => { 602 | let priority = Value::int(srv.priority() as i64, Span::unknown()); 603 | let weight = Value::int(srv.weight() as i64, Span::unknown()); 604 | let port = Value::int(srv.port() as i64, Span::unknown()); 605 | let target = Value::string(srv.target().to_string(), Span::unknown()); 606 | 607 | Value::record( 608 | record![ 609 | "priority" => priority, 610 | "weight" => weight, 611 | "port" => port, 612 | "target" => target 613 | ], 614 | Span::unknown(), 615 | ) 616 | } 617 | 618 | hickory_proto::rr::RData::SSHFP(sshfp) => { 619 | let algorithm = match sshfp.algorithm() { 620 | sshfp::Algorithm::Reserved => Value::string("reserved", Span::unknown()), 621 | sshfp::Algorithm::RSA => Value::string("RSA", Span::unknown()), 622 | sshfp::Algorithm::DSA => Value::string("DSA", Span::unknown()), 623 | sshfp::Algorithm::ECDSA => Value::string("ECDSA", Span::unknown()), 624 | sshfp::Algorithm::Ed25519 => Value::string("Ed25519", Span::unknown()), 625 | sshfp::Algorithm::Ed448 => Value::string("Ed448", Span::unknown()), 626 | sshfp::Algorithm::Unassigned(code) => Value::int(code as i64, Span::unknown()), 627 | }; 628 | 629 | let fingerprint_type = match sshfp.fingerprint_type() { 630 | sshfp::FingerprintType::Reserved => Value::string("reserved", Span::unknown()), 631 | sshfp::FingerprintType::SHA1 => Value::string("SHA-1", Span::unknown()), 632 | sshfp::FingerprintType::SHA256 => Value::string("SHA-256", Span::unknown()), 633 | sshfp::FingerprintType::Unassigned(code) => { 634 | Value::int(code as i64, Span::unknown()) 635 | } 636 | }; 637 | 638 | let fingerprint = Value::binary(sshfp.fingerprint(), Span::unknown()); 639 | 640 | Value::record( 641 | record![ 642 | "algorithm" => algorithm, 643 | "fingerprint_type" => fingerprint_type, 644 | "fingerprint" => fingerprint, 645 | ], 646 | Span::unknown(), 647 | ) 648 | } 649 | hickory_proto::rr::RData::TLSA(tlsa) => { 650 | let cert_usage = match tlsa.cert_usage() { 651 | tlsa::CertUsage::PkixTa => Value::string("PKIX-TA", Span::unknown()), 652 | tlsa::CertUsage::PkixEe => Value::string("PKIX-EE", Span::unknown()), 653 | tlsa::CertUsage::DaneTa => Value::string("DANE-TA", Span::unknown()), 654 | tlsa::CertUsage::DaneEe => Value::string("DANE-EE", Span::unknown()), 655 | tlsa::CertUsage::Private => Value::string("private", Span::unknown()), 656 | tlsa::CertUsage::Unassigned(code) => Value::int(code as i64, Span::unknown()), 657 | }; 658 | 659 | let selector = match tlsa.selector() { 660 | tlsa::Selector::Full => Value::string("full", Span::unknown()), 661 | tlsa::Selector::Spki => Value::string("spki", Span::unknown()), 662 | tlsa::Selector::Private => Value::string("private", Span::unknown()), 663 | tlsa::Selector::Unassigned(code) => Value::int(code as i64, Span::unknown()), 664 | }; 665 | 666 | let matching = match tlsa.matching() { 667 | tlsa::Matching::Raw => Value::string("raw", Span::unknown()), 668 | tlsa::Matching::Sha256 => Value::string("SHA-256", Span::unknown()), 669 | tlsa::Matching::Sha512 => Value::string("SHA-512", Span::unknown()), 670 | tlsa::Matching::Private => Value::string("private", Span::unknown()), 671 | tlsa::Matching::Unassigned(code) => Value::int(code as i64, Span::unknown()), 672 | }; 673 | 674 | let cert_data = Value::binary(tlsa.cert_data(), Span::unknown()); 675 | 676 | Value::record( 677 | record![ 678 | "cert_usage" => cert_usage, 679 | "selector" => selector, 680 | "matching" => matching, 681 | "cert_data" => cert_data, 682 | ], 683 | Span::unknown(), 684 | ) 685 | } 686 | hickory_proto::rr::RData::TXT(data) => Value::list( 687 | data.iter() 688 | .map(|txt_data| util::string_or_binary(Vec::from(txt_data.clone()))) 689 | .collect(), 690 | Span::unknown(), 691 | ), 692 | hickory_proto::rr::RData::DNSSEC(dnssec) => match dnssec { 693 | DNSSECRData::DNSKEY(dnskey) => { 694 | let dnskey = &dnskey; 695 | let zone_key = Value::bool(dnskey.zone_key(), Span::unknown()); 696 | let secure_entry_point = 697 | Value::bool(dnskey.secure_entry_point(), Span::unknown()); 698 | let revoke = Value::bool(dnskey.revoke(), Span::unknown()); 699 | 700 | let public_key = dnskey.public_key(); 701 | 702 | let algorithm = 703 | Value::string(public_key.algorithm().to_string(), Span::unknown()); 704 | let bytes = Value::binary(public_key.public_bytes(), Span::unknown()); 705 | 706 | Value::record( 707 | record![ 708 | "zone_key" => zone_key, 709 | "secure_entry_point" => secure_entry_point, 710 | "revoke" => revoke, 711 | "public_key" => Value::record( 712 | record![ 713 | "algorithm" => algorithm, 714 | "bytes" => bytes, 715 | ], 716 | Span::unknown() 717 | ), 718 | ], 719 | Span::unknown(), 720 | ) 721 | } 722 | DNSSECRData::CDNSKEY(cdnskey) => { 723 | let zone_key = Value::bool(cdnskey.zone_key(), Span::unknown()); 724 | let secure_entry_point = 725 | Value::bool(cdnskey.secure_entry_point(), Span::unknown()); 726 | let revoke = Value::bool(cdnskey.revoke(), Span::unknown()); 727 | 728 | let public_key = match cdnskey.public_key() { 729 | Some(public_key) => Value::record( 730 | record![ 731 | "algorithm" => Value::string(public_key.algorithm().to_string(), Span::unknown()), 732 | "bytes" => Value::binary(public_key.public_bytes(), Span::unknown()), 733 | ], 734 | Span::unknown(), 735 | ), 736 | None => Value::nothing(Span::unknown()), 737 | }; 738 | 739 | Value::record( 740 | record![ 741 | "zone_key" => zone_key, 742 | "secure_entry_point" => secure_entry_point, 743 | "revoke" => revoke, 744 | "public_key" => public_key, 745 | ], 746 | Span::unknown(), 747 | ) 748 | } 749 | DNSSECRData::DS(ds) => { 750 | let key_tag = Value::int(ds.key_tag() as i64, Span::unknown()); 751 | let algorithm = Value::string(ds.algorithm().to_string(), Span::unknown()); 752 | let digest_type = match ds.digest_type() { 753 | dnssec::DigestType::SHA1 => Value::string("SHA-1", Span::unknown()), 754 | dnssec::DigestType::SHA256 => Value::string("SHA-256", Span::unknown()), 755 | dnssec::DigestType::SHA384 => Value::string("SHA-384", Span::unknown()), 756 | dnssec::DigestType::Unknown(byte) => { 757 | Value::binary(vec![byte], Span::unknown()) 758 | } 759 | _ => Value::string("unknown", Span::unknown()), 760 | }; 761 | 762 | let digest = Value::binary(ds.digest(), Span::unknown()); 763 | Value::record( 764 | record![ 765 | "key_tag" => key_tag, 766 | "algorithm" => algorithm, 767 | "digest_type" => digest_type, 768 | "digest" => digest, 769 | ], 770 | Span::unknown(), 771 | ) 772 | } 773 | DNSSECRData::CDS(cds) => { 774 | let key_tag = Value::int(cds.key_tag() as i64, Span::unknown()); 775 | 776 | let algorithm = match cds.algorithm() { 777 | Some(alg) => Value::string(alg.to_string(), Span::unknown()), 778 | None => Value::nothing(Span::unknown()), 779 | }; 780 | 781 | let digest_type = Value::string( 782 | Into::::into(match cds.digest_type() { 783 | dnssec::DigestType::SHA1 => "SHA-1", 784 | dnssec::DigestType::SHA256 => "SHA-256", 785 | dnssec::DigestType::SHA384 => "SHA-384", 786 | _ => "unknown", 787 | }), 788 | Span::unknown(), 789 | ); 790 | 791 | let digest = Value::binary(cds.digest(), Span::unknown()); 792 | 793 | Value::record( 794 | record![ 795 | "key_tag" => key_tag, 796 | "algorithm" => algorithm, 797 | "digest_type" => digest_type, 798 | "digest" => digest, 799 | ], 800 | Span::unknown(), 801 | ) 802 | } 803 | DNSSECRData::KEY(key) => { 804 | let (key_authentication_prohibited, key_confidentiality_prohibited) = 805 | match key.key_trust() { 806 | KeyTrust::NotAuth => ( 807 | Value::bool(true, Span::unknown()), 808 | Value::bool(false, Span::unknown()), 809 | ), 810 | KeyTrust::NotPrivate => ( 811 | Value::bool(false, Span::unknown()), 812 | Value::bool(true, Span::unknown()), 813 | ), 814 | KeyTrust::AuthOrPrivate => ( 815 | Value::bool(false, Span::unknown()), 816 | Value::bool(false, Span::unknown()), 817 | ), 818 | KeyTrust::DoNotTrust => ( 819 | Value::bool(true, Span::unknown()), 820 | Value::bool(true, Span::unknown()), 821 | ), 822 | }; 823 | 824 | let key_type = Value::record( 825 | record![ 826 | "authentication_prohibited" => key_authentication_prohibited, 827 | "confidentiality_prohibited" => key_confidentiality_prohibited, 828 | ], 829 | Span::unknown(), 830 | ); 831 | 832 | let key_name_type = Value::string( 833 | Into::::into(match key.key_usage() { 834 | KeyUsage::Host => "host", 835 | #[allow(deprecated)] 836 | KeyUsage::Zone => "zone", 837 | KeyUsage::Entity => "entity", 838 | KeyUsage::Reserved => "reserved", 839 | }), 840 | Span::unknown(), 841 | ); 842 | 843 | let key_signatory = key.signatory(); 844 | 845 | #[allow(deprecated)] 846 | let signatory = Value::record( 847 | record![ 848 | "zone" => Value::bool(key_signatory.zone, Span::unknown()), 849 | "strong" => Value::bool(key_signatory.strong, Span::unknown()), 850 | "unique" => Value::bool(key_signatory.unique, Span::unknown()), 851 | "general" => Value::bool(key_signatory.general, Span::unknown()), 852 | ], 853 | Span::unknown(), 854 | ); 855 | 856 | #[allow(deprecated)] 857 | let protocol = match key.protocol() { 858 | dnssec::rdata::key::Protocol::Reserved => { 859 | Value::string("RESERVED", Span::unknown()) 860 | } 861 | dnssec::rdata::key::Protocol::TLS => Value::string("TLS", Span::unknown()), 862 | dnssec::rdata::key::Protocol::Email => { 863 | Value::string("EMAIL", Span::unknown()) 864 | } 865 | dnssec::rdata::key::Protocol::DNSSEC => { 866 | Value::string("DNSSEC", Span::unknown()) 867 | } 868 | dnssec::rdata::key::Protocol::IPSec => { 869 | Value::string("IPSEC", Span::unknown()) 870 | } 871 | dnssec::rdata::key::Protocol::Other(code) => { 872 | Value::int(code as i64, Span::unknown()) 873 | } 874 | dnssec::rdata::key::Protocol::All => Value::string("ALL", Span::unknown()), 875 | }; 876 | 877 | let algorithm = Value::string(key.algorithm().to_string(), Span::unknown()); 878 | let public_key = Value::binary(key.public_key(), Span::unknown()); 879 | 880 | Value::record( 881 | record![ 882 | "key_type" => key_type, 883 | "key_name_type" => key_name_type, 884 | "signatory" => signatory, 885 | "protocol" => protocol, 886 | "algorithm" => algorithm, 887 | "public_key" => public_key, 888 | ], 889 | Span::unknown(), 890 | ) 891 | } 892 | DNSSECRData::NSEC(nsec) => { 893 | let next_domain_name = 894 | Value::string(nsec.next_domain_name().to_string(), Span::unknown()); 895 | let types = Value::list( 896 | nsec.type_bit_maps() 897 | .map(|rtype| Value::string(rtype.to_string(), Span::unknown())) 898 | .collect(), 899 | Span::unknown(), 900 | ); 901 | 902 | Value::record( 903 | record![ 904 | "next_domain_name" => next_domain_name, 905 | "types" => types, 906 | ], 907 | Span::unknown(), 908 | ) 909 | } 910 | DNSSECRData::NSEC3(nsec3) => { 911 | let hash_algorithm = Value::string( 912 | Into::::into(match nsec3.hash_algorithm() { 913 | dnssec::Nsec3HashAlgorithm::SHA1 => "SHA-1", 914 | }), 915 | Span::unknown(), 916 | ); 917 | let opt_out = Value::bool(nsec3.opt_out(), Span::unknown()); 918 | let iterations = Value::int(nsec3.iterations() as i64, Span::unknown()); 919 | let salt = Value::binary(nsec3.salt(), Span::unknown()); 920 | let next_hashed_owner_name = 921 | Value::binary(nsec3.next_hashed_owner_name(), Span::unknown()); 922 | let types = Value::list( 923 | nsec3 924 | .type_bit_maps() 925 | .map(|rtype| Value::string(rtype.to_string(), Span::unknown())) 926 | .collect(), 927 | Span::unknown(), 928 | ); 929 | 930 | Value::record( 931 | record![ 932 | "hash_algorithm" => hash_algorithm, 933 | "opt_out" => opt_out, 934 | "iterations" => iterations, 935 | "salt" => salt, 936 | "next_hashed_owner_name" => next_hashed_owner_name, 937 | "types" => types, 938 | ], 939 | Span::unknown(), 940 | ) 941 | } 942 | DNSSECRData::NSEC3PARAM(nsec3param) => { 943 | let hash_algorithm = Value::string( 944 | Into::::into(match nsec3param.hash_algorithm() { 945 | dnssec::Nsec3HashAlgorithm::SHA1 => "SHA-1", 946 | }), 947 | Span::unknown(), 948 | ); 949 | let opt_out = Value::bool(nsec3param.opt_out(), Span::unknown()); 950 | let iterations = Value::int(nsec3param.iterations() as i64, Span::unknown()); 951 | let salt = Value::binary(nsec3param.salt(), Span::unknown()); 952 | let flags = Value::int(nsec3param.flags() as i64, Span::unknown()); 953 | 954 | Value::record( 955 | record![ 956 | "hash_algorithm" => hash_algorithm, 957 | "opt_out" => opt_out, 958 | "iterations" => iterations, 959 | "salt" => salt, 960 | "flags" => flags, 961 | ], 962 | Span::unknown(), 963 | ) 964 | } 965 | DNSSECRData::SIG(sig) => { 966 | let type_covered = 967 | Value::string(sig.type_covered().to_string(), Span::unknown()); 968 | let algorithm = Value::string(sig.algorithm().to_string(), Span::unknown()); 969 | let num_labels = Value::int(sig.num_labels() as i64, Span::unknown()); 970 | let original_ttl = util::sec_to_duration(sig.original_ttl()); 971 | let sig_expiration = 972 | util::sec_to_date(sig.sig_expiration().get(), Span::unknown())?; 973 | let sig_inception = 974 | util::sec_to_date(sig.sig_inception().get(), Span::unknown())?; 975 | let key_tag = Value::int(sig.key_tag() as i64, Span::unknown()); 976 | let signer_name = Value::string(sig.signer_name().to_string(), Span::unknown()); 977 | let sig = Value::binary(sig.sig(), Span::unknown()); 978 | 979 | Value::record( 980 | record![ 981 | "type_covered" => type_covered, 982 | "algorithm" => algorithm, 983 | "num_labels" => num_labels, 984 | "original_ttl" => original_ttl, 985 | "signature_expiration" => sig_expiration, 986 | "signature_inception" => sig_inception, 987 | "key_tag" => key_tag, 988 | "signer_name" => signer_name, 989 | "signature" => sig, 990 | ], 991 | Span::unknown(), 992 | ) 993 | } 994 | DNSSECRData::TSIG(tsig) => { 995 | // [NOTE] oid, error, and other do not have accessors 996 | let algorithm = Value::string(tsig.algorithm().to_string(), Span::unknown()); 997 | let time = util::sec_to_date(tsig.time() as i64, Span::unknown())?; 998 | let fudge = Value::int(tsig.fudge() as i64, Span::unknown()); 999 | let mac = Value::binary(tsig.mac(), Span::unknown()); 1000 | // let oid = Value::int(tsig.oid() as i64, Span::unknown()); 1001 | // let error = Value::int(tsig.error() as i64, Span::unknown()); 1002 | // let other = Value::binary(tsig.other(), Span::unknown()); 1003 | 1004 | Value::record( 1005 | record![ 1006 | "algorithm" => algorithm, 1007 | "time" => time, 1008 | "fudge" => fudge, 1009 | "mac" => mac, 1010 | // "oid" => oid, 1011 | // "error" => error, 1012 | // "other" => other, 1013 | ], 1014 | Span::unknown(), 1015 | ) 1016 | } 1017 | DNSSECRData::Unknown { code, rdata } => Value::record( 1018 | record![ 1019 | "code" => Value::int(code as i64, Span::unknown()), 1020 | "rdata" => Value::binary(rdata.anything(), Span::unknown()), 1021 | ], 1022 | Span::unknown(), 1023 | ), 1024 | rdata => Value::string(rdata.to_string(), Span::unknown()), 1025 | }, 1026 | hickory_proto::rr::RData::Unknown { code: rtype, rdata } => Value::record( 1027 | record![ 1028 | "code" => Value::int(u16::from(rtype) as i64, Span::unknown()), 1029 | "rdata" => Value::binary(rdata.anything(), Span::unknown()), 1030 | ], 1031 | Span::unknown(), 1032 | ), 1033 | rdata => Value::string(rdata.to_string(), Span::unknown()), 1034 | }; 1035 | 1036 | Ok(val) 1037 | } 1038 | } 1039 | 1040 | pub struct Edns(pub hickory_proto::op::Edns); 1041 | 1042 | impl Edns { 1043 | pub fn into_value(self) -> Value { 1044 | let edns = self.0; 1045 | let rcode_high = Value::int(edns.rcode_high() as i64, Span::unknown()); 1046 | let version = Value::int(edns.version() as i64, Span::unknown()); 1047 | 1048 | let flags = Value::record( 1049 | record![ 1050 | "dnssec_ok" => Value::bool(edns.flags().dnssec_ok, Span::unknown()), 1051 | ], 1052 | Span::unknown(), 1053 | ); 1054 | 1055 | let max_payload = Value::filesize(edns.max_payload() as i64, Span::unknown()); 1056 | let opts = Opt(edns.options()).into_value(); 1057 | 1058 | Value::record( 1059 | record![ 1060 | "rcode_high" => rcode_high, 1061 | "version" => version, 1062 | "flags" => flags, 1063 | "max_payload" => max_payload, 1064 | "opts" => opts, 1065 | ], 1066 | Span::unknown(), 1067 | ) 1068 | } 1069 | } 1070 | 1071 | pub struct Opt<'o>(pub &'o hickory_proto::rr::rdata::OPT); 1072 | 1073 | impl Opt<'_> { 1074 | pub fn into_value(self) -> Value { 1075 | let opts: HashMap<_, _> = self 1076 | .0 1077 | .as_ref() 1078 | .iter() 1079 | .map(|(code, option)| { 1080 | let code = match code { 1081 | EdnsCode::Zero => "zero".into(), 1082 | EdnsCode::LLQ => "LLQ".into(), 1083 | EdnsCode::UL => "UL".into(), 1084 | EdnsCode::NSID => "NSID".into(), 1085 | EdnsCode::DAU => "DAU".into(), 1086 | EdnsCode::DHU => "DHU".into(), 1087 | EdnsCode::N3U => "N3U".into(), 1088 | EdnsCode::Subnet => "subnet".into(), 1089 | EdnsCode::Expire => "EXPIRE".into(), 1090 | EdnsCode::Cookie => "cookie".into(), 1091 | EdnsCode::Keepalive => "keepalive".into(), 1092 | EdnsCode::Padding => "padding".into(), 1093 | EdnsCode::Chain => "chain".into(), 1094 | EdnsCode::Unknown(code) => format!("unknown({})", code), 1095 | ednscode => format!("unknown Edns: {:?}", ednscode), 1096 | }; 1097 | 1098 | let option = match option { 1099 | EdnsOption::DAU(supported) => Value::list( 1100 | supported 1101 | .iter() 1102 | .map(|alg| Value::string(alg.to_string(), Span::unknown())) 1103 | .collect(), 1104 | Span::unknown(), 1105 | ), 1106 | EdnsOption::Unknown(code, val) => Value::record( 1107 | record![ 1108 | "code" => Value::int(*code as i64, Span::unknown()), 1109 | "data" => util::string_or_binary(val.clone()), 1110 | ], 1111 | Span::unknown(), 1112 | ), 1113 | _ => todo!(), 1114 | }; 1115 | 1116 | (code, option) 1117 | }) 1118 | .collect(); 1119 | 1120 | Value::record(nu_protocol::Record::from_iter(opts), Span::unknown()) 1121 | } 1122 | } 1123 | 1124 | pub struct RType(pub hickory_proto::rr::RecordType); 1125 | 1126 | impl TryFrom<&Value> for RType { 1127 | type Error = LabeledError; 1128 | 1129 | fn try_from(value: &Value) -> Result { 1130 | let qtype_err = |err: ProtoError, span: Span| { 1131 | LabeledError::new("invalid record type") 1132 | .with_label(format!("Error parsing record type: {}", err), span) 1133 | }; 1134 | 1135 | match value { 1136 | Value::String { .. } => Ok(RType( 1137 | RecordType::from_str(&value.as_str().unwrap().to_uppercase()) 1138 | .map_err(|err| qtype_err(err, value.span()))?, 1139 | )), 1140 | Value::Int { val, .. } => { 1141 | let rtype = RecordType::from(*val as u16); 1142 | 1143 | if let RecordType::Unknown(r) = rtype { 1144 | return Err(LabeledError::new("invalid record type").with_label( 1145 | format!("Error parsing record type: unknown code: {}", r), 1146 | value.span(), 1147 | )); 1148 | } 1149 | 1150 | Ok(RType(rtype)) 1151 | } 1152 | value => Err(LabeledError::new("invalid record type").with_label( 1153 | "Invalid type for record type argument. Must be either string or int.", 1154 | value.span(), 1155 | )), 1156 | } 1157 | } 1158 | } 1159 | 1160 | pub struct DNSClass(pub hickory_proto::rr::DNSClass); 1161 | 1162 | impl TryFrom for DNSClass { 1163 | type Error = LabeledError; 1164 | 1165 | fn try_from(value: Value) -> Result { 1166 | let class_err = |err: ProtoError, span: Span| { 1167 | LabeledError::new("invalid DNS class") 1168 | .with_label(format!("Error parsing DNS class: {}", err), span) 1169 | }; 1170 | 1171 | let dns_class: DNSClass = match value { 1172 | Value::String { .. } => DNSClass( 1173 | hickory_proto::rr::DNSClass::from_str(&value.as_str().unwrap().to_uppercase()) 1174 | .map_err(|err| class_err(err, value.span()))?, 1175 | ), 1176 | Value::Int { val, .. } => DNSClass(hickory_proto::rr::DNSClass::from(val as u16)), 1177 | value => { 1178 | return Err(LabeledError::new("invalid DNS class").with_label( 1179 | "Invalid type for class type argument. Must be either string or int.", 1180 | value.span(), 1181 | )); 1182 | } 1183 | }; 1184 | 1185 | Ok(dns_class) 1186 | } 1187 | } 1188 | 1189 | pub struct Protocol(pub hickory_proto::xfer::Protocol); 1190 | 1191 | impl TryFrom for Protocol { 1192 | type Error = LabeledError; 1193 | 1194 | fn try_from(value: Value) -> Result { 1195 | let result = match value { 1196 | Value::String { .. } => match value.as_str().unwrap().to_uppercase().as_str() { 1197 | "UDP" => Protocol(hickory_proto::xfer::Protocol::Udp), 1198 | "TCP" => Protocol(hickory_proto::xfer::Protocol::Tcp), 1199 | "TLS" => Protocol(hickory_proto::xfer::Protocol::Tls), 1200 | "HTTPS" => Protocol(hickory_proto::xfer::Protocol::Https), 1201 | "QUIC" => Protocol(hickory_proto::xfer::Protocol::Quic), 1202 | proto => { 1203 | return Err(LabeledError::new("invalid protocol").with_label( 1204 | format!("Invalid or unsupported protocol: {proto}"), 1205 | value.span(), 1206 | )); 1207 | } 1208 | }, 1209 | _ => { 1210 | return Err(LabeledError::new("invalid input") 1211 | .with_label("Input must be a string", value.span())) 1212 | } 1213 | }; 1214 | 1215 | Ok(result) 1216 | } 1217 | } 1218 | 1219 | #[derive(Debug, Default, PartialEq)] 1220 | pub enum DnssecMode { 1221 | None, 1222 | 1223 | #[default] 1224 | Opportunistic, 1225 | } 1226 | 1227 | impl TryFrom for DnssecMode { 1228 | type Error = LabeledError; 1229 | 1230 | fn try_from(value: Value) -> Result { 1231 | match value { 1232 | Value::String { .. } => Ok(match value.as_str().unwrap().to_uppercase().as_str() { 1233 | "NONE" => DnssecMode::None, 1234 | "OPPORTUNISTIC" => DnssecMode::Opportunistic, 1235 | _ => { 1236 | return Err(LabeledError::new("invalid DNSSEC mode").with_label( 1237 | "Invalid DNSSEC mode. Must be one of: none, opportunistic", 1238 | value.span(), 1239 | )); 1240 | } 1241 | }), 1242 | _ => Err(LabeledError::new("invalid input") 1243 | .with_label("Input must be a string", value.span())), 1244 | } 1245 | } 1246 | } 1247 | 1248 | pub mod util { 1249 | use std::time::Duration; 1250 | 1251 | use chrono::TimeZone; 1252 | use nu_protocol::{LabeledError, Span, Value}; 1253 | 1254 | pub fn string_or_binary(bytes: V) -> Value 1255 | where 1256 | V: Into>, 1257 | { 1258 | match String::from_utf8(bytes.into()) { 1259 | Ok(s) => Value::string(s, Span::unknown()), 1260 | Err(err) => Value::binary(err.into_bytes(), Span::unknown()), 1261 | } 1262 | } 1263 | 1264 | pub fn sec_to_duration>(sec: U) -> Value { 1265 | Value::duration( 1266 | Duration::from_secs(sec.into()).as_nanos() as i64, 1267 | Span::unknown(), 1268 | ) 1269 | } 1270 | 1271 | pub fn sec_to_date>(sec: U, input_span: Span) -> Result { 1272 | let secs = sec.into(); 1273 | let datetime = match chrono::Utc.timestamp_opt(secs, 0) { 1274 | chrono::LocalResult::None => Err(LabeledError::new("invalid time") 1275 | .with_label(format!("Invalid time: {}", secs), input_span)), 1276 | chrono::LocalResult::Single(dt) => Ok(dt), 1277 | chrono::LocalResult::Ambiguous(dt1, dt2) => Err(LabeledError::new("invalid time") 1278 | .with_label( 1279 | format!( 1280 | "Time {} produced ambiguous result: {} vs {}", 1281 | secs, dt1, dt2 1282 | ), 1283 | input_span, 1284 | )), 1285 | }? 1286 | .fixed_offset(); 1287 | 1288 | Ok(Value::date(datetime, Span::unknown())) 1289 | } 1290 | } 1291 | -------------------------------------------------------------------------------- /src/dns/util.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! spanned { 3 | ($val:expr) => {{ 4 | nu_protocol::Spanned { 5 | item: $val, 6 | span: $val.span(), 7 | } 8 | }}; 9 | ($val:expr, $span:expr) => {{ 10 | nu_protocol::Spanned { 11 | item: $val, 12 | span: $span, 13 | } 14 | }}; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // nothing we can do about this as it's upstream 2 | #![allow(clippy::result_large_err)] 3 | 4 | pub mod dns; 5 | 6 | pub use dns::Dns; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use nu_plugin::{serve_plugin, MsgPackSerializer}; 2 | use nu_plugin_dns::Dns; 3 | 4 | fn main() { 5 | serve_plugin(&Dns::new(), MsgPackSerializer) 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/zones/nushell.sh.zone: -------------------------------------------------------------------------------- 1 | $ORIGIN nushell.sh. 2 | $TTL 30m 3 | 4 | @ IN SOA dns1.registrar-servers.com. hostmaster.registrar-servers.com. ( 5 | 1677639553 ; Serial 6 | 43200 ; Refresh 7 | 1800 ; Retry 8 | 1800 ; Expire 9 | 1800) ; Minimum TTL 10 | 11 | AAAA 2606:50c0:8000::153 12 | 13 | A 185.199.108.153 14 | A 185.199.109.153 15 | A 185.199.110.153 16 | A 185.199.111.153 17 | 18 | PTR ptr 19 | MX 10 mail1 20 | MX 20 mail2 21 | TXT "v=spf1 include:spf.nushell.sh. ?all" 22 | caldav CNAME nushell.sh. 23 | cal CNAME caldav.nushell.sh. 24 | 25 | acal ANAME nushell.sh. 26 | 27 | ; DNAME not supported by hickory 28 | ; cmds DNAME commands.nushell.sh. 29 | ; cd.commands TXT "cd" 30 | ; ls.commands TXT "ls" 31 | ; rm.commands TXT "rm" 32 | 33 | _caldav._tcp SRV 1 5 8080 caldav.nushell.sh. 34 | naptr NAPTR 100 10 "u" "E2U+pstn:tel" "\!^(.*)$\!tel:\\1\!" . 35 | cert CERT 1 12345 8 "Zm9vYmFy" 36 | 37 | issue.caa CAA 128 issue "dynadot.com; foo=bar; baz=quux" 38 | issuewild.caa CAA 0 issuewild "dynadot.com" 39 | issuewild.caa CAA 0 issuewild ";" 40 | report.caa CAA 0 iodef "mailto:bob@nushell.sh" 41 | report.caa CAA 0 iodef "https://nushell.sh/" 42 | 43 | csync CSYNC 42 3 A AAAA NS 44 | hinfo HINFO foo bar 45 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::SocketAddrV6, 3 | ops::Deref, 4 | path::PathBuf, 5 | str::FromStr, 6 | sync::{Arc, LazyLock}, 7 | }; 8 | 9 | use hickory_resolver::IntoName; 10 | use hickory_server::{ 11 | authority::{Catalog, ZoneType}, 12 | store::file::{FileAuthority, FileConfig}, 13 | ServerFuture, 14 | }; 15 | use nu_plugin_dns::{ 16 | dns::{self, constants}, 17 | Dns, 18 | }; 19 | use nu_plugin_test_support::PluginTest; 20 | use nu_protocol::{ 21 | record, IntoPipelineData, IntoValue, PipelineData, ShellError, Span, TryIntoValue, Value, 22 | }; 23 | use tokio::net::UdpSocket; 24 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 25 | 26 | mod query; 27 | 28 | const CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); 29 | 30 | static HARNESS: LazyLock = LazyLock::new(|| TestHarness::new().unwrap()); 31 | 32 | struct TestHarness { 33 | runtime: tokio::runtime::Runtime, 34 | _server: ServerFuture, 35 | } 36 | 37 | impl TestHarness { 38 | const TEST_RESOLVER_SOCKET_ADDR: &str = "[::1]:8053"; 39 | const ZONE_FILE_EXT: &str = ".zone"; 40 | 41 | pub fn new() -> std::io::Result { 42 | let runtime = tokio::runtime::Builder::new_multi_thread() 43 | .enable_all() 44 | .build()?; 45 | 46 | let _server = Self::init_hickory_server(&runtime); 47 | 48 | Ok(Self { runtime, _server }) 49 | } 50 | 51 | async fn collect_zones() -> Catalog { 52 | let root_dir = PathBuf::from_str(CARGO_MANIFEST_DIR) 53 | .unwrap() 54 | .join("tests/fixtures/zones"); 55 | 56 | let mut catalog = Catalog::new(); 57 | let mut entries = tokio::fs::read_dir(&root_dir).await.unwrap(); 58 | 59 | while let Some(entry) = entries.next_entry().await.unwrap() { 60 | if !entry.metadata().await.unwrap().is_file() { 61 | continue; 62 | } 63 | 64 | let file_name = entry.file_name().into_string().unwrap(); 65 | 66 | if !file_name.ends_with(Self::ZONE_FILE_EXT) { 67 | continue; 68 | } 69 | 70 | let origin = &file_name[..(file_name.len() - Self::ZONE_FILE_EXT.len() + 1)] 71 | .into_name() 72 | .unwrap(); 73 | 74 | tracing::debug!(?origin); 75 | 76 | let file_config = FileConfig { 77 | zone_file_path: entry.path(), 78 | }; 79 | 80 | let authority = FileAuthority::try_from_config( 81 | origin.clone(), 82 | ZoneType::Primary, 83 | false, 84 | Some(&root_dir), 85 | &file_config, 86 | None, 87 | ) 88 | .unwrap(); 89 | 90 | catalog.upsert(origin.into(), vec![Arc::new(authority)]); 91 | } 92 | 93 | catalog 94 | } 95 | 96 | fn test_plugin_config(test_config: Option) -> nu_protocol::Record { 97 | let mut config = record!( 98 | constants::flags::SERVER => Value::test_string(Self::TEST_RESOLVER_SOCKET_ADDR), 99 | constants::flags::CODE => true.into_value(Span::unknown()), 100 | constants::flags::DNSSEC => "none".into_value(Span::unknown()), 101 | ); 102 | 103 | if let Some(test_config) = test_config { 104 | test_config.into_iter().for_each(|(key, val)| { 105 | config.insert(key, val); 106 | }); 107 | } 108 | 109 | config 110 | } 111 | 112 | fn init_hickory_server(runtime: &tokio::runtime::Runtime) -> ServerFuture { 113 | let _ = tracing_subscriber::registry() 114 | .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr)) 115 | .with(tracing_subscriber::EnvFilter::from_default_env()) 116 | .try_init(); 117 | 118 | runtime.block_on(async { 119 | let socket = UdpSocket::bind( 120 | Self::TEST_RESOLVER_SOCKET_ADDR 121 | .parse::() 122 | .unwrap(), 123 | ) 124 | .await; 125 | 126 | let catalog = Self::collect_zones().await; 127 | let mut server = ServerFuture::new(catalog); 128 | server.register_socket(socket.unwrap()); 129 | server 130 | }) 131 | } 132 | 133 | fn plugin_test( 134 | &self, 135 | test_case: TestCase, 136 | expected_resp_code: HickoryResponseCode, 137 | validate: impl Fn(bool, &nu_protocol::Record), 138 | ) -> Result { 139 | let mut test = PluginTest::new(Dns::PLUGIN_NAME, nu_plugin_dns::Dns::new().into()).unwrap(); 140 | 141 | let state = test.engine_state_mut(); 142 | let mut config = state.get_config().deref().clone(); 143 | let plugin_config = Value::test_record(Self::test_plugin_config(test_case.config)); 144 | 145 | config 146 | .plugins 147 | .insert(Dns::PLUGIN_NAME.into(), plugin_config); 148 | 149 | state.set_config(Arc::new(config)); 150 | 151 | // add the table command for debugging 152 | test.add_decl(Box::new(nu_command::Table)).unwrap(); 153 | test.add_decl(Box::new(nu_command::Cd)).unwrap(); 154 | 155 | let code = test 156 | .engine_state() 157 | .get_plugin_config(Dns::PLUGIN_NAME) 158 | .unwrap() 159 | .get_data_by_key(constants::flags::CODE) 160 | .unwrap() 161 | .as_bool() 162 | .unwrap(); 163 | 164 | let input = test_case.input.unwrap_or(PipelineData::Empty); 165 | let actual = self.runtime.block_on(async { 166 | test.eval_with(test_case.cmd.as_ref(), input)? 167 | .into_value(Span::test_data()) 168 | })?; 169 | 170 | let table = self 171 | .runtime 172 | .block_on(async { 173 | test.eval_with( 174 | // this cd business is necessary because apparently the 175 | // nushell engine for these tests do not set $env.PWD, so 176 | // calling cd first sets it 177 | "let msg = $in; cd .; $msg | table -ew 1000000000", 178 | actual.clone().into_pipeline_data(), 179 | )? 180 | .into_value(Span::test_data()) 181 | })? 182 | .into_string() 183 | .unwrap(); 184 | 185 | tracing::debug!("\n{table}"); 186 | 187 | let mut values = actual.into_list().unwrap(); 188 | assert_eq!(1, values.len()); 189 | 190 | let message = values.pop().unwrap().into_record().unwrap(); 191 | assert_message_response(&message, expected_resp_code)?; 192 | 193 | validate(code, &message); 194 | 195 | Ok(test) 196 | } 197 | } 198 | 199 | pub struct TestCase<'c> { 200 | pub config: Option, 201 | pub input: Option, 202 | pub cmd: &'c str, 203 | } 204 | 205 | type HickoryResponseCode = hickory_proto::op::ResponseCode; 206 | 207 | fn assert_message_response( 208 | message: &nu_protocol::Record, 209 | expected_resp_code: HickoryResponseCode, 210 | ) -> Result<(), ShellError> { 211 | let header = message 212 | .get(constants::columns::message::HEADER) 213 | .unwrap() 214 | .as_record()?; 215 | 216 | let resp_code = header 217 | .get(constants::columns::message::header::RESPONSE_CODE) 218 | .unwrap() 219 | .as_record()? 220 | .get(constants::flags::CODE) 221 | .unwrap() 222 | .as_int()?; 223 | 224 | assert_eq!(expected_resp_code.low() as i64, resp_code); 225 | 226 | Ok(()) 227 | } 228 | 229 | fn record_values(code: bool, iter: I) -> Value 230 | where 231 | N: IntoName, 232 | I: IntoIterator, 233 | R: Into, 234 | { 235 | iter.into_iter() 236 | .map(|(name, ttl, rdata)| { 237 | dns::serde::Record(hickory_proto::rr::Record::from_rdata( 238 | name.into_name().unwrap(), 239 | ttl.num_seconds() as u32, 240 | rdata.into(), 241 | )) 242 | .into_value(code) 243 | .unwrap() 244 | }) 245 | .collect::>() 246 | .try_into_value(Span::unknown()) 247 | .unwrap() 248 | } 249 | -------------------------------------------------------------------------------- /tests/query/expected.rs: -------------------------------------------------------------------------------- 1 | pub mod name { 2 | use std::sync::LazyLock; 3 | 4 | use hickory_resolver::Name; 5 | 6 | pub static ORIGIN: LazyLock = LazyLock::new(|| "nushell.sh.".parse().unwrap()); 7 | pub static CALDAV: LazyLock = LazyLock::new(|| { 8 | let mut cname = ORIGIN.prepend_label("caldav").unwrap(); 9 | cname.set_fqdn(true); 10 | cname 11 | }); 12 | pub static CAL: LazyLock = LazyLock::new(|| { 13 | let mut cname = ORIGIN.prepend_label("cal").unwrap(); 14 | cname.set_fqdn(true); 15 | cname 16 | }); 17 | } 18 | 19 | pub mod rr { 20 | use hickory_resolver::Name; 21 | 22 | use std::net::{Ipv4Addr, Ipv6Addr}; 23 | use std::str::FromStr; 24 | use std::sync::LazyLock; 25 | 26 | pub const THIRTY_MIN: chrono::TimeDelta = chrono::TimeDelta::minutes(30); 27 | 28 | pub(crate) static SOA: LazyLock<(Name, chrono::TimeDelta, hickory_proto::rr::RData)> = 29 | LazyLock::new(|| { 30 | ( 31 | super::name::ORIGIN.clone(), 32 | THIRTY_MIN, 33 | hickory_proto::rr::RData::SOA(hickory_proto::rr::rdata::SOA::new( 34 | "dns1.registrar-servers.com.".parse().unwrap(), 35 | "hostmaster.registrar-servers.com.".parse().unwrap(), 36 | 1677639553, 37 | 43200, 38 | 1800, 39 | // [FIXME] so there is actually a bug in hickory-server 40 | // that parses the zone file such that it uses the 41 | // `expire` field of the SOA record as the TTL of the 42 | // record itself. Change this to something else in the 43 | // future once this is fixed upstream 44 | 1800, 45 | 1800, 46 | )), 47 | ) 48 | }); 49 | 50 | pub(crate) static A: LazyLock> = 51 | LazyLock::new(|| { 52 | [ 53 | "185.199.108.153", 54 | "185.199.109.153", 55 | "185.199.110.153", 56 | "185.199.111.153", 57 | ] 58 | .into_iter() 59 | .map(|ip| hickory_proto::rr::RData::from(Ipv4Addr::from_str(ip).unwrap())) 60 | .map(|ip| (super::name::ORIGIN.clone(), THIRTY_MIN, ip)) 61 | .collect() 62 | }); 63 | 64 | pub(crate) static AAAA: LazyLock> = 65 | LazyLock::new(|| { 66 | ["2606:50c0:8000::153"] 67 | .into_iter() 68 | .map(|ip| hickory_proto::rr::RData::from(Ipv6Addr::from_str(ip).unwrap())) 69 | .map(|ip| (super::name::ORIGIN.clone(), THIRTY_MIN, ip)) 70 | .collect() 71 | }); 72 | 73 | pub(crate) static CNAME_CALDAV: LazyLock< 74 | Vec<(Name, chrono::TimeDelta, hickory_proto::rr::RData)>, 75 | > = LazyLock::new(|| { 76 | [super::name::CALDAV.clone()] 77 | .into_iter() 78 | .map(|name| { 79 | ( 80 | name, 81 | super::rr::THIRTY_MIN, 82 | hickory_proto::rr::RData::CNAME(hickory_proto::rr::rdata::CNAME( 83 | super::name::ORIGIN.clone(), 84 | )), 85 | ) 86 | }) 87 | .collect() 88 | }); 89 | 90 | pub(crate) static CNAME_CAL: LazyLock< 91 | Vec<(Name, chrono::TimeDelta, hickory_proto::rr::RData)>, 92 | > = LazyLock::new(|| { 93 | [super::name::CAL.clone()] 94 | .into_iter() 95 | .map(|name| { 96 | ( 97 | name, 98 | super::rr::THIRTY_MIN, 99 | hickory_proto::rr::RData::CNAME(hickory_proto::rr::rdata::CNAME( 100 | super::name::CALDAV.clone(), 101 | )), 102 | ) 103 | }) 104 | .collect() 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /tests/query/mod.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use hickory_proto::rr::RecordType; 4 | use hickory_resolver::{IntoName, Name}; 5 | use nu_plugin_dns::dns::constants; 6 | use nu_protocol::{ShellError, Span, Value}; 7 | 8 | use super::{record_values, HickoryResponseCode, TestCase, HARNESS}; 9 | 10 | mod expected; 11 | 12 | #[test] 13 | pub(crate) fn rr_a() -> Result<(), ShellError> { 14 | HARNESS.plugin_test( 15 | TestCase { 16 | config: None, 17 | input: None, 18 | cmd: &format!("dns query --type a '{}'", *expected::name::ORIGIN), 19 | }, 20 | HickoryResponseCode::NoError, 21 | |code, message| { 22 | let expected = record_values(code, expected::rr::A.clone()); 23 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 24 | assert_eq!(&expected, actual); 25 | }, 26 | )?; 27 | 28 | Ok(()) 29 | } 30 | 31 | #[test] 32 | pub(crate) fn rr_aaaa() -> Result<(), ShellError> { 33 | HARNESS.plugin_test( 34 | TestCase { 35 | config: None, 36 | input: None, 37 | cmd: &format!("dns query --type aaaa '{}'", *expected::name::ORIGIN), 38 | }, 39 | HickoryResponseCode::NoError, 40 | |code, message| { 41 | let expected = record_values(code, expected::rr::AAAA.clone()); 42 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 43 | assert_eq!(&expected, actual); 44 | }, 45 | )?; 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | pub(crate) fn rr_aname() -> Result<(), ShellError> { 52 | let aname = expected::name::ORIGIN.prepend_label("acal").unwrap(); 53 | let expected_aname_rr = ( 54 | aname.clone(), 55 | expected::rr::THIRTY_MIN, 56 | hickory_proto::rr::RData::ANAME(hickory_proto::rr::rdata::ANAME( 57 | expected::name::ORIGIN.clone(), 58 | )), 59 | ); 60 | 61 | HARNESS.plugin_test( 62 | TestCase { 63 | config: None, 64 | input: None, 65 | cmd: &format!("dns query --type aname '{}'", aname), 66 | }, 67 | HickoryResponseCode::NoError, 68 | |code, message| { 69 | let expected = record_values(code, [expected_aname_rr.clone()]); 70 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 71 | assert_eq!(&expected, actual); 72 | }, 73 | )?; 74 | 75 | HARNESS.plugin_test( 76 | TestCase { 77 | config: None, 78 | input: None, 79 | cmd: &format!("dns query --type a '{}'", aname), 80 | }, 81 | HickoryResponseCode::NoError, 82 | |code, message| { 83 | let expected_answer = record_values( 84 | code, 85 | expected::rr::A 86 | .clone() 87 | .into_iter() 88 | // ttl of 0 ??? 89 | .map(|(_, _, rdata)| (aname.clone(), chrono::TimeDelta::zero(), rdata)), 90 | ); 91 | let actual_answer = message.get(constants::columns::message::ANSWER).unwrap(); 92 | assert_eq!(&expected_answer, actual_answer); 93 | 94 | let expected_additional = record_values( 95 | code, 96 | [expected_aname_rr.clone()] 97 | .into_iter() 98 | .chain(expected::rr::A.clone()), 99 | ); 100 | let actual_additional = message 101 | .get(constants::columns::message::ADDITIONAL) 102 | .unwrap(); 103 | assert_eq!(&expected_additional, actual_additional); 104 | }, 105 | )?; 106 | 107 | Ok(()) 108 | } 109 | 110 | #[test] 111 | pub(crate) fn rr_caa() -> Result<(), ShellError> { 112 | let caa_issue = expected::name::ORIGIN 113 | .prepend_label("caa") 114 | .unwrap() 115 | .prepend_label("issue") 116 | .unwrap(); 117 | 118 | HARNESS.plugin_test( 119 | TestCase { 120 | config: None, 121 | input: None, 122 | cmd: &format!("dns query --type caa '{}'", caa_issue), 123 | }, 124 | HickoryResponseCode::NoError, 125 | |code, message| { 126 | use hickory_proto::rr::rdata::caa::KeyValue; 127 | 128 | let expected = record_values( 129 | code, 130 | [hickory_proto::rr::RData::CAA( 131 | hickory_proto::rr::rdata::CAA::new_issue( 132 | true, 133 | Some("dynadot.com".into_name().unwrap()), 134 | vec![KeyValue::new("foo", "bar"), KeyValue::new("baz", "quux")], 135 | ), 136 | )] 137 | .into_iter() 138 | .map(|rdata| (caa_issue.clone(), expected::rr::THIRTY_MIN, rdata)), 139 | ); 140 | 141 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 142 | assert_eq!(&expected, actual); 143 | }, 144 | )?; 145 | 146 | let caa_issuewild = expected::name::ORIGIN 147 | .prepend_label("caa") 148 | .unwrap() 149 | .prepend_label("issuewild") 150 | .unwrap(); 151 | 152 | HARNESS.plugin_test( 153 | TestCase { 154 | config: None, 155 | input: None, 156 | cmd: &format!("dns query --type caa '{}'", caa_issuewild), 157 | }, 158 | HickoryResponseCode::NoError, 159 | |code, message| { 160 | let expected = record_values( 161 | code, 162 | [ 163 | hickory_proto::rr::RData::CAA(hickory_proto::rr::rdata::CAA::new_issuewild( 164 | false, 165 | Some("dynadot.com".into_name().unwrap()), 166 | vec![], 167 | )), 168 | hickory_proto::rr::RData::CAA(hickory_proto::rr::rdata::CAA::new_issuewild( 169 | false, 170 | None, 171 | vec![], 172 | )), 173 | ] 174 | .into_iter() 175 | .map(|rdata| (caa_issuewild.clone(), expected::rr::THIRTY_MIN, rdata)), 176 | ); 177 | 178 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 179 | assert_eq!(&expected, actual); 180 | }, 181 | )?; 182 | 183 | let caa_iodef = expected::name::ORIGIN 184 | .prepend_label("caa") 185 | .unwrap() 186 | .prepend_label("report") 187 | .unwrap(); 188 | 189 | HARNESS.plugin_test( 190 | TestCase { 191 | config: None, 192 | input: None, 193 | cmd: &format!("dns query --type caa '{}'", caa_iodef), 194 | }, 195 | HickoryResponseCode::NoError, 196 | |code, message| { 197 | let expected = record_values( 198 | code, 199 | [ 200 | hickory_proto::rr::RData::CAA(hickory_proto::rr::rdata::CAA::new_iodef( 201 | false, 202 | "mailto:bob@nushell.sh".parse().unwrap(), 203 | )), 204 | hickory_proto::rr::RData::CAA(hickory_proto::rr::rdata::CAA::new_iodef( 205 | false, 206 | "https://nushell.sh/".parse().unwrap(), 207 | )), 208 | ] 209 | .into_iter() 210 | .map(|rdata| (caa_iodef.clone(), expected::rr::THIRTY_MIN, rdata)), 211 | ); 212 | 213 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 214 | assert_eq!(&expected, actual); 215 | }, 216 | )?; 217 | 218 | Ok(()) 219 | } 220 | 221 | #[test] 222 | pub(crate) fn rr_cert() -> Result<(), ShellError> { 223 | let cert = expected::name::ORIGIN.prepend_label("cert").unwrap(); 224 | 225 | HARNESS.plugin_test( 226 | TestCase { 227 | config: None, 228 | input: None, 229 | cmd: &format!("dns query --type cert '{}'", cert), 230 | }, 231 | HickoryResponseCode::NoError, 232 | |code, message| { 233 | let expected = record_values( 234 | code, 235 | [hickory_proto::rr::RData::CERT( 236 | hickory_proto::rr::rdata::CERT::new( 237 | hickory_proto::rr::rdata::cert::CertType::PKIX, 238 | 12345, 239 | hickory_proto::rr::rdata::cert::Algorithm::RSASHA256, 240 | (*b"foobar").into(), 241 | ), 242 | )] 243 | .into_iter() 244 | .map(|rdata| (cert.clone(), expected::rr::THIRTY_MIN, rdata)), 245 | ); 246 | 247 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 248 | 249 | assert_eq!(&expected, actual); 250 | }, 251 | )?; 252 | 253 | Ok(()) 254 | } 255 | 256 | #[test] 257 | pub(crate) fn rr_cname() -> Result<(), ShellError> { 258 | // querying a cname specifically only returns the cname record 259 | HARNESS.plugin_test( 260 | TestCase { 261 | config: None, 262 | input: None, 263 | cmd: &format!("dns query --type cname '{}'", *expected::name::CALDAV), 264 | }, 265 | HickoryResponseCode::NoError, 266 | |code, message| { 267 | let expected = record_values(code, expected::rr::CNAME_CALDAV.clone()); 268 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 269 | assert_eq!(&expected, actual); 270 | }, 271 | )?; 272 | 273 | // querying for A on a CNAME returns the CNAME and A records 274 | HARNESS.plugin_test( 275 | TestCase { 276 | config: None, 277 | input: None, 278 | cmd: &format!("dns query --type a '{}'", *expected::name::CALDAV), 279 | }, 280 | HickoryResponseCode::NoError, 281 | |code, message| { 282 | let expected = record_values(code, expected::rr::CNAME_CALDAV.clone()); 283 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 284 | assert_eq!(&expected, actual); 285 | 286 | // apparently the A records get put into ADDITIONAL 287 | let expected = record_values(code, expected::rr::A.clone()); 288 | let actual = message 289 | .get(constants::columns::message::ADDITIONAL) 290 | .unwrap(); 291 | 292 | assert_eq!(&expected, actual); 293 | }, 294 | )?; 295 | 296 | // CNAME chain 297 | HARNESS.plugin_test( 298 | TestCase { 299 | config: None, 300 | input: None, 301 | cmd: &format!("dns query --type a '{}'", *expected::name::CAL), 302 | }, 303 | HickoryResponseCode::NoError, 304 | |code, message| { 305 | let expected = record_values(code, expected::rr::CNAME_CAL.clone()); 306 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 307 | 308 | assert_eq!(&expected, actual); 309 | 310 | let expected = record_values( 311 | code, 312 | expected::rr::CNAME_CALDAV 313 | .clone() 314 | .into_iter() 315 | .chain(expected::rr::A.clone()), 316 | ); 317 | 318 | let actual = message 319 | .get(constants::columns::message::ADDITIONAL) 320 | .unwrap(); 321 | 322 | assert_eq!(&expected, actual); 323 | }, 324 | )?; 325 | 326 | Ok(()) 327 | } 328 | 329 | #[test] 330 | pub(crate) fn rr_csync() -> Result<(), ShellError> { 331 | let csync = expected::name::ORIGIN.prepend_label("csync").unwrap(); 332 | 333 | HARNESS.plugin_test( 334 | TestCase { 335 | config: None, 336 | input: None, 337 | cmd: &format!("dns query --type csync '{}'", csync), 338 | }, 339 | HickoryResponseCode::NoError, 340 | |code, message| { 341 | // these are encoded in a bitmap, and therefore effectively sorted 342 | // by type code 343 | let mut types = vec![RecordType::A, RecordType::AAAA, RecordType::NS]; 344 | types.sort(); 345 | 346 | let expected = record_values( 347 | code, 348 | [( 349 | csync.clone(), 350 | expected::rr::THIRTY_MIN, 351 | hickory_proto::rr::RData::CSYNC(hickory_proto::rr::rdata::CSYNC::new( 352 | 42, true, true, types, 353 | )), 354 | )], 355 | ); 356 | 357 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 358 | assert_eq!(&expected, actual); 359 | }, 360 | )?; 361 | 362 | Ok(()) 363 | } 364 | 365 | #[test] 366 | pub(crate) fn rr_hinfo() -> Result<(), ShellError> { 367 | let hinfo = expected::name::ORIGIN.prepend_label("hinfo").unwrap(); 368 | 369 | HARNESS.plugin_test( 370 | TestCase { 371 | config: None, 372 | input: None, 373 | cmd: &format!("dns query --type hinfo '{}'", hinfo), 374 | }, 375 | HickoryResponseCode::NoError, 376 | |code, message| { 377 | let expected = record_values( 378 | code, 379 | [( 380 | hinfo.clone(), 381 | expected::rr::THIRTY_MIN, 382 | hickory_proto::rr::RData::HINFO(hickory_proto::rr::rdata::HINFO::new( 383 | "foo".into(), 384 | "bar".into(), 385 | )), 386 | )], 387 | ); 388 | 389 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 390 | assert_eq!(&expected, actual); 391 | }, 392 | )?; 393 | 394 | Ok(()) 395 | } 396 | 397 | #[test] 398 | #[ignore = "hickory missing support for dname in zone file parsing"] 399 | pub(crate) fn rr_dname() -> Result<(), ShellError> { 400 | Ok(()) 401 | } 402 | 403 | #[test] 404 | pub(crate) fn rr_mx() -> Result<(), ShellError> { 405 | HARNESS.plugin_test( 406 | TestCase { 407 | config: None, 408 | input: None, 409 | cmd: &format!("dns query --type mx '{}'", *expected::name::ORIGIN), 410 | }, 411 | HickoryResponseCode::NoError, 412 | |code, message| { 413 | let expected = record_values( 414 | code, 415 | [ 416 | hickory_proto::rr::rdata::MX::new( 417 | 10, 418 | expected::name::ORIGIN 419 | .clone() 420 | .prepend_label("mail1") 421 | .unwrap(), 422 | ), 423 | hickory_proto::rr::rdata::MX::new( 424 | 20, 425 | expected::name::ORIGIN 426 | .clone() 427 | .prepend_label("mail2") 428 | .unwrap(), 429 | ), 430 | ] 431 | .into_iter() 432 | .map(|mx| { 433 | ( 434 | expected::name::ORIGIN.clone(), 435 | expected::rr::THIRTY_MIN, 436 | hickory_proto::rr::RData::MX(mx), 437 | ) 438 | }), 439 | ); 440 | 441 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 442 | 443 | assert_eq!(&expected, actual); 444 | }, 445 | )?; 446 | 447 | Ok(()) 448 | } 449 | 450 | #[test] 451 | pub(crate) fn rr_naptr() -> Result<(), ShellError> { 452 | let naptr = expected::name::ORIGIN.prepend_label("naptr").unwrap(); 453 | 454 | HARNESS.plugin_test( 455 | TestCase { 456 | config: None, 457 | input: None, 458 | cmd: &format!("dns query --type naptr '{}'", naptr), 459 | }, 460 | HickoryResponseCode::NoError, 461 | |code, message| { 462 | let expected = record_values( 463 | code, 464 | [hickory_proto::rr::RData::NAPTR( 465 | hickory_proto::rr::rdata::NAPTR::new( 466 | 100, 467 | 10, 468 | (*b"u").into(), 469 | (*b"E2U+pstn:tel").into(), 470 | (*br"!^(.*)$!tel:\1!").into(), 471 | Name::from_str(".").unwrap(), 472 | ), 473 | )] 474 | .into_iter() 475 | .map(|rdata| (naptr.clone(), expected::rr::THIRTY_MIN, rdata)), 476 | ); 477 | 478 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 479 | 480 | assert_eq!(&expected, actual); 481 | }, 482 | )?; 483 | 484 | Ok(()) 485 | } 486 | 487 | #[test] 488 | pub(crate) fn rr_ptr() -> Result<(), ShellError> { 489 | HARNESS.plugin_test( 490 | TestCase { 491 | config: None, 492 | input: None, 493 | cmd: &format!("dns query --type ptr '{}'", *expected::name::ORIGIN), 494 | }, 495 | HickoryResponseCode::NoError, 496 | |code, message| { 497 | let expected = record_values( 498 | code, 499 | [expected::name::ORIGIN.clone().prepend_label("ptr").unwrap()] 500 | .into_iter() 501 | .map(|ptr_rr| { 502 | ( 503 | expected::name::ORIGIN.clone(), 504 | expected::rr::THIRTY_MIN, 505 | hickory_proto::rr::RData::PTR(hickory_proto::rr::rdata::PTR(ptr_rr)), 506 | ) 507 | }), 508 | ); 509 | 510 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 511 | 512 | assert_eq!(&expected, actual); 513 | }, 514 | )?; 515 | 516 | Ok(()) 517 | } 518 | 519 | #[test] 520 | pub(crate) fn rr_soa() -> Result<(), ShellError> { 521 | HARNESS.plugin_test( 522 | TestCase { 523 | config: None, 524 | input: None, 525 | cmd: &format!("dns query --type soa '{}'", *expected::name::ORIGIN), 526 | }, 527 | HickoryResponseCode::NoError, 528 | |code, message| { 529 | let expected = record_values(code, [expected::rr::SOA.clone()]); 530 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 531 | assert_eq!(&expected, actual); 532 | }, 533 | )?; 534 | 535 | Ok(()) 536 | } 537 | 538 | #[test] 539 | pub(crate) fn rr_srv() -> Result<(), ShellError> { 540 | let srv = expected::name::ORIGIN 541 | .prepend_label("_tcp") 542 | .unwrap() 543 | .prepend_label("_caldav") 544 | .unwrap(); 545 | 546 | HARNESS.plugin_test( 547 | TestCase { 548 | config: None, 549 | input: None, 550 | cmd: &format!("dns query --type srv '{}'", srv), 551 | }, 552 | HickoryResponseCode::NoError, 553 | |code, message| { 554 | let expected = record_values( 555 | code, 556 | [hickory_proto::rr::RData::SRV( 557 | hickory_proto::rr::rdata::SRV::new(1, 5, 8080, expected::name::CALDAV.clone()), 558 | )] 559 | .into_iter() 560 | .map(|srv_rr| (srv.clone(), expected::rr::THIRTY_MIN, srv_rr)), 561 | ); 562 | 563 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 564 | 565 | assert_eq!(&expected, actual); 566 | 567 | // let expected = record_values(code, expected_cname.clone().chain(expected::rr::A.clone())); 568 | let expected = record_values( 569 | code, 570 | expected::rr::CNAME_CALDAV 571 | .clone() 572 | .into_iter() 573 | .chain(expected::rr::A.clone()) 574 | .chain(expected::rr::AAAA.clone()), 575 | ); 576 | 577 | let actual = message 578 | .get(constants::columns::message::ADDITIONAL) 579 | .unwrap(); 580 | 581 | assert_eq!(&expected, actual); 582 | }, 583 | )?; 584 | 585 | Ok(()) 586 | } 587 | 588 | #[test] 589 | pub(crate) fn rr_txt() -> Result<(), ShellError> { 590 | HARNESS.plugin_test( 591 | TestCase { 592 | config: None, 593 | input: None, 594 | cmd: &format!("dns query --type txt '{}'", *expected::name::ORIGIN), 595 | }, 596 | HickoryResponseCode::NoError, 597 | |code, message| { 598 | let expected = record_values( 599 | code, 600 | [hickory_proto::rr::RData::TXT( 601 | hickory_proto::rr::rdata::TXT::new(vec![ 602 | "v=spf1 include:spf.nushell.sh. ?all".into() 603 | ]), 604 | )] 605 | .into_iter() 606 | .map(|txt| { 607 | ( 608 | expected::name::ORIGIN.clone(), 609 | expected::rr::THIRTY_MIN, 610 | txt, 611 | ) 612 | }), 613 | ); 614 | 615 | let actual = message.get(constants::columns::message::ANSWER).unwrap(); 616 | 617 | assert_eq!(&expected, actual); 618 | }, 619 | )?; 620 | 621 | Ok(()) 622 | } 623 | 624 | /// A zone with a name exists, but not with the record type in the request. An 625 | /// empty answer is returned. 626 | #[test] 627 | pub(crate) fn empty() -> Result<(), ShellError> { 628 | HARNESS.plugin_test( 629 | TestCase { 630 | config: None, 631 | input: None, 632 | cmd: &format!("dns query --type hinfo '{}'", *expected::name::ORIGIN), 633 | }, 634 | HickoryResponseCode::NoError, 635 | |code, message| { 636 | let expected_soa = record_values(code, [expected::rr::SOA.clone()]); 637 | 638 | let expected_answer = Value::list(Vec::new(), Span::unknown()); 639 | let actual_answer = message.get(constants::columns::message::ANSWER).unwrap(); 640 | 641 | assert_eq!(&expected_answer, actual_answer); 642 | 643 | // empty rrset has the soa included 644 | let actual_authority = message.get(constants::columns::message::AUTHORITY).unwrap(); 645 | assert_eq!(&expected_soa, actual_authority); 646 | }, 647 | )?; 648 | 649 | Ok(()) 650 | } 651 | --------------------------------------------------------------------------------