diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..059c0a65 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(all(target_arch = "x86_64", not(target_env = "musl")))'] +rustflags = ["-C", "target-cpu=x86-64-v2"] diff --git a/.dockerignore b/.dockerignore index 3fd21762..c8ac53dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,5 +12,6 @@ # because CI builds from a checkout and only the image build has a context. !vendor +!.cargo !Cargo.lock !Cargo.toml diff --git a/.gitignore b/.gitignore index 44de0f4a..c1a33ed4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ run.sh !.github !.gitlab-ci.yml !.gitea +!.cargo CLAUDE.md # The cutover rehearsal writes its fixture and state here. diff --git a/CHANGELOG.md b/CHANGELOG.md index d999d420..db262f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.16.26] - 2026-10-11 + +If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. + +## Added +- `sd_notify` support. +- JMAP: `Mailbox/set` allows moving a mailbox under a parent that already has a child with the same name, and rejects renames that only change the case of a mailbox name with `alreadyExists`. + +## Changed + +## Fixed +- DNSSEC: Use `hickory-resolver` patched fork at https://github.com/stalwartlabs/hickory-dns. +- RocksDB: Blob garbage collection rewrites every stored blob on each compaction of the blobs column family, about once per 128MB of new mail. +- Milter: End-of-headers is not sent to milters that negotiate `SMFIP_NOHDRS` without `SMFIP_NOEOH`, so the transaction stalls until it times out. +- OIDC: When the access token lacks the `claimUsername` claim but has an `email` claim, the account is built from `email` without consulting the userinfo endpoint. +- JMAP: + - `onSuccessUpdateEmail` and `onSuccessDestroyEmail` entries in `EmailSubmission/set` that name an existing submission by id update or destroy the Email with the same id instead of the submission's own Email. + - A `VacationResponse/set` that only sets `isEnabled` on an account without a vacation response activates an empty Sieve script. + ## [0.16.25] - 2026-10-05 If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. diff --git a/Cargo.lock b/Cargo.lock index 0ecffe31..b534d0ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ "once_cell", "serde", "version_check", - "zerocopy 0.8.59", + "zerocopy 0.8.61", ] [[package]] @@ -188,6 +188,12 @@ dependencies = [ "password-hash 0.6.1", ] +[[package]] +name = "array-const-fn-init" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bcb85e548c05d407fa6faff46b750ba287714ef32afc0f5e15b4641ffd6affb" + [[package]] name = "array-init" version = "2.1.0" @@ -330,13 +336,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -402,14 +408,14 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-creds" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b85155d265df828f84e53886ed9e427aed979dd8a39f5b8b2162c77e142d7" +checksum = "886acd98f2df6e988d5428e92cdf041792ffa87a494d2b5125982502ec726a9c" dependencies = [ "attohttpc", "home", "log", - "quick-xml 0.38.4", + "quick-xml 0.41.0", "rust-ini", "serde", "thiserror 2.0.21", @@ -443,9 +449,9 @@ dependencies = [ [[package]] name = "aws-region" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0" +checksum = "8f38493193e45cb4788ee7d479ee94a0a56721ccd28de79bbd0227bb8c11fee3" dependencies = [ "thiserror 2.0.21", ] @@ -1100,9 +1106,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "jobserver", @@ -1292,7 +1298,7 @@ dependencies = [ [[package]] name = "common" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes-gcm-siv", "ahash", @@ -1352,6 +1358,7 @@ dependencies = [ "rustls", "rustls-pemfile", "rustls-pki-types", + "sd-notify", "sequoia-openpgp", "serde", "serde_json", @@ -1477,7 +1484,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "coordinator" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-nats", "futures", @@ -1490,16 +1497,6 @@ dependencies = [ "zenoh", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1723,9 +1720,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] @@ -1889,7 +1886,7 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "dav" -version = "0.16.25" +version = "0.16.26" dependencies = [ "calcard", "chrono", @@ -1912,7 +1909,7 @@ dependencies = [ [[package]] name = "dav-proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "calcard", "chrono", @@ -2125,7 +2122,7 @@ dependencies = [ [[package]] name = "directory" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "argon2 0.6.0", @@ -2335,9 +2332,9 @@ dependencies = [ [[package]] name = "either" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "elliptic-curve" @@ -2366,7 +2363,7 @@ dependencies = [ [[package]] name = "email" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes 0.9.3", "aes-gcm 0.11.1", @@ -2408,9 +2405,9 @@ dependencies = [ [[package]] name = "encodify" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798c447647dd23f673748f2b868ef309a01dd86aaae999182559d36f06ac82f0" +checksum = "e95d6e27686e1d6991a3077f4dbec9d33d18903466d234165ad86cd08c3ebc79" dependencies = [ "memchr", "simdutf8", @@ -2484,7 +2481,7 @@ dependencies = [ [[package]] name = "event_macro" -version = "0.16.25" +version = "0.16.26" dependencies = [ "quote", "syn 3.0.6", @@ -3012,7 +3009,7 @@ dependencies = [ [[package]] name = "groupware" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "calcard", @@ -3040,9 +3037,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -3146,8 +3143,6 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-net" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d" dependencies = [ "async-trait", "aws-lc-rs", @@ -3182,8 +3177,6 @@ dependencies = [ [[package]] name = "hickory-proto" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d" dependencies = [ "aws-lc-rs", "bitflags 2.13.2", @@ -3206,8 +3199,6 @@ dependencies = [ [[package]] name = "hickory-resolver" version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981" dependencies = [ "cfg-if", "futures-util", @@ -3299,7 +3290,7 @@ dependencies = [ [[package]] name = "http" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-stream", "base64 0.23.1", @@ -3395,7 +3386,7 @@ dependencies = [ [[package]] name = "http_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -3449,9 +3440,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -3882,7 +3873,7 @@ checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" [[package]] name = "imap" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "common", @@ -3907,7 +3898,7 @@ dependencies = [ [[package]] name = "imap_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -3922,7 +3913,7 @@ dependencies = [ [[package]] name = "inbuxa" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "coordinator", @@ -3930,10 +3921,11 @@ dependencies = [ "directory", "email", "groupware", - "http 0.16.25", + "http 0.16.26", "http_proto", "imap", "jmap", + "libc", "managesieve", "migration", "pop3", @@ -4167,9 +4159,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" dependencies = [ "defmt", "jiff-core", @@ -4194,9 +4186,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" dependencies = [ "jiff-core", "proc-macro2", @@ -4206,9 +4198,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" [[package]] name = "jiff-tzdb-platform" @@ -4221,7 +4213,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.16.25" +version = "0.16.26" dependencies = [ "async-stream", "base64 0.23.1", @@ -4304,7 +4296,7 @@ dependencies = [ [[package]] name = "jmap_proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "calcard", @@ -4564,9 +4556,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libflate" @@ -4738,7 +4730,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226" dependencies = [ - "twox-hash 2.1.4", + "twox-hash 2.1.5", ] [[package]] @@ -4810,7 +4802,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -4945,7 +4937,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "migration" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "email", @@ -4991,9 +4983,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -5128,7 +5120,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "twox-hash 2.1.4", + "twox-hash 2.1.5", "url", "webpki-roots 1.0.9", ] @@ -5195,7 +5187,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "hashify", @@ -6027,7 +6019,7 @@ dependencies = [ [[package]] name = "pop3" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "directory", @@ -6102,9 +6094,9 @@ dependencies = [ [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "ppmd-rust" @@ -6118,7 +6110,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.59", + "zerocopy 0.8.61", ] [[package]] @@ -6129,13 +6121,15 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prefix-trie" -version = "0.8.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +checksum = "6369af434551b9bf8be05ab871ac1263687d6f0c9485e6d781f904f3e06d7d03" dependencies = [ + "array-const-fn-init", "either", "ipnet", "num-traits", + "smallvec", ] [[package]] @@ -6276,9 +6270,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.238" +version = "2.1.241" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da" +checksum = "c7319b480e204d67e759cad7886178335d26324c7c8573c126cc9dfa7350d064" dependencies = [ "psl-types", ] @@ -6334,16 +6328,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -6351,6 +6335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", + "serde", ] [[package]] @@ -6847,7 +6832,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "registry" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "hashify", @@ -7138,9 +7123,9 @@ dependencies = [ [[package]] name = "rust-s3" -version = "0.37.2" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeedb13abdaa7e48d391de05b0569b37fa0a7a64a668dff6ffb2141ad0c2527e" +checksum = "15d4ed794cdfede6c59f81d485cae6bb63f47a7e35d190148d40754f783073c4" dependencies = [ "async-trait", "aws-creds", @@ -7156,7 +7141,7 @@ dependencies = [ "maybe-async", "md5", "percent-encoding", - "quick-xml 0.38.4", + "quick-xml 0.41.0", "reqwest 0.12.28", "serde", "serde_derive", @@ -7276,7 +7261,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -7404,7 +7389,7 @@ dependencies = [ [[package]] name = "scim" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -7430,7 +7415,7 @@ dependencies = [ [[package]] name = "scim-proto" -version = "0.16.25" +version = "0.16.26" dependencies = [ "hashify", "serde", @@ -7456,6 +7441,15 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "sd-notify" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4ef7359e694bfaf1dd27a30f9d760b54c00dfae9f19bd0c05a39bc9128fe76" +dependencies = [ + "libc", +] + [[package]] name = "sec1" version = "0.7.3" @@ -7488,7 +7482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.13.2", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -7765,7 +7759,7 @@ dependencies = [ [[package]] name = "services" -version = "0.16.25" +version = "0.16.26" dependencies = [ "aes-gcm 0.11.1", "aho-corasick", @@ -8080,7 +8074,7 @@ checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "smtp" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -8171,7 +8165,7 @@ dependencies = [ [[package]] name = "spam-filter" -version = "0.16.25" +version = "0.16.26" dependencies = [ "common", "compact_str", @@ -8292,7 +8286,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "arc-swap", @@ -8470,12 +8464,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" dependencies = [ "bitflags 2.13.2", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys", ] @@ -8552,7 +8546,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "aws-lc-rs", @@ -8574,7 +8568,7 @@ dependencies = [ "form_urlencoded", "futures", "groupware", - "http 0.16.25", + "http 0.16.26", "http_proto", "hyper", "hyper-util", @@ -8777,9 +8771,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -8931,18 +8925,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.15+spec-1.1.0" +version = "0.25.16+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +checksum = "88f38f3af8bf71841c67a990f4e2b52143b2d0a1a994c72513ff4c14977fc722" dependencies = [ "indexmap 2.14.2", "toml_datetime", @@ -8952,9 +8946,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333" dependencies = [ "winnow", ] @@ -9167,7 +9161,7 @@ dependencies = [ [[package]] name = "trc" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "base64 0.23.1", @@ -9252,9 +9246,9 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.4" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" [[package]] name = "typed-path" @@ -9276,7 +9270,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "types" -version = "0.16.25" +version = "0.16.26" dependencies = [ "blake3", "compact_str", @@ -9310,9 +9304,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-bidi" @@ -9445,7 +9439,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utils" -version = "0.16.25" +version = "0.16.26" dependencies = [ "ahash", "arcstr", @@ -9476,9 +9470,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.26.1" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -9559,9 +9553,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -10647,11 +10641,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" dependencies = [ - "zerocopy-derive 0.8.59", + "zerocopy-derive 0.8.61", ] [[package]] @@ -10667,9 +10661,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.59" +version = "0.8.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" dependencies = [ "proc-macro2", "quote", @@ -10699,9 +10693,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index a2d211f7..ce4b3730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,5 +85,12 @@ rpath = false # (vnd.stalwart.*), which scripts `require` and ManageSieve advertises. # vendor/sieve-rs is the published 0.7.3 with those renamed; see its # VENDORED.md. Re-vendor when the version in Cargo.lock moves. +# +# inbuxa: hickory is upstream's patched branch (DNSSEC fixes), vendored at +# the commit upstream pins so builds never fetch from upstream's +# repositories; see vendor/hickory-dns/VENDORED.md. [patch.crates-io] sieve-rs = { path = "vendor/sieve-rs" } +hickory-proto = { path = "vendor/hickory-dns/hickory-proto" } +hickory-net = { path = "vendor/hickory-dns/hickory-net" } +hickory-resolver = { path = "vendor/hickory-dns/hickory-resolver" } diff --git a/Dockerfile.build b/Dockerfile.build index 4e0c3d07..d9b38347 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -4,7 +4,7 @@ # ***************** # Base image for planner & builder # ***************** -FROM --platform=$BUILDPLATFORM rust:1.98.1-slim-trixie AS base +FROM --platform=$BUILDPLATFORM rust:slim-trixie AS base ENV DEBIAN_FRONTEND="noninteractive" \ BINSTALL_DISABLE_TELEMETRY=true \ @@ -24,25 +24,24 @@ RUN \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl git jq xz-utils make libclang-19-dev -# Zig is pinned to 0.13.0. Zig 0.14+ changed how its default target ABI is -# resolved for `x86_64-linux-gnu`-style triples, and on Stalwart's dep tree -# (rocksdb, jemalloc, aws-lc-sys, libfdb_c.so, rust-std 1.87+) this surfaces -# as ld.lld undefined references to pthread_*/stat/pow@GLIBC_2.33+. An -# explicit glibc suffix (`.2.17`) is NOT a workaround — C deps compiled via -# zig cc against Zig 0.16 headers still emit @2.33+ symbols even when the -# linker target is 2.17. Zig 0.13 produces binaries with a max glibc ref of -# ~2.28 which stays compatible with Debian 11 / RHEL 8. Revisit this pin if -# Zig upstream stabilizes a backwards-compatible default or if Stalwart drops -# its hard C deps. RUN \ - ZIG_VERSION=0.13.0 && \ - curl --retry 5 -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \ - ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ + ZIG_VERSION=0.17.0 && \ + case "$(uname -m)" in \ + x86_64) ZIG_SHA256=1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026 ;; \ + aarch64) ZIG_SHA256=9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8 ;; \ + *) exit 1 ;; \ + esac && \ + curl --retry 5 -fsSLo /tmp/zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-$(uname -m)-linux-${ZIG_VERSION}.tar.xz" && \ + echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - && \ + tar -J -x -C /usr/local -f /tmp/zig.tar.xz && \ + rm /tmp/zig.tar.xz && \ + ln -s "/usr/local/zig-$(uname -m)-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ zig version # Install cargo-binstall RUN curl --retry 5 -fL --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash # Install cargo-chef & sccache & cargo-zigbuild -RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 cargo-zigbuild@0.23.2 +RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 +RUN cargo install --locked --git https://github.com/rust-cross/cargo-zigbuild --rev d9f7fd32fbac13cc26d23847835f7f23a5ed3a25 cargo-zigbuild # ***************** # Planner @@ -61,6 +60,7 @@ WORKDIR /app COPY --from=planner /app/recipe.json recipe.json ARG TARGET ARG BUILD_ENV +ARG GLIBC_VERSION=2.28 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install toolchain and specify some env variables RUN \ @@ -68,6 +68,13 @@ RUN \ rustup target add ${TARGET} && \ mkdir -p artifact && \ touch /env-cargo && \ + if [[ "${TARGET}" == *-gnu* ]]; then \ + echo "export BUILD_TARGET=${TARGET}.${GLIBC_VERSION}" >> /env-cargo; \ + mkdir -p target && \ + ln -s "${TARGET}" "target/${TARGET}.${GLIBC_VERSION}"; \ + else \ + echo "export BUILD_TARGET=${TARGET}" >> /env-cargo; \ + fi && \ if [ ! -z "${BUILD_ENV}" ]; then \ echo "export ${BUILD_ENV}" >> /env-cargo; \ echo "Setting up ${BUILD_ENV}"; \ @@ -85,15 +92,22 @@ RUN \ exit 1; \ ;; \ esac; \ + target_var="${TARGET^^}"; \ + echo "export FDB_RUSTFLAGS_VAR=CARGO_TARGET_${target_var//-/_}_RUSTFLAGS" >> /env-cargo; \ fi # Install FoundationDB (pinned to latest 7.4.x; Apple publishes 7.4 as # prereleases on GitHub, so the release list is fetched without the # prerelease filter and narrowed by tag name). ARG FDB_VERSION_RANGE="7.4" RUN \ + --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - curl --retry 5 -fLso fdb-client.deb "$(curl --retry 5 -fLs 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + auth=() && \ + if [ -n "${GITHUB_TOKEN}" ]; then auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi && \ + fdb_url="$(curl --retry 5 -fLs "${auth[@]}" 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + [ -n "${fdb_url}" ] && \ + curl --retry 5 -fLso fdb-client.deb "${fdb_url}" && \ mkdir -p /fdb && \ dpkg -x fdb-client.deb /fdb && \ mv /fdb/usr/include/foundationdb /usr/include && \ @@ -108,7 +122,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats"; \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats"; \ fi RUN \ --mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \ @@ -116,7 +130,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" + cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" # Copy the source code COPY . . ENV RUSTC_WRAPPER="sccache" \ @@ -129,7 +143,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats" && \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats" && \ mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa-foundationdb; \ fi # Build generic version @@ -139,7 +153,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ + cargo zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa # ***************** diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 24934720..49526bdc 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "0.16.25" +version = "0.16.26" edition = "2024" build = "build.rs" @@ -86,6 +86,9 @@ quick-xml = "0.41" privdrop = "0.5.6" libc = "0.2.189" +[target.'cfg(target_os = "linux")'.dependencies] +sd-notify = "0.5" + [target.'cfg(windows)'.dependencies] socket2 = "0.6" diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index a23a51f8..cedf5606 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -214,7 +214,6 @@ impl Resolvers { let config_dnssec = resolver_config.clone(); let mut opts_dnssec = opts.clone(); opts_dnssec.validate = true; - opts_dnssec.num_concurrent_reqs = 1; let dnssec = DnssecResolver { resolver: TokioResolver::builder_with_config( @@ -344,7 +343,6 @@ impl Default for Resolvers { let config_dnssec = config.clone(); let mut opts_dnssec = opts.clone(); opts_dnssec.validate = true; - opts_dnssec.num_concurrent_reqs = 1; Self { dns: MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"), diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index 9462c12e..b5353af9 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -25,6 +25,8 @@ pub mod first_party; pub mod granted_permissions; // inbuxa: permissions added after roles were stored pub mod restore; pub mod spam_rules; // inbuxa: rules bundled with the server +#[cfg(target_os = "linux")] +pub mod systemd; pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes(); pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes(); diff --git a/crates/common/src/manager/systemd.rs b/crates/common/src/manager/systemd.rs new file mode 100644 index 00000000..81200ac7 --- /dev/null +++ b/crates/common/src/manager/systemd.rs @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceState { + Ready, + Reloading, + Stopping, +} + +impl ServiceState { + pub fn notify(self) { + use sd_notify::{NotifyState, notify}; + + let _ = match self { + ServiceState::Ready => notify(&[NotifyState::Ready]), + ServiceState::Stopping => notify(&[NotifyState::Stopping]), + ServiceState::Reloading => NotifyState::monotonic_usec_now() + .and_then(|now| notify(&[NotifyState::Reloading, now])), + }; + } +} diff --git a/crates/coordinator/Cargo.toml b/crates/coordinator/Cargo.toml index 6751dfa5..e5f9bdcb 100644 --- a/crates/coordinator/Cargo.toml +++ b/crates/coordinator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coordinator" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 550b2e10..fc38ace2 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav-proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index e59c6b7b..0d4943d9 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index d123cc55..fcd438d0 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "directory" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index d2d9f00d..464c9dad 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -87,12 +87,14 @@ impl OpenIdDirectory { self.validate_scopes(&token_data.claims)?; let mut claims = token_data.claims; + let missing_username = + is_claim_missing(&claims, Some(&self.config.claim_email)); let jwt_email = self.resolve_email(&claims).ok(); let missing_profile = is_claim_missing(&claims, self.config.claim_name.as_ref()) || is_claim_missing(&claims, self.config.claim_groups.as_ref()); - if jwt_email.is_none() || missing_profile { + if jwt_email.is_none() || missing_username || missing_profile { match self.fetch_userinfo(token).await { Ok(userinfo) => { if let (Some(base), Value::Object(extra)) = @@ -111,8 +113,8 @@ impl OpenIdDirectory { } let email = match jwt_email { - Some(email) => email, - None => self.resolve_email(&claims)?, + Some(email) if !missing_username => email, + _ => self.resolve_email(&claims)?, }; return self.build_account(email, &claims); } diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index a595d0df..ce3827f2 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "email" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 3540044d..ff993fe9 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "groupware" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index 5de066d4..847e3765 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 66b2384a..d98f5d15 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index 9331a57a..7be3049f 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 4580e9e6..46e3ab22 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index bdba5357..3d57f317 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap_proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 3ab7325c..7ed0169c 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 07dc784e..b52dbf9e 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -589,14 +589,14 @@ impl MailboxSet for Server { if !changes.name.is_empty() { // Obtain parent mailbox id let lower_name = changes.name.to_lowercase(); - if update - .as_ref() - .is_none_or(|(_, m)| m.inner.name != changes.name) - && let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| { - m.name.to_lowercase() == lower_name - && m.parent_id().map_or(0, |id| id + 1) == changes.parent_id - }) - { + let current_id = update.as_ref().map(|(document_id, _)| *document_id); + if update.as_ref().is_none_or(|(_, m)| { + m.inner.name != changes.name || m.inner.parent_id != changes.parent_id + }) && let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| { + Some(m.document_id) != current_id + && m.parent_id().map_or(0, |id| id + 1) == changes.parent_id + && m.name.to_lowercase() == lower_name + }) { return Ok(Err(SetError::already_exists() .with_existing_id(Id::from(existing.document_id)) .with_description(format!( diff --git a/crates/jmap/src/registry/mapping/action.rs b/crates/jmap/src/registry/mapping/action.rs index 2b0eb80a..bf91cc16 100644 --- a/crates/jmap/src/registry/mapping/action.rs +++ b/crates/jmap/src/registry/mapping/action.rs @@ -7,6 +7,8 @@ */ use crate::registry::mapping::{RegistrySetResponse, map_bootstrap_error}; +#[cfg(target_os = "linux")] +use common::manager::systemd::ServiceState; use common::{ Server, config::mailstore::spamfilter::SpamFilterAction, @@ -88,8 +90,13 @@ pub(crate) async fn action_set( Action::ReloadBlockedIps => ObjectType::BlockedIp, _ => unreachable!(), }; + #[cfg(target_os = "linux")] + ServiceState::Reloading.notify(); let result = - Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await?; + Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await; + #[cfg(target_os = "linux")] + ServiceState::Ready.notify(); + let result = result?; if !result.has_errors() { set.server diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 18c5713f..6531eb7a 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -81,6 +81,7 @@ impl EmailSubmissionSet for Server { // Process creates let mut success_email_ids = HashMap::new(); + let mut changed_email_ids = HashMap::new(); let mut batch = BatchBuilder::new(); for (id, object) in request.unwrap_create() { match self @@ -263,6 +264,8 @@ impl EmailSubmissionSet for Server { // Update record let mut new_submission = submission.inner.clone(); new_submission.undo_status = UndoStatus::Canceled; + let email_id = + Id::from_parts(new_submission.thread_id, new_submission.email_id); batch .with_account_id(account_id) .with_collection(Collection::EmailSubmission) @@ -274,6 +277,7 @@ impl EmailSubmissionSet for Server { ) .caused_by(trc::location!())? .commit_point(); + changed_email_ids.insert(id, email_id); response.updated.append(id, None); } else { response.not_updated.append( @@ -315,19 +319,21 @@ impl EmailSubmissionSet for Server { .await? { // Update record + let submission = submission + .to_unarchived::() + .caused_by(trc::location!())?; + let email_id = Id::from_parts( + u32::from(submission.inner.thread_id), + u32::from(submission.inner.email_id), + ); batch .with_account_id(account_id) .with_collection(Collection::EmailSubmission) .with_document(document_id) - .custom( - ObjectIndexBuilder::<_, ()>::new().with_current( - submission - .to_unarchived::() - .caused_by(trc::location!())?, - ), - ) + .custom(ObjectIndexBuilder::<_, ()>::new().with_current(submission)) .caused_by(trc::location!())? .commit_point(); + changed_email_ids.insert(id, email_id); response.destroyed.push(id); } else { response.not_destroyed.append(id, SetError::not_found()); @@ -345,18 +351,34 @@ impl EmailSubmissionSet for Server { } // On success - if (request + let resolve_email_id = |id: MaybeIdReference| match id { + MaybeIdReference::Id(id) => changed_email_ids.get(&id).copied(), + MaybeIdReference::Reference(id_ref) => success_email_ids.get(&id_ref).copied(), + MaybeIdReference::Invalid(_) => None, + }; + let update = request + .arguments + .on_success_update_email + .map(|update| { + update + .into_iter() + .filter_map(|(id, value)| { + resolve_email_id(id).map(|id| (MaybeInvalid::Value(id), value)) + }) + .collect::>() + }) + .filter(|update| !update.is_empty()); + let destroy = request .arguments .on_success_destroy_email - .as_ref() - .is_some_and(|p| !p.is_empty()) - || request - .arguments - .on_success_update_email - .as_ref() - .is_some_and(|p| !p.is_empty())) - && response.has_changes() - { + .map(|ids| { + ids.into_iter() + .filter_map(resolve_email_id) + .map(MaybeInvalid::Value) + .collect::>() + }) + .filter(|ids| !ids.is_empty()); + if update.is_some() || destroy.is_some() { *next_call = Call { id: String::new(), name: MethodName::new(MethodObject::Email, MethodFunction::Set), @@ -364,38 +386,8 @@ impl EmailSubmissionSet for Server { account_id: request.account_id, if_in_state: None, create: None, - update: request.arguments.on_success_update_email.map(|update| { - update - .into_iter() - .filter_map(|(id, value)| { - ( - match id { - MaybeIdReference::Id(id) => MaybeInvalid::Value(id), - MaybeIdReference::Reference(id_ref) => { - MaybeInvalid::Value(*(success_email_ids.get(&id_ref)?)) - } - MaybeIdReference::Invalid(id) => MaybeInvalid::Invalid(id), - }, - value, - ) - .into() - }) - .collect() - }), - destroy: request.arguments.on_success_destroy_email.map(|ids| { - MaybeResultReference::Value( - ids.into_iter() - .filter_map(|id| match id { - MaybeIdReference::Id(id) => Some(id), - MaybeIdReference::Reference(id_ref) => { - success_email_ids.get(&id_ref).copied() - } - MaybeIdReference::Invalid(_) => None, - }) - .map(MaybeInvalid::Value) - .collect(), - ) - }), + update, + destroy: destroy.map(MaybeResultReference::Value), arguments: Default::default(), }))), } diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index bcdb3261..44524906 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -142,7 +142,7 @@ impl VacationResponseSet for Server { // Parse properties let mut is_active = document_id.is_some_and(|id| active_script_id == Some(id)); - let mut build_script = create_id.is_some(); + let mut build_script = create_id.is_some() || sieve.blob_hash.is_empty(); let vacation = sieve.vacation_response.as_mut().unwrap(); for (property, mut value) in changes.into_expanded_object() { diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 64ebcdde..a9cbc9f4 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] categories = ["email"] # Upstream offers AGPL-3.0-only OR LicenseRef-SEL; inbuxa takes the AGPL only. license = "AGPL-3.0-only" -version = "0.16.25" +version = "0.16.26" edition = "2024" [[bin]] @@ -41,6 +41,9 @@ migration = { path = "../migration" } tokio = { version = "1.53", features = ["full"] } rustls = { version = "0.23.43", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } +[target.'cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "freebsd")))'.dependencies] +libc = "0.2.189" + [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] tikv-jemallocator = "0.7" diff --git a/crates/main/src/cpu.rs b/crates/main/src/cpu.rs new file mode 100644 index 00000000..03227996 --- /dev/null +++ b/crates/main/src/cpu.rs @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + * + * Modified by Coffey Labs in 2026 for INBUXA. + */ + +use std::arch::x86_64::__cpuid; + +const LEAF_FEATURES: u32 = 0x0000_0001; +const LEAF_EXTENDED_MAX: u32 = 0x8000_0000; +const LEAF_EXTENDED_FEATURES: u32 = 0x8000_0001; + +const SSE3: u32 = 1 << 0; +const SSSE3: u32 = 1 << 9; +const CMPXCHG16B: u32 = 1 << 13; +const SSE4_1: u32 = 1 << 19; +const SSE4_2: u32 = 1 << 20; +const POPCNT: u32 = 1 << 23; +const LAHF_SAHF: u32 = 1 << 0; + +const FEATURES_X86_64_V2: u32 = SSE3 | SSSE3 | CMPXCHG16B | SSE4_1 | SSE4_2 | POPCNT; +const EXTENDED_FEATURES_X86_64_V2: u32 = LAHF_SAHF; + +const UNSUPPORTED_CPU: &[u8] = b"This build of inbuxa requires an x86-64-v2 compatible CPU (SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF). If inbuxa runs in a virtual machine, set its CPU type to 'host' or 'x86-64-v2'.\n"; +const EXIT_UNSUPPORTED_CPU: i32 = 1; + +#[used] +#[unsafe(link_section = ".init_array.00001")] +static ASSERT_X86_64_V2: extern "C" fn() = assert_x86_64_v2; + +extern "C" fn assert_x86_64_v2() { + let features = __cpuid(LEAF_FEATURES).ecx; + let extended_features = if __cpuid(LEAF_EXTENDED_MAX).eax >= LEAF_EXTENDED_FEATURES { + __cpuid(LEAF_EXTENDED_FEATURES).ecx + } else { + 0 + }; + + if features & FEATURES_X86_64_V2 != FEATURES_X86_64_V2 + || extended_features & EXTENDED_FEATURES_X86_64_V2 != EXTENDED_FEATURES_X86_64_V2 + { + unsafe { + libc::write( + libc::STDERR_FILENO, + UNSUPPORTED_CPU.as_ptr().cast(), + UNSUPPORTED_CPU.len(), + ); + libc::_exit(EXIT_UNSUPPORTED_CPU); + } + } +} diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index c61d5c30..7e03e8bb 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -11,6 +11,8 @@ #![warn(clippy::cast_possible_wrap)] #![warn(clippy::cast_sign_loss)] +#[cfg(target_os = "linux")] +use common::manager::systemd::ServiceState; use common::{ BuildServer, Inner, config::server::{Listener, ServerProtocol}, @@ -29,6 +31,12 @@ use tokio::sync::watch; use trc::Collector; use utils::wait_for_shutdown; +#[cfg(all( + target_arch = "x86_64", + target_feature = "sse4.2", + any(target_os = "linux", target_os = "freebsd") +))] +mod cpu; #[cfg(feature = "dev_mode")] pub mod test_data; @@ -106,9 +114,15 @@ async fn main() -> std::io::Result<()> { let inner = init.inner.clone(); spawn_broadcast_subscriber(init.inner, shutdown_rx); + #[cfg(target_os = "linux")] + ServiceState::Ready.notify(); + // Wait for shutdown signal wait_for_shutdown().await; + #[cfg(target_os = "linux")] + ServiceState::Stopping.notify(); + // inbuxa: hand back the task locks this node holds, so other nodes can // run those tasks now rather than when the locks expire services::task_manager::lock::release_task_locks(&inner.build_server()).await; diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 9b02fd7f..90eafdf5 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index f711de36..db658149 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migration" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index b8c95419..269ce4a3 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 211b87c0..59e41d8a 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pop3" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 263c2a12..fbecd43c 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "registry" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/scim-proto/Cargo.toml b/crates/scim-proto/Cargo.toml index bd822392..b9fb3719 100644 --- a/crates/scim-proto/Cargo.toml +++ b/crates/scim-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim-proto" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/scim/Cargo.toml b/crates/scim/Cargo.toml index 05055ee4..e9e727ed 100644 --- a/crates/scim/Cargo.toml +++ b/crates/scim/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scim" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 22fe66a4..f955d51c 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "services" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 8a68f5e7..c9f8066c 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -6,7 +6,7 @@ homepage = "https://inbuxa.org" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/smtp/src/inbound/milter/client.rs b/crates/smtp/src/inbound/milter/client.rs index df78929f..70a5c778 100644 --- a/crates/smtp/src/inbound/milter/client.rs +++ b/crates/smtp/src/inbound/milter/client.rs @@ -252,8 +252,9 @@ impl MilterClient { } } } + } - // Write EndOfHeaders + if !self.has_option(SMFIP_NOEOH) { self.write(Command::EndOfHeader).await?; if !self.has_option(SMFIP_NR_EOH) { return self.read().await?.into_action(); diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs index bc136624..f71189c3 100644 --- a/crates/smtp/src/outbound/dane/dnssec.rs +++ b/crates/smtp/src/outbound/dane/dnssec.rs @@ -436,7 +436,9 @@ impl TlsaLookup for Server { } // inbuxa: hickory 0.26.3 calls some valid answers bogus, and the queue then -// retries those hosts until the message expires. Two cases seen in production: +// retries those hosts until the message expires. Two cases seen in production +// (the hickory vendored since upstream v0.16.26 gets both right in the +// `--ignored` tests below; this stays as the fallback): // // - A zone delegated beneath an unsigned zone, such as `l.google.com` under // `google.com`. To prove the delegation insecure, hickory wants an SOA @@ -844,7 +846,10 @@ mod tests { } // Needs the network: a signed MX pointing into a zone delegated beneath an - // unsigned one. Run with `--ignored` to check a hickory upgrade. + // unsigned one. Run with `--ignored` to check a hickory upgrade. hickory + // 0.26.3 called it bogus and the recheck proved it insecure; the vendored + // hickory proves it itself. Either way the answer has to come out insecure. + // The TLSA test below prints whether hickory alone gets its case right. #[tokio::test] #[ignore] async fn validated_lookup_proves_delegation_below_unsigned_zone() { @@ -870,12 +875,17 @@ mod tests { }; let (dnssec, plain) = (build(true), build(false)); - let validated = - validated_lookup(&dnssec, &plain, name("aspmx.l.google.com."), RecordType::A) - .await - .unwrap(); - assert!(validated.insecure); + let query = name("aspmx.l.google.com."); + let validated = validated_lookup(&dnssec, &plain, query.clone(), RecordType::A) + .await + .unwrap(); assert!(!validated.lookup.answers().is_empty()); + let status = if validated.insecure { + DnssecStatus::Insecure + } else { + tlsa_base_status(&query, validated.lookup.answers(), RecordType::A) + }; + assert_eq!(status, DnssecStatus::Insecure); } // Needs the network: a TLSA name that is a signed CNAME to the zone apex, diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index edadd777..980a1a54 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spam-filter" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 0a421fc5..c20dc952 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "store" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] @@ -12,7 +12,7 @@ registry = { path = "../registry" } rocksdb = { version = "0.25", optional = true, features = ["multi-threaded-cf"] } foundationdb = { version = "0.11", features = ["embedded-fdb-include", "fdb-7_4"], optional = true } rusqlite = { version = "0.40", features = ["bundled"], optional = true } -rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"], optional = true } +rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"], optional = true } reqwest_s3 = { package = "reqwest", version = "0.12", default-features = false, features = ["rustls-tls-native-roots"], optional = true } azure_core = { version = "0.21.0", optional = true } azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true } diff --git a/crates/store/src/backend/rocksdb/main.rs b/crates/store/src/backend/rocksdb/main.rs index 798edf5a..e48b809b 100644 --- a/crates/store/src/backend/rocksdb/main.rs +++ b/crates/store/src/backend/rocksdb/main.rs @@ -74,9 +74,8 @@ impl RocksDbStore { let mut cf_opts = cf_options(CfProfile::Blob, &cache, write_buffer_size); cf_opts.set_enable_blob_files(true); cf_opts.set_min_blob_size(config.blob_size); - cf_opts.set_enable_blob_gc(true); + cf_opts.set_enable_blob_gc(false); cf_opts.set_blob_gc_age_cutoff(1.0); - cf_opts.set_blob_gc_force_threshold(0.5); cfs.push(ColumnFamilyDescriptor::new(CF_BLOBS, cf_opts)); // Other cfs diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index fbe77ade..3d8d2ac4 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -6,8 +6,8 @@ use super::{CF_INDEXES, CF_LOGS, CfHandle, RocksDbStore, into_error}; use crate::{ - Deserialize, IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, - SUBSPACE_QUOTA, + Deserialize, IndexKey, Key, LogKey, SUBSPACE_BLOBS, SUBSPACE_COUNTER, + SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA, backend::deserialize_i64_le, write::{ AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation, @@ -16,8 +16,8 @@ use crate::{ }; use rand::RngExt; use rocksdb::{ - BoundColumnFamily, ErrorKind, IteratorMode, OptimisticTransactionDB, - OptimisticTransactionOptions, WriteOptions, + BottommostLevelCompaction, BoundColumnFamily, CompactOptions, ErrorKind, IteratorMode, + OptimisticTransactionDB, OptimisticTransactionOptions, WriteOptions, }; use std::{ sync::Arc, @@ -25,6 +25,9 @@ use std::{ time::{Duration, Instant}, }; +const BLOB_GC_MIN_GARBAGE: u64 = 256 * 1024 * 1024; +const BLOB_GC_GARBAGE_RATIO: f64 = 0.25; + impl RocksDbStore { pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result { let db = self.db.clone(); @@ -114,6 +117,28 @@ impl RocksDbStore { } } + let cf = db.subspace_handle(SUBSPACE_BLOBS); + let total = db + .property_int_value_cf(&cf, "rocksdb.live-blob-file-size") + .map_err(into_error)? + .unwrap_or_default(); + let garbage = db + .property_int_value_cf(&cf, "rocksdb.live-blob-file-garbage-size") + .map_err(into_error)? + .unwrap_or_default(); + + if garbage >= BLOB_GC_MIN_GARBAGE + && garbage as f64 >= total as f64 * BLOB_GC_GARBAGE_RATIO + { + db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "true")]) + .map_err(into_error)?; + let mut opts = CompactOptions::default(); + opts.set_bottommost_level_compaction(BottommostLevelCompaction::Force); + db.compact_range_cf_opt(&cf, None::<&[u8]>, None::<&[u8]>, &opts); + db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "false")]) + .map_err(into_error)?; + } + Ok(()) }) .await diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 68dfd0a2..5049902a 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trc" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml index 094ad47c..9d62c9d8 100644 --- a/crates/trc/event-macro/Cargo.toml +++ b/crates/trc/event-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "event_macro" -version = "0.16.25" +version = "0.16.26" edition = "2024" [lib] diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 70ce4493..5fb53204 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "types" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index b9fd9331..09c235f5 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.16.25" +version = "0.16.26" edition = "2024" [dependencies] diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml index 85ae8e42..3391f73e 100644 --- a/crates/utils/proc-macros/Cargo.toml +++ b/crates/utils/proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proc_macros" -version = "0.16.25" +version = "0.16.26" edition = "2024" [lib] diff --git a/docker-bake.hcl b/docker-bake.hcl index ec490a35..2cc42fe5 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -17,7 +17,8 @@ target "docker-metadata-action" {} target "build" { secret = [ "type=env,id=ACTIONS_RESULTS_URL", - "type=env,id=ACTIONS_RUNTIME_TOKEN" + "type=env,id=ACTIONS_RUNTIME_TOKEN", + "type=env,id=GITHUB_TOKEN" ] args = { TARGET = "${TARGET}" diff --git a/resources/systemd/inbuxa.service b/resources/systemd/inbuxa.service index f52d708f..7c9e23b6 100644 --- a/resources/systemd/inbuxa.service +++ b/resources/systemd/inbuxa.service @@ -5,7 +5,7 @@ ConditionPathExists=__PATH__/etc/config.json After=network-online.target [Service] -Type=simple +Type=notify LimitNOFILE=65536 KillMode=process KillSignal=SIGINT diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 04c18f0f..80f0e529 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.16.25" +version = "0.16.26" edition = "2024" [features] @@ -89,7 +89,7 @@ sha2 = "0.11" zip = "8.6" # inbuxa: reading legal hold exports time = "0.3" testcontainers = { version = "0.28", features = ["reusable-containers"] } -rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"] } +rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"] } [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] tikv-jemallocator = "0.7" diff --git a/tests/docker/keycloak/stalwart-realm.json b/tests/docker/keycloak/stalwart-realm.json index e61a46bb..43b051b0 100644 --- a/tests/docker/keycloak/stalwart-realm.json +++ b/tests/docker/keycloak/stalwart-realm.json @@ -133,6 +133,72 @@ } } ] + }, + { + "clientId": "stalwart-userinfo-username", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "stalwart-userinfo-username-secret", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "publicClient": false, + "protocol": "openid-connect", + "directAccessGrantsEnabled": true, + "standardFlowEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "openid" + ], + "optionalClientScopes": [ + "email", + "profile", + "roles" + ], + "protocolMappers": [ + { + "name": "email-claim", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "userinfo.token.claim": "true", + "jsonType.label": "String" + } + }, + { + "name": "login-userinfo-only", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "consentRequired": false, + "config": { + "claim.name": "login", + "claim.value": "jdoe", + "id.token.claim": "false", + "access.token.claim": "false", + "userinfo.token.claim": "true", + "jsonType.label": "String" + } + }, + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "stalwart", + "id.token.claim": "false", + "access.token.claim": "true" + } + } + ] } ], "users": [ diff --git a/tests/src/jmap/mail/mailbox.rs b/tests/src/jmap/mail/mailbox.rs index 81e5fa71..32210b9f 100644 --- a/tests/src/jmap/mail/mailbox.rs +++ b/tests/src/jmap/mail/mailbox.rs @@ -229,6 +229,37 @@ pub async fn test(test: &TestServer) { "{result:?}", ); + // Moving a mailbox under a parent that has a sibling with the same name fails + let nested_id = client + .mailbox_create("level 2", Some(&id_map["l.1"]), Role::None) + .await + .unwrap() + .take_id(); + let result = client + .mailbox_move(&nested_id, Some(&id_map["inbox"])) + .await; + assert!( + matches!( + result, + Err(Error::Set(SetError { + type_: SetErrorType::AlreadyExists, + .. + })) + ), + "{result:?}", + ); + client.mailbox_destroy(&nested_id, true).await.unwrap(); + + // Changing only the case of a mailbox name succeeds + client + .mailbox_rename(&id_map["l.2"], "LEVEL 2") + .await + .unwrap(); + client + .mailbox_rename(&id_map["l.2"], "Level 2") + .await + .unwrap(); + // Circular relationship let mut request = client.build(); request diff --git a/tests/src/jmap/mail/set.rs b/tests/src/jmap/mail/set.rs index 660bc195..0080382e 100644 --- a/tests/src/jmap/mail/set.rs +++ b/tests/src/jmap/mail/set.rs @@ -363,7 +363,10 @@ async fn update_preserves_uids( assert_eq!(uids[&INBOX_ID], inbox_uid); assert_ne!(uids[&test_mailbox_document_id], 0); - client.mailbox_destroy(&test_mailbox_id, true).await.unwrap(); + client + .mailbox_destroy(&test_mailbox_id, true) + .await + .unwrap(); } async fn message_uids( diff --git a/tests/src/jmap/mail/submission.rs b/tests/src/jmap/mail/submission.rs index 110390a5..52827cd7 100644 --- a/tests/src/jmap/mail/submission.rs +++ b/tests/src/jmap/mail/submission.rs @@ -510,6 +510,117 @@ pub async fn test(test: &TestServer) { .unwrap() .is_none() ); + + // onSuccess actions keyed by an existing submission id act on its own email + let mut held = Vec::with_capacity(2); + for subject in ["held 1", "held 2"] { + let draft_id = client + .email_import( + format!("From: jdoe@example.com\r\nTo: jane_smith@remote.org\r\nSubject: {subject}\r\n\r\ntest") + .into_bytes(), + [&mailbox_id], + None::>, + None, + ) + .await + .unwrap() + .take_id(); + let submission_id = client + .email_submission_create_envelope( + &draft_id, + &identity_id, + Address::new("jdoe@example.com") + .parameter("HOLDUNTIL", Some(hold_until_date.to_string())), + ["jane_smith@remote.org"], + ) + .await + .unwrap() + .take_id(); + held.push((submission_id, draft_id)); + } + let [(submission_id_1, draft_id_1), (submission_id_2, draft_id_2)] = + <[_; 2]>::try_from(held).unwrap(); + let mut found_filler = false; + for _ in 0..64 { + if client + .email_get(&submission_id_1, None::>) + .await + .unwrap() + .is_some() + { + found_filler = true; + break; + } + client + .email_import( + b"From: jdoe@example.com\r\nSubject: filler\r\n\r\ntest".to_vec(), + [&mailbox_id_2], + None::>, + None, + ) + .await + .unwrap(); + } + assert!(found_filler, "No email shares the submission document id"); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request + .update(&submission_id_1) + .undo_status(UndoStatus::Canceled); + set_request + .arguments() + .on_success_destroy_email_id(&submission_id_1); + request.send().await.unwrap().unwrap_method_responses(); + assert!( + client + .email_get(&draft_id_1, None::>) + .await + .unwrap() + .is_none() + ); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + + // onSuccessUpdateEmail is skipped for a submission whose update failed + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request + .update(&submission_id_1) + .undo_status(UndoStatus::Canceled); + set_request + .update(&submission_id_2) + .undo_status(UndoStatus::Canceled); + set_request + .arguments() + .on_success_update_email_id(&submission_id_1) + .keyword("$draft", true); + set_request + .arguments() + .on_success_update_email_id(&submission_id_2) + .keyword("$draft", true) + .mailbox_id(&mailbox_id, false) + .mailbox_id(&mailbox_id_2, true); + request.send().await.unwrap().unwrap_method_responses(); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; + assert_email_properties(&client, &draft_id_2, &[&mailbox_id_2], &["$draft"]).await; + + // onSuccessDestroyEmail keyed by a destroyed submission id + let mut request = client.build(); + let set_request = request.set_email_submission(); + set_request.destroy([&submission_id_2]); + set_request + .arguments() + .on_success_destroy_email_id(&submission_id_2); + request.send().await.unwrap().unwrap_method_responses(); + assert!( + client + .email_get(&draft_id_2, None::>) + .await + .unwrap() + .is_none() + ); + assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await; smtp_settings.lock().do_stop = true; // Identities are created and destroyed as the account's addresses change diff --git a/tests/src/jmap/mail/vacation_response.rs b/tests/src/jmap/mail/vacation_response.rs index 0eb06502..d200f998 100644 --- a/tests/src/jmap/mail/vacation_response.rs +++ b/tests/src/jmap/mail/vacation_response.rs @@ -11,6 +11,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection}, }; use chrono::{TimeDelta, Utc}; +use jmap_client::core::response::VacationResponseSetResponse; use std::time::Instant; pub async fn test(test: &TestServer) { @@ -29,6 +30,42 @@ pub async fn test(test: &TestServer) { Instant::now() + std::time::Duration::from_secs(10), ); + // Enabling with only isEnabled on a fresh account uses the default response + let mut request = client.build(); + request + .set_vacation_response() + .update("singleton") + .is_enabled(true); + request + .send_single::() + .await + .unwrap() + .updated("singleton") + .unwrap(); + let mut lmtp = SmtpConnection::connect().await; + lmtp.ingest( + "michael@remote.org", + &["jdoe@example.com"], + concat!( + "From: michael@remote.org\r\n", + "To: jdoe@example.com\r\n", + "Subject: Printer\r\n", + "\r\n", + "PC load letter? What does that even mean?", + ), + ) + .await; + lmtp.quit().await; + assert_message_delivery( + &mut smtp_rx, + MockMessage::new( + "", + [""], + "@I am away.", + ), + ) + .await; + // Let people know that we'll be down in Kokomo client .vacation_response_enable( diff --git a/vendor/hickory-dns/VENDORED.md b/vendor/hickory-dns/VENDORED.md new file mode 100644 index 00000000..9c3cd1da --- /dev/null +++ b/vendor/hickory-dns/VENDORED.md @@ -0,0 +1,41 @@ +# hickory-dns, vendored + +`hickory-proto`, `hickory-net` and `hickory-resolver` 0.26.3 as upstream +builds them since v0.16.26: from its patched branch of +[hickory-dns](https://github.com/hickory-dns/hickory-dns), `stalwart/0.26` +at commit `f8b92b17fa6c7d09a6959672592106ca1410937b` (2026-10-11), under +MIT OR Apache-2.0. INBUXA takes them under MIT. The root `Cargo.toml` +patches them in with `[patch.crates-io]`, so builds never fetch from +upstream's repositories. + +That branch is hickory's own `release/0.26` (unreleased work after 0.26.3, +including DS lookups that check the parent zone when an answer carries no +insecure proof) plus nine DNSSEC commits: finding the zone cut with DS +queries, accepting opt-out DS proofs for the next closer name, setting the +CD bit while validating, searching ancestors when a DS lookup fails, +wildcard validation for all RRsets, and a TCP retry for a late truncated +UDP answer. + +## Changes + +None. Each directory is `cargo package --no-verify` of that commit (the +manifest normalized, so it builds outside hickory's workspace), without +`Cargo.lock` and `Cargo.toml.orig`. `.cargo_vcs_info.json` records the +commit. + +Both answers hickory 0.26.3 called bogus in production (an insecure +delegation below an unsigned zone, and a signed CNAME to a name without the +type asked for) validate with this copy: the `--ignored` tests at the end of +`crates/smtp/src/outbound/dane/dnssec.rs` show it, the TLSA one through +Hetzner's resolver (2026-10-12). `validated_lookup` there still rechecks +answers hickory calls bogus, as a fallback. + +## Updating + +When upstream moves its hickory pin, or hickory publishes a release that +carries these fixes, re-vendor: clone the branch at the new commit, run +`cargo package --no-verify -p hickory-proto -p hickory-net -p +hickory-resolver`, unpack each `.crate` here, and drop `Cargo.lock` and +`Cargo.toml.orig`. If the version changes, Cargo stops using these copies +and only warns that the patch went unused, so check `Cargo.lock` has no +`hickory-*` entry with a `source` line. Update the commit above. diff --git a/vendor/hickory-dns/hickory-net/Cargo.toml b/vendor/hickory-dns/hickory-net/Cargo.toml new file mode 100644 index 00000000..54ddf31b --- /dev/null +++ b/vendor/hickory-dns/hickory-net/Cargo.toml @@ -0,0 +1,376 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.88" +name = "hickory-net" +version = "0.26.3" +authors = ["The contributors to Hickory DNS"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = """ +hickory-net is a safe and secure low-level DNS library. This is the foundational DNS protocol +library used by the other higher-level Hickory DNS crates. +""" +homepage = "https://hickory-dns.org/" +documentation = "https://docs.rs/hickory-net" +readme = "README.md" +keywords = [ + "dns", + "dnssec", +] +categories = ["network-programming"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/hickory-dns/hickory-dns" + +[package.metadata.docs.rs] +all-features = true +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.cargo-all-features] +skip_optional_dependencies = true +max_combination_size = 2 +denylist = [ + "__tls", + "__https", + "__quic", + "__h3", + "__dnssec", +] + +[features] +__dnssec = [ + "dep:bitflags", + "dep:rustls-pki-types", + "dep:time", + "dep:lru-cache", + "dep:parking_lot", +] +__h3 = [ + "dep:h3", + "dep:h3-quinn", + "dep:http", +] +__https = [ + "dep:bytes", + "dep:h2", + "dep:http", +] +__quic = [ + "dep:bytes", + "dep:pin-project-lite", + "dep:quinn", +] +__tls = [ + "dep:bytes", + "dep:rustls", + "dep:tokio-rustls", + "tokio", +] +default = ["tokio"] +dnssec-aws-lc-rs = [ + "dep:aws-lc-rs", + "aws-lc-rs/aws-lc-sys", + "aws-lc-rs/ring-io", + "hickory-proto/dnssec-aws-lc-rs", + "__dnssec", +] +dnssec-ring = [ + "dep:ring", + "hickory-proto/dnssec-ring", + "__dnssec", +] +h3-aws-lc-rs = [ + "quic-aws-lc-rs", + "__h3", +] +h3-ring = [ + "quic-ring", + "__h3", +] +https-aws-lc-rs = [ + "tls-aws-lc-rs", + "__https", +] +https-ring = [ + "tls-ring", + "__https", +] +mdns = ["socket2/all"] +quic-aws-lc-rs = [ + "quinn/rustls-aws-lc-rs", + "tls-aws-lc-rs", + "__quic", +] +quic-ring = [ + "quinn/rustls-ring", + "tls-ring", + "__quic", +] +rustls-platform-verifier = ["dep:rustls-platform-verifier"] +serde = [ + "dep:serde", + "url/serde", +] +tls-aws-lc-rs = [ + "tokio-rustls/aws-lc-rs", + "__tls", +] +tls-ring = [ + "tokio-rustls/ring", + "__tls", +] +tokio = [ + "dep:tokio", + "tokio/net", + "tokio/rt", + "tokio/time", + "tokio/rt-multi-thread", +] + +[lib] +name = "hickory_net" +path = "src/lib.rs" + +[dependencies.async-trait] +version = "0.1.43" + +[dependencies.aws-lc-rs] +version = "1.12.3" +features = ["prebuilt-nasm"] +optional = true +default-features = false + +[dependencies.bitflags] +version = "2.4.1" +optional = true + +[dependencies.bytes] +version = "1" +optional = true + +[dependencies.cfg-if] +version = "1" + +[dependencies.data-encoding] +version = "2.2.0" +features = ["alloc"] +default-features = false + +[dependencies.futures-channel] +version = "0.3.5" +features = [ + "alloc", + "std", +] +default-features = false + +[dependencies.futures-io] +version = "0.3.5" +features = ["std"] +default-features = false + +[dependencies.futures-util] +version = "0.3.5" +features = [ + "alloc", + "async-await-macro", + "std", +] +default-features = false + +[dependencies.h2] +version = "0.4.0" +features = ["stream"] +optional = true + +[dependencies.h3] +version = "0.0.8" +optional = true + +[dependencies.h3-quinn] +version = "0.0.10" +optional = true + +[dependencies.hickory-proto] +version = "0.26.3" +features = ["std"] +default-features = false + +[dependencies.http] +version = "1.1" +optional = true + +[dependencies.idna] +version = "1.0.3" +features = [ + "alloc", + "compiled_data", +] +default-features = false + +[dependencies.ipnet] +version = "2.11.0" +default-features = false + +[dependencies.lru-cache] +version = "0.1.2" +optional = true + +[dependencies.parking_lot] +version = "0.12" +optional = true + +[dependencies.pin-project-lite] +version = "0.2" +optional = true + +[dependencies.quinn] +version = "0.11.2" +features = [ + "log", + "runtime-tokio", +] +optional = true +default-features = false + +[dependencies.rand] +version = "0.10.0" +features = [ + "alloc", + "std_rng", +] +default-features = false + +[dependencies.ring] +version = "0.17" +features = ["std"] +optional = true + +[dependencies.rustls] +version = "0.23.23" +features = [ + "logging", + "std", + "tls12", +] +optional = true +default-features = false + +[dependencies.rustls-pki-types] +version = "1.10" +optional = true + +[dependencies.rustls-platform-verifier] +version = "0.7" +optional = true + +[dependencies.serde] +version = "1.0" +features = ["derive"] +optional = true + +[dependencies.socket2] +version = "0.6" +optional = true + +[dependencies.thiserror] +version = "2" +default-features = false + +[dependencies.time] +version = "0.3" +optional = true + +[dependencies.tinyvec] +version = "1.1.1" +features = ["alloc"] + +[dependencies.tokio] +version = "1.36" +features = [ + "io-util", + "macros", +] +optional = true + +[dependencies.tokio-rustls] +version = "0.26" +features = ["early-data"] +optional = true +default-features = false + +[dependencies.tracing] +version = "0.1.30" +default-features = false + +[dependencies.url] +version = "2.5.4" +default-features = false + +[dependencies.webpki-roots] +version = "1" +optional = true + +[dev-dependencies.futures-executor] +version = "0.3.5" +features = ["std"] +default-features = false + +[dev-dependencies.tokio] +version = "1.36" +features = [ + "rt", + "time", + "macros", + "test-util", +] + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.22.1" + +[lints.clippy] +bool_to_int_with_if = "allow" +default_trait_access = "warn" +manual_let_else = "warn" +or_fun_call = "warn" +single_component_path_imports = "allow" +unimplemented = "warn" +upper_case_acronyms = "allow" +use_self = "warn" + +[lints.rust] +non_snake_case = "warn" +non_upper_case_globals = "warn" +rust_2018_idioms = "warn" +unnameable_types = "warn" +unreachable_pub = "warn" +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = ["cfg(nightly)"] diff --git a/vendor/hickory-dns/hickory-net/LICENSE-APACHE b/vendor/hickory-dns/hickory-net/LICENSE-APACHE new file mode 100644 index 00000000..d5dd862b --- /dev/null +++ b/vendor/hickory-dns/hickory-net/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/hickory-dns/hickory-net/LICENSE-MIT b/vendor/hickory-dns/hickory-net/LICENSE-MIT new file mode 100644 index 00000000..cc6341a3 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright (c) 2015 The Hickory DNS Developers +Copyright (c) 2017 Google LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/hickory-dns/hickory-net/README.md b/vendor/hickory-dns/hickory-net/README.md new file mode 100644 index 00000000..4fe4a59f --- /dev/null +++ b/vendor/hickory-dns/hickory-net/README.md @@ -0,0 +1,48 @@ +# Overview + +`hickory-net` provides network protocol implementations for DNS message transport. +This is a support library used by the other higher-level Hickory DNS crates. + +For higher-level interfaces, refer to the [`hickory-server`] and [`hickory-resolver`] +library crates instead. + +[`hickory-server`]: ../server +[`hickory-resolver`]: ../resolver + +## Cryptography provider + +Features requiring cryptography require selecting a specific cryptography +provider. See the [project README] for more information. + +[project README]: ../../README.md#Cryptography-provider + +## Protocol support + +The following DNS protocols are optionally supported: + +* DNS over TLS (DoT) +* DNS over HTTP/2 (DoH) +* DNS over QUIC (DoQ) +* DNS over HTTP/3 (DoH3) + +In order to use these optional protocols you must enable a cargo feature +corresponding to your desired cryptography provider: + +* DoT: `tls-aws-lc-rs` or `tls-ring`. +* DoH: `https-aws-lc-rs` or `https-ring` +* DoQ: `quic-aws-lc-rs` or `quic-ring` +* DoH3: `h3-aws-lc-rs` or `h3-ring` + +## DNSSEC + +In order to use DNSSEC you must enable a cargo feature corresponding to your +desired cryptography provider: + +* `dnssec-aws-lc-rs` +* `dnssec-ring` + +## Other crate features + +* `tokio` - support for the Tokio async runtime. +* `serde` - enable serde serialization support. +* `mdns` (experimental) - enable experimental mDNS support. diff --git a/vendor/hickory-dns/hickory-net/src/client/dnssec_client.rs b/vendor/hickory-dns/hickory-net/src/client/dnssec_client.rs new file mode 100644 index 00000000..cceb1df8 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/client/dnssec_client.rs @@ -0,0 +1,112 @@ +// Copyright 2015-2019 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::future::Future; +use core::pin::Pin; +use std::sync::Arc; + +use futures_util::stream::Stream; + +use super::Client; +use crate::dnssec::DnssecDnsHandle; +use crate::error::NetError; +use crate::proto::dnssec::TrustAnchors; +use crate::proto::op::{DnsRequest, DnsResponse}; +use crate::runtime::{TokioRuntimeProvider, TokioTime}; +use crate::xfer::{DnsExchangeBackground, DnsHandle, DnsRequestSender}; + +/// A DNSSEC Client implemented over futures-rs. +/// +/// This Client is generic and capable of wrapping UDP, TCP, and other underlying DNS protocol +/// implementations. +pub struct DnssecClient { + client: DnssecDnsHandle>, +} + +impl DnssecClient { + /// Returns a DNSSEC verifying client with a TrustAnchor that can be replaced + pub fn builder(connect_future: F) -> AsyncSecureClientBuilder + where + F: Future> + 'static + Send + Unpin, + S: DnsRequestSender, + { + AsyncSecureClientBuilder { + connect_future, + trust_anchor: None, + } + } + + /// Returns a DNSSEC verifying client with the default TrustAnchor + pub async fn connect( + connect_future: F, + ) -> Result<(Self, DnsExchangeBackground), NetError> + where + S: DnsRequestSender, + F: Future> + 'static + Send + Unpin, + { + Self::builder(connect_future).build().await + } + + /// Constructs a DNSSEC client with an existing client and the given trust anchors + pub fn from_client( + client: Client, + trust_anchor: Arc, + ) -> Self { + Self { + client: DnssecDnsHandle::with_trust_anchor(client, trust_anchor), + } + } +} + +impl Clone for DnssecClient { + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + } + } +} + +impl DnsHandle for DnssecClient { + type Response = Pin> + Send + 'static>>; + type Runtime = TokioRuntimeProvider; + + fn send(&self, request: DnsRequest) -> Self::Response { + self.client.send(request) + } +} + +/// A builder to allow a custom trust to be used for validating all signed records +pub struct AsyncSecureClientBuilder { + connect_future: F, + trust_anchor: Option, +} + +impl AsyncSecureClientBuilder +where + F: Future> + 'static + Send + Unpin, + S: DnsRequestSender, +{ + /// This variant allows for the trust_anchor to be replaced + /// + /// # Arguments + /// + /// * `trust_anchor` - the set of trusted DNSKEY public_keys, by default this only contains the + /// root public_key. + pub fn trust_anchor(mut self, trust_anchor: TrustAnchors) -> Self { + self.trust_anchor = Some(trust_anchor); + self + } + + /// Construct the new client + pub async fn build( + mut self, + ) -> Result<(DnssecClient, DnsExchangeBackground), NetError> { + let trust_anchor = Arc::new(self.trust_anchor.take().unwrap_or_default()); + let (client, bg) = Client::from_sender(self.connect_future.await?); + Ok((DnssecClient::from_client(client, trust_anchor), bg)) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/client/memoize_client_handle.rs b/vendor/hickory-dns/hickory-net/src/client/memoize_client_handle.rs new file mode 100644 index 00000000..a999e832 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/client/memoize_client_handle.rs @@ -0,0 +1,168 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use core::pin::Pin; +use std::collections::HashMap; +use std::sync::Arc; + +use futures_util::future::FutureExt; +use futures_util::lock::Mutex; +use futures_util::stream::Stream; + +use super::{ + ClientHandle, + rc_stream::{RcStream, rc_stream}, +}; +use crate::{ + NetError, + proto::op::{DnsRequest, DnsResponse, Query}, + xfer::DnsHandle, +}; + +// TODO: move to proto +/// A ClientHandle for memoized (cached) responses to queries. +/// +/// This wraps a ClientHandle, changing the implementation `send()` to store the response against +/// the Message.Query that was sent. This should reduce network traffic especially during things +/// like DNSSEC validation. *Warning* this will currently cache for the life of the Client. +#[derive(Clone)] +#[must_use = "queries can only be sent through a ClientHandle"] +pub struct MemoizeClientHandle { + client: H, + active_queries: Arc::Response>>>>, +} + +impl MemoizeClientHandle +where + H: ClientHandle, +{ + /// Returns a new handle wrapping the specified client + pub fn new(client: H) -> Self { + Self { + client, + active_queries: Arc::new(Mutex::new(HashMap::new())), + } + } + + async fn inner_send( + request: DnsRequest, + active_queries: Arc::Response>>>>, + client: H, + ) -> impl Stream> { + // TODO: what if we want to support multiple queries (non-standard)? + let query = request.queries.first().expect("no query!").clone(); + + // lock all the currently running queries + let mut active_queries = active_queries.lock().await; + + // TODO: we need to consider TTL on the records here at some point + // If the query is running, grab that existing one... + if let Some(rc_stream) = active_queries.get(&query) { + return rc_stream.clone(); + }; + + // Otherwise issue a new query and store in the map + active_queries + .entry(query) + .or_insert_with(|| rc_stream(client.send(request))) + .clone() + } +} + +impl DnsHandle for MemoizeClientHandle { + type Response = Pin> + Send>>; + type Runtime = H::Runtime; + + fn send(&self, request: DnsRequest) -> Self::Response { + Box::pin( + Self::inner_send( + request, + Arc::clone(&self.active_queries), + self.client.clone(), + ) + .flatten_stream(), + ) + } +} + +#[cfg(test)] +mod test { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use core::pin::Pin; + use std::sync::Arc; + + use futures_util::lock::Mutex; + use futures_util::stream; + + use super::*; + use crate::{ + proto::{ + op::{DnsRequest, DnsResponse, Message, MessageType, OpCode, Query}, + rr::RecordType, + }, + runtime::TokioRuntimeProvider, + xfer::{DnsHandle, FirstAnswer}, + }; + use test_support::subscribe; + + #[derive(Clone)] + struct TestClient { + i: Arc>, + } + + impl DnsHandle for TestClient { + type Response = Pin> + Send>>; + type Runtime = TokioRuntimeProvider; + + fn send(&self, request: DnsRequest) -> Self::Response { + let i = Arc::clone(&self.i); + Box::pin(stream::once(async move { + let mut i = i.lock().await; + let message = Message::new(*i, MessageType::Query, OpCode::Query).into_response(); + std::println!( + "sending {}: {}", + *i, + request.queries.first().expect("no query!").clone() + ); + + *i += 1; + + Ok(DnsResponse::from_message(message).unwrap()) + })) + } + } + + #[test] + fn test_memoized() { + use futures_executor::block_on; + + subscribe(); + + let client = MemoizeClientHandle::new(TestClient { + i: Arc::new(Mutex::new(0)), + }); + + let mut test1 = Message::query(); + test1.add_query(Query::new().set_query_type(RecordType::A).clone()); + + let mut test2 = Message::query(); + test2.add_query(Query::new().set_query_type(RecordType::AAAA).clone()); + + let result = block_on(client.send(DnsRequest::from(test1.clone())).first_answer()).unwrap(); + assert_eq!(result.id, 0); + + let result = block_on(client.send(DnsRequest::from(test2.clone())).first_answer()).unwrap(); + assert_eq!(result.id, 1); + + // should get the same result for each... + let result = block_on(client.send(DnsRequest::from(test1)).first_answer()).unwrap(); + assert_eq!(result.id, 0); + + let result = block_on(client.send(DnsRequest::from(test2)).first_answer()).unwrap(); + assert_eq!(result.id, 1); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/client/mod.rs b/vendor/hickory-dns/hickory-net/src/client/mod.rs new file mode 100644 index 00000000..c4f8e9ab --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/client/mod.rs @@ -0,0 +1,820 @@ +/* + * Copyright (C) 2015-2016 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! DNS Client associated classes for performing queries and other operations. + +use core::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +use futures_util::{ + ready, + stream::{Stream, StreamExt}, +}; +use tracing::debug; + +use crate::{ + error::NetError, + proto::{ + ProtoError, + op::{ + DEFAULT_MAX_PAYLOAD_LEN, DnsRequest, DnsRequestOptions, DnsResponse, Edns, Message, + OpCode, Query, update_message, + }, + rr::{DNSClass, Name, RData, Record, RecordSet, RecordType, rdata::SOA}, + }, + runtime::RuntimeProvider, + xfer::{ + BufDnsStreamHandle, DnsClientStream, DnsExchange, DnsExchangeBackground, DnsExchangeSend, + DnsHandle, DnsMultiplexer, DnsRequestSender, + }, +}; + +#[cfg(all(feature = "__dnssec", feature = "tokio"))] +pub(crate) mod dnssec_client; +#[cfg(all(feature = "__dnssec", feature = "tokio"))] +pub use dnssec_client::{AsyncSecureClientBuilder, DnssecClient}; + +mod memoize_client_handle; +pub use memoize_client_handle::MemoizeClientHandle; + +mod rc_stream; + +#[cfg(test)] +mod tests; + +/// A DNS Client implemented over futures-rs. +/// +/// This Client is generic and capable of wrapping UDP, TCP, and other underlying DNS protocol +/// implementations. +#[derive(Clone)] +pub struct Client

{ + exchange: DnsExchange

, + use_edns: bool, +} + +impl Client

{ + /// Spawns a new Client Stream. This uses a default timeout of 5 seconds for all requests. + /// + /// # Arguments + /// + /// * `stream` - A stream of bytes that can be used to send/receive DNS messages + /// (see TcpClientStream or UdpClientStream) + /// * `stream_handle` - The handle for the `stream` on which bytes can be sent/received. + pub fn new( + stream: S, + stream_handle: BufDnsStreamHandle, + ) -> (Self, DnsExchangeBackground, P::Timer>) { + Self::with_timeout(stream, stream_handle, Duration::from_secs(5)) + } + + /// Spawns a new Client Stream. + /// + /// # Arguments + /// + /// * `stream` - A stream of bytes that can be used to send/receive DNS messages + /// (see TcpClientStream or UdpClientStream) + /// * `stream_handle` - The handle for the `stream` on which bytes can be sent/received. + /// * `timeout_duration` - All requests may fail due to lack of response, this is the time to + /// wait for a response before canceling the request. + pub fn with_timeout( + stream: S, + stream_handle: BufDnsStreamHandle, + timeout_duration: Duration, + ) -> (Self, DnsExchangeBackground, P::Timer>) { + Self::from_sender(DnsMultiplexer::new(stream, stream_handle).with_timeout(timeout_duration)) + } + + /// Creates a Client from an existing DnsRequestSender + pub fn from_sender( + sender: S, + ) -> (Self, DnsExchangeBackground) { + let (exchange, bg) = DnsExchange::from_stream(sender); + ( + Self { + exchange, + use_edns: true, + }, + bg, + ) + } + + /// (Re-)enable usage of EDNS for outgoing messages + pub fn enable_edns(&mut self) { + self.use_edns = true; + } + + /// Disable usage of EDNS for outgoing messages + pub fn disable_edns(&mut self) { + self.use_edns = false; + } +} + +impl DnsHandle for Client

{ + type Response = DnsExchangeSend

; + type Runtime = P; + + fn send(&self, request: DnsRequest) -> Self::Response { + self.exchange.send(request) + } + + fn is_using_edns(&self) -> bool { + self.use_edns + } +} + +impl ClientHandle for T where T: DnsHandle {} + +/// A trait for implementing high level functions of DNS. +pub trait ClientHandle: 'static + Clone + DnsHandle + Send { + /// A *classic* DNS query + /// + /// *Note* As of now, this will not recurse on PTR or CNAME record responses, that is up to + /// the caller. + /// + /// # Arguments + /// + /// * `name` - the label to lookup + /// * `query_class` - most likely this should always be DNSClass::IN + /// * `query_type` - record type to lookup + fn query( + &mut self, + name: Name, + query_class: DNSClass, + query_type: RecordType, + ) -> ClientResponse<::Response> { + let mut query = Query::query(name, query_type); + query.set_query_class(query_class); + let mut options = DnsRequestOptions::default(); + options.use_edns = self.is_using_edns(); + ClientResponse(self.lookup(query, options)) + } + + /// Sends a NOTIFY message to the remote system + /// + /// [RFC 1996](https://tools.ietf.org/html/rfc1996), DNS NOTIFY, August 1996 + /// + /// + /// ```text + /// 1. Rationale and Scope + /// + /// 1.1. Slow propagation of new and changed data in a DNS zone can be + /// due to a zone's relatively long refresh times. Longer refresh times + /// are beneficial in that they reduce load on the Primary Zone Servers, but + /// that benefit comes at the cost of long intervals of incoherence among + /// authority servers whenever the zone is updated. + /// + /// 1.2. The DNS NOTIFY transaction allows Primary Zone Servers to inform Secondary + /// Zone Servers when the zone has changed -- an interrupt as opposed to poll + /// model -- which it is hoped will reduce propagation delay while not + /// unduly increasing the masters' load. This specification only allows + /// slaves to be notified of SOA RR changes, but the architecture of + /// NOTIFY is intended to be extensible to other RR types. + /// + /// 1.3. This document intentionally gives more definition to the roles + /// of "Primary", "Secondary" and "Stealth" servers, their enumeration in NS + /// RRs, and the SOA MNAME field. In that sense, this document can be + /// considered an addendum to [RFC1035]. + /// + /// ``` + /// + /// The below section describes how the Notify message should be constructed. The function + /// implementation accepts a Record, but the actual data of the record should be ignored by the + /// server, i.e. the server should make a request subsequent to receiving this Notification for + /// the authority record, but could be used to decide to request an update or not: + /// + /// ```text + /// 3.7. A NOTIFY request has QDCOUNT>0, ANCOUNT>=0, AUCOUNT>=0, + /// ADCOUNT>=0. If ANCOUNT>0, then the answer section represents an + /// unsecure hint at the new RRset for this . A + /// Secondary receiving such a hint is free to treat equivalence of this + /// answer section with its local data as a "no further work needs to be + /// done" indication. If ANCOUNT=0, or ANCOUNT>0 and the answer section + /// differs from the Secondary's local data, then the Secondary should query its + /// known Primaries to retrieve the new data. + /// ``` + /// + /// Client's should be ready to handle, or be aware of, a server response of NOTIMP: + /// + /// ```text + /// 3.12. If a NOTIFY request is received by a Secondary who does not + /// implement the NOTIFY opcode, it will respond with a NOTIMP + /// (unimplemented feature error) message. A Primary Zone Server who receives + /// such a NOTIMP should consider the NOTIFY transaction complete for + /// that Secondary. + /// ``` + /// + /// # Arguments + /// + /// * `name` - the label which is being notified + /// * `query_class` - most likely this should always be DNSClass::IN + /// * `query_type` - record type which has been updated + /// * `rrset` - the new version of the record(s) being notified + fn notify( + &mut self, + name: Name, + query_class: DNSClass, + query_type: RecordType, + rrset: Option, + ) -> ClientResponse<::Response> + where + R: Into, + { + debug!("notifying: {} {:?}", name, query_type); + + // build the message + let mut message = Message::query(); + // 3.3. NOTIFY is similar to QUERY in that it has a request message with + // the header QR flag "clear" and a response message with QR "set". The + // response message contains no useful information, but its reception by + // the Primary is an indication that the Secondary has received the NOTIFY + // and that the Primary Zone Server can remove the Secondary from any retry queue for + // this NOTIFY event. + message.metadata.op_code = OpCode::Notify; + + // Extended dns + if self.is_using_edns() { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + // add the query + let mut query: Query = Query::new(); + query + .set_name(name) + .set_query_class(query_class) + .set_query_type(query_type); + message.add_query(query); + + // add the notify message, see https://tools.ietf.org/html/rfc1996, section 3.7 + if let Some(rrset) = rrset { + message.add_answers(rrset.into()); + } + + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Sends a record to create on the server, this will fail if the record exists (atomicity + /// depends on the server) + /// + /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 + /// + /// ```text + /// 2.4.3 - RRset Does Not Exist + /// + /// No RRs with a specified NAME and TYPE (in the zone and class denoted + /// by the Zone Section) can exist. + /// + /// For this prerequisite, a requestor adds to the section a single RR + /// whose NAME and TYPE are equal to that of the RRset whose nonexistence + /// is required. The RDLENGTH of this record is zero (0), and RDATA + /// field is therefore empty. CLASS must be specified as NONE in order + /// to distinguish this condition from a valid RR whose RDLENGTH is + /// naturally zero (0) (for example, the NULL RR). TTL must be specified + /// as zero (0). + /// + /// 2.5.1 - Add To An RRset + /// + /// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH + /// and RDATA are those being added, and CLASS is the same as the zone + /// class. Any duplicate RRs will be silently ignored by the Primary Zone + /// Server. + /// ``` + /// + /// # Arguments + /// + /// * `rrset` - the record(s) to create + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection) + fn create( + &mut self, + rrset: R, + zone_origin: Name, + ) -> ClientResponse<::Response> + where + R: Into, + { + let rrset = rrset.into(); + let message = update_message::create(rrset, zone_origin, self.is_using_edns()); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Appends a record to an existing rrset, optionally require the rrset to exist (atomicity + /// depends on the server) + /// + /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 + /// + /// ```text + /// 2.4.1 - RRset Exists (Value Independent) + /// + /// At least one RR with a specified NAME and TYPE (in the zone and class + /// specified in the Zone Section) must exist. + /// + /// For this prerequisite, a requestor adds to the section a single RR + /// whose NAME and TYPE are equal to that of the zone RRset whose + /// existence is required. RDLENGTH is zero and RDATA is therefore + /// empty. CLASS must be specified as ANY to differentiate this + /// condition from that of an actual RR whose RDLENGTH is naturally zero + /// (0) (e.g., NULL). TTL is specified as zero (0). + /// + /// 2.5.1 - Add To An RRset + /// + /// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH + /// and RDATA are those being added, and CLASS is the same as the zone + /// class. Any duplicate RRs will be silently ignored by the Primary Zone + /// Server. + /// ``` + /// + /// # Arguments + /// + /// * `rrset` - the record(s) to append to an RRSet + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// * `must_exist` - if true, the request will fail if the record does not exist + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection). If + /// the rrset does not exist and must_exist is false, then the RRSet will be created. + fn append( + &mut self, + rrset: R, + zone_origin: Name, + must_exist: bool, + ) -> ClientResponse<::Response> + where + R: Into, + { + let rrset = rrset.into(); + let message = update_message::append(rrset, zone_origin, must_exist, self.is_using_edns()); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Compares and if it matches, swaps it for the new value (atomicity depends on the server) + /// + /// ```text + /// 2.4.2 - RRset Exists (Value Dependent) + /// + /// A set of RRs with a specified NAME and TYPE exists and has the same + /// members with the same RDATAs as the RRset specified here in this + /// section. While RRset ordering is undefined and therefore not + /// significant to this comparison, the sets be identical in their + /// extent. + /// + /// For this prerequisite, a requestor adds to the section an entire + /// RRset whose preexistence is required. NAME and TYPE are that of the + /// RRset being denoted. CLASS is that of the zone. TTL must be + /// specified as zero (0) and is ignored when comparing RRsets for + /// identity. + /// + /// 2.5.4 - Delete An RR From An RRset + /// + /// RRs to be deleted are added to the Update Section. The NAME, TYPE, + /// RDLENGTH and RDATA must match the RR being deleted. TTL must be + /// specified as zero (0) and will otherwise be ignored by the Primary + /// Zone Server. CLASS must be specified as NONE to distinguish this from an + /// RR addition. If no such RRs exist, then this Update RR will be + /// silently ignored by the Primary Zone Server. + /// + /// 2.5.1 - Add To An RRset + /// + /// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH + /// and RDATA are those being added, and CLASS is the same as the zone + /// class. Any duplicate RRs will be silently ignored by the Primary + /// Zone Server. + /// ``` + /// + /// # Arguments + /// + /// * `current` - the current rrset which must exist for the swap to complete + /// * `new` - the new rrset with which to replace the current rrset + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection). + fn compare_and_swap( + &mut self, + current: C, + new: N, + zone_origin: Name, + ) -> ClientResponse<::Response> + where + C: Into, + N: Into, + { + let current = current.into(); + let new = new.into(); + + let message = + update_message::compare_and_swap(current, new, zone_origin, self.is_using_edns()); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Deletes a record (by rdata) from an rrset, optionally require the rrset to exist. + /// + /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 + /// + /// ```text + /// 2.4.1 - RRset Exists (Value Independent) + /// + /// At least one RR with a specified NAME and TYPE (in the zone and class + /// specified in the Zone Section) must exist. + /// + /// For this prerequisite, a requestor adds to the section a single RR + /// whose NAME and TYPE are equal to that of the zone RRset whose + /// existence is required. RDLENGTH is zero and RDATA is therefore + /// empty. CLASS must be specified as ANY to differentiate this + /// condition from that of an actual RR whose RDLENGTH is naturally zero + /// (0) (e.g., NULL). TTL is specified as zero (0). + /// + /// 2.5.4 - Delete An RR From An RRset + /// + /// RRs to be deleted are added to the Update Section. The NAME, TYPE, + /// RDLENGTH and RDATA must match the RR being deleted. TTL must be + /// specified as zero (0) and will otherwise be ignored by the Primary + /// Zone Server. CLASS must be specified as NONE to distinguish this from an + /// RR addition. If no such RRs exist, then this Update RR will be + /// silently ignored by the Primary Zone Server. + /// ``` + /// + /// # Arguments + /// + /// * `rrset` - the record(s) to delete from a RRSet, the name, type and rdata must match the + /// record to delete + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// * `signer` - the signer, with private key, to use to sign the request + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection). If + /// the rrset does not exist and must_exist is false, then the RRSet will be deleted. + fn delete_by_rdata( + &mut self, + rrset: R, + zone_origin: Name, + ) -> ClientResponse<::Response> + where + R: Into, + { + let rrset = rrset.into(); + let message = update_message::delete_by_rdata(rrset, zone_origin, self.is_using_edns()); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Deletes an entire rrset, optionally require the rrset to exist. + /// + /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 + /// + /// ```text + /// 2.4.1 - RRset Exists (Value Independent) + /// + /// At least one RR with a specified NAME and TYPE (in the zone and class + /// specified in the Zone Section) must exist. + /// + /// For this prerequisite, a requestor adds to the section a single RR + /// whose NAME and TYPE are equal to that of the zone RRset whose + /// existence is required. RDLENGTH is zero and RDATA is therefore + /// empty. CLASS must be specified as ANY to differentiate this + /// condition from that of an actual RR whose RDLENGTH is naturally zero + /// (0) (e.g., NULL). TTL is specified as zero (0). + /// + /// 2.5.2 - Delete An RRset + /// + /// One RR is added to the Update Section whose NAME and TYPE are those + /// of the RRset to be deleted. TTL must be specified as zero (0) and is + /// otherwise not used by the Primary Zone Server. CLASS must be specified as + /// ANY. RDLENGTH must be zero (0) and RDATA must therefore be empty. + /// If no such RRset exists, then this Update RR will be silently ignored + /// by the Primary Zone Server. + /// ``` + /// + /// # Arguments + /// + /// * `record` - The name, class and record_type will be used to match and delete the RecordSet + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection). If + /// the rrset does not exist and must_exist is false, then the RRSet will be deleted. + fn delete_rrset( + &mut self, + record: Record, + zone_origin: Name, + ) -> ClientResponse<::Response> { + assert!(zone_origin.zone_of(&record.name)); + let message = update_message::delete_rrset(record, zone_origin, self.is_using_edns()); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Deletes all records at the specified name + /// + /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 + /// + /// ```text + /// 2.5.3 - Delete All RRsets From A Name + /// + /// One RR is added to the Update Section whose NAME is that of the name + /// to be cleansed of RRsets. TYPE must be specified as ANY. TTL must + /// be specified as zero (0) and is otherwise not used by the Primary + /// Zone Server. CLASS must be specified as ANY. RDLENGTH must be zero (0) + /// and RDATA must therefore be empty. If no such RRsets exist, then + /// this Update RR will be silently ignored by the Primary Zone Server. + /// ``` + /// + /// # Arguments + /// + /// * `name_of_records` - the name of all the record sets to delete + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// * `dns_class` - the class of the SOA + /// + /// The update must go to a zone authority (i.e. the server used in the ClientConnection). This + /// operation attempts to delete all resource record sets the specified name regardless of + /// the record type. + fn delete_all( + &mut self, + name_of_records: Name, + zone_origin: Name, + dns_class: DNSClass, + ) -> ClientResponse<::Response> { + assert!(zone_origin.zone_of(&name_of_records)); + let message = update_message::delete_all( + name_of_records, + zone_origin, + dns_class, + self.is_using_edns(), + ); + ClientResponse(self.send(DnsRequest::from(message))) + } + + /// Download all records from a zone, or all records modified since given SOA was observed. + /// The request will either be a AXFR Query (ask for full zone transfer) if a SOA was not + /// provided, or a IXFR Query (incremental zone transfer) if a SOA was provided. + /// + /// # Arguments + /// * `zone_origin` - the zone name to update, i.e. SOA name + /// * `last_soa` - the last SOA known, if any. If provided, name must match `zone_origin` + fn zone_transfer( + &mut self, + zone_origin: Name, + last_soa: Option, + ) -> ClientStreamXfr<::Response> { + let ixfr = last_soa.is_some(); + let message = update_message::zone_transfer(zone_origin, last_soa); + ClientStreamXfr::new(self.send(DnsRequest::from(message)), ixfr) + } +} + +/// A stream result of a Client Request +#[must_use = "stream do nothing unless polled"] +pub struct ClientStreamingResponse(pub(crate) R) +where + R: Stream> + Send + Unpin + 'static; + +impl Stream for ClientStreamingResponse +where + R: Stream> + Send + Unpin + 'static, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Poll::Ready(ready!(self.0.poll_next_unpin(cx))) + } +} + +/// A future result of a Client Request +#[must_use = "futures do nothing unless polled"] +pub struct ClientResponse(pub(crate) R) +where + R: Stream> + Send + Unpin + 'static; + +impl Future for ClientResponse +where + R: Stream> + Send + Unpin + 'static, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Poll::Ready(match ready!(self.0.poll_next_unpin(cx)) { + Some(r) => r, + None => Err(NetError::Timeout), + }) + } +} + +/// A stream result of a zone transfer Client Request +/// Accept messages until the end of a zone transfer. For AXFR, it search for a starting and an +/// ending SOA. For IXFR, it do so taking into account there will be other SOA inbetween +#[must_use = "stream do nothing unless polled"] +pub struct ClientStreamXfr +where + R: Stream> + Send + Unpin + 'static, +{ + state: ClientStreamXfrState, +} + +impl ClientStreamXfr +where + R: Stream> + Send + Unpin + 'static, +{ + fn new(inner: R, maybe_incr: bool) -> Self { + Self { + state: ClientStreamXfrState::Start { inner, maybe_incr }, + } + } +} + +/// State machine for ClientStreamXfr, implementing almost all logic +#[derive(Debug)] +enum ClientStreamXfrState { + Start { + inner: R, + maybe_incr: bool, + }, + Second { + inner: R, + expected_serial: u32, + maybe_incr: bool, + }, + Axfr { + inner: R, + expected_serial: u32, + }, + Ixfr { + inner: R, + even: bool, + expected_serial: u32, + }, + Ended, + Invalid, +} + +impl ClientStreamXfrState { + /// Helper to get the stream from the enum + fn inner(&mut self) -> &mut R { + use ClientStreamXfrState::*; + match self { + Start { inner, .. } => inner, + Second { inner, .. } => inner, + Axfr { inner, .. } => inner, + Ixfr { inner, .. } => inner, + Ended | Invalid => unreachable!(), + } + } + + /// Helper to ingest answer Records + // TODO: this is complex enough it should get its own tests + fn process(&mut self, answers: &[Record]) -> Result<(), NetError> { + use ClientStreamXfrState::*; + fn get_serial(r: &Record) -> Option { + match &r.data { + RData::SOA(soa) => Some(soa.serial), + _ => None, + } + } + + if answers.is_empty() { + return Ok(()); + } + match core::mem::replace(self, Invalid) { + Start { inner, maybe_incr } => { + if let Some(expected_serial) = get_serial(&answers[0]) { + *self = Second { + inner, + maybe_incr, + expected_serial, + }; + self.process(&answers[1..]) + } else { + *self = Ended; + Ok(()) + } + } + Second { + inner, + maybe_incr, + expected_serial, + } => { + if let Some(serial) = get_serial(&answers[0]) { + // maybe IXFR, or empty AXFR + if serial == expected_serial { + // empty AXFR + *self = Ended; + if answers.len() == 1 { + Ok(()) + } else { + // invalid answer : trailing records + Err("invalid zone transfer, contains trailing records".into()) + } + } else if maybe_incr { + *self = Ixfr { + inner, + expected_serial, + even: true, + }; + self.process(&answers[1..]) + } else { + *self = Ended; + Err("invalid zone transfer, expected AXFR, got IXFR".into()) + } + } else { + // standard AXFR + *self = Axfr { + inner, + expected_serial, + }; + self.process(&answers[1..]) + } + } + Axfr { + inner, + expected_serial, + } => { + let soa_count = answers + .iter() + .filter(|a| a.record_type() == RecordType::SOA) + .count(); + match soa_count { + 0 => { + *self = Axfr { + inner, + expected_serial, + }; + Ok(()) + } + 1 => { + *self = Ended; + match answers.last().map(|r| r.record_type()) { + Some(RecordType::SOA) => Ok(()), + _ => Err("invalid zone transfer, contains trailing records".into()), + } + } + _ => { + *self = Ended; + Err("invalid zone transfer, contains trailing records".into()) + } + } + } + Ixfr { + inner, + even, + expected_serial, + } => { + let even = answers + .iter() + .fold(even, |even, a| even ^ (a.record_type() == RecordType::SOA)); + if even { + if let Some(serial) = get_serial(answers.last().unwrap()) { + if serial == expected_serial { + *self = Ended; + return Ok(()); + } + } + } + *self = Ixfr { + inner, + even, + expected_serial, + }; + Ok(()) + } + Ended | Invalid => { + unreachable!(); + } + } + } +} + +impl Stream for ClientStreamXfr +where + R: Stream> + Send + Unpin + 'static, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use ClientStreamXfrState::*; + + if matches!(self.state, Ended) { + return Poll::Ready(None); + } + + let message = ready!(self.state.inner().poll_next_unpin(cx)).map(|response| { + let ok = response?; + self.state.process(&ok.answers)?; + Ok(ok) + }); + Poll::Ready(message) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/client/rc_stream.rs b/vendor/hickory-dns/hickory-net/src/client/rc_stream.rs new file mode 100644 index 00000000..c824934d --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/client/rc_stream.rs @@ -0,0 +1,121 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::sync::Arc; + +use futures_util::lock::Mutex; +use futures_util::stream::{Fuse, Stream, StreamExt}; +use futures_util::{FutureExt, ready}; + +#[allow(clippy::type_complexity)] +#[must_use = "stream do nothing unless polled"] +pub(crate) struct RcStream +where + S: Stream + Send + Unpin, + S::Item: Clone + Send, +{ + stream_and_result: Arc, Vec)>>, + pos: usize, +} + +pub(crate) fn rc_stream(stream: S) -> RcStream +where + S: Stream + Unpin + Send, + S::Item: Clone + Send, +{ + let stream_and_result = Arc::new(Mutex::new((stream.fuse(), vec![]))); + + RcStream { + stream_and_result, + pos: 0, + } +} + +impl Stream for RcStream +where + S: Stream + Send + Unpin, + S::Item: Clone + Send, +{ + type Item = S::Item; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // try and get a mutable reference to execute the future + // at least one caller should be able to get a mut reference... others will + // wait for it to complete. + let mut stream_and_result = ready!(self.stream_and_result.lock().poll_unpin(cx)); + let (stream, stored_result) = &mut *stream_and_result; + if stored_result.len() > self.pos { + let result = stored_result[self.pos].clone(); + drop(stream_and_result); + self.pos += 1; + return Poll::Ready(Some(result)); + } + + // if pending it's either done, or it's actually pending + match stream.poll_next_unpin(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(result) => { + if let Some(result) = &result { + stored_result.push(result.clone()); + } + Poll::Ready(result) + } + } + } +} + +impl Clone for RcStream +where + S: Stream + Send + Unpin, + S::Item: Clone + Send + Unpin, +{ + fn clone(&self) -> Self { + Self { + stream_and_result: Arc::clone(&self.stream_and_result), + pos: 0, // index is not kept to allow to read first messages + } + } +} + +#[cfg(test)] +mod tests { + use futures_executor::block_on; + use futures_util::future; + use futures_util::stream::once; + + use super::*; + use crate::NetError; + use crate::xfer::FirstAnswer; + + #[test] + fn test_rc_stream() { + let future = future::ok::(1_usize); + + let rc = rc_stream(once(future)); + + let i = block_on(rc.clone().first_answer()).unwrap(); + assert_eq!(i, 1); + + let i = block_on(rc.first_answer()).unwrap(); + assert_eq!(i, 1); + } + + #[test] + fn test_rc_stream_failed() { + let future = future::err::(NetError::Busy); + + let rc = rc_stream(once(future)); + + let i = block_on(rc.clone().first_answer()).unwrap_err(); + assert!(matches!(i, NetError::Busy)); + + let i = block_on(rc.first_answer()).unwrap_err(); + assert!(matches!(i, NetError::Busy)); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/client/tests.rs b/vendor/hickory-dns/hickory-net/src/client/tests.rs new file mode 100644 index 00000000..bfd224ad --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/client/tests.rs @@ -0,0 +1,345 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::net::SocketAddr; +use std::str::FromStr; + +use futures_util::stream::iter; +use test_support::subscribe; + +use super::ClientStreamXfrState::*; +use super::*; +use crate::{ + proto::rr::{ + RData, + rdata::{A, SOA}, + }, + runtime::TokioRuntimeProvider, +}; + +// Keep this in sync with the example in the README. +#[tokio::test] +async fn readme_example() { + subscribe(); + + use core::net::SocketAddr; + + use crate::client::{Client, ClientHandle}; + use crate::proto::rr::{DNSClass, Name, RecordType}; + use crate::runtime::TokioRuntimeProvider; + use crate::udp::UdpClientStream; + + let address = SocketAddr::from(([8, 8, 8, 8], 53)); + let conn = UdpClientStream::builder(address, TokioRuntimeProvider::default()).build(); + let (mut client, bg) = Client::::from_sender(conn); + tokio::spawn(bg); + + // Specify the name, note the final '.' which specifies it's an FQDN + let name = Name::from_str("www.example.com.").unwrap(); + + // NOTE: see 'Setup a connection' example above + // Send the query and get a message response, see RecordType for all supported options + let response = client + .query(name, DNSClass::IN, RecordType::A) + .await + .unwrap(); + + // Messages are the packets sent between client and server in DNS, DnsResponse's can be + // dereferenced to a Message. There are many fields to a Message, It's beyond the scope + // of these examples to explain them. See hickory_dns::op::message::Message for more details. + // generally we will be interested in the Message::answers + let answers = &response.answers; + + // Records are generic objects which can contain any data. + // In order to access it we need to first check what type of record it is + // In this case we are interested in A, IPv4 address + let a_data = answers + .iter() + .flat_map(|record| match record.data { + RData::A(addr) => Some(addr), + _ => None, + }) + .collect::>(); + assert!(!a_data.is_empty()); +} + +fn soa_record(serial: u32) -> Record { + let soa = RData::SOA(SOA::new( + Name::from_ascii("example.com.").unwrap(), + Name::from_ascii("admin.example.com.").unwrap(), + serial, + 60, + 60, + 60, + 60, + )); + Record::from_rdata(Name::from_ascii("example.com.").unwrap(), 600, soa) +} + +fn a_record(ip: u8) -> Record { + let a = RData::A(A::new(0, 0, 0, ip)); + Record::from_rdata(Name::from_ascii("www.example.com.").unwrap(), 600, a) +} + +fn get_stream_testcase( + records: Vec>, +) -> impl Stream> + Send + Unpin + 'static { + let stream = records.into_iter().map(|r| { + Ok({ + let mut m = Message::query(); + m.insert_answers(r); + DnsResponse::from_message(m.into_response()).unwrap() + }) + }); + iter(stream) +} + +#[tokio::test] +async fn test_stream_xfr_valid_axfr() { + subscribe(); + let stream = get_stream_testcase(vec![vec![ + soa_record(3), + a_record(1), + a_record(2), + soa_record(3), + ]]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 4); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_valid_axfr_multipart() { + subscribe(); + let stream = get_stream_testcase(vec![ + vec![soa_record(3)], + vec![a_record(1)], + vec![soa_record(3)], + vec![a_record(2)], // will be ignored as connection is dropped before reading this message + ]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Second { .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Axfr { .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 1); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_empty_axfr() { + subscribe(); + let stream = get_stream_testcase(vec![vec![soa_record(3)], vec![soa_record(3)]]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Second { .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 1); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_axfr_with_ixfr_reply() { + subscribe(); + let stream = get_stream_testcase(vec![vec![ + soa_record(3), + soa_record(2), + a_record(1), + soa_record(3), + a_record(2), + soa_record(3), + ]]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + stream.next().await.unwrap().unwrap_err(); + assert!(matches!(stream.state, Ended)); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_axfr_with_non_xfr_reply() { + subscribe(); + let stream = get_stream_testcase(vec![ + vec![a_record(1)], // assume this is an error response, not a zone transfer + vec![a_record(2)], + ]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 1); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_invalid_axfr_multipart() { + subscribe(); + let stream = get_stream_testcase(vec![ + vec![soa_record(3)], + vec![a_record(1)], + vec![soa_record(3), a_record(2)], + vec![soa_record(3)], + ]); + let mut stream = ClientStreamXfr::new(stream, false); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Second { .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Axfr { .. })); + assert_eq!(response.answers.len(), 1); + + stream.next().await.unwrap().unwrap_err(); + assert!(matches!(stream.state, Ended)); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_valid_ixfr() { + subscribe(); + let stream = get_stream_testcase(vec![vec![ + soa_record(3), + soa_record(2), + a_record(1), + soa_record(3), + a_record(2), + soa_record(3), + ]]); + let mut stream = ClientStreamXfr::new(stream, true); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 6); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn test_stream_xfr_valid_ixfr_multipart() { + subscribe(); + let stream = get_stream_testcase(vec![ + vec![soa_record(3)], + vec![soa_record(2)], + vec![a_record(1)], + vec![soa_record(3)], + vec![a_record(2)], + vec![soa_record(3)], + vec![a_record(3)], // + ]); + let mut stream = ClientStreamXfr::new(stream, true); + assert!(matches!(stream.state, Start { .. })); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Second { .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ixfr { even: true, .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ixfr { even: true, .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ixfr { even: false, .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ixfr { even: false, .. })); + assert_eq!(response.answers.len(), 1); + + let response = stream.next().await.unwrap().unwrap(); + assert!(matches!(stream.state, Ended)); + assert_eq!(response.answers.len(), 1); + + assert!(stream.next().await.is_none()); +} + +#[tokio::test] +async fn async_client() { + subscribe(); + use crate::client::{Client, ClientHandle}; + use crate::{ + proto::rr::{DNSClass, Name, RData, RecordType}, + tcp::TcpClientStream, + }; + use core::str::FromStr; + + // Since we used UDP in the previous examples, let's change things up a bit and use TCP here + let addr = SocketAddr::from(([8, 8, 8, 8], 53)); + let (future, sender) = TcpClientStream::new(addr, None, None, TokioRuntimeProvider::new()); + + // Create a new client, the bg is a background future which handles + // the multiplexing of the DNS requests to the server. + // the client is a handle to an unbounded queue for sending requests via the + // background. The background must be scheduled to run before the client can + // send any dns requests + let (mut client, bg) = Client::::new(future.await.unwrap(), sender); + + // make sure to run the background task + tokio::spawn(bg); + + // Create a query future + let query = client.query( + Name::from_str("dns.google.").unwrap(), + DNSClass::IN, + RecordType::A, + ); + + // wait for its response + let (message_returned, buffer) = query.await.unwrap().into_parts(); + + // extract A records in a stable order we can assert on + let assert_a_records_match = |answers: &[Record], expected: &[A]| { + let mut a_records = answers + .iter() + .filter_map(|record| match &record.data { + RData::A(addr) => Some(*addr), + _ => None, + }) + .collect::>(); + a_records.sort_by_key(|a| u32::from(**a)); + assert_eq!(a_records, expected); + }; + + let expected_answers = vec![A::new(8, 8, 4, 4), A::new(8, 8, 8, 8)]; + assert_a_records_match(&message_returned.answers, &expected_answers); + + let message_parsed = Message::from_vec(&buffer) + .expect("buffer was parsed already by Client so we should be able to do it again"); + + assert_a_records_match(&message_parsed.answers, &expected_answers); +} diff --git a/vendor/hickory-dns/hickory-net/src/dnssec/error.rs b/vendor/hickory-dns/hickory-net/src/dnssec/error.rs new file mode 100644 index 00000000..e746ba55 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/dnssec/error.rs @@ -0,0 +1,189 @@ +use core::fmt; + +use thiserror::Error; + +use crate::{ + error::NetError, + proto::{ + ProtoError, + dnssec::{Algorithm, Proof}, + op::Query, + rr::{Name, RecordType}, + }, +}; + +/// The error kind for dnssec errors that get returned in the crate +#[allow(unreachable_pub)] +#[derive(Debug, Error, Clone)] +#[non_exhaustive] +pub enum ProofErrorKind { + /// An error with an arbitrary message, stored as String + #[error("{0}")] + Msg(String), + + /// Algorithm mismatch between rrsig and dnskey + #[error("algorithm mismatch rrsig: {rrsig} dnskey: {dnskey}")] + AlgorithmMismatch { + /// Algorithm specified in the RRSIG + rrsig: Algorithm, + /// Algorithm supported in the DNSKEY + dnskey: Algorithm, + }, + + /// A DnsKey verification of rrset and rrsig failed + #[error("dnskey and rrset failed to verify: {name} key_tag: {key_tag}")] + DnsKeyVerifyRrsig { + /// The name/label of the DNSKEY + name: Name, + /// The key tag derived from the DNSKEY + key_tag: u16, + /// The Error that occurred during validation + error: ProtoError, + }, + + /// There was no DNSKEY found for verifying the DNSSEC of the zone + #[error("no dnskey was found: {name}")] + DnskeyNotFound { + /// The name of the missing DNSKEY + name: Name, + }, + + /// A DnsKey was revoked and could not be used for validation + #[error("dnskey revoked: {name}, key_tag: {key_tag}")] + DnsKeyRevoked { + /// The name of the DNSKEY that was revoked + name: Name, + /// The key tag derived from the DNSKEY + key_tag: u16, + }, + + /// The DNSKEY is not covered by a DS record + #[error("dnskey has no ds: {name}")] + DnsKeyHasNoDs { + /// The name of the DNSKEY + name: Name, + }, + + /// DS record exists but not a DNSKEY that matches + #[error("ds record exists, but no dnskey: {name}")] + DsRecordsButNoDnskey { + /// Name of the missing DNSKEY + name: Name, + }, + + /// DS record parent exists, but child does not + #[error("ds record should exist: {name}")] + DsRecordShouldExist { + /// Name fo the missing DS key + name: Name, + }, + + /// DS record does not exist, and this was proven with an NSEC or an + /// insecure parent zone + #[error("ds record does not exist: {name}")] + DsResponseInsecure { + /// The name of the DS record + name: Name, + }, + + /// An error ocurred while calculating the DNSKEY key tag + #[error("internal error computing the key tag for: {name}")] + ErrorComputingKeyTag { + /// The name of the DNSKEY record + name: Name, + }, + + /// The DNSKEY used was not verified as secure + #[error("dnskey insecure: {name}, key_tag: {key_tag}")] + InsecureDnsKey { + /// The name of the DNSKEY + name: Name, + /// The key tag derived from the DNSKEY + key_tag: u16, + }, + + /// The DnsKey is not marked as a zone key + #[error("not a zone signing key: {name} key_tag: {key_tag}")] + NotZoneDnsKey { + /// Name of the DNSKEY + name: Name, + /// The key tag derived from the DNSKEY + key_tag: u16, + }, + + /// There was a protocol error when looking up DNSSEC records + #[error("communication failure for query: {query}: {net}")] + Net { + /// Query that failed + query: Query, + /// Reasons fo the failure + net: NetError, + }, + + /// The RRSIGs for the rrset are not present. + /// It's indeterminate if DS records can't be found + /// It's bogus if the DS records are present + #[error("rrsigs are not present for: {name} record_type: {record_type}")] + RrsigsNotPresent { + /// Name that RRSIGS are missing for + name: Name, + /// The record type in question + record_type: RecordType, + }, + + /// The RRSIGs could not be verified or failed validation + #[error("rrsigs were not able to be verified: {name}, type: {record_type}")] + RrsigsUnverified { + /// Name that RRSIGS failed for + name: Name, + /// The record type in question + record_type: RecordType, + }, + + /// Unknown or reserved key algorithm + #[error("unknown or reserved key algorithm")] + UnknownKeyAlgorithm, + + /// Unsupported key algorithms + #[error("unsupported key algorithms")] + UnsupportedKeyAlgorithm, +} + +/// The error type for dnssec errors that get returned in the crate +#[non_exhaustive] +#[derive(Debug, Clone, Error)] +pub struct ProofError { + /// The proof derived from the failed state + pub proof: Proof, + /// The kind of error + pub kind: Box, +} + +impl ProofError { + /// Create an error with the given Proof and Associated Error + pub fn new(proof: Proof, kind: ProofErrorKind) -> Self { + Self { + proof, + kind: Box::new(kind), + } + } + + /// Get the kind of the error + pub fn kind(&self) -> &ProofErrorKind { + &self.kind + } + + /// Returns an error related to the absence of a DS record + pub fn ds_should_exist(name: Name) -> Self { + Self { + proof: Proof::Bogus, + kind: Box::new(ProofErrorKind::DsRecordShouldExist { name }), + } + } +} + +impl fmt::Display for ProofError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.proof, self.kind) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/dnssec/handle_tests.rs b/vendor/hickory-dns/hickory-net/src/dnssec/handle_tests.rs new file mode 100644 index 00000000..09f80e16 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/dnssec/handle_tests.rs @@ -0,0 +1,763 @@ +//! Tests for [`DnssecDnsHandle`] against a mock upstream returning canned responses. + +use core::mem; +use std::{ + collections::HashMap, + net::Ipv4Addr, + pin::Pin, + sync::{Arc, Mutex, PoisonError}, + time::Duration, +}; + +use data_encoding::BASE32_DNSSEC; +use futures_util::{ + future, + stream::{self, Stream}, +}; +use time::OffsetDateTime; + +use super::DnssecDnsHandle; +use crate::{ + error::{DnsError, NetError}, + proto::{ + dnssec::{ + DigestType, DnssecSigner, Nsec3HashAlgorithm, Proof, PublicKeyBuf, SigningKey, + TrustAnchors, + crypto::Ed25519SigningKey, + rdata::{DNSKEY, DNSSECRData, DS, NSEC, NSEC3, RRSIG}, + }, + op::{DnsRequest, DnsRequestOptions, DnsResponse, Message, Query, ResponseCode}, + rr::{ + DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType, + rdata::{ + A, CNAME, NS, SOA, TLSA, + tlsa::{CertUsage, Matching, Selector}, + }, + }, + }, + runtime::TokioRuntimeProvider, + xfer::{DnsHandle, FirstAnswer}, +}; +use test_support::subscribe; + +/// A server answering a DS query for an insecure zone with the child zone's own SOA record +/// (instead of the parent's) must not cause a validation loop, and the zone must be recognized +/// as insecure if its parent zone is insecure. +/// +/// Regression test for . +#[tokio::test] +async fn insecure_delegation_with_child_soa_in_ds_response() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let child = Name::from_ascii("child.example.com.").unwrap(); + fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&child)]); + let (handle, queries) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Insecure); + assert_eq!( + queries.count(&child, RecordType::DS), + 1, + "the DS query for the child zone should only be sent once" + ); +} + +/// A server answering a DS query for an insecure zone with an empty authority section must +/// still allow the zone to be recognized as insecure if its parent zone is insecure. +/// +/// Regression test for . +#[tokio::test] +async fn insecure_delegation_with_empty_ds_response() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let child = Name::from_ascii("child.example.com.").unwrap(); + fixture.respond(&child, RecordType::DS, [], []); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Insecure); +} + +/// The expected case: a DS query for an insecure zone is answered with the parent zone's SOA +/// record. +#[tokio::test] +async fn insecure_delegation_with_parent_soa_in_ds_response() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let child = Name::from_ascii("child.example.com.").unwrap(); + let parent = Name::from_ascii("example.com.").unwrap(); + fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&parent)]); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Insecure); +} + +/// If the parent zone is secure, an unauthenticated DS response for the child zone must not be +/// accepted as proof of an insecure delegation, regardless of the SOA record it carries. +#[tokio::test] +async fn bogus_delegation_with_child_soa_in_ds_response() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let child = Name::from_ascii("child.example.com.").unwrap(); + fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&child)]); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Bogus); +} + +/// If the parent zone is secure, an empty DS response for the child zone must not be accepted +/// as proof of an insecure delegation. +#[tokio::test] +async fn bogus_delegation_with_empty_ds_response() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let child = Name::from_ascii("child.example.com.").unwrap(); + fixture.respond(&child, RecordType::DS, [], []); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Bogus); +} + +/// A validating handle sets the CD bit on every query it sends upstream: the query being +/// validated and the DNSKEY and DS queries it makes through `lookup()`. +/// +/// RFC 6840 section 5.9: validating resolvers SHOULD set the CD bit on every upstream query. +/// Regression test for . +#[tokio::test] +async fn validating_queries_set_checking_disabled() -> Result<(), NetError> { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let child = Name::from_ascii("child.example.com.")?; + let parent = Name::from_ascii("example.com.")?; + fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&parent)]); + let (handle, queries) = fixture.build(); + + let response = lookup_child_a(&handle).await?; + assert_eq!( + response.answers.first().map(|record| record.proof), + Some(Proof::Insecure) + ); + + let logged = mem::take(&mut *queries.0.lock().unwrap_or_else(PoisonError::into_inner)); + for record_type in [RecordType::A, RecordType::DNSKEY, RecordType::DS] { + assert!( + logged + .iter() + .any(|entry| entry.query.query_type == record_type), + "expected at least one {record_type} query upstream" + ); + } + let cleared = logged + .iter() + .filter(|entry| !entry.checking_disabled) + .map(|entry| &entry.query) + .collect::>(); + assert!( + cleared.is_empty(), + "queries sent upstream with CD=0: {cleared:?}" + ); + Ok(()) +} + +/// A zone without NS records at its apex must not be mistaken for part of its signed parent zone. +/// +/// Regression test for . +#[tokio::test] +async fn insecure_delegation_without_apex_ns() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let com = Name::from_ascii("com.").unwrap(); + let example = Name::from_ascii("example.com.").unwrap(); + let www = Name::from_ascii("www.example.com.").unwrap(); + fixture.respond(&com, RecordType::NS, [ns(&com)], []); + fixture.respond(&example, RecordType::NS, [], [unsigned_soa(&example)]); + fixture.respond(&www, RecordType::NS, [], [unsigned_soa(&example)]); + fixture.respond(&www, RecordType::DS, [], [unsigned_soa(&example)]); + fixture.respond(&www, RecordType::A, [a(&www)], []); + let (handle, _) = fixture.build(); + + let response = handle + .lookup( + Query::query(www, RecordType::A), + DnsRequestOptions::default(), + ) + .first_answer() + .await + .unwrap(); + assert_eq!(response.answers[0].proof, Proof::Insecure); +} + +// A CNAME'd TLSA answer (RFC 7672 3.1.2) with the target's NSEC record in the authority section. +#[tokio::test] +async fn positive_answer_with_unrelated_nsec() { + subscribe(); + + let fixture = MockHandleBuilder::new(Parent::Secure); + let target = Name::from_ascii("tlsa201._dane.example.com.").unwrap(); + let nsec = NSEC::new( + Name::from_ascii("tlsa202._dane.example.com.").unwrap(), + [RecordType::TLSA, RecordType::RRSIG, RecordType::NSEC], + ); + let authorities = fixture.sign_example([Record::from_rdata( + target, + TTL, + RData::DNSSEC(DNSSECRData::NSEC(nsec)), + )]); + let (handle, _) = fixture.with_cname_tlsa(authorities).build(); + + assert_secure(lookup_tlsa(&handle).await); +} + +// Same as above, with an unrelated NSEC3 record in the authority section. +#[tokio::test] +async fn positive_answer_with_unrelated_nsec3() { + subscribe(); + + let fixture = MockHandleBuilder::new(Parent::Secure); + let owner_hash = nsec3_hash(&Name::from_ascii("tlsa201._dane.example.com.").unwrap()); + let mut next_hash = owner_hash.clone(); + for byte in next_hash.iter_mut().rev() { + *byte = byte.wrapping_add(1); + if *byte != 0 { + break; + } + } + let nsec3 = NSEC3::new( + Nsec3HashAlgorithm::SHA1, + false, + 0, + Vec::new(), + next_hash, + [RecordType::TLSA, RecordType::RRSIG], + ); + let owner = Name::from_ascii(BASE32_DNSSEC.encode(&owner_hash)) + .unwrap() + .append_domain(fixture.example.signer_name()) + .unwrap(); + let authorities = fixture.sign_example([Record::from_rdata( + owner, + TTL, + RData::DNSSEC(DNSSECRData::NSEC3(nsec3)), + )]); + let (handle, _) = fixture.with_cname_tlsa(authorities).build(); + + assert_secure(lookup_tlsa(&handle).await); +} + +// A wildcard expansion whose authority section only has an unrelated NSEC record. +#[tokio::test] +async fn wildcard_answer_with_unrelated_nsec() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let authorities = fixture.unrelated_nsec(); + let answers = fixture.wildcard_a(&wildcard_host()); + fixture.respond(&wildcard_host(), RecordType::A, answers, authorities); + let (handle, _) = fixture.build(); + + assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await); +} + +// A wildcard expansion without any denial records. +#[tokio::test] +async fn wildcard_answer_without_nsec() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let answers = fixture.wildcard_a(&wildcard_host()); + fixture.respond(&wildcard_host(), RecordType::A, answers, []); + let (handle, _) = fixture.build(); + + assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await); +} + +// A wildcard expansion with an NSEC record covering the next closer name. +#[tokio::test] +async fn wildcard_answer_with_next_closer_proof() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let nsec = NSEC::new( + Name::from_ascii("z.wild.example.com.").unwrap(), + [RecordType::A, RecordType::RRSIG, RecordType::NSEC], + ); + let authorities = fixture.sign_example([Record::from_rdata( + Name::from_ascii("a.wild.example.com.").unwrap(), + TTL, + RData::DNSSEC(DNSSECRData::NSEC(nsec)), + )]); + let answers = fixture.wildcard_a(&wildcard_host()); + fixture.respond(&wildcard_host(), RecordType::A, answers, authorities); + let (handle, _) = fixture.build(); + + assert_secure(lookup(&handle, wildcard_host(), RecordType::A).await); +} + +// A wildcard-expanded CNAME, not the queried RRset itself, still needs a next closer proof. +#[tokio::test] +async fn wildcard_cname_with_unrelated_nsec() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let target = Name::from_ascii("www.example.com.").unwrap(); + let cname = Record::from_rdata( + Name::from_ascii("*.wild.example.com.").unwrap(), + TTL, + RData::CNAME(CNAME(target.clone())), + ); + let mut answers = fixture.expand_wildcard(cname, &wildcard_host()); + answers.extend(fixture.sign_example([a(&target)])); + let authorities = fixture.unrelated_nsec(); + fixture.respond(&wildcard_host(), RecordType::A, answers, authorities); + let (handle, _) = fixture.build(); + + assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await); +} + +fn assert_secure(result: Result) { + let response = result.expect("response should validate"); + assert!(!response.answers.is_empty()); + for record in &response.answers { + assert_eq!(record.proof, Proof::Secure, "{record}"); + } +} + +fn assert_bogus(result: Result) { + match result { + Err(NetError::Dns(DnsError::Nsec { proof, .. })) => assert_eq!(proof, Proof::Bogus), + other => panic!("expected a bogus NSEC proof, got {other:?}"), + } +} + +fn wildcard_host() -> Name { + Name::from_ascii("host.wild.example.com.").unwrap() +} + +fn nsec3_hash(name: &Name) -> Vec { + Nsec3HashAlgorithm::SHA1 + .hash(&[], name, 0) + .unwrap() + .as_ref() + .to_vec() +} + +async fn lookup_tlsa(handle: &DnssecDnsHandle) -> Result { + lookup( + handle, + Name::from_ascii("_25._tcp.mx.example.com.").unwrap(), + RecordType::TLSA, + ) + .await +} + +async fn lookup( + handle: &DnssecDnsHandle, + name: Name, + record_type: RecordType, +) -> Result { + handle + .lookup( + Query::query(name, record_type), + DnsRequestOptions::default(), + ) + .first_answer() + .await +} + +/// Failed DS queries below an insecure delegation must not prevent the zone from being +/// recognized as insecure. +#[tokio::test] +async fn insecure_delegation_with_failed_ds_queries() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Insecure); + let child = Name::from_ascii("child.example.com.").unwrap(); + let www = Name::from_ascii("www.child.example.com.").unwrap(); + fixture.fail(&www, RecordType::DS); + fixture.fail(&child, RecordType::DS); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Insecure); +} + +/// Failed DS queries below a secure delegation must not be treated as an insecure delegation. +#[tokio::test] +async fn bogus_delegation_with_failed_ds_queries() { + subscribe(); + + let mut fixture = MockHandleBuilder::new(Parent::Secure); + let child = Name::from_ascii("child.example.com.").unwrap(); + let www = Name::from_ascii("www.child.example.com.").unwrap(); + fixture.fail(&www, RecordType::DS); + fixture.fail(&child, RecordType::DS); + let (handle, _) = fixture.build(); + + let response = lookup_child_a(&handle).await.unwrap(); + assert_eq!(response.answers[0].proof, Proof::Bogus); +} + +async fn lookup_child_a(handle: &DnssecDnsHandle) -> Result { + let name = Name::from_ascii("www.child.example.com.").unwrap(); + handle + .lookup( + Query::query(name, RecordType::A), + DnsRequestOptions::default(), + ) + .first_answer() + .await +} + +/// A signed root zone and a signed `com.` zone, delegating to `example.com.`, which in turn +/// delegates to `child.example.com.`. Both `example.com.` and `child.example.com.` are unsigned +/// zones, and `www.child.example.com.` has an A record. +/// +/// If the `parent` configuration value is [`Parent::Secure`], `com.` zone will include a DS +/// record for the `example.com.` zone, establishing a secure delegation, even though the +/// `example.com.` zone's records are unsigned. +struct MockHandleBuilder { + root: DnssecSigner, + example: DnssecSigner, + responses: HashMap<(LowerName, RecordType), Message>, +} + +impl MockHandleBuilder { + fn new(parent: Parent) -> Self { + let key = + Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap(); + let public_key = key.to_public_key().unwrap(); + let root = DnssecSigner::new( + DNSKEY::from_key(&public_key), + Box::new(key), + Name::root(), + Duration::from_secs(86400), + ); + + let key = + Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap(); + let example_public_key = key.to_public_key().unwrap(); + let example = DnssecSigner::new( + DNSKEY::from_key(&example_public_key), + Box::new(key), + Name::from_ascii("example.com.").unwrap(), + Duration::from_secs(86400), + ); + + let mut new = Self { + root, + example, + responses: HashMap::default(), + }; + + let key = + Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap(); + let com_public_key = key.to_public_key().unwrap(); + let com = DnssecSigner::new( + DNSKEY::from_key(&com_public_key), + Box::new(key), + Name::from_ascii("com.").unwrap(), + Duration::from_secs(86400), + ); + + let example = new.example.signer_name().clone(); + let child = Name::from_ascii("child.example.com.").unwrap(); + let www = Name::from_ascii("www.child.example.com.").unwrap(); + + new.respond( + &new.root.signer_name().clone(), + RecordType::DNSKEY, + sign([dnskey(&new.root)], &new.root), + [], + ); + new.respond( + com.signer_name(), + RecordType::DS, + sign([ds(&com, &com_public_key)], &new.root), + [], + ); + new.respond( + com.signer_name(), + RecordType::DNSKEY, + sign([dnskey(&com)], &com), + [], + ); + new.respond(&example, RecordType::NS, [ns(&example)], []); + + match parent { + Parent::Insecure => { + let mut authorities = sign([unsigned_soa(com.signer_name())], &com); + authorities.extend(sign( + [Record::from_rdata( + example.clone(), + TTL, + RData::DNSSEC(DNSSECRData::NSEC(NSEC::new( + Name::from_ascii("example0.com.").unwrap(), + [RecordType::NS, RecordType::RRSIG, RecordType::NSEC], + ))), + )], + &com, + )); + new.respond(&example, RecordType::DS, [], authorities); + } + Parent::Secure => { + let records = sign([ds(&new.example, &example_public_key)], &com); + new.respond(&example, RecordType::DS, records, []); + let records = sign([dnskey(&new.example)], &new.example); + new.respond(&example, RecordType::DNSKEY, records, []); + } + } + + new.respond(&child, RecordType::NS, [ns(&child)], []); + new.respond(&www, RecordType::NS, [], []); + new.respond(&www, RecordType::DS, [], [unsigned_soa(&child)]); + new.respond(&www, RecordType::A, [a(&www)], []); + new + } + + fn respond( + &mut self, + name: &Name, + record_type: RecordType, + answers: impl IntoIterator, + authorities: impl IntoIterator, + ) { + let mut message = Message::query(); + message.metadata.response_code = ResponseCode::NoError; + message.add_answers(answers); + message.add_authorities(authorities); + self.responses + .insert((LowerName::from(name), record_type), message); + } + + fn sign_example(&self, records: impl IntoIterator) -> Vec { + sign(records, &self.example) + } + + /// Sign a record owned by a wildcard name, and rename it as if it were expanded to `name`. + fn expand_wildcard(&self, record: Record, name: &Name) -> Vec { + let mut records = self.sign_example([record]); + for record in &mut records { + record.name = name.clone(); + } + records + } + + fn wildcard_a(&self, name: &Name) -> Vec { + self.expand_wildcard(a(&Name::from_ascii("*.wild.example.com.").unwrap()), name) + } + + fn unrelated_nsec(&self) -> Vec { + let nsec = NSEC::new( + Name::from_ascii("tlsa202._dane.example.com.").unwrap(), + [RecordType::TLSA, RecordType::RRSIG, RecordType::NSEC], + ); + self.sign_example([Record::from_rdata( + Name::from_ascii("tlsa201._dane.example.com.").unwrap(), + TTL, + RData::DNSSEC(DNSSECRData::NSEC(nsec)), + )]) + } + + /// Answer `_25._tcp.mx.example.com. TLSA` with a CNAME to `tlsa201._dane.example.com.` and + /// the TLSA record at the target. + fn with_cname_tlsa(mut self, authorities: Vec) -> Self { + let alias = Name::from_ascii("_25._tcp.mx.example.com.").unwrap(); + let target = Name::from_ascii("tlsa201._dane.example.com.").unwrap(); + let mut answers = self.sign_example([Record::from_rdata( + alias.clone(), + TTL, + RData::CNAME(CNAME(target.clone())), + )]); + answers.extend(self.sign_example([Record::from_rdata( + target, + TTL, + RData::TLSA(TLSA::new( + CertUsage::DaneEe, + Selector::Spki, + Matching::Sha256, + vec![0; 32], + )), + )])); + self.respond(&alias, RecordType::TLSA, answers, authorities); + self + } + + fn fail(&mut self, name: &Name, record_type: RecordType) { + let mut message = Message::query(); + message.metadata.response_code = ResponseCode::ServFail; + self.responses + .insert((LowerName::from(name), record_type), message); + } + + fn build(self) -> (DnssecDnsHandle, QueryLog) { + let Self { + root, responses, .. + } = self; + let queries = QueryLog::default(); + let handle = MockHandle { + responses: Arc::new(responses), + queries: queries.clone(), + }; + + let mut anchors = TrustAnchors::empty(); + anchors.insert(&root.key().to_public_key().unwrap()); + + ( + DnssecDnsHandle::with_trust_anchor(handle, Arc::new(anchors)), + queries, + ) + } +} + +#[derive(Clone, Copy)] +enum Parent { + Insecure, + Secure, +} + +/// Sign the given RRset with this zone's key, returning the records and their RRSIG. +fn sign(records: impl IntoIterator, signer: &DnssecSigner) -> Vec { + let mut records = records.into_iter().collect::>(); + let first = records.first().expect("RRset must not be empty"); + let mut rrset = RecordSet::with_ttl(first.name.clone(), first.record_type(), TTL); + for record in &records { + rrset.insert(record.clone(), 0); + } + + let inception = OffsetDateTime::now_utc() - Duration::from_secs(3600); + let rrsig = RRSIG::from_rrset(&rrset, DNSClass::IN, inception, signer).unwrap(); + records.push(Record::from_rdata( + first.name.clone(), + TTL, + RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), + )); + records +} + +fn ds(signer: &DnssecSigner, pub_key: &PublicKeyBuf) -> Record { + let ds = DS::from_key(pub_key, signer.signer_name(), DigestType::SHA256).unwrap(); + Record::from_rdata( + signer.signer_name().clone(), + TTL, + RData::DNSSEC(DNSSECRData::DS(ds)), + ) +} + +fn dnskey(signer: &DnssecSigner) -> Record { + Record::from_rdata( + signer.signer_name().clone(), + TTL, + RData::DNSSEC(DNSSECRData::DNSKEY(signer.to_dnskey())), + ) +} + +fn unsigned_soa(name: &Name) -> Record { + Record::from_rdata( + name.clone(), + TTL, + RData::SOA(SOA::new( + Name::from_ascii("ns.") + .unwrap() + .append_domain(name) + .unwrap(), + Name::from_ascii("admin.") + .unwrap() + .append_domain(name) + .unwrap(), + 1, + 3600, + 3600, + 3600, + 3600, + )), + ) +} + +fn ns(name: &Name) -> Record { + Record::from_rdata( + name.clone(), + TTL, + RData::NS(NS(Name::from_ascii("ns.") + .unwrap() + .append_domain(name) + .unwrap())), + ) +} + +fn a(name: &Name) -> Record { + Record::from_rdata(name.clone(), TTL, RData::A(A(Ipv4Addr::new(192, 0, 2, 1)))) +} + +#[derive(Clone, Default)] +struct QueryLog(Arc>>); + +/// A query the mock upstream received, with the CD bit of the request that carried it. +struct LoggedQuery { + query: Query, + checking_disabled: bool, +} + +impl QueryLog { + fn count(&self, name: &Name, record_type: RecordType) -> usize { + self.0 + .lock() + .unwrap() + .iter() + .filter(|logged| &logged.query.name == name && logged.query.query_type == record_type) + .count() + } +} + +#[derive(Clone)] +struct MockHandle { + responses: Arc>, + queries: QueryLog, +} + +impl DnsHandle for MockHandle { + type Response = Pin> + Send>>; + type Runtime = TokioRuntimeProvider; + + fn send(&self, request: DnsRequest) -> Self::Response { + let query = request.queries[0].clone(); + self.queries + .0 + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(LoggedQuery { + query: query.clone(), + checking_disabled: request.metadata.checking_disabled, + }); + + let key = (LowerName::from(&query.name), query.query_type); + let Some(message) = self.responses.get(&key) else { + return Box::pin(stream::once(future::err(NetError::from(format!( + "no canned response for {query}" + ))))); + }; + + if message.response_code != ResponseCode::NoError { + return Box::pin(stream::once(future::err(NetError::from( + DnsError::ResponseCode(message.response_code), + )))); + } + + let mut message = message.clone(); + message.metadata.id = request.metadata.id; + message.add_query(query); + let response = DnsResponse::from_message(message.into_response()).unwrap(); + Box::pin(stream::once(future::ok(response))) + } +} + +const TTL: u32 = 3600; diff --git a/vendor/hickory-dns/hickory-net/src/dnssec/mod.rs b/vendor/hickory-dns/hickory-net/src/dnssec/mod.rs new file mode 100644 index 00000000..88d7b45c --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/dnssec/mod.rs @@ -0,0 +1,3605 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! The `DnssecDnsHandle` is used to validate all DNS responses for correct DNSSEC signatures. + +use core::{ + clone::Clone, + fmt::Display, + hash::{Hash, Hasher}, + mem, + ops::RangeInclusive, + pin::Pin, + time::Duration, +}; +use std::{ + collections::{HashMap, HashSet, hash_map::DefaultHasher}, + sync::Arc, + time::Instant, +}; + +use futures_util::{ + future::{self, FutureExt}, + stream::{self, Stream, StreamExt}, +}; +use lru_cache::LruCache; +use parking_lot::Mutex; +use tracing::{debug, error, trace, warn}; + +use crate::{ + error::{DnsError, NetError, NoRecords}, + proto::{ + dnssec::{ + Proof, TrustAnchors, Verifier, + rdata::{DNSKEY, DNSSECRData, DS, NSEC, NSEC3, RRSIG, SigInput}, + }, + op::{ + DnsRequest, DnsRequestOptions, DnsResponse, Edns, Message, OpCode, Query, ResponseCode, + }, + rr::{ + LowerName, Name, RData, Record, RecordRef, RecordSet, RecordSetParts, RecordType, + SerialNumber, + }, + }, + runtime::{RuntimeProvider, Time}, + xfer::{FirstAnswer, dns_handle::DnsHandle}, +}; + +mod error; +pub use error::{ProofError, ProofErrorKind}; + +mod nsec3; +use nsec3::{verify_nsec3, verify_nsec3_insecure_delegation, verify_nsec3_next_closer}; + +#[cfg(all(test, feature = "tokio"))] +mod handle_tests; + +/// Performs DNSSEC validation of all DNS responses from the wrapped DnsHandle +/// +/// This wraps a DnsHandle, changing the implementation `send()` to validate all +/// message responses for Query operations. Update operation responses are not validated by +/// this process. +#[derive(Clone)] +#[must_use = "queries can only be sent through a DnsHandle"] +pub struct DnssecDnsHandle { + handle: H, + trust_anchor: Arc, + request_depth: usize, + nsec3_soft_iteration_limit: u16, + nsec3_hard_iteration_limit: u16, + validation_cache: ValidationCache, +} + +impl DnssecDnsHandle { + /// Create a new DnssecDnsHandle wrapping the specified handle. + /// + /// This uses the compiled in TrustAnchor default trusted keys. + /// + /// # Arguments + /// * `handle` - handle to use for all connections to a remote server. + pub fn new(handle: H) -> Self { + Self::with_trust_anchor(handle, Arc::new(TrustAnchors::default())) + } + + /// Create a new DnssecDnsHandle wrapping the specified handle. + /// + /// This allows a custom TrustAnchor to be define. + /// + /// # Arguments + /// * `handle` - handle to use for all connections to a remote server. + /// * `trust_anchor` - custom DNSKEYs that will be trusted, can be used to pin trusted keys. + pub fn with_trust_anchor(handle: H, trust_anchor: Arc) -> Self { + Self { + handle, + trust_anchor, + request_depth: 0, + // These default values are based on + // [RFC 9276 Appendix A](https://www.rfc-editor.org/rfc/rfc9276.html#appendix-A) + nsec3_soft_iteration_limit: 100, + nsec3_hard_iteration_limit: 500, + validation_cache: ValidationCache::new(DEFAULT_VALIDATION_CACHE_SIZE), + } + } + + /// Set custom NSEC3 iteration limits + /// + /// # Arguments + /// * `soft_limit` - the soft limit for NSEC3 iterations. NSEC3 records with iteration counts + /// above this limit, but below the hard limit will evaluate to Proof::Insecure. + /// * `hard_limit` - the hard limit for NSEC3 iterations. NSEC3 records with iteration counts + /// above this limit will evaluate to Proof::Bogus. + pub fn nsec3_iteration_limits( + mut self, + soft_limit: Option, + hard_limit: Option, + ) -> Self { + if let Some(soft) = soft_limit { + self.nsec3_soft_iteration_limit = soft; + } + + if let Some(hard) = hard_limit { + self.nsec3_hard_iteration_limit = hard; + } + + self + } + + /// Set a custom validation cache size + /// + /// # Arguments + /// * `capacity` - the desired capacity of the DNSSEC validation cache. + pub fn validation_cache_size(mut self, capacity: usize) -> Self { + self.validation_cache = ValidationCache::new(capacity); + self + } + + /// Set custom negative response validation cache TTL range + /// + /// # Arguments + /// * `ttl` - A range of permissible TTL values for negative responses. + /// + /// Validation cache TTLs are based on the Rrset TTL value, but will be clamped to + /// this value, if specified, for negative responses. + pub fn negative_validation_ttl(mut self, ttl: RangeInclusive) -> Self { + self.validation_cache.negative_ttl = Some(ttl); + self + } + + /// Set custom positive response validation cache TTL range + /// + /// # Arguments + /// * `ttl` - A range of permissible TTL values for positive responses. + /// + /// Validation cache TTLs are based on the Rrset TTL value, but will be clamped to + /// this value, if specified, for positive responses. + pub fn positive_validation_ttl(mut self, ttl: RangeInclusive) -> Self { + self.validation_cache.positive_ttl = Some(ttl); + self + } + + async fn verify_response( + self, + result: Result, + query: Query, + options: DnsRequestOptions, + ) -> Result { + let mut message = match result { + Ok(response) => response, + // Translate NoRecordsFound errors into a DnsResponse message so the rest of the + // DNSSEC handler chain can validate negative responses. + Err(NetError::Dns(DnsError::NoRecordsFound(NoRecords { + query, + soa, + authorities, + response_code, + .. + }))) => { + debug!("translating NoRecordsFound to DnsResponse for {query}"); + let mut msg = Message::query(); + msg.add_query(*query); + msg.metadata.response_code = response_code; + + if let Some(authorities) = authorities { + for record in authorities.iter() { + msg.add_authority(record.clone()); + } + } + + // Make sure we preserve the SOA record. It is not obvious whether we need this; + // most callers should preserve it, but not every caller might. + // TODO: be more principled about how this is handled. + if let Some(soa) = soa { + let soa = soa.into_record_of_rdata(); + if !msg.authorities.iter().any(|r| r == &soa) { + msg.add_authority(soa); + } + } + + match DnsResponse::from_message(msg.into_response()) { + Ok(response) => response, + Err(err) => { + return Err(NetError::from(format!( + "unable to construct DnsResponse: {err:?}" + ))); + } + } + } + Err(err) => return Err(err), + }; + + // Reject foreign-class records in answers/authorities/additionals. + // We only issue IN-class queries. + for record in message + .answers + .iter() + .chain(message.authorities.iter()) + .chain(message.additionals.iter()) + { + if record.dns_class == query.query_class { + continue; + } + error!( + %query, + record_name = %record.name, + record_class = %record.dns_class, + record_type = %record.record_type(), + "rejecting response: record class does not match query class", + ); + return Err(NetError::ForeignClassRecord { + record_name: record.name.clone(), + record_class: record.dns_class, + record_type: record.record_type(), + }); + } + + debug!( + "validating message_response: {}, with {} trust_anchors", + message.id, + self.trust_anchor.len(), + ); + + // use the same current time value for all rrsig + rrset pairs. + let current_time = ::Timer::current_time() as u32; + + // group the record sets by name and type + // each rrset type needs to validated independently + let answers = mem::take(&mut message.answers); + let authorities = mem::take(&mut message.authorities); + let additionals = mem::take(&mut message.additionals); + + let answers = self + .verify_rrsets(&query, answers, options, current_time) + .await; + let authorities = self + .verify_rrsets(&query, authorities, options, current_time) + .await; + let additionals = self + .verify_rrsets(&query, additionals, options, current_time) + .await; + + message.insert_answers(answers); + message.insert_authorities(authorities); + message.insert_additionals(additionals); + + // Check NSEC3 iteration count against limits. + // + // [RFC 9276 3.2](https://www.rfc-editor.org/rfc/rfc9276.html#name-recommendation-for-validati). + let max_iterations = message + .authorities + .iter() + .filter(|rr| rr.proof == Proof::Secure) + .filter_map(|rr| match &rr.data { + RData::DNSSEC(DNSSECRData::NSEC3(nsec3)) => Some(nsec3.iterations()), + _ => None, + }) + .max() + .unwrap_or_default(); + if max_iterations > self.nsec3_hard_iteration_limit { + debug!( + %query, + iterations = max_iterations, + nsec3_hard_iteration_limit = self.nsec3_hard_iteration_limit, + "iteration count is over hard limit" + ); + return Err(NetError::from(DnsError::Nsec { + query: Box::new(query.clone()), + response: Box::new(message), + proof: Proof::Bogus, + })); + } + if max_iterations > self.nsec3_soft_iteration_limit { + debug!( + %query, + iterations = max_iterations, + nsec3_soft_iteration_limit = self.nsec3_soft_iteration_limit, + "iteration count is over soft limit" + ); + return Err(NetError::from(DnsError::Nsec { + query: Box::new(query.clone()), + response: Box::new(message), + proof: Proof::Insecure, + })); + } + + // Verify that wildcard expansion was performed correctly on each RRset. + let unproven_wildcards = unproven_wildcard_rrsets(&message); + if !unproven_wildcards.is_empty() { + let Message { + answers, + authorities, + additionals, + .. + } = &mut *message; + for record in answers.iter_mut().chain(authorities).chain(additionals) { + if unproven_wildcards.iter().any(|(name, record_type)| { + record.record_type() == *record_type && record.name == *name + }) { + record.proof = Proof::Bogus; + } + } + return Err(NetError::from(DnsError::Nsec { + query: Box::new(query.clone()), + response: Box::new(message), + proof: Proof::Bogus, + })); + } + + if !message.authorities.is_empty() + && message + .authorities + .iter() + .all(|x| x.proof == Proof::Insecure) + { + return Ok(message); + } + + // Return Ok if this is a valid positive response. + if message.answers.iter().any(|record| { + record.name == query.name + && (record.record_type() == RecordType::CNAME + || record.record_type() == query.query_type) + }) { + return Ok(message); + } + + let nsec3s = message + .authorities + .iter() + .filter_map(|rr| { + if rr.proof == Proof::Secure { + match &rr.data { + RData::DNSSEC(DNSSECRData::NSEC3(nsec3)) => Some((&rr.name, nsec3)), + _ => None, + } + } else { + None + } + }) + .collect::>(); + + let nsecs = message + .authorities + .iter() + .filter_map(|rr| { + if rr.proof == Proof::Secure { + match &rr.data { + RData::DNSSEC(DNSSECRData::NSEC(nsec)) => Some((&rr.name, nsec)), + _ => None, + } + } else { + None + } + }) + .collect::>(); + + // Both NSEC and NSEC3 records cannot coexist during + // transition periods, as per RFC 5515 10.4.3 and + // 10.5.2 + let nsec_proof = match (!nsec3s.is_empty(), !nsecs.is_empty()) { + (true, false) => verify_nsec3( + &query, + find_soa_name(&message), + message.response_code, + &nsec3s, + ), + (false, true) => verify_nsec( + &query, + find_soa_name(&message), + message.response_code, + &nsecs, + ), + (true, true) => { + warn!( + "response contains both NSEC and NSEC3 records\nQuery:\n{query:?}\nResponse:\n{message:?}" + ); + Proof::Bogus + } + (false, false) => { + // Calling find_ds_records for a DS query will cause a validation loop if the zone being + // queried is insecure and its parent zone is insecure (no DS records will exist and no + // NSEC records will be available to prove that non-existence.) Return ok/insecure: + // + // * If the query type is DS *and* the parent zone is Insecure + // * For other query types, if the queried name is provably Insecure + if let Err(err) = self + .find_ds_records( + match query.query_type() { + RecordType::DS => query.name().base_name(), + _ => query.name().clone(), + }, + options, + ) + .await + { + if err.proof == Proof::Insecure { + return Ok(message); + } + } + + // If neither of the two conditions above are true, the response is Bogus - we should + // have a covering NSEC/NSEC3 record for this scenario. + warn!( + "response does not contain NSEC or NSEC3 records. Query: {query:?} response: {message:?}" + ); + Proof::Bogus + } + }; + + if !nsec_proof.is_secure() { + debug!("returning Nsec error for {} {nsec_proof}", query.name()); + // TODO change this to remove the NSECs, like we do for the others? + return Err(NetError::from(DnsError::Nsec { + query: Box::new(query.clone()), + response: Box::new(message), + proof: nsec_proof, + })); + } + + Ok(message) + } + + /// This pulls all answers returned in a Message response and returns a future which will + /// validate all of them. + async fn verify_rrsets( + &self, + query: &Query, + records: Vec, + options: DnsRequestOptions, + current_time: u32, + ) -> Vec { + let mut rrset_types: HashSet<(Name, RecordType)> = HashSet::new(); + + for rrset in records + .iter() + .filter(|rr| { + rr.record_type() != RecordType::RRSIG && + // if we are at a depth greater than 1, we are only interested in proving evaluation chains + // this means that only DNSKEY, DS, NSEC, and NSEC3 are interesting at that point. + // this protects against looping over things like NS records and DNSKEYs in responses. + // TODO: is there a cleaner way to prevent cycles in the evaluations? + (self.request_depth <= 1 || matches!( + rr.record_type(), + RecordType::DNSKEY | RecordType::DS | RecordType::NSEC | RecordType::NSEC3 | RecordType::SOA, + )) + }) + .map(|rr| (rr.name.clone(), rr.record_type())) + { + rrset_types.insert(rrset); + } + + // there were no records to verify + if rrset_types.is_empty() { + return records; + } + + // Records for return, eventually, all records will be returned in here + let mut return_records = Vec::with_capacity(records.len()); + + // Removing the RRSIGs from the original records, the rest of the records will be mutable to remove those evaluated + // and the remainder after all evalutions will be returned. + let (mut rrsigs, mut records) = records + .into_iter() + .partition::, _>(|r| r.record_type().is_rrsig()); + + for (name, record_type) in rrset_types { + // collect all the rrsets to verify + let current_rrset; + (current_rrset, records) = records + .into_iter() + .partition::, _>(|rr| rr.record_type() == record_type && rr.name == name); + + let current_rrsigs; + (current_rrsigs, rrsigs) = rrsigs.into_iter().partition::, _>(|rr| { + rr.try_borrow::() + .map(|rr| rr.name() == &name && rr.data().input().type_covered == record_type) + .unwrap_or_default() + }); + + let mut rrset = RecordSet::new(name.clone(), record_type, 0); + rrset.set_records(current_rrset); + rrset.set_rrsigs(current_rrsigs); + + debug!( + "verifying: {name} record_type: {record_type}, rrsigs: {rrsig_len}", + rrsig_len = rrsigs.len() + ); + + // verify this rrset + let context = RrsetVerificationContext { + query, + rrset: &rrset, + options, + current_time, + }; + let key = context.key(); + let proof = match self.validation_cache.get(&key, &context) { + Some(cached) => cached, + None => { + // Generally, the RRSET will be validated by `verify_default_rrset()`. There are additional + // checks that happen after the RRSET is successfully validated. In the case of DNSKEYs this + // triggers `verify_dnskey_rrset()`. If it's an NSEC record, then the NSEC record will be + // validated to prove it's correctness. There is a special case for DNSKEY, where if the RRSET + // is unsigned, `rrsigs` is empty, then an immediate `verify_dnskey_rrset()` is triggered. In + // this case, it's possible the DNSKEY is a trust_anchor and is not self-signed. + let proof = match context.rrset.record_type() { + RecordType::DNSKEY => self.verify_dnskey_rrset(&context).await, + _ => self.verify_default_rrset(&context).await, + }; + + match &proof { + // These could be transient errors that should be retried. + Err(e) if matches!(e.kind(), ProofErrorKind::Net { .. }) => { + debug!("not caching DNSSEC validation with ProofErrorKind::Net") + } + _ => { + self.validation_cache.insert(proof.clone(), key, &context); + } + } + + proof + } + }; + + let proof = match proof { + Ok(proof) => { + debug!("verified: {name} record_type: {record_type}",); + proof + } + Err(err) => { + match err.kind() { + ProofErrorKind::DsResponseInsecure { .. } => { + debug!("verified insecure {name}/{record_type}") + } + kind => { + debug!("failed to verify: {name} record_type: {record_type}: {kind}") + } + } + RrsetProof { + proof: err.proof, + adjusted_ttl: None, + rrsig_index: None, + } + } + }; + + let RrsetProof { + proof, + adjusted_ttl, + rrsig_index: rrsig_idx, + } = proof; + + let RecordSetParts { + records: current_rrset, + rrsigs: current_rrsigs, + .. + } = rrset.into_parts(); + + for mut record in current_rrset { + record.proof = proof; + if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) { + record.ttl = ttl; + } + + return_records.push(record); + } + + // only mark the RRSIG used for the proof + let mut current_rrsigs = current_rrsigs; + if let Some(rrsig_idx) = rrsig_idx { + if let Some(rrsig) = current_rrsigs.get_mut(rrsig_idx) { + rrsig.proof = proof; + if let (Proof::Secure, Some(ttl)) = (proof, adjusted_ttl) { + rrsig.ttl = ttl; + } + } else { + warn!( + "bad rrsig index {rrsig_idx} rrsigs.len = {}", + current_rrsigs.len() + ); + } + } + + // push all the RRSIGs back to the return + return_records.extend(current_rrsigs); + } + + // Add back all the RRSIGs and any records that were not verified + return_records.extend(rrsigs); + return_records.extend(records); + return_records + } + + /// DNSKEY-specific verification + /// + /// A DNSKEY needs to be checked against a DS record provided by the parent zone. + /// + /// A DNSKEY that's part of the trust anchor does not need to have its DS record (which may + /// not exist as it's the case of the root zone) nor its RRSIG validated. If an RRSIG is present + /// it will be validated. + /// + /// # Return + /// + /// If Ok, returns an RrsetProof containing the proof, adjusted TTL, and an index of the RRSIG used for + /// validation of the rrset. + /// + /// # Panics + /// + /// This method should only be called to validate DNSKEYs, see `verify_default_rrset` for other record types. + /// if a non-DNSKEY RRSET is passed into this method it will always panic. + async fn verify_dnskey_rrset( + &self, + context: &RrsetVerificationContext<'_>, + ) -> Result { + let RrsetVerificationContext { + rrset, + current_time, + options, + .. + } = context; + + // Ensure that this method is not misused + if RecordType::DNSKEY != rrset.record_type() { + panic!("All other RRSETs must use verify_default_rrset"); + } + + debug!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + "validating rrset with dnskeys", + ); + + let mut dnskey_proofs = + Vec::<(Proof, Option, Option)>::with_capacity(rrset.records_count()); + dnskey_proofs.resize(rrset.records_count(), (Proof::Bogus, None, None)); + + // Check if the DNSKEYs are in the trust anchors. + for (r, proof) in rrset.records(false).zip(dnskey_proofs.iter_mut()) { + let Some(dnskey) = r.try_borrow::() else { + continue; + }; + + proof.0 = self.is_dnskey_in_root_store(&dnskey, &rrset.name().into()); + } + + // If not all DNSKEYs are trust anchors, and this isn't the root DNSKEY RRset, then we + // should look for DS records to possibly verify more keys. + let ds_records = + if !dnskey_proofs.iter().all(|p| p.0.is_secure()) && !rrset.name().is_root() { + // Need to get DS records for each DNSKEY. + // Every DNSKEY other than the root zone's keys may have a corresponding DS record. + self.fetch_ds_records(rrset.name().clone(), *options) + .await? + } else { + debug!("ignoring DS lookup for root zone or registered keys"); + Vec::default() + }; + + // if the DS records are not empty and they also have no supported algorithms, then this is INSECURE + // for secure DS records the BOGUS check happens after DNSKEYs are evaluated against the DS + if ds_records + .iter() + .filter(|ds| ds.proof.is_secure() || ds.proof.is_insecure()) + .all(|ds| !ds.data.algorithm().is_supported() || !ds.data.digest_type().is_supported()) + && !ds_records.is_empty() + { + debug!( + "all dnskeys use unsupported algorithms and there are no supported DS records in the parent zone" + ); + // cannot validate; mark as insecure + return Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::UnsupportedKeyAlgorithm, + )); + } + + // verify all dnskeys individually against the DS records + for (r, proof) in rrset.records(false).zip(dnskey_proofs.iter_mut()) { + let Some(dnskey) = r.try_borrow() else { + continue; + }; + + if proof.0.is_secure() { + continue; + } + + // need to track each proof on each dnskey to ensure they are all validated + match verify_dnskey(&dnskey, &ds_records) { + Ok(pf) => *proof = (pf, None, None), + Err(err) => *proof = (err.proof, None, None), + } + } + + // There may have been a key-signing key for the zone, + // we need to verify all the other DNSKEYS in the zone against it (i.e. the rrset) + for (i, rrsig) in rrset.rrsigs().iter().enumerate() { + // These should all match, but double checking... + let Some(rrsig) = rrsig.try_borrow::() else { + continue; + }; + let signer_name = &rrsig.data().input().signer_name; + + let rrset_proof = rrset + .records(false) + .zip(dnskey_proofs.iter()) + .filter(|(_, (proof, ..))| proof.is_secure()) + .filter(|(r, _)| &r.name == signer_name) + .filter_map(|(r, (proof, ..))| { + RecordRef::<'_, DNSKEY>::try_from(r) + .ok() + .map(|r| (r, proof)) + }) + .find_map(|(dnskey, proof)| { + verify_rrset_with_dnskey(dnskey, *proof, &rrsig, rrset, *current_time).ok() + }); + + if let Some(rrset_proof) = rrset_proof { + return Ok(RrsetProof { + proof: rrset_proof.0, + adjusted_ttl: rrset_proof.1, + rrsig_index: Some(i), + }); + } + } + + // if it was just the root DNSKEYS with no RRSIG, we'll accept the entire set, or none + if dnskey_proofs.iter().all(|(proof, ..)| proof.is_secure()) { + let proof = dnskey_proofs.pop().unwrap(/* This can not happen due to above test */); + return Ok(RrsetProof { + proof: proof.0, + adjusted_ttl: proof.1, + rrsig_index: proof.2, + }); + } + + if !ds_records.is_empty() { + // there were DS records, but no DNSKEYs, we're in a bogus state + trace!( + rrset_name = ?rrset.name(), + ?ds_records, + "bogus validation: missing dnskeys, but have ds records", + ); + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::DsRecordsButNoDnskey { + name: rrset.name().clone(), + }, + )); + } + + // There were DS records or RRSIGs, but none of the signatures could be validated, so we're in a + // bogus state. If there was no DS record, it should have gotten an NSEC upstream, and returned + // early above. + trace!(rrset_name = ?rrset.name(), "no dnskey found"); + Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::DnskeyNotFound { + name: rrset.name().clone(), + }, + )) + } + + /// Checks whether a DS RRset exists for the zone containing a name. + /// + /// Returns an error with an `Insecure` proof if the zone is proven to be insecure. Returns + /// `Ok(())` if the zone is secure. + /// + /// This requests the DS RRset at the given name and then at each of its ancestors, until one + /// is found or a delegation is proven to be insecure. The DS responses are validated to + /// determine if any DS records exist or not, and thus whether the zone is secure, insecure, or + /// bogus. Zone cuts are not discovered with NS queries, because some zones lack NS records at + /// their apex. Failed DS lookups do not stop the search, because some servers fail DS queries + /// for names in unsigned zones; an insecure result still requires a validated proof. + async fn find_ds_records( + &self, + name: Name, + options: DnsRequestOptions, + ) -> Result<(), ProofError> { + let mut ancestor = name.clone(); + let mut first_error = None; + loop { + if ancestor.is_root() { + return Err(first_error.unwrap_or_else(|| ProofError::ds_should_exist(name))); + } + + match self.lookup_ds_records(ancestor.clone(), options).await { + Ok(_) => return Ok(()), + Err(err) if err.proof == Proof::Insecure => return Err(err), + Err(err) if matches!(err.kind(), ProofErrorKind::DsRecordShouldExist { .. }) => {} + Err(err) => { + first_error.get_or_insert(err); + } + } + + ancestor = ancestor.base_name(); + } + } + + /// Retrieves DS records for the given zone. + async fn fetch_ds_records( + &self, + zone: Name, + options: DnsRequestOptions, + ) -> Result>, ProofError> { + let err = match self.lookup_ds_records(zone.clone(), options).await { + Err(err) if matches!(err.kind(), ProofErrorKind::DsRecordShouldExist { .. }) => err, + result => return result, + }; + + // The response does not contain any records that could carry an insecure proof (some + // servers return an empty authority section for DS queries in insecure zones). Check + // whether the parent zone is insecure directly instead. + match Box::pin(self.find_ds_records(zone.base_name(), options)).await { + Err(parent) if parent.proof == Proof::Insecure => { + debug!( + %zone, + "marking zone as insecure based on insecure parent zone lookup", + ); + Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::DsResponseInsecure { name: zone }, + )) + } + Ok(()) | Err(_) => Err(err), + } + } + + /// Retrieves DS records for the given zone, without checking ancestors of the zone. + async fn lookup_ds_records( + &self, + zone: Name, + options: DnsRequestOptions, + ) -> Result>, ProofError> { + let mut response = match self + .lookup(Query::query(zone.clone(), RecordType::DS), options) + .first_answer() + .await + { + Ok(response) => response, + Err(net) => { + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::Net { + query: Query::query(zone.clone(), RecordType::DS), + net, + }, + )); + } + }; + + if response + .answers + .iter() + .filter(|r| r.record_type() == RecordType::DS) + .any(|r| r.proof.is_secure()) + { + // This is a secure DS RRset. + let all_records = mem::take(&mut response.answers) + .into_iter() + .filter_map(|r| { + r.map(|data| match data { + RData::DNSSEC(DNSSECRData::DS(ds)) => Some(ds), + _ => None, + }) + }); + + let mut supported_records = vec![]; + let mut all_unknown = None; + for record in all_records { + // A chain can be either SECURE or INSECURE, but we should not trust BOGUS or other + // records. + if (!record.data.algorithm().is_supported() + || !record.data.digest_type().is_supported()) + && (record.proof.is_secure() || record.proof.is_insecure()) + { + all_unknown.get_or_insert(true); + continue; + } + all_unknown = Some(false); + + supported_records.push(record); + } + + return if all_unknown.unwrap_or(false) { + Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::UnknownKeyAlgorithm, + )) + } else if !supported_records.is_empty() { + Ok(supported_records) + } else { + Err(ProofError::ds_should_exist(zone)) + }; + } + + if response + .answers + .iter() + .any(|r| r.record_type() == RecordType::DS) + { + return Err(ProofError::ds_should_exist(zone)); + } + + // Per RFC 6840 section 4.4, beyond checking the proof of nonexistence of a DS + // record at this name, we also need to check for proof that there is an NS + // record, or could be an NS record, at this name. + + // Case 1: Matching NSEC record exists that proves NS is present and DS is not + // present. This is definitively an insecure delegation. + if response.authorities.iter().any(|record| { + if record.proof != Proof::Secure { + return false; + } + let RData::DNSSEC(DNSSECRData::NSEC(nsec)) = &record.data else { + return false; + }; + record.name == zone + && nsec.type_set().contains(RecordType::NS) + && !nsec.type_set().contains(RecordType::DS) + && !nsec.type_set().contains(RecordType::SOA) + }) { + debug!( + %zone, + "marking zone as insecure based on secure NSEC proof", + ); + return Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::DsResponseInsecure { name: zone }, + )); + } + + // Gather NSEC3 records. + let nsec3s = response + .authorities + .iter() + .filter_map(|record| { + if record.proof != Proof::Secure { + return None; + } + + let RData::DNSSEC(DNSSECRData::NSEC3(nsec3)) = &record.data else { + return None; + }; + + Some((&record.name, nsec3)) + }) + .collect::>(); + + // Case 2: Matching NSEC3 record exists that proves NS is present and DS is not + // present. This is definitively an insecure delegation. + // + // Case 3: There is an NSEC3 record that covers the name and has the opt-out bit + // set. There could be an insecure delegation at this name, but there is no way + // to know for certain due to the use of the opt-out flag. + if !nsec3s.is_empty() && verify_nsec3_insecure_delegation(&zone, &nsec3s) { + debug!( + %zone, + "marking zone as insecure based on secure NSEC3 proof", + ); + return Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::DsResponseInsecure { name: zone }, + )); + } + + // Case 4: There is an insecure delegation further up the tree. + // + // We check that parent zone is insecure by looking for at least one `Proof::Insecure` + // attached to a record with name that is at or above the child zone. Most servers + // will include an SOA record in NODATA responses, so we should see an insecure + // proof on at least that record. + if response + .all_sections() + .any(|r| r.name.zone_of(&zone) && r.proof == Proof::Insecure) + { + debug!( + %zone, + "marking zone as insecure based on insecure parent zone", + ); + return Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::DsResponseInsecure { name: zone }, + )); + } + + debug!( + %zone, + "could not prove insecure delegation", + ); + Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::DsRecordShouldExist { name: zone }, + )) + } + + /// Verifies that the key is a trust anchor. + /// + /// # Returns + /// + /// Proof::Secure if registered in the root store, Proof::Bogus if not + fn is_dnskey_in_root_store(&self, rr: &RecordRef<'_, DNSKEY>, name: &LowerName) -> Proof { + let dns_key = rr.data(); + let pub_key = dns_key.public_key(); + + // Checks to see if the key is valid against the registered root certificates + if self.trust_anchor.contains_with_name(pub_key, name) { + debug!( + "validated dnskey with trust_anchor: {}, {dns_key}", + rr.name(), + ); + + Proof::Secure + } else { + Proof::Bogus + } + } + + /// Verifies that a given RRSET is validly signed by any of the specified RRSIGs. + /// + /// Invalid RRSIGs will be ignored. RRSIGs will only be validated against DNSKEYs which can + /// be validated through a chain back to the `trust_anchor`. As long as one RRSIG is valid, + /// then the RRSET will be valid. + /// + /// # Returns + /// + /// On Ok, the set of (Proof, AdjustedTTL, and IndexOfRRSIG) is returned, where the index is the one of the RRSIG that validated + /// the Rrset + /// + /// # Panics + /// + /// This method should never be called to validate DNSKEYs, see `verify_dnskey_rrset` instead. + /// if a DNSKEY RRSET is passed into this method it will always panic. + async fn verify_default_rrset( + &self, + context: &RrsetVerificationContext<'_>, + ) -> Result { + let RrsetVerificationContext { + query: original_query, + rrset, + current_time, + options, + } = context; + + // Ensure that this method is not misused + if RecordType::DNSKEY == rrset.record_type() { + panic!("DNSKEYs must be validated with verify_dnskey_rrset"); + } + + if rrset.rrsigs().is_empty() { + // Decide if we're: + // 1) "insecure", the zone has a valid NSEC for the DS record in the parent zone + // 2) "bogus", the parent zone has a valid DS record, but the child zone didn't have the RRSIGs/DNSKEYs + // or the parent zone has a DS record without covering RRSIG records. + if rrset.record_type() != RecordType::DS { + let mut search_name = rrset.name().clone(); + if rrset.record_type() == RecordType::NSEC3 { + // No need to look for a zone cut at an NSEC3 owner name. Look at its parent + // instead, which ought to be a zone apex. + search_name = search_name.base_name(); + } + + // If this is the response to a DS query and the unsigned record is at or below + // the queried name, searching for DS records from `search_name` would find the + // same zone cut and issue the same DS query again, recursing until the maximum + // request depth is exceeded. This happens with servers that answer DS queries + // from the child zone, returning the child zone's SOA record instead of the + // parent's. The only thing such a record can tell us is whether the parent zone + // is insecure, so start the search at the parent of the queried name instead. + if original_query.query_type == RecordType::DS + && original_query.name.zone_of(&search_name) + { + search_name = original_query.name.base_name(); + } + + self.find_ds_records(search_name, *options).await?; // insecure will return early here + } + + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::RrsigsNotPresent { + name: rrset.name().clone(), + record_type: rrset.record_type(), + }, + )); + } + + // the record set is going to be shared across a bunch of futures, Arc for that. + trace!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + "default rrset validation", + ); + + // we can validate with any of the rrsigs... + // i.e. the first that validates is good enough + // TODO: could there be a cert downgrade attack here with a MITM stripping stronger RRSIGs? + // we could check for the strongest RRSIG and only use that... + // though, since the entire package isn't signed any RRSIG could have been injected, + // right? meaning if there is an attack on any of the acceptable algorithms, we'd be + // susceptible until that algorithm is removed as an option. + // dns over TLS will mitigate this. + // TODO: strip RRSIGS to accepted algorithms and make algorithms configurable. + let verifications = rrset + .rrsigs() + .iter() + .enumerate() + .filter_map(|(i, rrsig)| { + let rrsig = rrsig.try_borrow::()?; + if i > MAX_RRSIGS_PER_RRSET { + warn!("too many ({i}) RRSIGs for rrset {rrset:?}; skipping"); + return None; + } + + // RFC 4035 §5.3.1: signer must be an ancestor of (or equal to) the RRset owner + if !rrsig.data().input().signer_name.zone_of(rrset.name()) { + warn!( + signer_name = %rrsig.data().input().signer_name, + rrset_name = %rrset.name(), + "RRSIG signer name is not in the same zone as the RRset; skipping", + ); + return None; + } + + let query = + Query::query(rrsig.data().input().signer_name.clone(), RecordType::DNSKEY); + + // Break verification cycle + if query.name() == original_query.name() + && query.query_type() == original_query.query_type() + { + warn!( + query_name = %query.name(), + query_type = %query.query_type(), + original_query_name = %original_query.name(), + original_query_type = %original_query.query_type(), + "stopping verification cycle in verify_default_rrset", + ); + return None; + } + + Some( + self.lookup(query.clone(), *options) + .first_answer() + .map(move |result| match result { + Ok(message) => { + // Report a failed signature as an error, so that select_ok() + // below tries the remaining RRSIGs. + match verify_rrsig_with_keys(message, &rrsig, rrset, *current_time) + { + Some((proof, adjusted_ttl)) => Ok(RrsetProof { + proof, + adjusted_ttl, + rrsig_index: Some(i), + }), + None => Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::RrsigsUnverified { + name: rrset.name().clone(), + record_type: rrset.record_type(), + }, + )), + } + } + Err(net) => Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::Net { query, net }, + )), + }), + ) + }) + .collect::>(); + + // if there are no available verifications, then we are in a failed state. + if verifications.is_empty() { + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::RrsigsNotPresent { + name: rrset.name().clone(), + record_type: rrset.record_type(), + }, + )); + } + + // as long as any of the verifications is good, then the RRSET is valid. + let (proof, rest) = future::select_ok(verifications).await?; + drop(rest); + + Ok(proof) + } + + /// An internal function used to clone the handle, but maintain some information back to the + /// original handle, such as the request_depth such that infinite recursion does + /// not occur. + fn clone_with_context(&self) -> Self { + Self { + handle: self.handle.clone(), + trust_anchor: Arc::clone(&self.trust_anchor), + request_depth: self.request_depth + 1, + nsec3_soft_iteration_limit: self.nsec3_soft_iteration_limit, + nsec3_hard_iteration_limit: self.nsec3_hard_iteration_limit, + validation_cache: self.validation_cache.clone(), + } + } + + /// Get a reference to the underlying handle. + pub fn inner(&self) -> &H { + &self.handle + } +} + +impl DnsHandle for DnssecDnsHandle { + type Response = Pin> + Send>>; + type Runtime = H::Runtime; + + fn is_verifying_dnssec(&self) -> bool { + // This handler is always verifying... + true + } + + fn send(&self, mut request: DnsRequest) -> Self::Response { + // backstop + if self.request_depth > request.options().max_request_depth { + error!("exceeded max validation depth"); + return Box::pin(stream::once(future::err(NetError::from( + "exceeded max validation depth", + )))); + } + + // dnssec only matters on queries. + match request.op_code { + OpCode::Query => {} + _ => return Box::pin(self.handle.send(request)), + } + + // This will fail on no queries, that is a very odd type of request, isn't it? + // TODO: with mDNS there can be multiple queries + let Some(query) = request.queries.first().cloned() else { + return Box::pin(stream::once(future::err(NetError::from( + "no query in request", + )))); + }; + + let handle = self.clone_with_context(); + request.edns.get_or_insert_with(Edns::new).enable_dnssec(); + + request.metadata.authentic_data = true; + // Validating resolvers SHOULD set CD on every upstream query (RFC 6840 section 5.9), so + // that an upstream validator returns data that fails its own validation and this handle + // reaches its own verdict. + request.metadata.checking_disabled = true; + let options = *request.options(); + + Box::pin(self.handle.send(request).then(move |result| { + handle + .clone() + .verify_response(result, query.clone(), options) + })) + } +} + +fn verify_rrsig_with_keys( + dnskey_message: DnsResponse, + rrsig: &RecordRef<'_, RRSIG>, + rrset: &RecordSet, + current_time: u32, +) -> Option<(Proof, Option)> { + let mut tag_count = HashMap::::new(); + + if (rrset.record_type() == RecordType::NSEC || rrset.record_type() == RecordType::NSEC3) + && rrset.name().num_labels() != rrsig.data().input().num_labels + { + warn!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + "record signature claims to be expanded from a wildcard", + ); + return None; + } + if rrset.record_type() == RecordType::NSEC3 + && rrset.name().base_name() != rrsig.data().input().signer_name + { + warn!( + name = %rrset.name(), + signer_name = %rrsig.data().input().signer_name, + "NSEC3 record name is malformed" + ); + return None; + } + + // DNSKEYs were already validated by the inner query in the above lookup + let dnskeys = dnskey_message.answers.iter().filter_map(|r| { + let dnskey = r.try_borrow::()?; + + let tag = match dnskey.data().calculate_key_tag() { + Ok(tag) => tag, + Err(e) => { + warn!("unable to calculate key tag: {e:?}; skipping key"); + return None; + } + }; + + match tag_count.get_mut(&tag) { + Some(n_keys) => { + *n_keys += 1; + if *n_keys > MAX_KEY_TAG_COLLISIONS { + warn!("too many ({n_keys}) DNSKEYs with key tag {tag}; skipping"); + return None; + } + } + None => _ = tag_count.insert(tag, 1), + } + + Some(dnskey) + }); + + let mut all_insecure = None; + for dnskey in dnskeys { + match dnskey.proof() { + Proof::Secure => { + all_insecure = Some(false); + if let Ok(proof) = + verify_rrset_with_dnskey(dnskey, dnskey.proof(), rrsig, rrset, current_time) + { + return Some((proof.0, proof.1)); + } + } + Proof::Insecure => { + all_insecure.get_or_insert(true); + } + _ => all_insecure = Some(false), + } + } + + if all_insecure.unwrap_or(false) { + // inherit Insecure state + Some((Proof::Insecure, None)) + } else { + None + } +} + +/// Find the SOA record, if present, in the response and return its name. +/// +/// Note that a SOA record may not be present in all responses that must be NSEC/NSEC3 validated. +/// See RFC 4035 B.4 - Referral to Signed Zone, B.5 Referral to Unsigned Zone, B.6 - Wildcard +/// Expansion, RFC 5155 B.3 - Referral to an Opt-Out Unsigned Zone, and B.4 - Wildcard Expansion. +fn find_soa_name(verified_message: &DnsResponse) -> Option<&Name> { + for record in &verified_message.authorities { + if record.record_type() == RecordType::SOA && record.proof == Proof::Secure { + return Some(&record.name); + } + } + + None +} + +/// This verifies a DNSKEY record against DS records from a secure delegation. +fn verify_dnskey( + rr: &RecordRef<'_, DNSKEY>, + ds_records: &[Record], +) -> Result { + let key_rdata = rr.data(); + let key_tag = key_rdata.calculate_key_tag().map_err(|_| { + ProofError::new( + Proof::Insecure, + ProofErrorKind::ErrorComputingKeyTag { + name: rr.name().clone(), + }, + ) + })?; + let key_algorithm = key_rdata.algorithm(); + + if !key_algorithm.is_supported() { + return Err(ProofError::new( + Proof::Insecure, + ProofErrorKind::UnsupportedKeyAlgorithm, + )); + } + + // DS check if covered by DS keys + let mut key_authentication_attempts = 0; + for r in ds_records.iter().filter(|ds| ds.proof.is_secure()) { + if r.data.algorithm() != key_algorithm { + trace!( + "skipping DS record due to algorithm mismatch, expected algorithm {}: ({}, {})", + key_algorithm, r.name, r.data, + ); + + continue; + } + + if r.data.key_tag() != key_tag { + trace!( + "skipping DS record due to key tag mismatch, expected tag {key_tag}: ({}, {})", + r.name, r.data, + ); + + continue; + } + + // Count the number of DS records with the same algorithm and key tag as this DNSKEY. + // Ignore remaining DS records if there are too many key tag collisions. Doing so before + // checking hashes or signatures protects us from KeyTrap denial of service attacks. + key_authentication_attempts += 1; + if key_authentication_attempts > MAX_KEY_TAG_COLLISIONS { + warn!( + key_tag, + attempts = key_authentication_attempts, + "too many DS records with same key tag; skipping" + ); + continue; + } + + if !r.data.covers(rr.name(), key_rdata).unwrap_or(false) { + continue; + } + + debug!( + "validated dnskey ({}, {key_rdata}) with {} {}", + rr.name(), + r.name, + r.data, + ); + + // If this key is valid, then it is secure + return Ok(Proof::Secure); + } + + trace!("bogus dnskey: {}", rr.name()); + Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::DnsKeyHasNoDs { + name: rr.name().clone(), + }, + )) +} + +/// Verifies the given SIG of the RRSET with the DNSKEY. +fn verify_rrset_with_dnskey( + dnskey: RecordRef<'_, DNSKEY>, + dnskey_proof: Proof, + rrsig: &RecordRef<'_, RRSIG>, + rrset: &RecordSet, + current_time: u32, +) -> Result<(Proof, Option), ProofError> { + match dnskey_proof { + Proof::Secure => (), + proof => { + debug!("insecure dnskey {} {}", dnskey.name(), dnskey.data()); + return Err(ProofError::new( + proof, + ProofErrorKind::InsecureDnsKey { + name: dnskey.name().clone(), + key_tag: rrsig.data().input().key_tag, + }, + )); + } + } + + if dnskey.data().revoke() { + debug!("revoked dnskey {} {}", dnskey.name(), dnskey.data()); + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::DnsKeyRevoked { + name: dnskey.name().clone(), + key_tag: rrsig.data().input().key_tag, + }, + )); + } // TODO: does this need to be validated? RFC 5011 + if !dnskey.data().zone_key() { + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::NotZoneDnsKey { + name: dnskey.name().clone(), + key_tag: rrsig.data().input().key_tag, + }, + )); + } + if dnskey.data().algorithm() != rrsig.data().input().algorithm { + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::AlgorithmMismatch { + rrsig: rrsig.data().input().algorithm, + dnskey: dnskey.data().algorithm(), + }, + )); + } + + let validity = RrsigValidity::check(*rrsig, rrset, dnskey, current_time); + if !matches!(validity, RrsigValidity::ValidRrsig) { + // TODO better error handling when the error payload is not immediately discarded by + // the caller + return Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::Msg(format!("{validity:?}")), + )); + } + + dnskey + .data() + .verify_rrsig( + rrset.name(), + rrset.dns_class(), + rrsig.data(), + rrset.records(false), + ) + .map(|_| { + if let Some(record) = rrset.record() { + debug!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + dnskey_name = ?dnskey.name(), + dnskey_data = ?dnskey.data(), + "validated rrset with dnskey", + ); + ( + Proof::Secure, + Some(rrsig.data().authenticated_ttl(record, current_time)), + ) + } else { + debug!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + "unable to validate record: no record in rrset", + ); + (Proof::Bogus, None) + } + }) + .map_err(|e| { + debug!( + rrset_name = ?rrset.name(), + rrset_type = ?rrset.record_type(), + dnskey_name = ?dnskey.name(), + dnskey_data = ?dnskey.data(), + "failed rrset validation", + ); + ProofError::new( + Proof::Bogus, + ProofErrorKind::DnsKeyVerifyRrsig { + name: dnskey.name().clone(), + key_tag: rrsig.data().input().key_tag, + error: e, + }, + ) + }) +} + +#[derive(Clone, Copy, Debug)] +enum RrsigValidity { + /// RRSIG has already expired + ExpiredRrsig, + /// RRSIG is valid + ValidRrsig, + /// DNSKEY does not match RRSIG + WrongDnskey, + /// RRSIG does not match RRset + WrongRrsig, +} + +impl RrsigValidity { + // see section 5.3.1 of RFC4035 "Checking the RRSIG RR Validity" + fn check( + rrsig: RecordRef<'_, RRSIG>, + rrset: &RecordSet, + dnskey: RecordRef<'_, DNSKEY>, + current_time: u32, + ) -> Self { + let Ok(dnskey_key_tag) = dnskey.data().calculate_key_tag() else { + return Self::WrongDnskey; + }; + + let current_time = SerialNumber::new(current_time); + let sig_input = rrsig.data().input(); + if !( + // "The RRSIG RR and the RRset MUST have the same owner name and the same class" + rrsig.name() == rrset.name() && + rrsig.dns_class() == rrset.dns_class() && + + // "The RRSIG RR's Signer's Name field MUST be the name of the zone that contains the RRset" + sig_input.signer_name.zone_of(rrset.name()) && + + // "The RRSIG RR's Type Covered field MUST equal the RRset's type" + sig_input.type_covered == rrset.record_type() && + + // "The number of labels in the RRset owner name MUST be greater than or equal to the value + // in the RRSIG RR's Labels field" + rrset.name().num_labels() >= sig_input.num_labels + ) { + return Self::WrongRrsig; + } + + // Section 3.1.5 of RFC4034 states that 'all comparisons involving these fields MUST use + // "Serial number arithmetic", as defined in RFC1982' + if !( + // "The validator's notion of the current time MUST be less than or equal to the time listed + // in the RRSIG RR's Expiration field" + current_time <= sig_input.sig_expiration && + + // "The validator's notion of the current time MUST be greater than or equal to the time + // listed in the RRSIG RR's Inception field" + current_time >= sig_input.sig_inception + ) { + return Self::ExpiredRrsig; + } + + if !( + // "The RRSIG RR's Signer's Name, Algorithm, and Key Tag fields MUST match the owner name, + // algorithm, and key tag for some DNSKEY RR in the zone's apex DNSKEY RRset" + &sig_input.signer_name == dnskey.name() && + sig_input.algorithm == dnskey.data().algorithm() && + sig_input.key_tag == dnskey_key_tag && + // "The matching DNSKEY RR MUST be present in the zone's apex DNSKEY RRset, and MUST have the + // Zone Flag bit (DNSKEY RDATA Flag bit 7) set" + dnskey.data().zone_key() + ) { + return Self::WrongDnskey; + } + + Self::ValidRrsig + } +} + +#[derive(Clone)] +struct RrsetProof { + proof: Proof, + adjusted_ttl: Option, + rrsig_index: Option, +} + +#[derive(Clone)] +#[allow(clippy::type_complexity)] +struct ValidationCache { + inner: Arc)>>>, + negative_ttl: Option>, + positive_ttl: Option>, +} + +impl ValidationCache { + fn new(capacity: usize) -> Self { + Self { + inner: Arc::new(Mutex::new(LruCache::new(capacity))), + negative_ttl: Some(Duration::from_secs(1)..=BOGUS_CACHE_TTL), + positive_ttl: None, + } + } + + fn get( + &self, + key: &ValidationCacheKey, + context: &RrsetVerificationContext<'_>, + ) -> Option> { + let (ttl, cached) = self.inner.lock().get_mut(key)?.clone(); + + if Instant::now() < ttl { + debug!( + name = ?context.rrset.name(), + record_type = ?context.rrset.record_type(), + "returning cached DNSSEC validation", + ); + Some(cached) + } else { + debug!( + name = ?context.rrset.name(), + record_type = ?context.rrset.record_type(), + "cached DNSSEC validation expired" + ); + None + } + } + + fn insert( + &self, + proof: Result, + key: ValidationCacheKey, + cx: &RrsetVerificationContext<'_>, + ) { + debug!( + name = ?cx.rrset.name(), + record_type = ?cx.rrset.record_type(), + "inserting DNSSEC validation cache entry", + ); + + let (mut min, mut max) = (Duration::from_secs(0), Duration::from_secs(u64::MAX)); + if proof.is_err() { + if let Some(negative_bounds) = self.negative_ttl.clone() { + (min, max) = negative_bounds.into_inner(); + } + } else if let Some(positive_bounds) = self.positive_ttl.clone() { + (min, max) = positive_bounds.into_inner(); + } + + let Some(record) = cx.rrset.record() else { + debug!( + name = ?cx.rrset.name(), + record_type = ?cx.rrset.record_type(), + "unable to insert cache entry - no record in rrset", + ); + return; + }; + + self.inner.lock().insert( + key, + ( + Instant::now() + Duration::from_secs(record.ttl.into()).clamp(min, max), + proof.clone(), + ), + ); + } +} + +struct RrsetVerificationContext<'a> { + query: &'a Query, + rrset: &'a RecordSet, + options: DnsRequestOptions, + current_time: u32, +} + +impl<'a> RrsetVerificationContext<'a> { + // Build a cache lookup key based on the query, rrset, and rrsigs contents, minus the TTLs + // for each, since the recursor cache will return an adjusted TTL for each request and + // cause cache misses. + fn key(&self) -> ValidationCacheKey { + let mut hasher = DefaultHasher::new(); + self.query.name().hash(&mut hasher); + self.query.query_class().hash(&mut hasher); + self.query.query_type().hash(&mut hasher); + self.rrset.name().hash(&mut hasher); + self.rrset.dns_class().hash(&mut hasher); + self.rrset.record_type().hash(&mut hasher); + + for rec in self.rrset.records(true) { + rec.name.hash(&mut hasher); + rec.dns_class.hash(&mut hasher); + rec.data.hash(&mut hasher); + } + + ValidationCacheKey(hasher.finish(), self.query.clone()) + } +} + +#[derive(Eq, PartialEq)] +struct ValidationCacheKey(u64, Query); + +impl Hash for ValidationCacheKey { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +/// Verifies NSEC records +/// +/// ```text +/// RFC 4035 DNSSEC Protocol Modifications March 2005 +/// +/// 5.4. Authenticated Denial of Existence +/// +/// A resolver can use authenticated NSEC RRs to prove that an RRset is +/// not present in a signed zone. Security-aware name servers should +/// automatically include any necessary NSEC RRs for signed zones in +/// their responses to security-aware resolvers. +/// +/// Denial of existence is determined by the following rules: +/// +/// o If the requested RR name matches the owner name of an +/// authenticated NSEC RR, then the NSEC RR's type bit map field lists +/// all RR types present at that owner name, and a resolver can prove +/// that the requested RR type does not exist by checking for the RR +/// type in the bit map. If the number of labels in an authenticated +/// NSEC RR's owner name equals the Labels field of the covering RRSIG +/// RR, then the existence of the NSEC RR proves that wildcard +/// expansion could not have been used to match the request. +/// +/// o If the requested RR name would appear after an authenticated NSEC +/// RR's owner name and before the name listed in that NSEC RR's Next +/// Domain Name field according to the canonical DNS name order +/// defined in [RFC4034], then no RRsets with the requested name exist +/// in the zone. However, it is possible that a wildcard could be +/// used to match the requested RR owner name and type, so proving +/// that the requested RRset does not exist also requires proving that +/// no possible wildcard RRset exists that could have been used to +/// generate a positive response. +/// +/// In addition, security-aware resolvers MUST authenticate the NSEC +/// RRsets that comprise the non-existence proof as described in Section +/// 5.3. +/// +/// To prove the non-existence of an RRset, the resolver must be able to +/// verify both that the queried RRset does not exist and that no +/// relevant wildcard RRset exists. Proving this may require more than +/// one NSEC RRset from the zone. If the complete set of necessary NSEC +/// RRsets is not present in a response (perhaps due to message +/// truncation), then a security-aware resolver MUST resend the query in +/// order to attempt to obtain the full collection of NSEC RRs necessary +/// to verify the non-existence of the requested RRset. As with all DNS +/// operations, however, the resolver MUST bound the work it puts into +/// answering any particular query. +/// +/// Since a validated NSEC RR proves the existence of both itself and its +/// corresponding RRSIG RR, a validator MUST ignore the settings of the +/// NSEC and RRSIG bits in an NSEC RR. +/// ``` +fn verify_nsec( + query: &Query, + soa_name: Option<&Name>, + response_code: ResponseCode, + nsecs: &[(&Name, &NSEC)], +) -> Proof { + // TODO: consider converting this to Result, and giving explicit reason for the failure + + let nsec1_yield = + |proof: Proof, msg: &str| -> Proof { proof_log_yield(proof, query, "nsec1", msg) }; + + if response_code != ResponseCode::NXDomain && response_code != ResponseCode::NoError { + return nsec1_yield(Proof::Bogus, "unsupported response code"); + } + + // The SOA name, if present, must be an ancestor of the query name. If a SOA is present, + // we'll use that as the starting value for next_closest_encloser, otherwise, fall back to + // the parent of the query name. + let mut next_closest_encloser = if let Some(soa_name) = soa_name { + if !soa_name.zone_of(query.name()) { + return nsec1_yield(Proof::Bogus, "SOA record is for the wrong zone"); + } + soa_name.clone() + } else { + query.name().base_name() + }; + + // For a no data response with a directly matching NSEC record, we just need to verify the NSEC + // type set does not contain the query type or CNAME. + if let Some((_, nsec_data)) = nsecs.iter().find(|(name, _)| query.name() == *name) { + let types = nsec_data.type_set(); + return if types.contains(query.query_type()) || types.contains(RecordType::CNAME) { + nsec1_yield(Proof::Bogus, "direct match, record type should be present") + } else if query.query_type() != RecordType::DS && nsec_data.is_ancestor_delegation() { + // RFC 6840 §4.1: a parent-side NSEC at the cut only authenticates the + // absence of DS at that owner; it cannot deny other types there. + nsec1_yield(Proof::Bogus, "direct match is an ancestor-delegation NSEC") + } else if query.query_type == RecordType::DS + && nsec_data.type_set().contains(RecordType::SOA) + { + // RFC 4035 §5.2: "A security-aware resolver MUST use the parent NSEC RR when attempting + // to prove that a DS RRset does not exist." + nsec1_yield( + Proof::Bogus, + "direct match for DS query is an NSEC record from the child side of the zone cut", + ) + } else if response_code == ResponseCode::NoError { + nsec1_yield(Proof::Secure, "direct match") + } else { + nsec1_yield( + Proof::Bogus, + "nxdomain response or answers present when direct match exists", + ) + }; + } + + let Some((covering_nsec_name, covering_nsec_data)) = + find_nsec_covering_record(query.name(), nsecs) + else { + return nsec1_yield( + Proof::Bogus, + "no NSEC record matches or covers the query name", + ); + }; + + // RFC 4035 §3.1.3.2 / §5.4 / RFC 8020 §3.1: a next-domain below the + // query name puts the query name on the path from owner to next, so + // it exists as an empty non-terminal. NODATA is correct, NXDOMAIN + // contradicts the proof. Skip the wrap-around NSEC (next is the + // apex). + let covering_next = covering_nsec_data.next_domain_name(); + if Some(covering_next) != soa_name && query.name.zone_of(covering_next) { + return if response_code == ResponseCode::NXDomain { + nsec1_yield(Proof::Bogus, "NXDOMAIN at proven empty non-terminal") + } else { + nsec1_yield(Proof::Secure, "empty non-terminal NODATA") + }; + } + + // Identify the names that exist (including names of empty non terminals) that are parents of + // the query name. Pick the longest such name, because wildcard synthesis would start looking + // for a wildcard record there. + for seed_name in [covering_nsec_name, covering_nsec_data.next_domain_name()] { + let mut candidate_name = seed_name.clone(); + while candidate_name.num_labels() > next_closest_encloser.num_labels() { + if &candidate_name != query.name() && candidate_name.zone_of(query.name()) { + next_closest_encloser = candidate_name; + break; + } + candidate_name = candidate_name.base_name(); + } + } + + let Ok(wildcard_name) = next_closest_encloser.prepend_label("*") else { + // This fails if the prepended label is invalid or if the wildcard name would be too long. + // However, we already know that the query name is not too long. The next closest enclosing + // name must be strictly shorter than the query name, since we know that there is no NSEC + // record matching the query name. Thus the query name must be as long or longer than this + // wildcard name we are trying to construct, because we removed at least one label from the + // query name, and tried to add a single-byte label. This error condition should thus be + // unreachable. + return nsec1_yield(Proof::Bogus, "unreachable error constructing wildcard name"); + }; + + debug!(%wildcard_name, "looking for NSEC for wildcard"); + + // Identify the name of wildcard used to generate the no data response. We have to recover the + // base name from a wildcard NSEC record as there are no answer RRSIGs present. This will be + // used to prove that no closer matches exist between the query name and the wildcard. + let wildcard_base_name = nsecs + .iter() + .filter(|(name, _)| name.is_wildcard() && name.base_name().zone_of(query.name())) + .min_by_key(|(name, _)| name.num_labels()) + .map(|(name, _)| (*name).clone()); + + match find_nsec_covering_record(&wildcard_name, nsecs) { + // For NXDomain responses, we've already proved the record does not exist. Now we just need to prove + // the wildcard name is covered. + Some((_, _)) if response_code == ResponseCode::NXDomain => { + nsec1_yield(Proof::Secure, "no direct match, no wildcard") + } + // For wildcard no data responses, we need to prove a wildcard matching wildcard_name does not contain + // the requested record type and that no closer match exists. (RFC 4035 3.1.3.4 and B.7/C.7) + None if response_code == ResponseCode::NoError + && nsecs.iter().any(|(name, nsec_data)| { + name == &&wildcard_name + && !nsec_data.type_set().contains(query.query_type()) + && !nsec_data.type_set().contains(RecordType::CNAME) + && no_closer_matches(query.name(), soa_name, nsecs, wildcard_base_name.as_ref()) + }) => + { + nsec1_yield(Proof::Secure, "no direct match, covering wildcard present") + } + _ => nsec1_yield( + Proof::Bogus, + "no NSEC record matches or covers the wildcard name", + ), + } +} + +// Prove that no closer name exists between the query name and wildcard_base_name +fn no_closer_matches( + query_name: &Name, + soa: Option<&Name>, + nsecs: &[(&'_ Name, &'_ NSEC)], + wildcard_base_name: Option<&Name>, +) -> bool { + let Some(wildcard_base_name) = wildcard_base_name else { + return false; + }; + + // If the SOA name is present, the query name and wildcard base name must be children of it. + if let Some(soa) = soa { + if !soa.zone_of(wildcard_base_name) { + debug!(%wildcard_base_name, %soa, "wildcard_base_name is not a child of SOA"); + return false; + } + + if !soa.zone_of(query_name) { + debug!(%query_name, %soa, "query_name is not a child of SOA"); + return false; + } + } + + if wildcard_base_name.num_labels() > query_name.num_labels() { + debug!(%wildcard_base_name, %query_name, "wildcard_base_name cannot have more labels than query_name"); + return false; + } + + // The query name must be a child of the wildcard (minus the *) + if !wildcard_base_name.base_name().zone_of(query_name) { + debug!(%wildcard_base_name, %query_name, "query_name is not a child of wildcard_name"); + return false; + } + + // Verify that an appropriate proof exists for each wildcard between query.name() and wildcard_base_name. + let mut name = query_name.base_name(); + while name.num_labels() > wildcard_base_name.num_labels() { + let Ok(wildcard) = name.prepend_label("*") else { + return false; + }; + + if find_nsec_covering_record(&wildcard, nsecs).is_none() { + debug!(%wildcard, %name, ?nsecs, "covering record does not exist for name"); + return false; + } + + name = name.base_name(); + } + + true +} + +/// Find the NSEC record covering `test_name`, if any. +/// +/// A zone-wrap NSEC (the last record in the zone) is detected by checking +/// that `next_domain_name <= nsec_name`, which is an intrinsic property of +/// the NSEC chain and does not depend on the SOA owner name. +fn find_nsec_covering_record<'a>( + test_name: &Name, + nsecs: &[(&'a Name, &'a NSEC)], +) -> Option<(&'a Name, &'a NSEC)> { + nsecs.iter().copied().find(|(nsec_name, nsec_data)| { + // RFC 6840 §4.1: a parent-side NSEC at the cut MUST NOT be used to + // assume nonexistence of any name below the cut. + if nsec_data.is_ancestor_delegation() && nsec_name.zone_of(test_name) { + return false; + } + + let next_domain_name = nsec_data.next_domain_name(); + test_name > nsec_name + && (test_name < next_domain_name + || next_domain_name <= nsec_name && next_domain_name.zone_of(test_name)) + }) +} + +/// Returns the RRsets in this message that were expanded from a wildcard without a valid proof +/// that the next closer name does not exist (RFC 4035 5.3.4, RFC 5155 8.8). +fn unproven_wildcard_rrsets(message: &Message) -> Vec<(Name, RecordType)> { + let mut denials = None; + let mut unproven = Vec::new(); + for record in message + .answers + .iter() + .chain(&message.authorities) + .chain(&message.additionals) + { + let Some(rrsig) = secure_rrsig(record) else { + continue; + }; + let input = rrsig.input(); + if input.num_labels == record.name.num_labels() { + continue; + } + + let denials = denials.get_or_insert_with(|| DenialRecords::new(&message.authorities)); + if !denials.proves_wildcard_expansion(&record.name, input) { + warn!( + name = %record.name, + rrsig_labels = input.num_labels, + "no valid next closer name nonexistence proof accompanying wildcard record" + ); + unproven.push((record.name.clone(), input.type_covered)); + } + } + unproven +} + +fn secure_rrsig(record: &Record) -> Option<&RRSIG> { + match &record.data { + RData::DNSSEC(DNSSECRData::RRSIG(rrsig)) if record.proof == Proof::Secure => Some(rrsig), + _ => None, + } +} + +/// Secure NSEC and NSEC3 records from an authority section, with the signer name of the RRSIG +/// that validated each of them. +struct DenialRecords<'a> { + nsecs: Vec<(&'a Name, &'a Name, &'a NSEC)>, + nsec3s: Vec<(&'a Name, &'a Name, &'a NSEC3)>, +} + +impl<'a> DenialRecords<'a> { + fn new(authorities: &'a [Record]) -> Self { + let mut nsecs = Vec::new(); + let mut nsec3s = Vec::new(); + for rrsig_record in authorities { + let Some(rrsig) = secure_rrsig(rrsig_record) else { + continue; + }; + let input = rrsig.input(); + let owner = &rrsig_record.name; + let signer = &input.signer_name; + let rrset = authorities + .iter() + .filter(|record| record.proof == Proof::Secure && record.name == *owner); + match input.type_covered { + RecordType::NSEC => { + if !signer.zone_of(owner) || owner.num_labels() != input.num_labels { + warn!( + nsec_name = %owner, + signer_name = %signer, + "ignoring NSEC record outside of signer's zone or expanded from wildcard" + ); + continue; + } + nsecs.extend(rrset.filter_map(|record| match &record.data { + RData::DNSSEC(DNSSECRData::NSEC(nsec)) => Some((signer, owner, nsec)), + _ => None, + })); + } + RecordType::NSEC3 => { + if !signer.zone_of(owner) || owner.num_labels() != signer.num_labels() + 1 { + warn!( + nsec3_name = %owner, + signer_name = %signer, + "ignoring NSEC3 record with invalid name" + ); + continue; + } + nsec3s.extend(rrset.filter_map(|record| match &record.data { + RData::DNSSEC(DNSSECRData::NSEC3(nsec3)) => Some((signer, owner, nsec3)), + _ => None, + })); + } + _ => {} + } + } + Self { nsecs, nsec3s } + } + + /// Checks for a nonexistence proof of the next closer name of an RRset expanded from a + /// wildcard, using only records from the zone that signed the RRset. + fn proves_wildcard_expansion(&self, owner: &Name, input: &SigInput) -> bool { + if input.num_labels > owner.num_labels() { + return false; + } + let next_closer = owner.trim_to(usize::from(input.num_labels) + 1); + let zone = &input.signer_name; + if !zone.zone_of(&next_closer) { + return false; + } + + let nsecs = self + .nsecs + .iter() + .filter(|(signer, ..)| *signer == zone) + .map(|&(_, name, nsec)| (name, nsec)) + .collect::>(); + let nsec_proof_valid = find_nsec_covering_record(&next_closer, &nsecs).is_some_and( + |(covering_name, covering_nsec)| { + let next_domain_name = covering_nsec.next_domain_name(); + // A next domain name below the next closer name makes it an empty non-terminal. + covering_name >= next_domain_name || !next_closer.zone_of(next_domain_name) + }, + ); + if nsec_proof_valid { + return true; + } + + let nsec3s = self + .nsec3s + .iter() + .filter(|(signer, ..)| *signer == zone) + .map(|&(_, name, nsec3)| (name, nsec3)) + .collect::>(); + verify_nsec3_next_closer(&next_closer, &nsec3s) + } +} + +/// Logs a debug message and yields a Proof type for return +pub(super) fn proof_log_yield( + proof: Proof, + query: &Query, + nsec_type: &str, + msg: impl Display, +) -> Proof { + debug!( + "{nsec_type} proof for {name}, returning {proof}: {msg}", + name = query.name() + ); + proof +} + +/// Maximum cache lifetime for an RRset with BOGUS validation. +/// +/// RFC 4035 §4.7 requires implementations to assign a small TTL since the wire TTL +/// is attacker-influenced and RFC 9520 §3.2 caps cached resolution failures at 300s. +pub const BOGUS_CACHE_TTL: Duration = Duration::from_secs(60); + +/// The maximum number of key tag collisions to accept when: +/// +/// 1) Retrieving DNSKEY records for a zone +/// 2) Retrieving DS records from a parent zone +/// +/// Any colliding records encountered beyond this limit will be discarded. +const MAX_KEY_TAG_COLLISIONS: usize = 2; + +/// The maximum number of RRSIGs to attempt to validate for each RRSET. +const MAX_RRSIGS_PER_RRSET: usize = 8; + +/// The default validation cache size. This is somewhat arbitrary, but set to the same size as the default +/// recursor response cache +const DEFAULT_VALIDATION_CACHE_SIZE: usize = 1_048_576; + +#[cfg(test)] +mod test { + use std::{ + net::Ipv4Addr, + time::{Duration, Instant}, + }; + + use data_encoding::BASE32_DNSSEC; + + use super::{ + RrsigValidity, find_nsec_covering_record, no_closer_matches, unproven_wildcard_rrsets, + verify_nsec, + }; + use crate::{ + dnssec::{ + DnsRequestOptions, Proof, ProofError, ProofErrorKind, RrsetVerificationContext, + ValidationCache, + }, + proto::{ + ProtoError, + dnssec::{ + Algorithm, Nsec3HashAlgorithm, PublicKeyBuf, + rdata::{ + DNSKEY as rdataDNSKEY, DNSSECRData, NSEC as rdataNSEC, NSEC3 as rdataNSEC3, + RRSIG as rdataRRSIG, SigInput, + }, + }, + op::{Message, Query, ResponseCode}, + rr::{ + Name, RData, Record, RecordSet, + RecordType::{self, A, AAAA, DNSKEY, DS, MX, NS, NSEC, NSEC3, RRSIG, SOA, TXT}, + SerialNumber, rdata, + }, + }, + }; + use test_support::subscribe; + + #[test] + fn test_no_closer_matches() -> Result<(), ProtoError> { + subscribe(); + + assert!(no_closer_matches( + &Name::from_ascii("a.a.a.z.w.example")?, + Some(&Name::from_ascii("example.")?), + &[ + // This NSEC encloses the query name and proves that no closer wildcard match + // exists in the zone. + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ), + ], + Some(&Name::from_ascii("*.w.example.")?), + ),); + + assert!(!no_closer_matches( + &Name::from_ascii("a.a.a.z.w.example")?, + Some(&Name::from_ascii("example.")?), + &[ + // This doesn't prove the non-existence of the closer wildcard + ( + &Name::from_ascii("*.w.example.")?, + &rdataNSEC::new(Name::from_ascii("z.w.example.")?, [MX, NSEC, RRSIG],), + ), + ], + Some(&Name::from_ascii("*.w.example.")?), + ),); + + assert!(!no_closer_matches( + &Name::from_ascii("a.a.a.z.w.example")?, + Some(&Name::from_ascii("example.")?), + &[( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ),], + // no_closer_matches requires a wildcard base name be present + None, + ),); + + // SOA mismatch + assert!(!no_closer_matches( + &Name::from_ascii("a.a.a.z.w.example")?, + Some(&Name::from_ascii("z.example.")?), + &[ + // This NSEC encloses the query name and proves that no closer wildcard match + // exists in the zone. + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ), + // This NSEC proves the requested record type does not exist at the wildcard + ( + &Name::from_ascii("*.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), + ), + ], + Some(&Name::from_ascii("*.w.example.")?), + ),); + + // Irrelevant wildcard. + assert!(!no_closer_matches( + &Name::from_ascii("a.a.a.z.w.example")?, + Some(&Name::from_ascii("example.")?), + &[ + // This NSEC encloses the query name and proves that no closer wildcard match + // exists in the zone. + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ), + // This NSEC proves the requested record type does not exist at the wildcard + ( + &Name::from_ascii("*.x.example.")?, + &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), + ), + ], + Some(&Name::from_ascii("*.x.example.")?), + ),); + + Ok(()) + } + + // These test cases prove a name does not exist + #[test] + fn nsec_name_error() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 4035 B.2 - Name Error + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ml.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // This NSEC encloses the query name and proves the record does not exist. + ( + &Name::from_ascii("b.example.")?, + &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), + ), + // This NSEC proves no covering wildcard record exists (i.e., it encloses + // *.example. and thus proves that record does not exist.) + ( + &Name::from_ascii("example.")?, + &rdataNSEC::new( + Name::from_ascii("a.example.")?, + [DNSKEY, MX, NS, NSEC, RRSIG, SOA], + ), + ) + ], + ), + Proof::Secure + ); + + // Single NSEC that proves the record does not exist, and no covering wildcard exists. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("a.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[( + &Name::from_ascii("example.")?, + &rdataNSEC::new(Name::from_ascii("c.example.")?, [SOA, NS, RRSIG, NSEC],), + ),], + ), + Proof::Secure + ); + + Ok(()) + } + + /// Ensure invalid name error NSEC scenarios fail + #[test] + fn nsec_invalid_name_error() -> Result<(), ProtoError> { + subscribe(); + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ml.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // This NSEC does not enclose the query name and so should cause this + // verification to fail + ( + &Name::from_ascii("ml.example.")?, + &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), + ), + // This NSEC proves no covering wildcard record exists (i.e., it encloses + // *.example. and thus proves that record does not exist.) + ( + &Name::from_ascii("example.")?, + &rdataNSEC::new( + Name::from_ascii("a.example.")?, + [DNSKEY, MX, NS, NSEC, RRSIG, SOA], + ), + ) + ], + ), + Proof::Bogus + ); + + // Test without proving wildcard non-existence. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ml.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // This NSEC encloses the query name and proves the record does not exist. + ( + &Name::from_ascii("ml.example.")?, + &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), + ), + ], + ), + Proof::Bogus + ); + + // Invalid SOA + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ml.example.")?, A), + Some(&Name::from_ascii("example2.")?), + ResponseCode::NXDomain, + &[ + // This NSEC encloses the query name and proves the record does not exist. + ( + &Name::from_ascii("b.example.")?, + &rdataNSEC::new(Name::from_ascii("ns1.example.")?, [NS, RRSIG, NSEC],), + ), + // This NSEC proves no covering wildcard record exists (i.e., it encloses + // *.example. and thus proves that record does not exist.) + ( + &Name::from_ascii("example.")?, + &rdataNSEC::new( + Name::from_ascii("a.example.")?, + [DNSKEY, MX, NS, NSEC, RRSIG, SOA], + ), + ) + ], + ), + Proof::Bogus + ); + + Ok(()) + } + + // RFC 6840 §4.1: an ancestor-delegation NSEC at a zone cut (parent-side, with NS set + // and SOA clear) MUST NOT be used to prove nonexistence of any name strictly below + // that cut. The signer is the parent and the only authenticated denial it can carry is + // for DS at the cut owner itself. + #[test] + fn nsec_invalid_ancestor_delegation_nxdomain() -> Result<(), ProtoError> { + subscribe(); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("www.deleg.")?, A), + Some(&Name::root()), + ResponseCode::NXDomain, + &[ + // Parent-side ancestor-delegation NSEC at the zone cut. + ( + &Name::from_ascii("deleg.")?, + &rdataNSEC::new(Name::from_ascii("honest.")?, [NS, DS, RRSIG, NSEC],), + ), + // Root NSEC covering `*.` (the slot where a wildcard at the + // root would live), used here to attempt to prove that no + // wildcard at the parent could synthesize the answer. + ( + &Name::root(), + &rdataNSEC::new( + Name::from_ascii("deleg.")?, + [NS, SOA, RRSIG, NSEC, DNSKEY], + ), + ), + ], + ), + Proof::Bogus + ); + + // NODATA at the cut owner itself: the same parent-side NSEC must not + // authenticate the absence of any type other than DS at `deleg.`. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("deleg.")?, A), + Some(&Name::root()), + ResponseCode::NoError, + &[( + &Name::from_ascii("deleg.")?, + &rdataNSEC::new(Name::from_ascii("honest.")?, [NS, DS, RRSIG, NSEC],), + )], + ), + Proof::Bogus + ); + + // RFC 6840 §4.1 exception: the parent-side NSEC at the cut DOES + // authenticate the absence of DS at the cut owner. This is the only + // legitimate use of this NSEC and must continue to validate. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("insecure-deleg.")?, DS), + Some(&Name::root()), + ResponseCode::NoError, + &[( + &Name::from_ascii("insecure-deleg.")?, + &rdataNSEC::new(Name::from_ascii("next.")?, [NS, RRSIG, NSEC],), + )], + ), + Proof::Secure + ); + + Ok(()) + } + + // RFC 4035 §3.1.3.2 / §5.4 / RFC 8020 §3.1: an NSEC whose Next Domain + // Name is a strict descendant of the query name proves that the query + // name exists as an empty non-terminal (ENT). Such an NSEC must not + // authenticate NXDOMAIN at the query name. The correct rcode is + // NOERROR/NODATA. + #[test] + fn nsec_invalid_empty_non_terminal_nxdomain() -> Result<(), ProtoError> { + subscribe(); + + // Single-label ENT covered by the apex NSEC. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[( + &Name::from_ascii("poc.")?, + &rdataNSEC::new( + Name::from_ascii("a.b.poc.")?, + [NS, SOA, TXT, RRSIG, NSEC, DNSKEY], + ), + )], + ), + Proof::Bogus + ); + + // Deeper ENT covered by a mid-zone NSEC. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("y.z.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[( + &Name::from_ascii("z.poc.")?, + &rdataNSEC::new(Name::from_ascii("x.y.z.poc.")?, [TXT, RRSIG, NSEC],), + )], + ), + Proof::Bogus + ); + + // Single-label ENT covered by a non-apex NSEC. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[( + &Name::from_ascii("a.poc.")?, + &rdataNSEC::new(Name::from_ascii("c.b.poc.")?, [TXT, RRSIG, NSEC],), + )], + ), + Proof::Bogus + ); + + // Same proof shape with rcode=NoError is the legitimate ENT NODATA + // response per RFC 8020 §3.1 and must validate. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NoError, + &[( + &Name::from_ascii("poc.")?, + &rdataNSEC::new( + Name::from_ascii("a.b.poc.")?, + [NS, SOA, TXT, RRSIG, NSEC, DNSKEY], + ), + )], + ), + Proof::Secure + ); + + // Deeper ENT NODATA companion. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("y.z.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NoError, + &[( + &Name::from_ascii("z.poc.")?, + &rdataNSEC::new(Name::from_ascii("x.y.z.poc.")?, [TXT, RRSIG, NSEC],), + )], + ), + Proof::Secure + ); + + // True NXDOMAIN with a sibling next-domain must remain Secure. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[ + ( + &Name::from_ascii("a.poc.")?, + &rdataNSEC::new(Name::from_ascii("c.poc.")?, [TXT, RRSIG, NSEC],), + ), + // Apex NSEC covering `*.poc.` to rule out a wildcard. + ( + &Name::from_ascii("poc.")?, + &rdataNSEC::new( + Name::from_ascii("a.poc.")?, + [NS, SOA, RRSIG, NSEC, DNSKEY], + ), + ), + ], + ), + Proof::Secure + ); + + // Ancestor-delegation NSEC (NS set, SOA clear) at `a.poc.` marks an + // insecure delegation at a sibling of the ENT. Its signer is the + // parent zone `poc.`, and a next-domain of `c.b.poc.` proves that + // `b.poc.` exists as an ENT in `poc.`. RFC 6840 §4.1 only forbids + // using this NSEC to deny names below the cut at `a.poc.`; using it + // to prove ENT existence at the sibling `b.poc.` is legitimate. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NoError, + &[( + &Name::from_ascii("a.poc.")?, + &rdataNSEC::new(Name::from_ascii("c.b.poc.")?, [NS, RRSIG, NSEC],), + )], + ), + Proof::Secure + ); + + // Same shape under NXDOMAIN: the rcode contradicts the proof that + // `b.poc.` exists, so the validator must reject as Bogus regardless + // of the covering NSEC's ancestor-delegation flag. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("b.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[( + &Name::from_ascii("a.poc.")?, + &rdataNSEC::new(Name::from_ascii("c.b.poc.")?, [NS, RRSIG, NSEC],), + )], + ), + Proof::Bogus + ); + + // A NoError response with answer records must not short-circuit to + // Secure based on an ENT-shaped NSEC alone. Wildcard expansion + // requires the full closest-encloser proof. + let input = SigInput { + type_covered: TXT, + algorithm: Algorithm::ED25519, + num_labels: 1, + original_ttl: 3600, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(0), + key_tag: 0, + signer_name: Name::root(), + }; + let rrsig = rdataRRSIG::from_sig(input, vec![]); + let mut rrsig_record = Record::from_rdata( + Name::from_ascii("b.poc.")?, + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), + ); + rrsig_record.proof = Proof::Secure; + let answers = [ + Record::from_rdata( + Name::from_ascii("b.poc.")?, + 3600, + RData::TXT(rdata::TXT::new(vec!["forged".to_string()])), + ), + rrsig_record, + ]; + assert!(!proves_wildcard_expansion( + &answers, + &make_nsec_and_rrsig( + Name::from_ascii("poc.")?, + Name::from_ascii("a.b.poc.")?, + [NS, SOA, TXT, RRSIG, NSEC, DNSKEY], + Name::root(), + ), + )); + + // Wrap-around NSEC where next-domain is the SOA owner. The SOA is + // an ancestor of every name in the zone, so this case must be + // excluded from the ENT check. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("zzz.poc.")?, TXT), + Some(&Name::from_ascii("poc.")?), + ResponseCode::NXDomain, + &[ + ( + &Name::from_ascii("last.poc.")?, + &rdataNSEC::new(Name::from_ascii("poc.")?, [TXT, RRSIG, NSEC],), + ), + // Apex NSEC covering `*.poc.` to rule out a wildcard. + ( + &Name::from_ascii("poc.")?, + &rdataNSEC::new( + Name::from_ascii("a.poc.")?, + [NS, SOA, RRSIG, NSEC, DNSKEY], + ), + ), + ], + ), + Proof::Secure + ); + + Ok(()) + } + + // These test cases prove that the requested record type does not exist at the query name + #[test] + fn nsec_no_data_error() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 4035 B.3 - No Data Error + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC encloses the query name and proves the record does exist, but + // the requested record type does not. + ( + &Name::from_ascii("ns1.example.")?, + &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), + ), + ], + ), + Proof::Secure + ); + + // Record type at the SOA does not exist. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC encloses the query name and proves the record does exist, but + // the requested record type does not. + ( + &Name::from_ascii("example.")?, + &rdataNSEC::new(Name::from_ascii("a.example.")?, [A, NSEC, RRSIG, SOA],), + ), + ], + ), + Proof::Secure + ); + + Ok(()) + } + + // Ensure invalid no data NSEC scenarios fails + #[test] + fn nsec_invalid_no_data_error() -> Result<(), ProtoError> { + subscribe(); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC claims the requested record type DOES exist at ns1.example. + ( + &Name::from_ascii("ns1.example.")?, + &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG, MX],), + ), + ], + ), + Proof::Bogus + ); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // In this case, the response indicates *some* record exists at ns1.example., just not an + // MX record. This NSEC claims ns1.example. does not exist at all. + ( + &Name::from_ascii("ml.example.")?, + &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), + ), + ], + ), + Proof::Bogus + ); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC claims nothing exists from the SOA to ns2.example. + ( + &Name::from_ascii("example.")?, + &rdataNSEC::new(Name::from_ascii("ns2.example.")?, [A, NSEC, RRSIG],), + ), + ], + ), + Proof::Bogus + ); + + Ok(()) + } + + // Ensure that positive answers expanded from wildcards pass validation + #[test] + fn nsec_wildcard_expansion() -> Result<(), ProtoError> { + subscribe(); + + let input = SigInput { + type_covered: MX, + algorithm: Algorithm::ED25519, + num_labels: 2, + original_ttl: 3600, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(0), + key_tag: 0, + signer_name: Name::from_ascii("example.")?, + }; + + let rrsig = rdataRRSIG::from_sig(input, vec![]); + let mut rrsig_record = Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), + ); + rrsig_record.proof = Proof::Secure; + + let answers = [ + Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)), + ), + rrsig_record, + ]; + + // Based on RFC 4035 B.6 - Wildcard Expansion + assert!(proves_wildcard_expansion( + &answers, + // This NSEC record encloses the query name and proves that no closer wildcard match + // exists in the zone. + &make_nsec_and_rrsig( + Name::from_ascii("x.y.w.example.")?, + Name::from_ascii("xx.example.")?, + [MX, NSEC, RRSIG], + Name::from_ascii("example.")?, + ), + )); + + // This response doesn't include an appropriate NSEC record. + assert!(!proves_wildcard_expansion( + &answers, + &[ + // This NSEC record is entirely after w.example. + make_nsec_and_rrsig( + Name::from_ascii("y.example.")?, + Name::from_ascii("example.")?, + [A, NSEC, RRSIG], + Name::from_ascii("example.")?, + ), + // This NSEC record is entirely before w.example. + make_nsec_and_rrsig( + Name::from_ascii("example.")?, + Name::from_ascii("a.example.")?, + [MX, NS, NSEC, RRSIG, SOA], + Name::from_ascii("example.")?, + ), + ] + .concat(), + )); + + Ok(()) + } + + // Ensure that defective wildcard expansion positive answer scenarios fail validation + #[test] + fn nsec_invalid_wildcard_expansion() -> Result<(), ProtoError> { + subscribe(); + + let input = SigInput { + type_covered: MX, + algorithm: Algorithm::ED25519, + num_labels: 2, + original_ttl: 0, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(0), + key_tag: 0, + signer_name: Name::from_ascii("example.")?, + }; + + let rrsig = rdataRRSIG::from_sig(input, vec![]); + let mut rrsig_record = Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rrsig)), + ); + rrsig_record.proof = Proof::Secure; + + let answers = [ + Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)), + ), + rrsig_record, + ]; + + assert!(!proves_wildcard_expansion( + &answers, + // This NSEC record does not prove the non-existence of z.w.example. + &make_nsec_and_rrsig( + Name::from_ascii("x.y.w.example.")?, + Name::from_ascii("z.w.example.")?, + [MX, NSEC, RRSIG], + Name::from_ascii("example.")?, + ), + )); + + assert!(!proves_wildcard_expansion(&answers, &[])); + + Ok(()) + } + + #[test] + fn nsec_wildcard_no_data_error() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 4035 B.7 - Wildcard No Data Error + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC encloses the query name and proves that no closer wildcard match + // exists in the zone. + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ), + // This NSEC proves the requested record type does not exist at the wildcard + ( + &Name::from_ascii("*.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xw.example.")?, [MX, NSEC, RRSIG],), + ), + ], + ), + Proof::Secure + ); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("zzzzzz.hickory-dns.testing.")?, TXT), + Some(&Name::from_ascii("hickory-dns.testing.")?), + ResponseCode::NoError, + &[ + // This NSEC proves zzzzzz.hickory-dns.testing. does not exist. + ( + &Name::from_ascii("record.hickory-dns.testing.")?, + &rdataNSEC::new( + Name::from_ascii("hickory-dns.testing.")?, + [A, NSEC, RRSIG], + ), + ), + // This NSEC proves a wildcard does exist at *.hickory-dns.testing. but does not contain the + // requested record type. + ( + &Name::from_ascii("*.hickory-dns.testing.")?, + &rdataNSEC::new( + Name::from_ascii("primary0.hickory-dns.testing.")?, + [A, NSEC, RRSIG], + ), + ), + ], + ), + Proof::Secure + ); + + Ok(()) + } + + #[test] + fn nsec_invalid_wildcard_no_data_error() -> Result<(), ProtoError> { + subscribe(); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC doesn't prove the non-existence of the query name + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("z.w.example.")?, [MX, NSEC, RRSIG],), + ), + // This NSEC proves the wildcard does not contain the requested record type + ( + &Name::from_ascii("*.w.example.")?, + &rdataNSEC::new(Name::from_ascii("x.y.w.example.")?, [MX, NSEC, RRSIG],), + ), + ], + ), + Proof::Bogus + ); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // This NSEC proves the query name does not exist + ( + &Name::from_ascii("x.y.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xx.example.")?, [MX, NSEC, RRSIG],), + ), + // This NSEC proves the requested record type exists at the wildcard + ( + &Name::from_ascii("*.w.example.")?, + &rdataNSEC::new(Name::from_ascii("xw.example.")?, [AAAA, MX, NSEC, RRSIG],), + ), + ], + ), + Proof::Bogus + ); + + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("r.hickory-dns.testing.")?, TXT), + Some(&Name::from_ascii("hickory-dns.testing.")?), + ResponseCode::NoError, + &[ + // There is no NSEC proving the non-existence of r.hickory-dns.testing. + + // This NSEC proves a wildcard does exist at *.hickory-dns.testing. but does not contain the + // requested record type. + ( + &Name::from_ascii("*.hickory-dns.testing.")?, + &rdataNSEC::new( + Name::from_ascii("primary0.hickory-dns.testing.")?, + [A, NSEC, RRSIG], + ), + ), + ], + ), + Proof::Bogus + ); + + Ok(()) + } + + #[test] + fn rrsig_off_tree_signer_rejected() { + subscribe(); + let (rrset, rrsig_rec, dnskey_rec) = + make_rrsig_check_inputs("www.victim.hack.", "evil.hack."); + let rrsig_ref = rrsig_rec.try_borrow::().unwrap(); + let dnskey_ref = dnskey_rec.try_borrow::().unwrap(); + assert!(matches!( + RrsigValidity::check(rrsig_ref, &rrset, dnskey_ref, 1000), + RrsigValidity::WrongRrsig + )); + } + + #[test] + fn rrsig_zone_signer_accepted() { + subscribe(); + let (rrset, rrsig_rec, dnskey_rec) = + make_rrsig_check_inputs("www.victim.hack.", "victim.hack."); + let rrsig_ref = rrsig_rec.try_borrow::().unwrap(); + let dnskey_ref = dnskey_rec.try_borrow::().unwrap(); + assert!(matches!( + RrsigValidity::check(rrsig_ref, &rrset, dnskey_ref, 1000), + RrsigValidity::ValidRrsig + )); + } + + #[test] + fn rrsig_apex_self_signed_accepted() { + subscribe(); + let (rrset, rrsig_rec, dnskey_rec) = + make_rrsig_check_inputs("victim.hack.", "victim.hack."); + let rrsig_ref = rrsig_rec.try_borrow::().unwrap(); + let dnskey_ref = dnskey_rec.try_borrow::().unwrap(); + assert!(matches!( + RrsigValidity::check(rrsig_ref, &rrset, dnskey_ref, 1000), + RrsigValidity::ValidRrsig + )); + } + + #[test] + fn rrsig_parent_signer_accepted() { + subscribe(); + let (rrset, rrsig_rec, dnskey_rec) = make_rrsig_check_inputs("victim.hack.", "hack."); + let rrsig_ref = rrsig_rec.try_borrow::().unwrap(); + let dnskey_ref = dnskey_rec.try_borrow::().unwrap(); + assert!(matches!( + RrsigValidity::check(rrsig_ref, &rrset, dnskey_ref, 1000), + RrsigValidity::ValidRrsig + )); + } + + #[test] + fn rrsig_child_cannot_sign_parent() { + subscribe(); + let (rrset, rrsig_rec, dnskey_rec) = make_rrsig_check_inputs("hack.", "victim.hack."); + let rrsig_ref = rrsig_rec.try_borrow::().unwrap(); + let dnskey_ref = dnskey_rec.try_borrow::().unwrap(); + assert!(matches!( + RrsigValidity::check(rrsig_ref, &rrset, dnskey_ref, 1000), + RrsigValidity::WrongRrsig + )); + } + + /// Build a matching (rrset, rrsig_record, dnskey_record) triplet for testing + /// `RrsigValidity::check()`. The RRSIG and DNSKEY have consistent key tags, + /// algorithms, and time windows so that only the signer-name/zone relationship + /// determines the outcome. + fn make_rrsig_check_inputs(rrset_name: &str, signer_name: &str) -> (RecordSet, Record, Record) { + let rrset_name = Name::from_ascii(rrset_name).unwrap(); + let signer_name = Name::from_ascii(signer_name).unwrap(); + + let dnskey = rdataDNSKEY::new( + true, // zone_key + true, // secure_entry_point + false, // revoke + PublicKeyBuf::new(vec![0u8; 32], Algorithm::ED25519), + ); + let key_tag = dnskey.calculate_key_tag().unwrap(); + + let sig_input = SigInput { + type_covered: A, + algorithm: Algorithm::ED25519, + num_labels: rrset_name.num_labels(), + original_ttl: 300, + sig_expiration: SerialNumber::new(1_000_000), + sig_inception: SerialNumber::new(0), + key_tag, + signer_name: signer_name.clone(), + }; + let rrsig = rdataRRSIG::from_sig(sig_input, vec![0u8; 64]); + + let rrset = RecordSet::new(rrset_name.clone(), A, 0); + let rrsig_record = + Record::from_rdata(rrset_name, 300, RData::DNSSEC(DNSSECRData::RRSIG(rrsig))); + let dnskey_record = + Record::from_rdata(signer_name, 300, RData::DNSSEC(DNSSECRData::DNSKEY(dnskey))); + + (rrset, rrsig_record, dnskey_record) + } + + #[test] + fn validation_cache_clamps_bogus_ttl() { + // RFC 4035 §4.7: + // Since RRsets that fail to validate do not have trustworthy TTLs, + // the implementation MUST assign a TTL. This TTL SHOULD be small, + // in order to mitigate the effect of caching the results of an + // attack. + + let name = Name::from_ascii("bogus.example.").unwrap(); + let query = Query::query(name.clone(), A); + + let mut rrset = RecordSet::new(name.clone(), A, 0); + let record = Record::from_rdata(name, 3600, RData::A(rdata::A::new(10, 0, 0, 1))); + rrset.set_records(vec![record]); + + let cx = RrsetVerificationContext { + query: &query, + rrset: &rrset, + options: DnsRequestOptions::default(), + current_time: 0, + }; + + let cache = ValidationCache::new(8); + let before = Instant::now(); + cache.insert( + Err(ProofError::new( + Proof::Bogus, + ProofErrorKind::Msg("forced bogus".into()), + )), + cx.key(), + &cx, + ); + + let stored_until = cache + .inner + .lock() + .get_mut(&cx.key()) + .expect("entry just inserted") + .0; + let lifetime = stored_until.duration_since(before); + assert!( + // RFC 4035 §3.2: + // resolution failures MUST NOT be cached for longer than 5 minutes. + lifetime <= Duration::from_secs(60 * 5), + "Bogus validation cached for {lifetime:?}, must be <= 5m", + ); + } + + /// Zone-wrap NSEC detection uses the intrinsic `next <= owner` property + /// instead of relying on the SOA owner name. + #[test] + fn find_nsec_covering_record_zone_wrap() -> Result<(), ProtoError> { + // A zone-wrap NSEC: last record in the zone points back to the zone apex. + // `record.example. NSEC example.` covers everything > record.example. + let nsec_name = Name::from_ascii("record.example.")?; + let nsec_data = rdataNSEC::new(Name::from_ascii("example.")?, [A, NSEC, RRSIG]); + let nsecs = [(&nsec_name, &nsec_data)]; + + // A name canonically after the NSEC owner should be covered by the wrap. + let covered = Name::from_ascii("zzzz.example.")?; + assert!(find_nsec_covering_record(&covered, &nsecs).is_some()); + + // A name canonically before the NSEC owner should NOT be covered. + let not_covered = Name::from_ascii("aaa.example.")?; + assert!(find_nsec_covering_record(¬_covered, &nsecs).is_none()); + + Ok(()) + } + + /// Confirm that a wraparound NSEC record from a different zone won't be accepted as a covering + /// NSEC record. + #[test] + fn find_nsec_covering_record_unrelated_zone() -> Result<(), ProtoError> { + subscribe(); + // A zone-wrap NSEC record from a different zone. + let nsecs = [( + &Name::from_ascii("record.aaaaaa.")?, + &rdataNSEC::new(Name::from_ascii("aaaaaa.")?, [A, NSEC, RRSIG]), + )]; + + // This should fail to find a covering record since the the NSEC record and the target name + // are in different zones. + assert_eq!( + find_nsec_covering_record(&Name::from_ascii("zzzz.example.")?, &nsecs), + None + ); + + Ok(()) + } + + /// Regression test for forged-SOA NXDOMAIN attack. + /// + /// An attacker harvests a legitimate signed NSEC `a.zone. -> b.zone.` and + /// forges an unsigned SOA with owner `b.zone.` (the NSEC's next field). + /// The old code would treat the gap NSEC as the zone-wrap record because + /// `next == soa_name`, accepting the forged denial. The fix detects wrap + /// via `next <= owner` which is false for `b > a`, so the NSEC does NOT + /// cover the target and the proof must be Bogus. + #[test] + fn forged_soa_nxdomain_attack() -> Result<(), ProtoError> { + subscribe(); + + // Attacker-replayed NSEC: a.zone.test. -> b.zone.test. + // This is NOT a zone-wrap NSEC because b > a. + let nsec_name = Name::from_ascii("a.zone.test.")?; + let nsec_data = rdataNSEC::new(Name::from_ascii("b.zone.test.")?, [A, NSEC, RRSIG]); + let nsecs = [(&nsec_name, &nsec_data)]; + + // Target: target.zone.test. (canonically > b.zone.test.) + // The forged SOA has owner b.zone.test., so old code would match. + // With the fix, this NSEC does not cover the target. + let target = Name::from_ascii("target.zone.test.")?; + assert!(find_nsec_covering_record(&target, &nsecs).is_none()); + + // Full verify_nsec must also reject the forged denial. + // The attacker's forged SOA at b.zone.test. is not Proof::Secure, + // so find_soa_name won't return it. Even passing the forged SOA name + // directly, the proof should be Bogus. + assert_eq!( + verify_nsec( + &Query::query(Name::from_ascii("target.zone.test.")?, A), + Some(&Name::from_ascii("b.zone.test.")?), + ResponseCode::NXDomain, + &[(&nsec_name, &nsec_data)], + ), + Proof::Bogus + ); + + Ok(()) + } + + /// Ensure that the forged-SOA attack also fails for wildcard coverage. + #[test] + fn forged_soa_wildcard_attack() -> Result<(), ProtoError> { + subscribe(); + + // The attacker uses the same NSEC a.zone.test. -> b.zone.test. and + // a forged SOA at b.zone.test. to try to prove wildcard non-existence. + let nsec_name = Name::from_ascii("a.zone.test.")?; + let nsec_data = rdataNSEC::new(Name::from_ascii("b.zone.test.")?, [A, NSEC, RRSIG]); + let nsecs = [(&nsec_name, &nsec_data)]; + + // *.zone.test. is canonically > b.zone.test. so the old code would + // have treated the NSEC as covering it via the forged SOA match. + let wildcard = Name::from_ascii("*.zone.test.")?; + assert!(find_nsec_covering_record(&wildcard, &nsecs).is_none()); + + Ok(()) + } + + /// Regression test for GHSA-896p-wpw8-vf4r. + /// + /// This scenario involves a parent zone with a wildcard record, a child zone with a wildcard + /// record, and a network attacker that combines records retrieved from both name servers. The + /// response claims to be a wildcard expansion of the parent zone's wildcard record, for a query + /// that should instead have its response expanded from the child zone's wildcard record. It + /// includes the NSEC record from the parent that is at the delegation point to the child zone. + /// The issue was that the verifier accepted this NSEC record as a purported proof the child + /// zone's wildcard record doesn't exist, though it can't prove anything about names directly + /// descended from the delegation point. + /// + /// Parent zone: + /// + /// ```text + /// com. SOA ... + /// com. NS ... + /// com. NSEC *.com. NS SOA RRSIG NSEC + /// com. RRSIG SOA ... + /// com. RRSIG NS ... + /// com. RRSIG NSEC ... + /// *.com. A 192.0.2.1 + /// *.com. NSEC example.com. A RRSIG NSEC + /// *.com. RRSIG A ... + /// *.com. RRSIG NSEC ... + /// example.com. NS ... + /// example.com. DS ... + /// example.com. NSEC foobar.com. NS DS RRSIG NSEC + /// example.com. RRSIG DS ... + /// example.com. RRSIG NSEC ... + /// foobar.com. NS ... + /// foobar.com. NSEC com. NS RRSIG NSEC + /// foobar.com. RRSIG NSEC ... + /// ``` + /// + /// Child zone: + /// + /// ```text + /// example.com. SOA ... + /// example.com. NS ... + /// example.com. NSEC *.example.com. NS SOA RRSIG NSEC + /// example.com. RRSIG SOA ... + /// example.com. RRSIG NSEC ... + /// *.example.com. A 192.0.2.2 + /// *.example.com. NSEC example.com. A RRSIG NSEC + /// *.example.com. RRSIG A ... + /// *.example.com. RRSIG NSEC ... + /// ``` + #[test] + fn nsec_invalid_wildcard_expansion_parent_nsec_record() -> Result<(), ProtoError> { + subscribe(); + + let parent_zone = Name::parse("com.", None)?; + let child_zone = Name::parse("example.com.", None)?; + let qname = Name::parse("www.example.com.", None)?; + let answers = [ + mark_secure(Record::from_rdata( + qname.clone(), + 0, + RData::A(Ipv4Addr::new(192, 0, 2, 1).into()), + )), + mark_secure(Record::from_rdata( + qname.clone(), + 0, + RData::DNSSEC(DNSSECRData::RRSIG(rdataRRSIG::from_sig( + SigInput { + type_covered: A, + algorithm: Algorithm::ECDSAP256SHA256, + num_labels: 1, // expanded from *.com. + original_ttl: 0, + sig_expiration: 0.into(), + sig_inception: 0.into(), + key_tag: 0, + signer_name: parent_zone.clone(), + }, + Vec::new(), + ))), + )), + ]; + // Both of these NSEC records are taken from the parent zone. + let authorities = [ + // Matching record for com. + make_nsec_and_rrsig( + parent_zone.clone(), + Name::parse("*.com.", None)?, + [NS, SOA, RRSIG, NSEC], + parent_zone.clone(), + ), + // Bogus proof of nonexistence of *.example.com. + make_nsec_and_rrsig( + child_zone, + Name::parse("foobar.com.", None)?, + [NS, DS, RRSIG, NSEC], + parent_zone.clone(), + ), + ] + .concat(); + + assert!(!proves_wildcard_expansion(&answers, &authorities)); + + Ok(()) + } + + /// Set the proof on a record to Secure and return it. + fn mark_secure(mut record: Record) -> Record { + record.proof = Proof::Secure; + record + } + + /// Based on RFC 5155 B.4 - Wildcard Expansion + #[test] + fn nsec3_wildcard_expansion_tests() -> Result<(), ProtoError> { + subscribe(); + + let zone = Name::from_ascii("example.")?; + + let wildcard_rrsig_record = Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rdataRRSIG::from_sig( + SigInput { + type_covered: MX, + algorithm: Algorithm::ED25519, + num_labels: 2, + original_ttl: 0, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(0), + key_tag: 0, + signer_name: zone.clone(), + }, + vec![], + ))), + ); + + let answers = [ + Record::from_rdata( + Name::from_ascii("a.z.w.example.")?, + 3600, + RData::MX(rdata::MX::new(10, Name::from_ascii("a.z.w.example.")?)), + ), + wildcard_rrsig_record, + ]; + + let hash = |name: &Name| -> Vec { + Nsec3HashAlgorithm::SHA1 + .hash(KNOWN_SALT, name, ITERATIONS) + .unwrap() + .as_ref() + .to_vec() + }; + let make_nsec3 = |name, next_name, record_types| { + let nsec3_name = zone + .prepend_label(BASE32_DNSSEC.encode(&hash(&name))) + .unwrap(); + let nsec3_record = Record::from_rdata( + nsec3_name.clone(), + 3600, + RData::DNSSEC(DNSSECRData::NSEC3(rdataNSEC3::new( + Nsec3HashAlgorithm::SHA1, + false, + ITERATIONS, + KNOWN_SALT.to_vec(), + hash(&next_name), + record_types, + ))), + ); + let rrsig_record = Record::from_rdata( + nsec3_name, + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rdataRRSIG::from_sig( + SigInput { + type_covered: NSEC3, + algorithm: Algorithm::ECDSAP256SHA256, + num_labels: 2, + original_ttl: 3600, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(1), + key_tag: 0, + signer_name: zone.clone(), + }, + Vec::new(), + ))), + ); + [nsec3_record, rrsig_record] + }; + + assert!(proves_wildcard_expansion( + &answers, + // Covers the next-closer name + &make_nsec3( + Name::from_ascii("ns2.example")?, + Name::from_ascii("*.w.example.")?, + vec![A, RRSIG], + ) + )); + + assert!(!proves_wildcard_expansion( + &answers, + // Fails to cover the next-closer name + &make_nsec3( + Name::from_ascii("example.")?, + Name::from_ascii("a.example.")?, + vec![A, RRSIG], + ) + )); + + assert!(!proves_wildcard_expansion( + &answers, + // Matches the next-closer name. + &make_nsec3( + Name::from_ascii("z.w.example.")?, + Name::from_ascii("a.example.")?, + vec![A, RRSIG], + ) + )); + + assert!(!proves_wildcard_expansion( + &answers, + // Fails to cover the next-closer name + &make_nsec3( + Name::from_ascii("ns2.example.")?, + Name::from_ascii("z.w.example.")?, + vec![A, RRSIG], + ) + )); + + Ok(()) + } + + fn make_nsec_and_rrsig( + name: Name, + next_name: Name, + record_types: impl IntoIterator, + signer_name: Name, + ) -> [Record; 2] { + [ + Record::from_rdata( + name.clone(), + 3600, + RData::DNSSEC(DNSSECRData::NSEC(rdataNSEC::new(next_name, record_types))), + ), + Record::from_rdata( + name.clone(), + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(rdataRRSIG::from_sig( + SigInput { + type_covered: NSEC, + algorithm: Algorithm::ED25519, + num_labels: name.num_labels(), + original_ttl: 3600, + sig_expiration: SerialNumber::new(0), + sig_inception: SerialNumber::new(0), + key_tag: 0, + signer_name, + }, + Vec::new(), + ))), + ), + ] + } + + /// Runs the wildcard expansion check with every record treated as having a valid signature. + fn proves_wildcard_expansion(answers: &[Record], authorities: &[Record]) -> bool { + let mut message = Message::query(); + message.add_answers(answers.iter().cloned().map(mark_secure)); + message.add_authorities(authorities.iter().cloned().map(mark_secure)); + unproven_wildcard_rrsets(&message).is_empty() + } + + const KNOWN_SALT: &[u8] = &[0xAAu8, 0xBBu8, 0xCCu8, 0xDDu8]; + const ITERATIONS: u16 = 12; +} diff --git a/vendor/hickory-dns/hickory-net/src/dnssec/nsec3.rs b/vendor/hickory-dns/hickory-net/src/dnssec/nsec3.rs new file mode 100644 index 00000000..3d25a977 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/dnssec/nsec3.rs @@ -0,0 +1,1712 @@ +//! Throughout this module several NSEC3 specific terms are used. +//! +//! "closest_encloser" - a name that is a longest parent / grandparent / +//! great-grandparent, etc. of a `query_name`, that DOES exist. +//! +//! "next_closer" - a name that is one level deeper in the hierarchy from +//! `closest_encloser`, that DOES NOT exist in the zone. +//! +//! "wildcard of closest encloser" - a name in a form of `*.closest_encloser`. +//! If it exists then `query_name` would be serviced by the wildcard. +//! +//! "covering NSEC3 record" - NSEC3 record name has a hash (`hashed_owner_name`), +//! and inside the record data there's `next_hashed_owner_name`. +//! If the hash of `query_name` fits between the two hashes then the record +//! "covers" `query_name` +//! +//! "matching NSEC3 record" - exists for *existing* names only. +//! The `hashed_owner_name` would match one of the names *exactly*. +//! +//! In general: +//! * if a name exists we would expect to see its "matching" NSEC3 record, +//! * if the name doesn't exist we would expect to see a "covering" NSEC3 record. +//! +//! A various combinations of matching and covering records for `closest_encloser`, +//! `next_closer`, and `wildcard_encloser` can tell us whether `query_name` +//! exists, whether `wildcard_encloser` exists, and whether its own or +//! `wildcard_encloser`'s records have a record of a requested type. +//! +//! NOTE: A single NSEC3 record can in theory address multiple situations. +//! Thus, the number of records is not important as long as all +//! conditions are represented by them. +//! +//! NOTE: Normally, when there exist a wildcard that a given name fits into, +//! it is said that the wildcard "covers" the name. But since the wording +//! around NSEC3 records uses "cover" in a specific sense we use the word +//! "service" instead: +//! `*.w.soa.name` *services* `x.w.soa.name` +//! +//! In general, avoid using the word "cover" for anything not related to +//! "covering NSEC3 record" +//! +//! ## Examples +//! +//! To explain them let's use the following example zone: +//! +//! soa.name +//! *.w.soa.name +//! c.e.soa.name +//! +//! if we request: +//! `query_name` = `x.y.z.nc.c.e.soa.name` +//! then: +//! `closest_encloser` == `c.e.soa.name` (the first existing ancestor) +//! `next_closer` == `nc.c.e.soa.name` (doesn't exist in the zone) +//! `wildcard_encloser` == `*.c.e.soa.name` +//! +//! if we request: +//! `query_name` = `x.y.z.nc.w.soa.name` +//! then: +//! `closest_encloser` == `w.soa.name` +//! `next_closer` == `nc.w.soa.name` +//! `wildcard_encloser` == `*.w.soa.name` +//! +//! if we request: +//! `query_name` = `x.soa.name` +//! then: +//! `closest_encloser` == `soa.name` +//! `next_closer` == `x.soa.name` +//! `wildcard_encloser` == `*.soa.name` +//! +//! if we request: +//! `query_name` = `e.soa.name` +//! then: +//! `closest_encloser` == `e.soa.name` +//! `next_closer` == doesn't exist +//! `wildcard_encloser` == `*.soa.name` +//! + +/* + * Copyright (C) 2015 Benjamin Fry + * Copyright (C) 2017 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use core::{fmt::Display, slice}; + +use super::{Proof, proof_log_yield}; +use crate::proto::{ + dnssec::{Nsec3HashAlgorithm, rdata::NSEC3}, + op::{Query, ResponseCode}, + rr::{Label, Name, RecordType}, +}; + +pub(super) fn verify_nsec3( + query: &Query, + soa: Option<&Name>, + response_code: ResponseCode, + nsec3s: &[(&Name, &NSEC3)], +) -> Proof { + debug_assert!(!nsec3s.is_empty()); // checked in the caller + + let records = match convert_nsec3_records(query, soa, nsec3s) { + Ok(records) => records, + Err(proof) => return proof, + }; + debug_assert!(!records.is_empty()); // `nsec3s` was not empty, and we returned on any invalid values + + let cx = match Context::new(query, soa, &records) { + Ok(cx) => cx, + Err(proof) => return proof, + }; + + // Basic sanity checks are done. + // From here on 3 big situations are possible: + // 1. No such name, and no servicing wildcard + // 2. Name exists but there's no record of this type + // 3. Name is serviced by wildcard that doesn't have a record of this type + + match response_code { + // Case 1: + ResponseCode::NXDomain => validate_nxdomain_response(&cx), + + // RFC 5155: NoData + // Cases 2, 3, and 4: + ResponseCode::NoError => validate_nodata_response(query.query_type(), &cx), + _ => cx.proof( + Proof::Bogus, + format_args!("unsupported response code ({response_code})"), + ), + } +} + +/// Verifies whether these NSEC3 records prove that there is an insecure delegation at this name, or +/// that there could be. +/// +/// This returns true if either there is an NSEC3 record matching the zone name with the NS bit set, +/// the SOA bit unset, and the DS bit unset, or if there is an opt-out NSEC3 record that covers the +/// zone name or the next closer name of a closest encloser proof for the zone name. +pub(super) fn verify_nsec3_insecure_delegation(zone: &Name, nsec3s: &[(&Name, &NSEC3)]) -> bool { + let query = Query::query(zone.clone(), RecordType::DS); + let Ok(records) = convert_nsec3_records(&query, None, nsec3s) else { + return false; + }; + + // No SOA here, so bound the closest encloser proof by the zone of the NSEC3 records. + let parent = records.first().and_then(|first| { + records + .iter() + .all(|record| record.zone_name == first.zone_name) + .then_some(&first.zone_name) + }); + + let Ok(cx) = Context::new(&query, parent, &records) else { + return false; + }; + + // Check for matching record. + let (hashed_zone_name, base32_hashed_zone_name) = cx.hash_and_label(zone); + if let Some(info) = records + .iter() + .find(|info| info.matches(zone, &base32_hashed_zone_name)) + { + let type_set = info.nsec3_data.type_set(); + return type_set.contains(RecordType::NS) + && !type_set.contains(RecordType::DS) + && !type_set.contains(RecordType::SOA); + } + + // Check for covering record with opt-out flag set. + if let Some(info) = + find_covering_record(&records, zone, &hashed_zone_name, &base32_hashed_zone_name) + { + return info.nsec3_data.opt_out(); + } + + cx.next_closer_opt_out_proof() +} + +/// Convert pairs of record names and NSEC3 RDATA to structures containing hashed record names, zone +/// names, and NSEC3 RDATA. +fn convert_nsec3_records<'a>( + query: &Query, + soa: Option<&Name>, + nsec3s: &'a [(&Name, &'a NSEC3)], +) -> Result>, Proof> { + nsec3s + .iter() + .map(|(name, data)| Nsec3RecordInfo::new(name, data, query, soa)) + .collect() +} + +/// There is no such `query_name` in the zone and there's no wildcard that +/// can be expanded to service this `query_name`. +/// +/// Expecting the following records: +/// * closest encloser - *matching* NSEC3 record +/// * next closer - *covering* NSEC3 record +/// * wildcard of closest encloser - *covering* NSEC3 record +fn validate_nxdomain_response(cx: &Context<'_>) -> Proof { + // The response is NXDomain but there's a record for query_name + let (_, base32_hashed_query_name) = cx.hash_and_label(cx.query.name()); + if cx + .nsec3s + .iter() + .any(|r| r.matches(&cx.query.name, &base32_hashed_query_name)) + { + return cx.proof(Proof::Bogus, "NXDomain response with record for query name"); + } + + // Note that the three fields may hold references to the same NSEC3 + // record, because the interval of base32_hashed_name + // and next_hashed_owner_name happen to match / cover all three components + // of closest encloser proof. + let ( + ClosestEncloserProofInfo { + closest_encloser, + next_closer, + }, + closest_encloser_wildcard, + ) = cx.closest_encloser_proof_with_wildcard(false); + + if closest_encloser.is_none() && next_closer.is_none() { + return cx.proof(Proof::Bogus, "returning early proof"); + } + + match (closest_encloser, next_closer, closest_encloser_wildcard) { + // Got all three components - we proved that there's no `query_name` + // in the zone + (Some(_), Some(_), Some(_)) => cx.proof(Proof::Secure, "direct proof"), + // `query_name`'s parent is the zone apex itself, so there's no need to send its NSEC3 + // record. Still we have to show that both `query_name` doesn't exist and there's no + // wildcard to service it. + (None, Some((_, next_closer_nsec3)), Some((_, wildcard_nsec3))) + if cx.query.name().base_name() == next_closer_nsec3.zone_name + && next_closer_nsec3.zone_name == wildcard_nsec3.zone_name => + { + cx.proof( + Proof::Secure, + "no direct or wildcard proof, but parent name of query is zone apex", + ) + } + _ => cx.proof(Proof::Bogus, "no proof of non-existence"), + } +} + +/// This function addresses three situations: +/// +/// Case 2. Name exists but there's no record of this type +/// Case 3. Opt-out proof for Name exists +/// Case 4. Name is serviced by wildcard that doesn't have a record of this type +fn validate_nodata_response(query_type: RecordType, cx: &Context<'_>) -> Proof { + // 2. Name exists but there's no record of this type + // 3. Opt-out proof for Name exists + // 4. Name is serviced by wildcard that doesn't have a record of this type + + let (hashed_query_name, base32_hashed_query_name) = cx.hash_and_label(cx.query.name()); + let query_name_record = cx + .nsec3s + .iter() + .find(|record| record.matches(&cx.query.name, &base32_hashed_query_name)); + + // Case 2: + // Name exists but there's no record of this type + // + // RFC 5155 § 8.5 et seq. + // + // 8.5. Validating No Data Responses, QTYPE is not DS + // + // The validator MUST verify that an NSEC3 RR that matches QNAME is + // present and that both the QTYPE and the CNAME type are not set in its + // Type Bit Maps field. + // + // Note that this test also covers the case where the NSEC3 RR exists + // because it corresponds to an empty non-terminal, in which case the + // NSEC3 RR will have an empty Type Bit Maps field. + // + // 8.6. Validating No Data Responses, QTYPE is DS + // + // If there is an NSEC3 RR that matches QNAME present in the response, + // then that NSEC3 RR MUST NOT have the bits corresponding to DS and + // CNAME set in its Type Bit Maps field. + // + // If there is no such NSEC3 RR, then the validator MUST verify that a + // closest provable encloser proof for QNAME is present in the response, + // and that the NSEC3 RR that covers the "next closer" name has the Opt- + // Out bit set. + if let Some(query_record) = query_name_record { + if query_record.nsec3_data.type_set().contains(query_type) + || query_record + .nsec3_data + .type_set() + .contains(RecordType::CNAME) + { + return cx.proof( + Proof::Bogus, + format_args!("nsec3 type map covers {query_type} or CNAME"), + ); + } else if query_type != RecordType::DS && query_record.nsec3_data.is_ancestor_delegation() { + // RFC 6840 §4.1: a parent-side NSEC3 at the cut only authenticates + // the absence of DS at that owner, it cannot deny other types there. + return cx.proof(Proof::Bogus, "direct match is an ancestor-delegation NSEC3"); + } else if query_type == RecordType::DS + && query_record.nsec3_data.type_set().contains(RecordType::SOA) + { + // RFC 5155 §8.9: "The validator MUST also ensure that the NSEC3 RR is from the correct + // (i.e., parent) zone. This is done by ensuring that the SOA bit is not set in the Type + // Bit Maps field of this NSEC3 RR." + return cx.proof( + Proof::Bogus, + "direct match for DS query is an NSEC3 record from the child side of the zone cut", + ); + } else { + return cx.proof( + Proof::Secure, + format_args!("type map does not cover {query_type} or CNAME"), + ); + } + } + + // Case 3: + // Query type is DS, records for name exist, but there are no DS records (opt-out proof) + // + // RFC 5155 § 6 + // + // In this specification, as in [RFC4033], [RFC4034] and [RFC4035], NS + // RRSets at delegation points are not signed and may be accompanied by + // a DS RRSet. With the Opt-Out bit clear, the security status of the + // child zone is determined by the presence or absence of this DS RRSet, + // cryptographically proven by the signed NSEC3 RR at the hashed owner + // name of the delegation. Setting the Opt-Out flag modifies this by + // allowing insecure delegations to exist within the signed zone without + // a corresponding NSEC3 RR at the hashed owner name of the delegation. + // + // An Opt-Out NSEC3 RR is said to cover a delegation if the hash of the + // owner name or "next closer" name of the delegation is between the + // owner name of the NSEC3 RR and the next hashed owner name. + // + // An Opt-Out NSEC3 RR does not assert the existence or non-existence of + // the insecure delegations that it may cover. This allows for the + // addition or removal of these delegations without recalculating or re- + // signing RRs in the NSEC3 RR chain. However, Opt-Out NSEC3 RRs do + // assert the (non)existence of other, authoritative RRSets. + // + // An Opt-Out NSEC3 RR MAY have the same original owner name as an + // insecure delegation. In this case, the delegation is proven insecure + // by the lack of a DS bit in the type map and the signed NSEC3 RR does + // assert the existence of the delegation. + // + // Zones using Opt-Out MAY contain a mixture of Opt-Out NSEC3 RRs and + // non-Opt-Out NSEC3 RRs. If an NSEC3 RR is not Opt-Out, there MUST NOT + // be any hashed owner names of insecure delegations (nor any other RRs) + // between it and the name indicated by the next hashed owner name in + // the NSEC3 RDATA. If it is Opt-Out, it MUST only cover hashed owner + // names or hashed "next closer" names of insecure delegations. + // + // The effects of the Opt-Out flag on signing, serving, and validating + // responses are covered in following sections. + // + // *Note*: the case of an opt-out NSEC3 record having the same original owner + // name as the hashed query name and not having the DS bit set in the type flags + // is covered here by case 2. + if query_type == RecordType::DS + && find_covering_record( + cx.nsec3s, + &cx.query.name, + &hashed_query_name, + &base32_hashed_query_name, + ) + .is_some_and(|x| x.nsec3_data.opt_out()) + { + return cx.proof(Proof::Secure, "DS query covered by opt-out proof"); + } + + // RFC 5155 § 8.6: the next closer name may be an empty non-terminal above the query name. + if query_type == RecordType::DS && cx.next_closer_opt_out_proof() { + return cx.proof( + Proof::Secure, + "DS query next closer name covered by opt-out proof", + ); + } + + // Case 4: + // Name is serviced by wildcard that doesn't have a record of this type + // Verify the wildcard type set does not match the query type (or CNAME) - RFC 5155 8.7. + let ( + ClosestEncloserProofInfo { + closest_encloser, + next_closer, + }, + closest_encloser_wildcard, + ) = cx.closest_encloser_proof_with_wildcard(true); + match (closest_encloser, next_closer, closest_encloser_wildcard) { + (Some(_), Some(_), Some((_, wildcard))) + if !wildcard.nsec3_data.type_set().contains(query_type) + && !wildcard.nsec3_data.type_set().contains(RecordType::CNAME) => + { + cx.proof( + Proof::Secure, + "servicing wildcard with closest encloser proof", + ) + } + (None, None, None) if Some(cx.query.name()) == cx.soa => { + cx.proof(Proof::Bogus, "apex NODATA with no NSEC3 matching H(QNAME)") + } + _ => cx.proof(Proof::Bogus, "no valid servicing wildcard proof"), + } +} + +/// Checks whether any of these NSEC3 records covers the hash of `next_closer`, as required for a +/// wildcard expansion (RFC 5155 8.8). Each record is checked using its own NSEC3 parameters. +pub(super) fn verify_nsec3_next_closer(next_closer: &Name, nsec3s: &[(&Name, &NSEC3)]) -> bool { + let mut hashes: Vec<(&NSEC3, Vec, Label)> = Vec::new(); + nsec3s.iter().any(|&(name, nsec3_data)| { + let Some((first_label, zone_name)) = split_first_label(name) else { + return false; + }; + let Ok(base32_hashed_name) = Label::from_raw_bytes(first_label) else { + return false; + }; + let record = Nsec3RecordInfo { + base32_hashed_name, + zone_name, + nsec3_data, + }; + + let index = match hashes.iter().position(|(other, ..)| { + other.hash_algorithm() == nsec3_data.hash_algorithm() + && other.salt() == nsec3_data.salt() + && other.iterations() == nsec3_data.iterations() + }) { + Some(index) => index, + None => { + let Ok(digest) = nsec3_data.hash_algorithm().hash( + nsec3_data.salt(), + next_closer, + nsec3_data.iterations(), + ) else { + return false; + }; + let hashed_name = digest.as_ref().to_vec(); + let Ok(label) = + Label::from_ascii(&data_encoding::BASE32_DNSSEC.encode(&hashed_name)) + else { + return false; + }; + hashes.push((nsec3_data, hashed_name, label)); + hashes.len() - 1 + } + }; + let (_, hashed_name, base32_hashed_name) = &hashes[index]; + + find_covering_record( + slice::from_ref(&record), + next_closer, + hashed_name, + base32_hashed_name, + ) + .is_some() + }) +} + +fn split_first_label(name: &Name) -> Option<(&[u8], Name)> { + let first_label = name.iter().next()?; + let base = name.base_name(); + Some((first_label, base)) +} + +#[derive(Default)] +struct ClosestEncloserProofInfo<'a> { + closest_encloser: Option<(HashedNameInfo, &'a Nsec3RecordInfo<'a>)>, + next_closer: Option<(HashedNameInfo, &'a Nsec3RecordInfo<'a>)>, +} + +struct Context<'a> { + query: &'a Query, + soa: Option<&'a Name>, + nsec3s: &'a [Nsec3RecordInfo<'a>], + hash_algorithm: Nsec3HashAlgorithm, + salt: &'a [u8], + iterations: u16, +} + +impl<'a> Context<'a> { + /// Checks for consistent NSEC3 parameters, and constructs a verification context. + fn new( + query: &'a Query, + soa: Option<&'a Name>, + nsec3s: &'a [Nsec3RecordInfo<'a>], + ) -> Result { + // RFC 5155 8.2 - all NSEC3 records share the same NSEC3 params + let first = &nsec3s[0]; + let hash_algorithm = first.nsec3_data.hash_algorithm(); + let salt = first.nsec3_data.salt(); + let iterations = first.nsec3_data.iterations(); + if nsec3s.iter().any(|r| { + r.nsec3_data.hash_algorithm() != hash_algorithm + || r.nsec3_data.salt() != salt + || r.nsec3_data.iterations() != iterations + }) { + return Err(nsec3_yield(Proof::Bogus, query, "parameter mismatch")); + } + Ok(Self { + query, + soa, + nsec3s, + hash_algorithm, + salt, + iterations, + }) + } + + /// Return a closest encloser proof and a proof of non-existence for the wildcard at the closest encloser. + /// + /// For example, if the closest encloser is `w.example.`, the wildcard at the closest encloser is `*.w.example`. + /// + /// If matching is set to true, look for an NSEC3 that matches the wildcard name (used in wildcard no data responses.) + /// If matching is set to false, look for an NSEC3 that covers the wildcard name (used in name error responses.) + fn closest_encloser_proof_with_wildcard( + &'a self, + matching: bool, + ) -> ( + ClosestEncloserProofInfo<'a>, + Option<(HashedNameInfo, &'a Nsec3RecordInfo<'a>)>, + ) { + let closest_encloser_proof = self.closest_encloser_proof(); + + let closest_encloser_name = match closest_encloser_proof.closest_encloser.as_ref() { + Some((name_info, _)) => name_info.name.clone(), + None => return (closest_encloser_proof, None), + }; + + let Ok(wildcard_encloser_name) = closest_encloser_name.prepend_label("*") else { + return (closest_encloser_proof, None); + }; + + let wildcard_name_info = HashedNameInfo::new(wildcard_encloser_name, self); + let wildcard_record = if matching { + self.nsec3s.iter().find(|record| { + record.matches( + &wildcard_name_info.name, + &wildcard_name_info.base32_hashed_name, + ) + }) + } else { + find_covering_record( + self.nsec3s, + &wildcard_name_info.name, + &wildcard_name_info.hashed_name, + &wildcard_name_info.base32_hashed_name, + ) + }; + + ( + closest_encloser_proof, + wildcard_record.map(|record| (wildcard_name_info, record)), + ) + } + + /// Find the NSEC3 record(s) constituting the closest encloser proof (RFC 5155 7.2.1) consisting of: + /// + /// * closest encloser - *matching* NSEC3 record + /// * next closer - *covering* NSEC3 record + /// + /// Example: for `a.z.w.example.`, the closest encloser might be `w.example.` in which case the next closer name + /// would be `z.w.example.` + fn closest_encloser_proof(&'a self) -> ClosestEncloserProofInfo<'a> { + // For `a.b.c.soa.name` the `closest_encloser_candidates` will have: + // [ + // (a.b.c.soa.name, h(a.b.c.soa.name), b32h(a.b.c.soa.name)), + // (b.c.soa.name, h(b.c.soa.name), b32h(b.c.soa.name)), + // (c.soa.name, h(c.soa.name), b32h(c.soa.name)), + // (soa.name, h(soa.name), b32h(soa.name)), + // ] + let mut closest_encloser_candidates = self + .encloser_candidates() + .map(|name| HashedNameInfo::new(name, self)) + .collect::>(); + + // Find the longest candidate name, if any, with a matching NSEC3 record and get the record. + // + // Per RFC 5155 §8.3 / RFC 6840 §4.1, an ancestor-delegation NSEC3 + // (parent-side at a zone cut) cannot serve as a closest-encloser match. + // It does not authenticate names below the cut. + let Some(closest_encloser_matching_record) = + closest_encloser_candidates.iter().find_map(|candidate| { + self.nsec3s.iter().find(|nsec| { + nsec.matches(&candidate.name, &candidate.base32_hashed_name) + && !nsec.nsec3_data.is_ancestor_delegation() + }) + }) + else { + return ClosestEncloserProofInfo::default(); + }; + + // Find the index in the candidate list associated with the closest encloser name. + let Some(closest_encloser_index) = closest_encloser_candidates + .iter() + .enumerate() + .skip(1) // The closest encloser name can't be at index 0 (it cannot be the longest name) + .find(|(_, candidate)| { + candidate.base32_hashed_name == closest_encloser_matching_record.base32_hashed_name + }) + .map(|(i, _)| i) + else { + return ClosestEncloserProofInfo::default(); + }; + + let closest_encloser_name_info = + closest_encloser_candidates.swap_remove(closest_encloser_index); + let next_closer_name_info = + closest_encloser_candidates.swap_remove(closest_encloser_index - 1); + + // Now find a covering record for the next closer, which is one label longer than the closest encloser + let next_closer_covering_record = find_covering_record( + self.nsec3s, + &next_closer_name_info.name, + &next_closer_name_info.hashed_name, + &next_closer_name_info.base32_hashed_name, + ); + + ClosestEncloserProofInfo { + closest_encloser: Some((closest_encloser_name_info, closest_encloser_matching_record)), + next_closer: next_closer_covering_record.map(|record| (next_closer_name_info, record)), + } + } + + /// Checks for a closest encloser proof whose next closer name is covered by an opt-out record. + fn next_closer_opt_out_proof(&'a self) -> bool { + self.closest_encloser_proof() + .next_closer + .is_some_and(|(_, record)| record.nsec3_data.opt_out()) + } + + /// Hashes a name and returns both the hash digest and the base32-encoded form. + fn hash_and_label(&self, name: &Name) -> (Vec, Label) { + let hash = self + .hash_algorithm + .hash(self.salt, name, self.iterations) + // We only compute hashes of names between `query_name` and `soa_name` + // and wildcards between `*.query_name.base_name()` and `*.soa_name`. + // All of them are guaranteed to be valid names. + .unwrap() + .as_ref() + .to_vec(); + + let base32_encoded = data_encoding::BASE32_DNSSEC.encode(&hash); + // Unwrap safety: The length of the hashed name is valid because it is the output of the above + // hash function. The input is all alphanumeric ASCII characters by construction. + let label = Label::from_ascii(&base32_encoded).unwrap(); + (hash, label) + } + + fn encloser_candidates(&self) -> EncloserCandidates<'a> { + // The query name must be a descendant of the zone name for encloser candidate enumeration + // to make sense. When CNAME chasing crosses zone boundaries, the query name might not be + // within this zone at all. + EncloserCandidates { + cur: match self.soa { + Some(soa) if soa.zone_of(self.query.name()) => Some(self.query.name().clone()), + _ => None, + }, + soa: self.soa, + } + } + + fn proof(&self, proof: Proof, msg: impl Display) -> Proof { + nsec3_yield(proof, self.query, msg) + } +} + +fn find_covering_record<'a>( + nsec3s: &'a [Nsec3RecordInfo<'a>], + name: &Name, + target_hashed_name: &[u8], + // Strictly speaking we don't need this parameter, we can calculate + // base32(target_hashed_name) inside the function. + // However, we already have it available at call sites, may as well use + // it and save on repeated base32 encodings. + target_base32_hashed_name: &Label, +) -> Option<&'a Nsec3RecordInfo<'a>> { + nsec3s.iter().find(|record| { + let Some(record_next_hashed_owner_name_base32) = + record.nsec3_data.next_hashed_owner_name_base32() + else { + return false; + }; + + // Ignore NSEC3 records from outside the zone. + if !record.zone_name.zone_of(name) { + return false; + } + + // Matching records don't count as covering records + if record.base32_hashed_name == *target_base32_hashed_name { + return false; + } + + if record.base32_hashed_name < *record_next_hashed_owner_name_base32 { + // Normal case: target must be between the hashed owner name and the next hashed owner + // name. + record.base32_hashed_name < *target_base32_hashed_name + && target_hashed_name < record.nsec3_data.next_hashed_owner_name() + } else { + // Wraparound case: target must be greater than the hashed owner name or less than the + // next hashed owner name. + *target_base32_hashed_name > record.base32_hashed_name + || target_hashed_name < record.nsec3_data.next_hashed_owner_name() + } + }) +} + +// NSEC3 records use a base32 hashed name as a record name component. +// But within the record body the hash is stored as a binary blob. +// Thus we need both for comparisons. +#[derive(Clone, Debug)] +struct HashedNameInfo { + name: Name, + hashed_name: Vec, + base32_hashed_name: Label, +} + +impl HashedNameInfo { + /// Hash a query name and store all representations of it. + fn new(name: Name, cx: &Context<'_>) -> Self { + let (hashed_name, base32_hashed_name) = cx.hash_and_label(&name); + Self { + name, + hashed_name, + base32_hashed_name, + } + } +} + +struct EncloserCandidates<'a> { + cur: Option, + soa: Option<&'a Name>, +} + +impl Iterator for EncloserCandidates<'_> { + type Item = Name; + + fn next(&mut self) -> Option { + let cur = self.cur.take()?; + let soa = self.soa?; + + if &cur != soa { + let next = cur.base_name(); + debug_assert_ne!(next, Name::root()); + self.cur = Some(next); + } + + Some(cur) + } +} + +/// Logs a debug message and returns a [`Proof`]. This is specific to NSEC3 validation. +fn nsec3_yield(proof: Proof, query: &Query, msg: impl Display) -> Proof { + proof_log_yield(proof, query, "nsec3", msg) +} + +/// Preprocessed form of an NSEC3 record. +struct Nsec3RecordInfo<'a> { + /// The leftmost label of the NSEC3 record's name. + base32_hashed_name: Label, + /// The remainder of the NSEC3 record's name. + zone_name: Name, + /// The NSEC3 record's RDATA. + nsec3_data: &'a NSEC3, +} + +impl<'a> Nsec3RecordInfo<'a> { + /// Preprocesses an NSEC3 record and performs sanity checks. + fn new( + name: &'a Name, + rdata: &'a NSEC3, + query: &Query, + soa: Option<&Name>, + ) -> Result { + let Some((base32_hashed_name, base)) = split_first_label(name) else { + return Err(nsec3_yield( + Proof::Bogus, + query, + "record name format is invalid", + )); + }; + + // If the SOA record is present, the base name of any NSEC3 records must match it. + if soa.is_some_and(|soa| &base != soa) { + return Err(nsec3_yield( + Proof::Bogus, + query, + "record name is not in the zone", + )); + } + + let Ok(base32_hashed_name) = Label::from_raw_bytes(base32_hashed_name) else { + return Err(nsec3_yield( + Proof::Bogus, + query, + "base32-hashed name is invalid", + )); + }; + + Ok(Nsec3RecordInfo { + base32_hashed_name, + zone_name: base, + nsec3_data: rdata, + }) + } + + /// Determines whether this NSEC3 record matches a name. + /// + /// # Arguments + /// + /// * `name` - The name to test against this NSEC3 record + /// * `base32_hashed_name` - A base32-encoded label representing the hash of `name` + /// + /// While `base32_hashed_name` could be recomputed from `name`, each call site has both values + /// available already, so we take both as separate arguments to avoid redundant work. + fn matches(&self, name: &Name, base32_hashed_name: &Label) -> bool { + self.zone_name.zone_of(name) && &self.base32_hashed_name == base32_hashed_name + } +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + + use super::*; + use crate::proto::{ + ProtoError, + rr::RecordType::{A, AAAA, DNAME, DNSKEY, DS, MX, NS, NSEC3PARAM, RRSIG, SOA}, + }; + + use test_support::subscribe; + + use data_encoding::BASE32_DNSSEC; + + #[test] + fn test_hash() { + let name = Name::from_str("www.example.com").unwrap(); + let salt: Vec = vec![1, 2, 3, 4]; + + assert_eq!( + Nsec3HashAlgorithm::SHA1 + .hash(&salt, &name, 0) + .unwrap() + .as_ref() + .len(), + 20 + ); + assert_eq!( + Nsec3HashAlgorithm::SHA1 + .hash(&salt, &name, 1) + .unwrap() + .as_ref() + .len(), + 20 + ); + assert_eq!( + Nsec3HashAlgorithm::SHA1 + .hash(&salt, &name, 3) + .unwrap() + .as_ref() + .len(), + 20 + ); + } + + #[test] + fn test_known_hashes() { + // H(example) = 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom + assert_eq!( + hash_with_base32("example"), + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom" + ); + assert_eq!( + hash_with_base32("EXAMPLE"), + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom" + ); + + // H(a.example) = 35mthgpgcu1qg68fab165klnsnk3dpvl + assert_eq!( + hash_with_base32("a.example"), + "35mthgpgcu1qg68fab165klnsnk3dpvl" + ); + + // H(ai.example) = gjeqe526plbf1g8mklp59enfd789njgi + assert_eq!( + hash_with_base32("ai.example"), + "gjeqe526plbf1g8mklp59enfd789njgi" + ); + + // H(ns1.example) = 2t7b4g4vsa5smi47k61mv5bv1a22bojr + assert_eq!( + hash_with_base32("ns1.example"), + "2t7b4g4vsa5smi47k61mv5bv1a22bojr" + ); + + // H(ns2.example) = q04jkcevqvmu85r014c7dkba38o0ji5r + assert_eq!( + hash_with_base32("ns2.example"), + "q04jkcevqvmu85r014c7dkba38o0ji5r" + ); + + // H(w.example) = k8udemvp1j2f7eg6jebps17vp3n8i58h + assert_eq!( + hash_with_base32("w.example"), + "k8udemvp1j2f7eg6jebps17vp3n8i58h" + ); + + // H(*.w.example) = r53bq7cc2uvmubfu5ocmm6pers9tk9en + assert_eq!( + hash_with_base32("*.w.example"), + "r53bq7cc2uvmubfu5ocmm6pers9tk9en" + ); + + // H(x.w.example) = b4um86eghhds6nea196smvmlo4ors995 + assert_eq!( + hash_with_base32("x.w.example"), + "b4um86eghhds6nea196smvmlo4ors995" + ); + + // H(y.w.example) = ji6neoaepv8b5o6k4ev33abha8ht9fgc + assert_eq!( + hash_with_base32("y.w.example"), + "ji6neoaepv8b5o6k4ev33abha8ht9fgc" + ); + + // H(x.y.w.example) = 2vptu5timamqttgl4luu9kg21e0aor3s + assert_eq!( + hash_with_base32("x.y.w.example"), + "2vptu5timamqttgl4luu9kg21e0aor3s" + ); + + // H(xx.example) = t644ebqk9bibcna874givr6joj62mlhv + assert_eq!( + hash_with_base32("xx.example"), + "t644ebqk9bibcna874givr6joj62mlhv" + ); + } + + #[test] + fn nsec3_name_error_tests() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 5155 B.1 - Name Error + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.c.x.w.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // Covers the next closer name (c.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?, + hash("ns1.example."), + [MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG], + ) + .as_ref(), + // Matches the closest encloser (x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("x.w.example"))?, + hash("ai.example."), + [MX, RRSIG], + ) + .as_ref(), + // Covers the wildcard at the closest encloser (*.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("a.example"))?, + hash("x.w.example."), + [DS, NS, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Secure, + ); + + // Missing wildcard at the closest encloser + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.c.x.w.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // Covers the next closer name (c.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?, + hash("ns1.example."), + [MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG], + ) + .as_ref(), + // Matches the closest encloser (x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("x.w.example"))?, + hash("ai.example."), + [MX, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // No record matching the next closer name + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.c.x.w.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NXDomain, + &[ + // Matches the closest encloser (x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("x.w.example"))?, + hash("ai.example."), + [MX, RRSIG], + ) + .as_ref(), + // Covers the wildcard at the closest encloser (*.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("a.example"))?, + hash("x.w.example."), + [DS, NS, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // Invalid SOA + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.c.x.w.example.")?, A), + Some(&Name::from_ascii("x.w.example.")?), + ResponseCode::NXDomain, + &[ + // Covers the next closer name (c.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?, + hash("ns1.example."), + [MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG], + ) + .as_ref(), + // Matches the closest encloser (x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("x.w.example"))?, + hash("ai.example."), + [MX, RRSIG], + ) + .as_ref(), + // Covers the wildcard at the closest encloser (*.x.w.example.) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("a.example"))?, + hash("x.w.example."), + [DS, NS, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + Ok(()) + } + + #[test] + fn nsec3_no_data_error_tests() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 5155 B.2 - No Data Error + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the query name and proves the record type does not exist. + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns1.example"))?, + hash("x.y.w.example."), + [A, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Secure, + ); + + // Based on RFC 5155 B.2.1 - No Data Error, Empty Non-Terminal + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("y.w.example.")?, A), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the query name and proves the record type does not exist. + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("y.w.example"))?, + hash("w.example."), + [], + ) + .as_ref(), + ], + ), + Proof::Secure, + ); + + // NSEC Type map doesn't disprove the queried record type + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the query name and proves the record type does not exist. + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns1.example"))?, + hash("x.y.w.example."), + [A, RRSIG, MX], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // NSEC3 doesn't match the query name. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("ns2.example"))?, + hash("x.y.w.example."), + [A, RRSIG], + ) + .as_ref(),], + ), + Proof::Bogus, + ); + + // NSEC3 covers the query name. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("ns1.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("example"))?, + hash("a.example."), + [A, SOA, DNSKEY, RRSIG], + ) + .as_ref(),], + ), + Proof::Bogus, + ); + + // Apex NODATA without an NSEC3 matching H(QNAME). + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[Nsec3Pair::new( + Name::from_ascii("example.")?.prepend_label(hash_with_base32("ns1.example"))?, + hash("x.y.w.example."), + [A, RRSIG], + ) + .as_ref(),], + ), + Proof::Bogus, + ); + + Ok(()) + } + + #[test] + fn nsec3_wildcard_no_data_error_tests() -> Result<(), ProtoError> { + subscribe(); + + // Based on RFC 5155 B.5 - Wildcard No Data Error + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the closest encloser + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("w.example"))?, + hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"), + [], + ) + .as_ref(), + // Covers the next-closer name (z.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns2.example"))?, + hash("*.w.example"), + [A, RRSIG], + ) + .as_ref(), + // Matches the wildcard at the closest encloser (*.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("*.w.example"))?, + hash("xx.example"), + [MX, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Secure, + ); + + // Missing an NSEC matching the closest encloser. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Covers the next-closer name (z.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns2.example"))?, + hash("*.w.example"), + [A, RRSIG], + ) + .as_ref(), + // Matches the wildcard at the closest encloser (*.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("*.w.example"))?, + hash("xx.example"), + [MX, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // No record covering the next-closer + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the closest encloser + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("w.example"))?, + hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"), + [], + ) + .as_ref(), + // Matches the wildcard at the closest encloser (*.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("*.w.example"))?, + hash("xx.example"), + [MX, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // No record matching the wildcard at the closest encloser. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[ + // Matches the closest encloser + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("w.example"))?, + hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"), + [], + ) + .as_ref(), + // Covers the next-closer name (z.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns2.example"))?, + hash("*.w.example"), + [A, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // No SOA record + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("a.z.w.example.")?, AAAA), + None, + ResponseCode::NoError, + &[ + // Matches the closest encloser + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("w.example"))?, + hash("2t7b4g4vsa5smi47k61mv5bv1a22bojr.example"), + [], + ) + .as_ref(), + // Covers the next-closer name (z.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("ns2.example"))?, + hash("*.w.example"), + [A, RRSIG], + ) + .as_ref(), + // Matches the wildcard at the closest encloser (*.w.example) + Nsec3Pair::new( + Name::from_ascii("example.")? + .prepend_label(hash_with_base32("*.w.example"))?, + hash("xx.example"), + [MX, RRSIG], + ) + .as_ref(), + ], + ), + Proof::Bogus + ); + + Ok(()) + } + + // RFC 6840 §4.1 / RFC 5155 §8.3: an ancestor-delegation NSEC3 (a parent-side + // record at a zone cut, with NS set and SOA clear, or with DNAME set) MUST + // NOT be used to prove nonexistence of any name strictly below the cut, nor + // of any type other than DS at the cut owner itself. + #[test] + fn nsec3_invalid_ancestor_delegation_nxdomain() -> Result<(), ProtoError> { + subscribe(); + + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("www.deleg.signed.")?, A), + Some(&Name::from_ascii("signed.")?), + ResponseCode::NXDomain, + &[ + // Parent-side NSEC3 at the cut: matches H(deleg.signed.) and + // also covers the next-closer H(www.deleg.signed.) since its + // interval is [H(deleg.signed.), H(*.signed.)). + Nsec3Pair::new( + Name::from_ascii("signed.")? + .prepend_label(hash_with_base32("deleg.signed"))?, + hash("*.signed."), + [NS, DS, RRSIG], + ) + .as_ref(), + // Apex NSEC3, owned by H(signed.), covers the wildcard + // H(*.deleg.signed.) since its interval is + // [H(signed.), H(ns1.signed.)). + Nsec3Pair::new( + Name::from_ascii("signed.")?.prepend_label(hash_with_base32("signed"))?, + hash("ns1.signed."), + [NS, SOA, RRSIG, NSEC3PARAM, DNSKEY], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + // NODATA at the cut owner itself: the same parent-side NSEC3 must not + // authenticate the absence of any type other than DS at `deleg.signed.`. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("deleg.signed.")?, AAAA), + Some(&Name::from_ascii("signed.")?), + ResponseCode::NoError, + &[Nsec3Pair::new( + Name::from_ascii("signed.")?.prepend_label(hash_with_base32("deleg.signed"))?, + hash("*.signed."), + [NS, DS, RRSIG], + ) + .as_ref(),], + ), + Proof::Bogus, + ); + + // RFC 5155 §8.6 exemption: the parent-side NSEC3 at the cut DOES + // authenticate the absence of DS at that owner. This is the one + // legitimate use of an ancestor-delegation NSEC3 and must continue to + // validate. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("deleg.signed.")?, DS), + Some(&Name::from_ascii("signed.")?), + ResponseCode::NoError, + &[Nsec3Pair::new( + Name::from_ascii("signed.")?.prepend_label(hash_with_base32("deleg.signed"))?, + hash("*.signed."), + [NS, RRSIG], + ) + .as_ref(),], + ), + Proof::Secure, + ); + + // RFC 6840 §4.1 also covers the case of an NSEC3 whose bitmap asserts + // a DNAME at the owner: such an NSEC3 is an ancestor delegation and + // must not prove nonexistence of names below. + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("www.deleg.signed.")?, A), + Some(&Name::from_ascii("signed.")?), + ResponseCode::NXDomain, + &[ + Nsec3Pair::new( + Name::from_ascii("signed.")? + .prepend_label(hash_with_base32("deleg.signed"))?, + hash("*.signed."), + [DNAME, RRSIG], + ) + .as_ref(), + Nsec3Pair::new( + Name::from_ascii("signed.")?.prepend_label(hash_with_base32("signed"))?, + hash("ns1.signed."), + [NS, SOA, RRSIG, NSEC3PARAM, DNSKEY], + ) + .as_ref(), + ], + ), + Proof::Bogus, + ); + + Ok(()) + } + + #[test] + fn nsec3_ds_no_data_opt_out_next_closer() -> Result<(), ProtoError> { + subscribe(); + + let [closest_encloser, next_closer] = opt_out_empty_non_terminal_nsec3s(true); + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("d.e.example.")?, DS), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[closest_encloser.as_ref(), next_closer.as_ref()], + ), + Proof::Secure, + ); + + // Not a DS query + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("d.e.example.")?, MX), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[closest_encloser.as_ref(), next_closer.as_ref()], + ), + Proof::Bogus, + ); + + // No closest encloser match + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("d.e.example.")?, DS), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[next_closer.as_ref()], + ), + Proof::Bogus, + ); + + // Next closer not covered by opt-out + let [closest_encloser, next_closer] = opt_out_empty_non_terminal_nsec3s(false); + assert_eq!( + verify_nsec3( + &Query::query(Name::from_ascii("d.e.example.")?, DS), + Some(&Name::from_ascii("example.")?), + ResponseCode::NoError, + &[closest_encloser.as_ref(), next_closer.as_ref()], + ), + Proof::Bogus, + ); + + Ok(()) + } + + #[test] + fn nsec3_insecure_delegation_opt_out_next_closer() -> Result<(), ProtoError> { + subscribe(); + + let [closest_encloser, next_closer] = opt_out_empty_non_terminal_nsec3s(true); + assert!(verify_nsec3_insecure_delegation( + &Name::from_ascii("d.e.example.")?, + &[closest_encloser.as_ref(), next_closer.as_ref()], + )); + + // Delegation covered directly + assert!(verify_nsec3_insecure_delegation( + &Name::from_ascii("e.example.")?, + &[next_closer.as_ref()], + )); + + // No closest encloser match + assert!(!verify_nsec3_insecure_delegation( + &Name::from_ascii("d.e.example.")?, + &[next_closer.as_ref()], + )); + + // NSEC3 records from different zones + let other_zone = Nsec3Pair( + Name::from_ascii("nu74sith5gkbvmv0sco6aqfocnegg16t.e.example.")?, + next_closer.1.clone(), + ); + assert!(!verify_nsec3_insecure_delegation( + &Name::from_ascii("d.e.example.")?, + &[closest_encloser.as_ref(), other_zone.as_ref()], + )); + + // Next closer not covered by opt-out + let [closest_encloser, next_closer] = opt_out_empty_non_terminal_nsec3s(false); + assert!(!verify_nsec3_insecure_delegation( + &Name::from_ascii("d.e.example.")?, + &[closest_encloser.as_ref(), next_closer.as_ref()], + )); + + Ok(()) + } + + /// Closest encloser proof for `d.e.example.`, below the empty non-terminal `e.example.`. + fn opt_out_empty_non_terminal_nsec3s(next_closer_opt_out: bool) -> [Nsec3Pair; 2] { + let nsec3 = |owner: &str, next: &str, opt_out, types: &[RecordType]| { + Nsec3Pair( + Name::from_ascii(owner).unwrap(), + NSEC3::new( + Nsec3HashAlgorithm::SHA1, + opt_out, + 12, + KNOWN_SALT.to_vec(), + BASE32_DNSSEC.decode(next.as_bytes()).unwrap(), + types.iter().copied(), + ), + ) + }; + + [ + // H(example.) + nsec3( + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3ton", + true, + &[MX, DNSKEY, NS, SOA, NSEC3PARAM, RRSIG], + ), + // Covers H(e.example.) + nsec3( + "nu74sith5gkbvmv0sco6aqfocnegg16t.example.", + "nu74sith5gkbvmv0sco6aqfocnegg16v", + next_closer_opt_out, + &[], + ), + ] + } + + #[test] + fn nsec3_covering_mutual_exclusion() { + // Mock up an NSEC3 ring with only two records. + let record_1_hash = [0x55; 20]; + let record_2_hash = [0xBB; 20]; + let record_1_label = Label::from_ascii(&BASE32_DNSSEC.encode(&record_1_hash)).unwrap(); + let record_2_label = Label::from_ascii(&BASE32_DNSSEC.encode(&record_2_hash)).unwrap(); + let record_1_rdata = NSEC3::new( + Nsec3HashAlgorithm::SHA1, + false, + 0, + Vec::new(), + record_2_hash.to_vec(), + [], + ); + let record_2_rdata = NSEC3::new( + Nsec3HashAlgorithm::SHA1, + false, + 0, + Vec::new(), + record_1_hash.to_vec(), + [], + ); + + let zone_name = Name::parse("example.", None).unwrap(); + let record_1_pair = Nsec3RecordInfo { + base32_hashed_name: record_1_label, + zone_name: zone_name.clone(), + nsec3_data: &record_1_rdata, + }; + let record_2_pair = Nsec3RecordInfo { + base32_hashed_name: record_2_label, + zone_name: zone_name.clone(), + nsec3_data: &record_2_rdata, + }; + + // Each hash should be covered by only one of the two NSEC3 records. + for query_hash in [[0x44; 20], [0x88; 20], [0xDD; 20]] { + let query_label = Label::from_ascii(&BASE32_DNSSEC.encode(&query_hash)).unwrap(); + let covered_by_1 = find_covering_record( + slice::from_ref(&record_1_pair), + &zone_name, + &query_hash, + &query_label, + ); + let covered_by_2 = find_covering_record( + slice::from_ref(&record_2_pair), + &zone_name, + &query_hash, + &query_label, + ); + let covered_by = covered_by_1 + .iter() + .chain(covered_by_2.iter()) + .map(|pair| { + ( + pair.base32_hashed_name.clone(), + pair.nsec3_data + .next_hashed_owner_name_base32() + .unwrap() + .clone(), + ) + }) + .collect::>(); + assert!( + covered_by_1.is_some() ^ covered_by_2.is_some(), + "{query_label} is covered by {covered_by:?}" + ); + } + } + + #[derive(Debug)] + struct Nsec3Pair(Name, NSEC3); + + impl Nsec3Pair { + fn new( + rr_name: Name, + next_name: Vec, + rrset: impl IntoIterator, + ) -> Self { + Self( + rr_name, + NSEC3::new( + Nsec3HashAlgorithm::SHA1, + false, + 12, + KNOWN_SALT.to_vec(), + next_name, + rrset, + ), + ) + } + + fn as_ref(&self) -> (&Name, &NSEC3) { + (&self.0, &self.1) + } + } + + fn hash(name: &str) -> Vec { + // NSEC3PARAM 1 0 12 aabbccdd + let known_name = Name::from_ascii(name).unwrap(); + Nsec3HashAlgorithm::SHA1 + .hash(KNOWN_SALT, &known_name, 12) + .unwrap() + .as_ref() + .to_vec() + } + + #[cfg(test)] + fn hash_with_base32(name: &str) -> String { + use data_encoding::BASE32_DNSSEC; + + BASE32_DNSSEC.encode(&hash(name)) + } + + const KNOWN_SALT: &[u8] = &[0xAAu8, 0xBBu8, 0xCCu8, 0xDDu8]; +} diff --git a/vendor/hickory-dns/hickory-net/src/error.rs b/vendor/hickory-dns/hickory-net/src/error.rs new file mode 100644 index 00000000..1237de26 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/error.rs @@ -0,0 +1,605 @@ +// Copyright 2015-2020 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Error types for the crate + +#![deny(missing_docs)] + +use core::num::ParseIntError; +use std::io; +use std::sync::Arc; + +#[cfg(any(feature = "__https", feature = "__h3"))] +use http::header::ToStrError; +use thiserror::Error; +use tracing::debug; + +use crate::proto::ProtoError; +#[cfg(feature = "__dnssec")] +use crate::proto::dnssec::Proof; +use crate::proto::op::{DnsResponse, Query, ResponseCode}; +use crate::proto::rr::RData; +use crate::proto::rr::{DNSClass, Name, Record, RecordRef, RecordType, rdata::SOA}; +use crate::proto::serialize::binary::DecodeError; + +/// The error type for network protocol errors (UDP, TCP, QUIC, H2, H3) +#[non_exhaustive] +#[derive(Error, Clone, Debug)] +pub enum NetError { + /// The underlying resource is too busy + /// + /// This is a signal that an internal resource is too busy. The intended action should be tried + /// again, ideally after waiting for a little while for the situation to improve. Alternatively, + /// the action could be tried on another resource (for example, in a name server pool). + #[error("resource too busy")] + Busy, + + /// Unable to decode HTTP header value to string + #[cfg(any(feature = "__https", feature = "__h3"))] + #[error("header decode error: {0}")] + Decode(Arc), + + /// Semantic DNS errors + #[error("DNS error: {0}")] + Dns(#[from] DnsError), + + /// An HTTP/2 related error + #[error("H2 error: {0}")] + #[cfg(feature = "__https")] + H2(Arc), + + /// An HTTP/3 related error + #[error("H3 error: {0}")] + #[cfg(feature = "__h3")] + H3(Arc), + + /// An error with an arbitrary message, referenced as &'static str + #[error("{0}")] + Message(&'static str), + + /// An error with an arbitrary message, stored as String + #[error("{0}")] + Msg(String), + + /// Unable to parse header value as number + #[error("unable to parse number: {0}")] + ParseInt(#[from] ParseIntError), + + /// No connections available + #[error("no connections available")] + NoConnections, + + /// Protocol error from higher layers + #[error("protocol error: {0}")] + Proto(#[from] ProtoError), + + // foreign + /// An error got returned from IO + #[error("io error: {0}")] + Io(Arc), + + /// A request was too large + #[cfg(any(feature = "__https", feature = "__h3"))] + #[error("request too large")] + RequestTooLarge, + + /// A request timed out + #[error("request timed out")] + Timeout, + + /// A Quinn (Quic) connection error occurred + #[cfg(feature = "__quic")] + #[error("error creating quic connection: {0}")] + QuinnConnect(#[from] quinn::ConnectError), + + /// A Quinn (QUIC) connection error occurred + #[cfg(feature = "__quic")] + #[error("error with quic connection: {0}")] + QuinnConnection(#[from] quinn::ConnectionError), + + /// A Quinn (QUIC) write error occurred + #[cfg(feature = "__quic")] + #[error("error writing to quic connection: {0}")] + QuinnWriteError(#[from] quinn::WriteError), + + /// A Quinn (QUIC) read error occurred + #[cfg(feature = "__quic")] + #[error("error writing to quic read: {0}")] + QuinnReadError(#[from] quinn::ReadExactError), + + /// A Quinn (QUIC) stream error occurred + #[cfg(feature = "__quic")] + #[error("referenced a closed QUIC stream: {0}")] + QuinnStreamError(#[from] quinn::ClosedStream), + + /// A Quinn (QUIC) configuration error occurred + #[cfg(feature = "__quic")] + #[error("error constructing quic configuration: {0}")] + QuinnConfigError(#[from] quinn::ConfigError), + + /// QUIC TLS config must include an AES-128-GCM cipher suite + #[cfg(feature = "__quic")] + #[error("QUIC TLS config must include an AES-128-GCM cipher suite")] + QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite), + + /// Unknown QUIC stream used + #[cfg(feature = "__quic")] + #[error("an unknown quic stream was used")] + QuinnUnknownStreamError, + + /// A quic message id should always be 0 + #[cfg(feature = "__quic")] + #[error("quic messages should always be 0, got: {0}")] + QuicMessageIdNot0(u16), + + /// A Rustls error occurred + #[cfg(feature = "__tls")] + #[error("rustls construction error: {0}")] + RustlsError(#[from] rustls::Error), + + /// Case randomization is enabled, and a server did not echo a query name back with the same + /// case. + #[error("case of query name in response did not match")] + QueryCaseMismatch, + + /// An upstream response contained a record whose class did not match the IN query class. + #[error("foreign class record in response: {record_name} {record_class} {record_type}")] + ForeignClassRecord { + /// The name of the offending record. + record_name: Name, + /// The class of the offending record. + record_class: DNSClass, + /// The type of the offending record. + record_type: RecordType, + }, + + /// Received a truncated response + #[error("response was truncated; TCP already tried or no other transport available")] + Truncated, +} + +impl NetError { + /// Returns true if the domain does not exist + #[inline] + pub fn is_nx_domain(&self) -> bool { + matches!( + self, + Self::Dns(DnsError::NoRecordsFound(NoRecords { + response_code: ResponseCode::NXDomain, + .. + })) + ) + } + + /// Returns true if the error represents NoRecordsFound + #[inline] + pub fn is_no_records_found(&self) -> bool { + matches!(self, Self::Dns(DnsError::NoRecordsFound { .. })) + } + + /// Returns true for a transport-level connection error: the connection was + /// closed, reset, or refused, as opposed to a timeout or a server-side (DNS) + /// error. + pub fn is_connection_closed(&self) -> bool { + match self { + #[cfg(feature = "__https")] + Self::H2(err) => { + err.is_io() || err.is_go_away() || err.reason() == Some(h2::Reason::REFUSED_STREAM) + } + #[cfg(feature = "__h3")] + Self::H3(err) => err.is_h3_no_error(), + Self::Io(err) => matches!( + err.kind(), + io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::BrokenPipe + | io::ErrorKind::NotConnected + | io::ErrorKind::UnexpectedEof + ), + #[cfg(feature = "__quic")] + Self::QuinnConnection(err) => matches!( + err, + quinn::ConnectionError::ConnectionClosed(_) + | quinn::ConnectionError::ApplicationClosed(_) + | quinn::ConnectionError::Reset + ), + _ => false, + } + } + + /// Returns the SOA record, if the error contains one + #[inline] + pub fn into_soa(self) -> Option>> { + match self { + Self::Dns(DnsError::NoRecordsFound(NoRecords { soa, .. })) => soa, + _ => None, + } + } + + /// Returns a representative string of the error for use as a metrics label. + pub fn as_metrics_label(&self) -> &'static str { + match self { + Self::Busy => "busy", + #[cfg(any(feature = "__https", feature = "__h3"))] + Self::Decode(_) => "http_header_decode", + Self::Dns(dns_err) => dns_err.as_metrics_label(), + #[cfg(feature = "__https")] + Self::H2(_) => "http2", + #[cfg(feature = "__h3")] + Self::H3(_) => "http3", + Self::ParseInt(_) => "parse_header_value", + Self::NoConnections => "no_connections", + Self::Proto(_) => "proto", + Self::Io(e) => { + use std::io::ErrorKind::*; + match e.kind() { + NotFound => "io_not_found", + PermissionDenied => "io_permission_denied", + ConnectionRefused => "io_connection_refused", + ConnectionReset => "io_connection_reset", + HostUnreachable => "io_host_unreachable", + NetworkUnreachable => "io_network_unreachable", + ConnectionAborted => "io_connection_aborted", + NotConnected => "io_not_connected", + AddrInUse => "io_addr_in_use", + AddrNotAvailable => "io_addr_not_available", + NetworkDown => "io_network_down", + BrokenPipe => "io_broken_pipe", + AlreadyExists => "io_already_exists", + WouldBlock => "io_would_block", + InvalidInput => "io_invalid_input", + InvalidData => "io_invalid_data", + TimedOut => "io_timed_out", + WriteZero => "io_write_zero", + QuotaExceeded => "io_quota_exceeded", + ResourceBusy => "io_resource_busy", + Deadlock => "io_deadlock", + Interrupted => "io_interrupted", + Unsupported => "io_unsupported", + OutOfMemory => "io_out_of_memory", + Other => "io_other", + + // It's highly unlikely that any of these would actually be returned. + NotADirectory => "io_not_a_directory", + IsADirectory => "io_is_a_directory", + DirectoryNotEmpty => "io_directory_not_empty", + ReadOnlyFilesystem => "io_read_only_filesystem", + StaleNetworkFileHandle => "io_stale_network_file_handle", + StorageFull => "io_storage_full", + NotSeekable => "io_not_seekable", + FileTooLarge => "io_file_too_large", + ExecutableFileBusy => "io_executable_file_busy", + CrossesDevices => "io_crosses_devices", + TooManyLinks => "io_too_many_links", + InvalidFilename => "io_invalid_filename", + ArgumentListTooLong => "io_argument_list_too_long", + UnexpectedEof => "io_unexpected_eof", + + _ => "io_unknown", + } + } + #[cfg(any(feature = "__https", feature = "__h3"))] + Self::RequestTooLarge => "request_too_large", + Self::Timeout => "timeout", + #[cfg(feature = "__quic")] + Self::QuinnConnect(_) => "quic_connect", + #[cfg(feature = "__quic")] + Self::QuinnConnection(_) => "quic_connection", + #[cfg(feature = "__quic")] + Self::QuinnWriteError(_) => "quic_write", + #[cfg(feature = "__quic")] + Self::QuinnReadError(_) => "quic_read", + #[cfg(feature = "__quic")] + Self::QuinnStreamError(_) => "quic_stream", + #[cfg(feature = "__quic")] + Self::QuinnConfigError(_) => "quic_config", + #[cfg(feature = "__quic")] + Self::QuinnTlsConfigError(_) => "quic_tls_config_error", + #[cfg(feature = "__quic")] + Self::QuinnUnknownStreamError => "quic_unknown_stream", + #[cfg(feature = "__quic")] + Self::QuicMessageIdNot0(_) => "quic_message_id_not_0", + #[cfg(feature = "__tls")] + Self::RustlsError(_) => "tls", + Self::QueryCaseMismatch => "query_case_mismatch", + Self::ForeignClassRecord { .. } => "foreign_class_record", + Self::Truncated => "truncated", + + // Don't report these because the format is arbitrary, and in the case of Msg, dynamic. + Self::Message(_) | Self::Msg(_) => "message", + } + } +} + +impl From for NetError { + fn from(no_records: NoRecords) -> Self { + Self::Dns(DnsError::NoRecordsFound(no_records)) + } +} + +impl From for NetError { + fn from(e: DecodeError) -> Self { + Self::Proto(e.into()) + } +} + +#[cfg(feature = "__h3")] +impl From for NetError { + fn from(e: h3::error::StreamError) -> Self { + Self::H3(Arc::new(e)) + } +} + +#[cfg(feature = "__https")] +impl From for NetError { + fn from(e: h2::Error) -> Self { + Self::H2(Arc::new(e)) + } +} + +#[cfg(any(feature = "__https", feature = "__h3"))] +impl From for NetError { + fn from(e: ToStrError) -> Self { + Self::Decode(Arc::new(e)) + } +} + +impl From for NetError { + fn from(e: io::Error) -> Self { + match e.kind() { + io::ErrorKind::TimedOut => Self::Timeout, + _ => Self::Io(Arc::new(e)), + } + } +} + +impl From for NetError { + fn from(msg: String) -> Self { + Self::Msg(msg) + } +} + +impl From<&'static str> for NetError { + fn from(msg: &'static str) -> Self { + Self::Message(msg) + } +} + +/// Semantic DNS errors +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum DnsError { + /// Received an error response code from the server + #[error("error response: {0}")] + ResponseCode(ResponseCode), + /// No records were found for a query + #[error("no records found for {:?}", .0.query)] + NoRecordsFound(NoRecords), + /// No Records and there is a corresponding DNSSEC Proof for NSEC + #[cfg(feature = "__dnssec")] + #[non_exhaustive] + #[error("DNSSEC Negative Record Response for {query}, {proof}")] + Nsec { + /// Query for which the NSEC was returned + query: Box, + /// Response for which the NSEC was returned + response: Box, + /// DNSSEC proof of the record + proof: Proof, + }, + /// DNSSEC validation failure + #[error("DNSSEC validation failed")] + DnssecBogus, +} + +impl DnsError { + /// A conversion to determine if the response is an error + pub fn from_response(response: DnsResponse) -> Result { + use ResponseCode::*; + debug!("response: {}", *response); + + match response.response_code { + Refused => Err(Self::ResponseCode(Refused)), + code @ ServFail + | code @ FormErr + | code @ NotImp + | code @ YXDomain + | code @ YXRRSet + | code @ NXRRSet + | code @ NotAuth + | code @ NotZone + | code @ BADVERS + | code @ BADSIG + | code @ BADKEY + | code @ BADTIME + | code @ BADMODE + | code @ BADNAME + | code @ BADALG + | code @ BADTRUNC + | code @ BADCOOKIE => Err(Self::ResponseCode(code)), + // Some NXDOMAIN responses contain CNAME referrals, that will not be an error + code @ NXDomain | + // No answers are available, CNAME referrals are not failures + code @ NoError + if !response.contains_answer() && !response.truncation => { + // TODO: if authoritative, this is cacheable, store a TTL (currently that requires time, need a "now" here) + // let valid_until = if response.authoritative() { now + response.negative_ttl() }; + let soa = response.soa().as_ref().map(RecordRef::to_owned); + + // Collect any referral nameservers and associated glue records + let mut referral_name_servers = vec![]; + for ns in response.authorities.iter().filter(|ns| ns.record_type() == RecordType::NS) { + let glue = response + .additionals + .iter() + .filter_map(|record| { + if let RData::NS(ns_data) = &ns.data { + if record.name == **ns_data && matches!(&record.data, RData::A(_) | RData::AAAA(_)) { + return Some(Record::to_owned(record)); + } + } + + None + }) + .collect::>(); + referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue: glue.into() }) + } + + let option_ns = if !referral_name_servers.is_empty() { + Some(referral_name_servers.into()) + } else { + None + }; + + let authorities = if !response.authorities.is_empty() { + Some(response.authorities.to_owned().into()) + } else { + None + }; + + let negative_ttl = response.negative_ttl(); + let query = response.into_message().queries.drain(..).next().unwrap_or_default(); + + Err(Self::NoRecordsFound(NoRecords { + query: Box::new(query), + soa: soa.map(Box::new), + ns: option_ns, + negative_ttl, + response_code: code, + authorities, + })) + } + NXDomain + | NoError + | Unknown(_) => Ok(response), + } + } + + /// Returns the DNS error as a representative string for use as a metrics label. + pub fn as_metrics_label(&self) -> &'static str { + use hickory_proto::op::ResponseCode::*; + match self { + Self::ResponseCode(NoError) => "dns_response_code_noerror", + Self::ResponseCode(FormErr) => "dns_response_code_formerror", + Self::ResponseCode(ServFail) => "dns_response_code_servfail", + Self::ResponseCode(NXDomain) => "dns_response_code_nxdomain", + Self::ResponseCode(NotImp) => "dns_response_code_notimp", + Self::ResponseCode(Refused) => "dns_response_code_refused", + Self::ResponseCode(YXDomain) => "dns_response_code_yxdomain", + Self::ResponseCode(YXRRSet) => "dns_response_code_yxrrset", + Self::ResponseCode(NXRRSet) => "dns_response_code_nxrrset", + Self::ResponseCode(NotAuth) => "dns_response_code_notauth", + Self::ResponseCode(NotZone) => "dns_response_code_notzone", + Self::ResponseCode(BADVERS) => "dns_response_code_badvers", + Self::ResponseCode(BADSIG) => "dns_response_code_badsig", + Self::ResponseCode(BADKEY) => "dns_response_code_badkey", + Self::ResponseCode(BADTIME) => "dns_response_code_badtime", + Self::ResponseCode(BADMODE) => "dns_response_code_badmode", + Self::ResponseCode(BADNAME) => "dns_response_code_badname", + Self::ResponseCode(BADALG) => "dns_response_code_badalg", + Self::ResponseCode(BADTRUNC) => "dns_response_code_badtrunc", + Self::ResponseCode(BADCOOKIE) => "dns_response_code_badcookie", + Self::ResponseCode(Unknown(_)) => "dns_response_code_unknown", + Self::NoRecordsFound(_) => "dns_no_records", + #[cfg(feature = "__dnssec")] + Self::Nsec { .. } => "dns_nsec", + Self::DnssecBogus => "dns_dnssec_bogus", + } + } +} + +/// Response where no records were found +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct NoRecords { + /// The query for which no records were found. + pub query: Box, + /// If an SOA is present, then this is an authoritative response or a referral to another nameserver, see the negative_type field. + pub soa: Option>>, + /// Nameservers may be present in addition to or in lieu of an SOA for a referral + /// The tuple struct layout is vec[(Nameserver, [vec of glue records])] + pub ns: Option>, + /// negative ttl, as determined from DnsResponse::negative_ttl + /// this will only be present if the SOA was also present. + pub negative_ttl: Option, + /// ResponseCode, if `NXDOMAIN`, the domain does not exist (and no other types). + /// If `NoError`, then the domain exists but there exist either other types at the same label, or subzones of that label. + pub response_code: ResponseCode, + /// Authority records from the query. These are important to preserve for DNSSEC validation. + pub authorities: Option>, +} + +impl NoRecords { + /// Construct a new [`NoRecords`] from a query and a response code + pub fn new(query: impl Into>, response_code: ResponseCode) -> Self { + Self { + query: query.into(), + soa: None, + ns: None, + negative_ttl: None, + response_code, + authorities: None, + } + } +} + +/// Data needed to process a NS-record-based referral. +#[derive(Clone, Debug)] +pub struct ForwardNSData { + /// The referant NS record + pub ns: Record, + /// Any glue records associated with the referant NS record. + pub glue: Arc<[Record]>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_connection_closed_io_kinds() { + for kind in [ + io::ErrorKind::ConnectionReset, + io::ErrorKind::ConnectionAborted, + io::ErrorKind::BrokenPipe, + io::ErrorKind::NotConnected, + io::ErrorKind::UnexpectedEof, + ] { + assert!( + NetError::from(io::Error::from(kind)).is_connection_closed(), + "{kind:?}" + ); + } + + assert!( + !NetError::from(io::Error::from(io::ErrorKind::PermissionDenied)) + .is_connection_closed() + ); + assert!(!NetError::Timeout.is_connection_closed()); + assert!(!NetError::Message("server error").is_connection_closed()); + } + + #[cfg(feature = "__https")] + #[test] + fn is_connection_closed_h2() { + // REFUSED_STREAM: the server never processed the request, so it's safe to retry. + assert!(NetError::from(h2::Error::from(h2::Reason::REFUSED_STREAM)).is_connection_closed()); + // A protocol error mid-exchange is not a clean connection-closed signal. + assert!( + !NetError::from(h2::Error::from(h2::Reason::INTERNAL_ERROR)).is_connection_closed() + ); + } + + #[cfg(feature = "__quic")] + #[test] + fn is_connection_closed_quic_excludes_timeout() { + assert!(NetError::QuinnConnection(quinn::ConnectionError::Reset).is_connection_closed()); + assert!( + !NetError::QuinnConnection(quinn::ConnectionError::TimedOut).is_connection_closed() + ); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/h2.rs b/vendor/hickory-dns/hickory-net/src/h2.rs new file mode 100644 index 00000000..5b9cf439 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/h2.rs @@ -0,0 +1,776 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! TLS protocol related components for DNS over HTTPS (DoH) + +use core::fmt::Debug; +use core::future::Future; +use core::net::SocketAddr; +use core::pin::Pin; +use core::str::FromStr; +use core::task::{Context, Poll}; +use core::time::Duration; +use std::io; +use std::sync::Arc; + +use bytes::{Buf, Bytes, BytesMut}; +use futures_util::stream::Stream; +use h2::client::SendRequest; +use http::header::{self, CONTENT_LENGTH}; +use http::{Method, Request}; +use rustls::ClientConfig; +use rustls::pki_types::ServerName; +use tokio::time::timeout; +use tokio_rustls::TlsConnector; +use tracing::{debug, warn}; + +use crate::error::NetError; +use crate::http::{RequestContext, SetHeaders, Version, fetch_body}; +use crate::proto::op::{DnsRequest, DnsResponse}; +use crate::runtime::iocompat::AsyncIoStdAsTokio; +use crate::runtime::{DnsTcpStream, RuntimeProvider, Spawn}; +use crate::xfer::{CONNECT_TIMEOUT, DnsExchange, DnsRequestSender, DnsResponseStream}; + +/// A DNS client connection for DNS-over-HTTPS +#[derive(Clone)] +#[must_use = "futures do nothing unless polled"] +pub struct HttpsClientStream { + context: Arc, + h2: SendRequest, + is_shutdown: bool, + request_timeout: Option, +} + +impl HttpsClientStream { + /// Constructs a new HttpsClientStreamBuilder with the associated ClientConfig + pub fn builder( + client_config: Arc, + provider: P, + ) -> HttpsClientStreamBuilder

{ + HttpsClientStreamBuilder { + provider, + client_config, + bind_addr: None, + set_headers: None, + request_timeout: None, + } + } +} + +impl DnsRequestSender for HttpsClientStream { + /// This indicates that the HTTP message was successfully sent, and we now have the response.RecvStream + /// + /// If the request fails, this will return the error, and it should be assumed that the Stream portion of + /// this will have no date. + /// + /// ```text + /// RFC 8484 DNS Queries over HTTPS (DoH) October 2018 + /// + /// + /// 4.2. The HTTP Response + /// + /// The only response type defined in this document is "application/dns- + /// message", but it is possible that other response formats will be + /// defined in the future. A DoH server MUST be able to process + /// "application/dns-message" request messages. + /// + /// Different response media types will provide more or less information + /// from a DNS response. For example, one response type might include + /// information from the DNS header bytes while another might omit it. + /// The amount and type of information that a media type gives are solely + /// up to the format, which is not defined in this protocol. + /// + /// Each DNS request-response pair is mapped to one HTTP exchange. The + /// responses may be processed and transported in any order using HTTP's + /// multi-streaming functionality (see Section 5 of [RFC7540]). + /// + /// Section 5.1 discusses the relationship between DNS and HTTP response + /// caching. + /// + /// 4.2.1. Handling DNS and HTTP Errors + /// + /// DNS response codes indicate either success or failure for the DNS + /// query. A successful HTTP response with a 2xx status code (see + /// Section 6.3 of [RFC7231]) is used for any valid DNS response, + /// regardless of the DNS response code. For example, a successful 2xx + /// HTTP status code is used even with a DNS message whose DNS response + /// code indicates failure, such as SERVFAIL or NXDOMAIN. + /// + /// HTTP responses with non-successful HTTP status codes do not contain + /// replies to the original DNS question in the HTTP request. DoH + /// clients need to use the same semantic processing of non-successful + /// HTTP status codes as other HTTP clients. This might mean that the + /// DoH client retries the query with the same DoH server, such as if + /// there are authorization failures (HTTP status code 401; see + /// Section 3.1 of [RFC7235]). It could also mean that the DoH client + /// retries with a different DoH server, such as for unsupported media + /// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or + /// where the server cannot generate a representation suitable for the + /// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so + /// on. + /// ``` + fn send_message(&mut self, mut request: DnsRequest) -> DnsResponseStream { + if self.is_shutdown { + panic!("can not send messages after stream is shutdown") + } + + // per the RFC, a zero id allows for the HTTP packet to be cached better + request.metadata.id = 0; + + let bytes = match request.to_vec() { + Ok(bytes) => bytes, + Err(err) => return NetError::from(err).into(), + }; + + let send_fut = send(self.h2.clone(), Bytes::from(bytes), self.context.clone()); + + let Some(request_timeout) = self.request_timeout else { + return Box::pin(send_fut).into(); + }; + + Box::pin(async move { + timeout(request_timeout, send_fut) + .await + .map_err(|_| NetError::Timeout)? + }) + .into() + } + + fn shutdown(&mut self) { + self.is_shutdown = true; + } + + fn is_shutdown(&self) -> bool { + self.is_shutdown + } +} + +impl Stream for HttpsClientStream { + type Item = Result<(), NetError>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self.is_shutdown { + return Poll::Ready(None); + } + + // just checking if the connection is ok + match self.h2.poll_ready(cx) { + Poll::Ready(Ok(())) => Poll::Ready(Some(Ok(()))), + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => Poll::Ready(Some(Err(NetError::from(format!( + "h2 stream errored: {e}", + ))))), + } + } +} + +/// A HTTPS connection builder for DNS-over-HTTPS +#[derive(Clone)] +pub struct HttpsClientStreamBuilder

{ + provider: P, + client_config: Arc, + bind_addr: Option, + set_headers: Option>, + request_timeout: Option, +} + +impl HttpsClientStreamBuilder

{ + /// Sets the address to connect from. + pub fn bind_addr(&mut self, bind_addr: SocketAddr) { + self.bind_addr = Some(bind_addr); + } + + /// Set the [`SetHeaders`] trait object used to inject dynamic headers into the DoH request + pub fn set_headers(&mut self, headers: Arc) { + self.set_headers.replace(headers); + } + + /// Set the per-request timeout applied to each DNS send over the H2 connection. + /// + /// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with + /// [`NetError::Timeout`] if no response arrives within this duration. + pub fn request_timeout(&mut self, timeout: Duration) { + self.request_timeout = Some(timeout); + } + + /// Creates a new [`DnsExchange`] wrapping the [`HttpsClientStream`] from this builder + pub async fn exchange( + self, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> Result, NetError> { + let mut handle = self.provider.create_handle(); + let stream = self.build(name_server, server_name, path).await?; + let (exchange, bg) = DnsExchange::from_stream(stream); + handle.spawn_bg(bg); + Ok(exchange) + } + + /// Creates a new HttpsStream to the specified name_server + /// + /// # Arguments + /// + /// * `name_server` - IP and Port for the remote DNS resolver + /// * `dns_name` - The DNS name associated with a certificate + /// * `http_endpoint` - The HTTP endpoint where the remote DNS resolver provides service, typically `/dns-query` + pub fn build( + self, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> impl Future> + Send + 'static { + let request_timeout = self.request_timeout; + let connect_fut = connect( + self.provider.connect_tcp(name_server, self.bind_addr, None), + self.client_config, + name_server, + server_name, + path, + self.set_headers, + ); + async move { + let mut stream = connect_fut.await?; + stream.request_timeout = request_timeout; + Ok(stream) + } + } +} + +/// Creates a new HttpsStream with existing connection +pub fn connect( + tcp: impl Future> + Send + 'static, + mut client_config: Arc, + name_server: SocketAddr, + server_name: Arc, + query_path: Arc, + set_headers: Option>, +) -> impl Future> + Send + 'static { + // ensure the ALPN protocol is set correctly + if client_config.alpn_protocols.is_empty() { + let mut client_cfg = (*client_config).clone(); + client_cfg.alpn_protocols = vec![ALPN_H2.to_vec()]; + + client_config = Arc::new(client_cfg); + } + + let context = Arc::new(RequestContext { + version: Version::Http2, + server_name, + query_path, + set_headers, + }); + + async move { + let tls_server_name = match ServerName::try_from(&*context.server_name) { + Ok(dns_name) => dns_name.to_owned(), + Err(err) => { + return Err(NetError::from(format!( + "bad server name {:?}: {err}", + context.server_name + ))); + } + }; + + let tcp = tcp.await?; + let future = timeout( + CONNECT_TIMEOUT, + TlsConnector::from(client_config).connect(tls_server_name, AsyncIoStdAsTokio(tcp)), + ); + + let tls = match future.await { + Ok(Ok(tls)) => tls, + Ok(Err(err)) => return Err(NetError::from(err)), + Err(_) => return Err(NetError::Timeout), + }; + + let mut handshake = h2::client::Builder::new(); + handshake.enable_push(false); + let (h2, driver) = handshake.handshake(tls).await?; + + debug!("h2 connection established to: {name_server}"); + tokio::spawn(async { + if let Err(e) = driver.await { + warn!("h2 connection failed: {e}"); + } + }); + + Ok(HttpsClientStream { + h2, + context, + is_shutdown: false, + request_timeout: None, + }) + } +} + +async fn send( + h2: SendRequest, + message: Bytes, + cx: Arc, +) -> Result { + let mut h2 = h2.ready().await?; + + // build up the http request + let request = cx.build(message.remaining())?; + + debug!("request: {:#?}", request); + + // Send the request + let (response_future, mut send_stream) = h2.send_request(request, false)?; + send_stream.send_data(message, true)?; + + let mut response_stream = response_future.await?; + + debug!("got response: {:#?}", response_stream); + + // get the length of packet + let content_length = response_stream + .headers() + .get(CONTENT_LENGTH) + .map(|v| v.to_str()) + .transpose() + .map_err(|e| NetError::from(format!("bad headers received: {e}")))? + .map(usize::from_str) + .transpose() + .map_err(|e| NetError::from(format!("bad headers received: {e}")))?; + + // read the response body + let response_bytes = fetch_body(response_stream.body_mut(), content_length).await?; + + // Was it a successful request? + if !response_stream.status().is_success() { + let error_string = String::from_utf8_lossy(response_bytes.as_ref()); + + // TODO: make explicit error type + return Err(NetError::from(format!( + "http unsuccessful code: {}, message: {}", + response_stream.status(), + error_string + ))); + } else { + // verify content type + { + // in the case that the ContentType is not specified, we assume it's the standard DNS format + let content_type = response_stream + .headers() + .get(header::CONTENT_TYPE) + .map(|h| { + h.to_str().map_err(|err| { + // TODO: make explicit error type + NetError::from(format!("ContentType header not a string: {err}")) + }) + }) + .unwrap_or(Ok(crate::http::MIME_APPLICATION_DNS))?; + + if content_type != crate::http::MIME_APPLICATION_DNS { + return Err(NetError::from(format!( + "ContentType unsupported (must be '{}'): '{}'", + crate::http::MIME_APPLICATION_DNS, + content_type + ))); + } + } + }; + + // and finally convert the bytes into a DNS message + DnsResponse::from_buffer(response_bytes.to_vec()).map_err(NetError::from) +} + +/// Given an HTTP request, return a future that will result in the next sequence of bytes. +/// +/// To allow downstream clients to do something interesting with the lifetime of the bytes, this doesn't +/// perform a conversion to a Message, only collects all the bytes. +pub async fn message_from( + this_server_name: Option>, + this_server_endpoint: Arc, + request: Request, +) -> Result +where + R: Stream> + 'static + Send + Debug + Unpin, +{ + debug!("Received request: {:#?}", request); + + let this_server_name = this_server_name.as_deref(); + match crate::http::verify( + Version::Http2, + this_server_name, + &this_server_endpoint, + &request, + ) { + Ok(_) => (), + Err(err) => return Err(err), + } + + // attempt to get the content length + let mut content_length = None; + if let Some(length) = request.headers().get(CONTENT_LENGTH) { + let length = usize::from_str(length.to_str()?)?; + debug!("got message length: {}", length); + content_length = Some(length); + } + + match *request.method() { + Method::GET => Err(format!("GET unimplemented: {}", request.method()).into()), + Method::POST => fetch_body(request.into_body(), content_length).await, + _ => Err(format!("bad method: {}", request.method()).into()), + } +} + +const ALPN_H2: &[u8] = b"h2"; + +#[cfg(test)] +mod tests { + use core::net::SocketAddr; + + use rustls::KeyLogFile; + use test_support::subscribe; + + use super::*; + use crate::proto::op::{DnsRequestOptions, Edns, Message, Query}; + use crate::proto::rr::{Name, RData, RecordType}; + use crate::runtime::TokioRuntimeProvider; + use crate::tls::client_config; + use crate::xfer::FirstAnswer; + + #[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))] + #[tokio::test] + async fn test_https_google() { + subscribe(); + + let google = SocketAddr::from(([8, 8, 8, 8], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut client_config = client_config_h2(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let provider = TokioRuntimeProvider::new(); + let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider); + let connect = https_builder.build(google, Arc::from("dns.google"), Arc::from("/dns-query")); + + let mut https = connect.await.expect("https connect failed"); + + let response = https + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = https + .send_message(request.clone()) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + #[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))] + #[tokio::test] + async fn test_https_google_with_pure_ip_address_server() { + subscribe(); + + let google = SocketAddr::from(([8, 8, 8, 8], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut client_config = client_config_h2(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let provider = TokioRuntimeProvider::new(); + let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider); + let connect = https_builder.build( + google, + Arc::from(google.ip().to_string()), + Arc::from("/dns-query"), + ); + + let mut https = connect.await.expect("https connect failed"); + + let response = https + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = https + .send_message(request.clone()) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + #[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))] + #[tokio::test] + #[ignore = "cloudflare has been unreliable as a public test service"] + async fn test_https_cloudflare() { + subscribe(); + + let cloudflare = SocketAddr::from(([1, 1, 1, 1], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let client_config = client_config_h2(); + let provider = TokioRuntimeProvider::new(); + let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider); + let connect = https_builder.build( + cloudflare, + Arc::from("cloudflare-dns.com"), + Arc::from("/dns-query"), + ); + + let mut https = connect.await.expect("https connect failed"); + + let response = https + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = https + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + fn client_config_h2() -> ClientConfig { + let mut config = client_config().unwrap(); + config.alpn_protocols = vec![ALPN_H2.to_vec()]; + config + } + + #[tokio::test] + async fn test_from_post() { + subscribe(); + let message = Message::query(); + let msg_bytes = message.to_vec().unwrap(); + let len = msg_bytes.len(); + let stream = TestBytesStream(vec![Ok(Bytes::from(msg_bytes))]); + let cx = RequestContext { + version: Version::Http2, + server_name: Arc::from("ns.example.com"), + query_path: Arc::from("/dns-query"), + set_headers: None, + }; + + let request = cx.build(len).unwrap(); + let request = request.map(|()| stream); + + let bytes = message_from( + Some(Arc::from("ns.example.com")), + "/dns-query".into(), + request, + ) + .await + .unwrap(); + + let msg_from_post = Message::from_vec(bytes.as_ref()).expect("bytes failed"); + assert_eq!(message, msg_from_post); + } + + /// Verifies that `request_timeout` cancels a stalled DNS-over-H2 request. + /// + /// Uses an in-process `tokio::io::duplex` pipe — no TLS or external servers needed. + /// The server half accepts the H2 request but never sends a response, simulating + /// a hung upstream DoH server. The test asserts that `send_message` returns + /// `Err(NetError::Timeout)` within the configured timeout. + /// + /// This test also serves as the representative coverage for the identical + /// `request_timeout` logic in `QuicClientStream` and `H3ClientStream`: all three + /// apply the same `tokio::time::timeout` wrapper in `send_message`. In-process + /// QUIC/H3 tests are impractical because QUIC mandates TLS with no plain-transport + /// equivalent of the duplex trick used here. + #[tokio::test] + async fn test_request_timeout() { + use std::str::FromStr; + + use futures_util::StreamExt as _; + + subscribe(); + + let (client_io, server_io) = tokio::io::duplex(64 * 1024); + + // Both H2 handshakes must run concurrently to exchange SETTINGS frames. + let (client_result, server_result) = tokio::join!( + h2::client::Builder::new() + .enable_push(false) + .handshake(client_io), + h2::server::handshake(server_io), + ); + let (h2_send, h2_conn) = client_result.expect("client h2 handshake"); + let mut server_conn = server_result.expect("server h2 handshake"); + + tokio::spawn(async move { + let _ = h2_conn.await; + }); + + // Accept the incoming request but hold the `SendResponse` without calling + // `send_response` — the client's `response_future.await` will block forever. + tokio::spawn(async move { + if let Some(Ok((_req, respond))) = server_conn.next().await { + tokio::time::sleep(Duration::from_secs(60)).await; + drop(respond); + } + }); + + let context = Arc::new(RequestContext { + version: Version::Http2, + server_name: Arc::from("test.example"), + query_path: Arc::from("/dns-query"), + set_headers: None, + }); + + let mut stream = HttpsClientStream { + h2: h2_send, + context, + is_shutdown: false, + request_timeout: Some(Duration::from_millis(50)), + }; + + let mut msg = Message::query(); + msg.add_query(Query::query( + Name::from_str("example.com.").unwrap(), + RecordType::A, + )); + let request = DnsRequest::new(msg, DnsRequestOptions::default()); + + let result = stream.send_message(request).first_answer().await; + assert!( + matches!(result, Err(NetError::Timeout)), + "expected Timeout, got: {result:?}" + ); + } + + #[derive(Debug)] + struct TestBytesStream(Vec>); + + impl Stream for TestBytesStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + match self.0.pop() { + Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes))), + Some(Err(err)) => Poll::Ready(Some(Err(err))), + None => Poll::Ready(None), + } + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/h3/h3_client_stream.rs b/vendor/hickory-dns/hickory-net/src/h3/h3_client_stream.rs new file mode 100644 index 00000000..161494a6 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/h3/h3_client_stream.rs @@ -0,0 +1,726 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::fmt::{self, Display}; +use core::future::{Future, poll_fn}; +use core::net::SocketAddr; +use core::pin::Pin; +use core::str::FromStr; +use core::task::{Context, Poll}; +use core::time::Duration; +use std::sync::Arc; + +use bytes::{Buf, Bytes}; +use futures_util::stream::Stream; +use h3::client::SendRequest; +use h3_quinn::OpenStreams; +use http::header::{self, CONTENT_LENGTH}; +use quinn::{Endpoint, EndpointConfig, TransportConfig}; +use tokio::sync::mpsc; +use tokio::time::timeout; +use tracing::{debug, warn}; + +use super::{ALPN_H3, BodyStream}; +use crate::error::NetError; +use crate::http::{RequestContext, SetHeaders, Version, fetch_body}; +use crate::proto::ProtoError; +use crate::proto::op::{DnsRequest, DnsResponse}; +use crate::quic::connect_quic; +use crate::runtime::{RuntimeProvider, Spawn}; +use crate::tls::client_config; +use crate::udp::UdpSocket; +use crate::xfer::{DnsExchange, DnsRequestSender, DnsResponseStream}; + +/// A DNS client connection for DNS-over-HTTP/3 +#[derive(Clone)] +#[must_use = "futures do nothing unless polled"] +pub struct H3ClientStream { + // Corresponds to the dns-name of the HTTP/3 server + name_server: SocketAddr, + send_request: SendRequest, + context: Arc, + shutdown_tx: mpsc::Sender<()>, + is_shutdown: bool, + request_timeout: Option, +} + +impl H3ClientStream { + /// Builder for H3ClientStream + pub fn builder() -> H3ClientStreamBuilder { + H3ClientStreamBuilder { + crypto_config: None, + transport_config: Arc::new(super::transport()), + bind_addr: None, + set_headers: None, + disable_grease: false, + request_timeout: None, + } + } + + async fn inner_send( + mut h3: SendRequest, + message: Bytes, + cx: Arc, + ) -> Result { + // build up the http request + let request = cx.build(message.remaining())?; + debug!("request: {:#?}", request); + + // Send the request + let mut stream = h3.send_request(request).await?; + stream.send_data(message).await?; + stream.finish().await?; + + let response = stream.recv_response().await?; + debug!("got response: {:#?}", response); + + // get the length of packet + let content_length = response + .headers() + .get(CONTENT_LENGTH) + .map(|v| v.to_str()) + .transpose() + .map_err(|e| NetError::from(format!("bad headers received: {e}")))? + .map(usize::from_str) + .transpose() + .map_err(|e| NetError::from(format!("bad headers received: {e}")))?; + + // Was it a successful request? + let response_bytes = fetch_body( + BodyStream::from(|cx: &mut Context<'_>| stream.poll_recv_data(cx)), + content_length, + ) + .await?; + + if !response.status().is_success() { + let error_string = String::from_utf8_lossy(response_bytes.as_ref()); + + // TODO: make explicit error type + return Err(NetError::from(format!( + "http unsuccessful code: {}, message: {}", + response.status(), + error_string + ))); + } + + // in the case that the ContentType is not specified, we assume it's the standard DNS format + let content_type = response + .headers() + .get(header::CONTENT_TYPE) + .map(|h| { + h.to_str().map_err(|err| { + // TODO: make explicit error type + NetError::from(format!("ContentType header not a string: {err}")) + }) + }) + .unwrap_or(Ok(crate::http::MIME_APPLICATION_DNS))?; + + if content_type != crate::http::MIME_APPLICATION_DNS { + return Err(NetError::from(format!( + "ContentType unsupported (must be '{}'): '{}'", + crate::http::MIME_APPLICATION_DNS, + content_type + ))); + } + + // and finally convert the bytes into a DNS message + DnsResponse::from_buffer(response_bytes.to_vec()).map_err(NetError::from) + } +} + +impl DnsRequestSender for H3ClientStream { + /// This indicates that the HTTP message was successfully sent, and we now have the response.RecvStream + /// + /// If the request fails, this will return the error, and it should be assumed that the Stream portion of + /// this will have no date. + /// + /// ```text + /// RFC 8484 DNS Queries over HTTPS (DoH) October 2018 + /// + /// + /// 4.2. The HTTP Response + /// + /// The only response type defined in this document is "application/dns- + /// message", but it is possible that other response formats will be + /// defined in the future. A DoH server MUST be able to process + /// "application/dns-message" request messages. + /// + /// Different response media types will provide more or less information + /// from a DNS response. For example, one response type might include + /// information from the DNS header bytes while another might omit it. + /// The amount and type of information that a media type gives are solely + /// up to the format, which is not defined in this protocol. + /// + /// Each DNS request-response pair is mapped to one HTTP exchange. The + /// responses may be processed and transported in any order using HTTP's + /// multi-streaming functionality (see Section 5 of [RFC7540]). + /// + /// Section 5.1 discusses the relationship between DNS and HTTP response + /// caching. + /// + /// 4.2.1. Handling DNS and HTTP Errors + /// + /// DNS response codes indicate either success or failure for the DNS + /// query. A successful HTTP response with a 2xx status code (see + /// Section 6.3 of [RFC7231]) is used for any valid DNS response, + /// regardless of the DNS response code. For example, a successful 2xx + /// HTTP status code is used even with a DNS message whose DNS response + /// code indicates failure, such as SERVFAIL or NXDOMAIN. + /// + /// HTTP responses with non-successful HTTP status codes do not contain + /// replies to the original DNS question in the HTTP request. DoH + /// clients need to use the same semantic processing of non-successful + /// HTTP status codes as other HTTP clients. This might mean that the + /// DoH client retries the query with the same DoH server, such as if + /// there are authorization failures (HTTP status code 401; see + /// Section 3.1 of [RFC7235]). It could also mean that the DoH client + /// retries with a different DoH server, such as for unsupported media + /// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or + /// where the server cannot generate a representation suitable for the + /// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so + /// on. + /// ``` + fn send_message(&mut self, mut request: DnsRequest) -> DnsResponseStream { + if self.is_shutdown { + panic!("can not send messages after stream is shutdown") + } + + // per the RFC, a zero id allows for the HTTP packet to be cached better + request.metadata.id = 0; + + let bytes = match request.to_vec() { + Ok(bytes) => bytes, + Err(err) => return NetError::from(err).into(), + }; + + let send_fut = Self::inner_send( + self.send_request.clone(), + Bytes::from(bytes), + self.context.clone(), + ); + + let Some(request_timeout) = self.request_timeout else { + return Box::pin(send_fut).into(); + }; + + Box::pin(async move { + timeout(request_timeout, send_fut) + .await + .map_err(|_| NetError::Timeout)? + }) + .into() + } + + fn shutdown(&mut self) { + self.is_shutdown = true; + } + + fn is_shutdown(&self) -> bool { + self.is_shutdown + } +} + +impl Stream for H3ClientStream { + type Item = Result<(), NetError>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + if self.is_shutdown { + return Poll::Ready(None); + } + + // just checking if the connection is ok + if self.shutdown_tx.is_closed() { + return Poll::Ready(Some(Err(NetError::from( + "h3 connection is already shutdown", + )))); + } + + Poll::Ready(Some(Ok(()))) + } +} + +impl Display for H3ClientStream { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + formatter, + "H3({},{})", + self.name_server, self.context.server_name + ) + } +} + +/// A H3 connection builder for DNS-over-HTTP/3 +#[derive(Clone)] +pub struct H3ClientStreamBuilder { + crypto_config: Option, + transport_config: Arc, + bind_addr: Option, + set_headers: Option>, + disable_grease: bool, + request_timeout: Option, +} + +impl H3ClientStreamBuilder { + /// Constructs a new H3ClientStreamBuilder with the associated ClientConfig + pub fn crypto_config(mut self, crypto_config: rustls::ClientConfig) -> Self { + self.crypto_config = Some(crypto_config); + self + } + + /// Sets the address to connect from. + pub fn bind_addr(mut self, bind_addr: SocketAddr) -> Self { + self.bind_addr = Some(bind_addr); + self + } + + /// Set the [`SetHeaders`] trait object used to inject dynamic headers into the DoH request + pub fn set_headers(&mut self, headers: Arc) { + self.set_headers.replace(headers); + } + + /// Sets whether to disable GREASE + pub fn disable_grease(mut self, disable_grease: bool) -> Self { + self.disable_grease = disable_grease; + self + } + + /// Set the per-request timeout applied to each DNS send over the H3 connection. + /// + /// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with + /// [`NetError::Timeout`] if no response arrives within this duration. + pub fn request_timeout(mut self, timeout: Duration) -> Self { + self.request_timeout = Some(timeout); + self + } + + /// Creates a new H3Stream to the specified name_server + /// + /// # Arguments + /// + /// * `name_server` - IP and Port for the remote DNS resolver + /// * `server_name` - The DNS name associated with a certificate + pub fn build( + self, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> impl Future> + Send + 'static { + self.connect(name_server, server_name, path) + } + + /// Creates a new [`DnsExchange`] wrapping the [`H3ClientStream`] from this builder + pub async fn exchange( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + provider: P, + ) -> Result, NetError> { + let stream = self + .connect_with_future(socket, name_server, server_name, path) + .await?; + let (exchange, bg) = DnsExchange::from_stream(stream); + provider.create_handle().spawn_bg(bg); + Ok(exchange) + } + + /// Creates a new H3Stream with existing connection + pub fn build_with_future( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> impl Future> + Send + 'static { + self.connect_with_future(socket, name_server, server_name, path) + } + + async fn connect_with_future( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> Result { + let endpoint = Endpoint::new_with_abstract_socket( + EndpointConfig::default(), + None, + socket, + Arc::new(quinn::TokioRuntime), + )?; + self.connect_inner(endpoint, name_server, server_name, path) + .await + } + + async fn connect( + self, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> Result { + let connect = if let Some(bind_addr) = self.bind_addr { + ::connect_with_bind(name_server, bind_addr) + } else { + ::connect(name_server) + }; + + let socket = connect.await?; + let socket = socket.into_std()?; + let endpoint = Endpoint::new( + EndpointConfig::default(), + None, + socket, + Arc::new(quinn::TokioRuntime), + )?; + self.connect_inner(endpoint, name_server, server_name, path) + .await + } + + async fn connect_inner( + self, + endpoint: Endpoint, + name_server: SocketAddr, + server_name: Arc, + path: Arc, + ) -> Result { + let quic_connection = connect_quic( + name_server, + server_name.clone(), + ALPN_H3, + match self.crypto_config { + Some(crypto_config) => crypto_config, + None => client_config()?, + }, + self.transport_config, + endpoint, + ) + .await?; + + let h3_connection = h3_quinn::Connection::new(quic_connection); + let (mut driver, send_request) = h3::client::builder() + .send_grease(!self.disable_grease) + .build(h3_connection) + .await + .map_err(|e| ProtoError::from(format!("h3 connection failed: {e}")))?; + + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); + + // TODO: hand this back for others to run rather than spawning here? + debug!("h3 connection is ready: {}", name_server); + tokio::spawn(async move { + tokio::select! { + error = poll_fn(|cx| driver.poll_close(cx)) => { + // `poll_close()` strangely unconditionally returns a `ConnectionError` + if !error.is_h3_no_error() { + warn!(%error, "h3 connection failed to close") + } + } + _ = shutdown_rx.recv() => { + debug!("h3 connection is shutting down: {}", name_server); + } + } + }); + + Ok(H3ClientStream { + name_server, + send_request, + context: Arc::new(RequestContext { + version: Version::Http3, + server_name, + query_path: path, + set_headers: self.set_headers, + }), + shutdown_tx, + is_shutdown: false, + request_timeout: self.request_timeout, + }) + } +} + +#[cfg(all( + test, + any(feature = "rustls-platform-verifier", feature = "webpki-roots") +))] +mod tests { + use core::net::SocketAddr; + use core::str::FromStr; + use std::println; + + use rustls::KeyLogFile; + use test_support::subscribe; + use tokio::task::JoinSet; + + use super::*; + use crate::proto::op::{DnsRequestOptions, Edns, Message, Query}; + use crate::proto::rr::{Name, RData, RecordType}; + use crate::xfer::FirstAnswer; + + #[tokio::test] + async fn test_h3_google() { + subscribe(); + + let google = SocketAddr::from(([8, 8, 8, 8], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut client_config = client_config().unwrap(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let mut h3 = H3ClientStream::builder() + .crypto_config(client_config) + .build(google, Arc::from("dns.google"), Arc::from("/dns-query")) + .await + .expect("h3 connect failed"); + + let response = h3 + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = h3 + .send_message(request.clone()) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + #[tokio::test] + async fn test_h3_google_with_pure_ip_address_server() { + subscribe(); + + let google = SocketAddr::from(([8, 8, 8, 8], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut client_config = client_config().unwrap(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let mut h3 = H3ClientStream::builder() + .crypto_config(client_config) + .build( + google, + Arc::from(google.ip().to_string()), + Arc::from("/dns-query"), + ) + .await + .expect("h3 connect failed"); + + let response = h3 + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = h3 + .send_message(request.clone()) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + #[tokio::test] + async fn test_h3_cloudflare() { + subscribe(); + + let cloudflare = SocketAddr::from(([1, 1, 1, 1], 443)); + let mut request = Message::query(); + let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut client_config = client_config().unwrap(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let mut h3 = H3ClientStream::builder() + .crypto_config(client_config) + // Currently CF is using a broken GREASE implementation, see . + .disable_grease(true) + .build( + cloudflare, + Arc::from("cloudflare-dns.com"), + Arc::from("/dns-query"), + ) + .await + .expect("h3 connect failed"); + + let response = h3 + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::A(_))) + ); + + // + // assert that the connection works for a second query + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + request.metadata.recursion_desired = true; + let mut edns = Edns::new(); + edns.set_version(0); + edns.set_max_payload(1232); + request.edns = Some(edns); + + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let response = h3 + .send_message(request) + .first_answer() + .await + .expect("send_message failed"); + + assert!( + response + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(_))) + ); + } + + #[tokio::test] + #[allow(clippy::print_stdout)] + async fn test_h3_client_stream_clonable() { + subscribe(); + + // use google + let google = SocketAddr::from(([8, 8, 8, 8], 443)); + + let mut client_config = client_config().unwrap(); + client_config.key_log = Arc::new(KeyLogFile::new()); + + let h3 = H3ClientStream::builder() + .crypto_config(client_config) + .build(google, Arc::from("dns.google"), Arc::from("/dns-query")) + .await + .expect("h3 connect failed"); + + // prepare request + let mut request = Message::query(); + let query = Query::query( + Name::from_str("www.example.com.").unwrap(), + RecordType::AAAA, + ); + request.add_query(query); + let request = DnsRequest::new(request, DnsRequestOptions::default()); + + let mut join_set = JoinSet::new(); + + for i in 0..50 { + let mut h3 = h3.clone(); + let request = request.clone(); + + join_set.spawn(async move { + let start = std::time::Instant::now(); + h3.send_message(request) + .first_answer() + .await + .expect("send_message failed"); + println!("request[{i}] completed: {:?}", start.elapsed()); + }); + } + + let total = join_set.len(); + let mut idx = 0usize; + while join_set.join_next().await.is_some() { + println!("join_set completed {idx}/{total}"); + idx += 1; + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/h3/h3_server.rs b/vendor/hickory-dns/hickory-net/src/h3/h3_server.rs new file mode 100644 index 00000000..f4fa60d0 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/h3/h3_server.rs @@ -0,0 +1,147 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! HTTP/3 related server items + +use core::net::SocketAddr; +use std::io; +use std::sync::Arc; +use std::time::Duration; + +use bytes::Bytes; +use h3::server::{Connection, RequestStream}; +use h3_quinn::{BidiStream, Endpoint}; +use http::Request; +use quinn::crypto::rustls::QuicServerConfig; +use quinn::{EndpointConfig, ServerConfig}; +use rustls::server::ResolvesServerCert; +use rustls::server::ServerConfig as TlsServerConfig; +use rustls::version::TLS13; +use tokio::time::timeout; + +use crate::{error::NetError, tls::default_provider, udp::UdpSocket}; + +use super::ALPN_H3; + +/// A DNS-over-HTTP/3 Server, see H3ClientStream for the client counterpart +pub struct H3Server { + endpoint: Endpoint, +} + +impl H3Server { + /// Construct the new Acceptor with the associated pkcs12 data + pub async fn new( + name_server: SocketAddr, + server_cert_resolver: Arc, + ) -> Result { + // setup a new socket for the server to use + let socket = ::bind(name_server).await?; + Self::with_socket(socket, server_cert_resolver) + } + + /// Construct the new server with an existing socket and default TLS config. + pub fn with_socket( + socket: tokio::net::UdpSocket, + server_cert_resolver: Arc, + ) -> Result { + let mut config = TlsServerConfig::builder_with_provider(Arc::new(default_provider())) + .with_protocol_versions(&[&TLS13]) + .expect("TLS1.3 not supported") + .with_no_client_auth() + .with_cert_resolver(server_cert_resolver); + + config.alpn_protocols = vec![ALPN_H3.to_vec()]; + + Self::with_socket_and_tls_config(socket, Arc::new(config)) + } + + /// Construct the new server with an existing socket and custom TLS config. + /// + /// The TLS configuration should support TLS 1.3 and have the H3 ALPN protocol enabled. + pub fn with_socket_and_tls_config( + socket: tokio::net::UdpSocket, + tls_config: Arc, + ) -> Result { + let mut server_config = + ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(tls_config).unwrap())); + server_config.transport = Arc::new(super::transport()); + + let socket = socket.into_std()?; + + let endpoint = Endpoint::new( + EndpointConfig::default(), + Some(server_config), + socket, + Arc::new(quinn::TokioRuntime), + )?; + + Ok(Self { endpoint }) + } + + /// Accept the next incoming connection. + /// + /// # Returns + /// + /// A remote connection that could accept many potential requests and the remote socket address + pub async fn accept(&mut self) -> Result, NetError> { + let Some(connecting) = self.endpoint.accept().await else { + return Ok(None); + }; + + let remote_addr = connecting.remote_address(); + let future = async { + let connection = connecting.await?; + let connection = Connection::new(h3_quinn::Connection::new(connection)) + .await + .map_err(|e| NetError::from(format!("h3 connection failed: {e}")))?; + Ok::<_, NetError>(H3Connection { connection }) + }; + let Ok(result) = timeout(Duration::from_secs(5), future).await else { + return Err(NetError::from("h3 timeout expired during handshake")); + }; + Ok(Some((result?, remote_addr))) + } + + /// Returns the address this server is listening on + /// + /// This can be useful in tests, where a random port can be associated with the server by binding on `127.0.0.1:0` and then getting the + /// associated port address with this function. + pub fn local_addr(&self) -> Result { + self.endpoint.local_addr() + } +} + +/// A HTTP/3 connection. +pub struct H3Connection { + connection: Connection, +} + +impl H3Connection { + /// Accept the next request from the client + pub async fn accept( + &mut self, + ) -> Option, RequestStream, Bytes>), NetError>> { + match self.connection.accept().await { + Ok(Some(resolver)) => match resolver.resolve_request().await { + Ok((request, stream)) => Some(Ok((request, stream))), + Err(e) => Some(Err(NetError::from(format!( + "h3 request resolution failed: {e}" + )))), + }, + Ok(None) => None, + Err(e) => Some(Err(NetError::from(format!("h3 request failed: {e}")))), + } + } + + /// Shutdown the connection. + pub async fn shutdown(&mut self) -> Result<(), NetError> { + self.connection + .shutdown(0) + .await + .map_err(|e| NetError::from(format!("h3 connection shutdown failed: {e}"))) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/h3/mod.rs b/vendor/hickory-dns/hickory-net/src/h3/mod.rs new file mode 100644 index 00000000..4e34372b --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/h3/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! TLS protocol related components for DNS over HTTP/3 (DoH3) + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Buf; +use futures_util::Stream; +use quinn::{TransportConfig, VarInt}; + +use crate::NetError; + +mod h3_client_stream; +pub use h3_client_stream::{H3ClientStream, H3ClientStreamBuilder}; +pub mod h3_server; + +const ALPN_H3: &[u8] = b"h3"; + +/// Returns a default endpoint configuration for DNS-over-QUIC +fn transport() -> TransportConfig { + let mut transport_config = TransportConfig::default(); + + transport_config.datagram_receive_buffer_size(None); + transport_config.datagram_send_buffer_size(0); + // clients never accept new bidirectional streams + transport_config.max_concurrent_bidi_streams(VarInt::from_u32(3)); + // - SETTINGS + // - QPACK encoder + // - QPACK decoder + // - RESERVED (GREASE) + transport_config.max_concurrent_uni_streams(VarInt::from_u32(4)); + + transport_config +} + +/// [`Stream`] adapter for h3 body streaming. +pub struct BodyStream(T); + +impl Stream for BodyStream +where + T: FnMut(&mut Context<'_>) -> Poll, h3::error::StreamError>> + Unpin, + B: Buf, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let Poll::Ready(result) = (this.0)(cx) else { + return Poll::Pending; + }; + + Poll::Ready(match result { + Ok(Some(buf)) => Some(Ok(buf)), + Ok(None) => None, + Err(e) => Some(Err(NetError::from(format!( + "h3 stream receive data failed: {e}" + )))), + }) + } +} + +impl From for BodyStream +where + T: FnMut(&mut Context<'_>) -> Poll, h3::error::StreamError>> + Unpin, + B: Buf, +{ + fn from(stream: T) -> Self { + Self(stream) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/http.rs b/vendor/hickory-dns/hickory-net/src/http.rs new file mode 100644 index 00000000..d51e15e3 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/http.rs @@ -0,0 +1,412 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! HTTP protocol related components for DNS over HTTP/2 (DoH) and HTTP/3 (DoH3) + +use core::str::FromStr; +use std::sync::Arc; + +use bytes::{Buf, BufMut, BytesMut}; +use futures_util::{Stream, StreamExt}; +use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; +use http::{HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header, uri}; +use tracing::debug; + +use crate::error::NetError; + +pub(crate) struct RequestContext { + pub(crate) version: Version, + pub(crate) server_name: Arc, + pub(crate) query_path: Arc, + pub(crate) set_headers: Option>, +} + +impl RequestContext { + /// Create a new Request for an http dns-message request + /// + /// ```text + /// RFC 8484 DNS Queries over HTTPS (DoH) October 2018 + /// + /// The URI Template defined in this document is processed without any + /// variables when the HTTP method is POST. When the HTTP method is GET, + /// the single variable "dns" is defined as the content of the DNS + /// request (as described in Section 6), encoded with base64url + /// [RFC4648]. + /// ``` + pub(crate) fn build(&self, message_len: usize) -> Result, NetError> { + let mut parts = uri::Parts::default(); + parts.path_and_query = Some( + uri::PathAndQuery::try_from(&*self.query_path) + .map_err(|e| NetError::from(format!("invalid DoH path: {e}")))?, + ); + parts.scheme = Some(uri::Scheme::HTTPS); + parts.authority = Some( + uri::Authority::from_str(&self.server_name) + .map_err(|e| NetError::from(format!("invalid authority: {e}")))?, + ); + + let url = + Uri::from_parts(parts).map_err(|e| NetError::from(format!("uri parse error: {e}")))?; + + // TODO: add user agent to TypedHeaders + let mut request = Request::builder() + .method("POST") + .uri(url) + .version(self.version.to_http()) + .header(CONTENT_TYPE, MIME_APPLICATION_DNS) + .header(ACCEPT, MIME_APPLICATION_DNS) + .header(CONTENT_LENGTH, message_len); + + if let Some(headers) = &self.set_headers { + if let Some(map) = request.headers_mut() { + headers.set_headers(map)?; + } + } + + request + .body(()) + .map_err(|e| NetError::from(format!("http stream errored: {e}"))) + } +} + +/// Verifies the request is well-formed for the name-server and supported protocols +pub fn verify( + version: Version, + name_server: Option<&str>, + query_path: &str, + request: &Request, +) -> Result<(), NetError> { + // Verify all HTTP parameters + let uri = request.uri(); + + // validate path + if uri.path() != query_path { + return Err(format!("bad path: {}, expected: {}", uri.path(), query_path).into()); + } + + // we only accept HTTPS + if Some(&uri::Scheme::HTTPS) != uri.scheme() { + return Err("must be HTTPS scheme".into()); + } + + // the authority must match our nameserver name + if let Some(name_server) = name_server { + if let Some(authority) = uri.authority() { + if authority.host() != name_server { + return Err("incorrect authority".into()); + } + } else { + return Err("no authority in HTTPS request".into()); + } + } + + // TODO: switch to mime::APPLICATION_DNS when that stabilizes + match request.headers().get(CONTENT_TYPE).map(|v| v.to_str()) { + Some(Ok(ctype)) if ctype == MIME_APPLICATION_DNS => {} + _ => return Err("unsupported content type".into()), + }; + + // TODO: switch to mime::APPLICATION_DNS when that stabilizes + match request.headers().get(ACCEPT).map(|v| v.to_str()) { + Some(Ok(ctype)) => { + let mut found = false; + for mime_and_quality in ctype.split(',') { + let mut parts = mime_and_quality.splitn(2, ';'); + match parts.next() { + Some(mime) if mime.trim() == MIME_APPLICATION_DNS => { + found = true; + break; + } + Some(mime) if mime.trim() == "application/*" => { + found = true; + break; + } + _ => continue, + } + } + + if !found { + return Err("does not accept content type".into()); + } + } + Some(Err(e)) => return Err(e.into()), + None => return Err("Accept is unspecified".into()), + }; + + if request.version() != version.to_http() { + let message = match version { + #[cfg(feature = "__https")] + Version::Http2 => "only HTTP/2 supported", + #[cfg(feature = "__h3")] + Version::Http3 => "only HTTP/3 supported", + }; + return Err(message.into()); + } + + debug!( + "verified request from: {}", + request + .headers() + .get(header::USER_AGENT) + .map(|h| h.to_str().unwrap_or("bad user agent")) + .unwrap_or("unknown user agent") + ); + + Ok(()) +} + +/// Fetch the body of the request from the stream +pub async fn fetch_body>( + mut stream: impl Stream> + Unpin, + length: Option, +) -> Result { + let mut bytes = BytesMut::with_capacity(512); + loop { + match stream.next().await { + Some(Ok(frame)) => match bytes.len() + frame.remaining() > MAX_REQUEST_SIZE { + true => return Err(NetError::RequestTooLarge), + false => bytes.put(frame), + }, + Some(Err(err)) => return Err(err.into()), + None => match length { + Some(length) if bytes.len() == length => return Ok(bytes), + Some(_) => return Err("body size does not match expected content-length".into()), + None => return Ok(bytes), + }, + }; + } +} + +const MAX_REQUEST_SIZE: usize = u16::MAX as usize; + +/// Create a new Response for an http dns-message request +/// +/// ```text +/// RFC 8484 DNS Queries over HTTPS (DoH) October 2018 +/// +/// 4.2.1. Handling DNS and HTTP Errors +/// +/// DNS response codes indicate either success or failure for the DNS +/// query. A successful HTTP response with a 2xx status code (see +/// Section 6.3 of [RFC7231]) is used for any valid DNS response, +/// regardless of the DNS response code. For example, a successful 2xx +/// HTTP status code is used even with a DNS message whose DNS response +/// code indicates failure, such as SERVFAIL or NXDOMAIN. +/// +/// HTTP responses with non-successful HTTP status codes do not contain +/// replies to the original DNS question in the HTTP request. DoH +/// clients need to use the same semantic processing of non-successful +/// HTTP status codes as other HTTP clients. This might mean that the +/// DoH client retries the query with the same DoH server, such as if +/// there are authorization failures (HTTP status code 401; see +/// Section 3.1 of [RFC7235]). It could also mean that the DoH client +/// retries with a different DoH server, such as for unsupported media +/// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or +/// where the server cannot generate a representation suitable for the +/// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so +/// on. +/// ``` +pub fn response(version: Version, message_len: usize) -> Result, NetError> { + Response::builder() + .status(StatusCode::OK) + .version(version.to_http()) + .header(CONTENT_TYPE, MIME_APPLICATION_DNS) + .header(CONTENT_LENGTH, message_len) + .body(()) + .map_err(|e| NetError::from(format!("invalid response: {e}"))) +} + +/// Represents a version of the HTTP spec. +#[derive(Clone, Copy, Debug)] +pub enum Version { + /// HTTP/2 for DoH. + #[cfg(feature = "__https")] + Http2, + /// HTTP/3 for DoH3. + #[cfg(feature = "__h3")] + Http3, +} + +impl Version { + fn to_http(self) -> http::Version { + match self { + #[cfg(feature = "__https")] + Self::Http2 => http::Version::HTTP_2, + #[cfg(feature = "__h3")] + Self::Http3 => http::Version::HTTP_3, + } + } +} + +/// Helper trait to update HTTP headers on requests +/// +/// For instance a DoH server may require authentication based +/// on per-request HTTP headers and this trait allows their addition. +pub trait SetHeaders: Send + Sync + 'static { + /// Get a set of headers to add to the query + fn set_headers(&self, headers: &mut HeaderMap) -> Result<(), NetError>; +} + +pub(crate) const MIME_APPLICATION_DNS: &str = "application/dns-message"; + +/// The default query path for DNS-over-HTTPS if none was given. +pub const DEFAULT_DNS_QUERY_PATH: &str = "/dns-query"; + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use futures_util::stream; + use http::{ + HeaderMap, + header::{HeaderName, HeaderValue}, + }; + + use super::*; + + #[test] + #[cfg(feature = "__https")] + fn test_new_verify_h2() { + let cx = RequestContext { + version: Version::Http2, + server_name: Arc::from("ns.example.com"), + query_path: Arc::from("/dns-query"), + set_headers: None, + }; + + let request = cx.build(512).expect("error converting to http"); + assert!( + verify( + Version::Http2, + Some("ns.example.com"), + "/dns-query", + &request + ) + .is_ok() + ); + } + + #[test] + #[cfg(feature = "__https")] + fn test_additional_headers() { + let cx = RequestContext { + version: Version::Http2, + server_name: Arc::from("ns.example.com"), + query_path: Arc::from("/dns-query"), + set_headers: Some(Arc::new(vec![( + HeaderName::from_static("test-header"), + HeaderValue::from_static("test-header-value"), + )]) as Arc), + }; + + let request = cx.build(512).expect("error converting to http"); + assert!( + verify( + Version::Http2, + Some("ns.example.com"), + "/dns-query", + &request + ) + .is_ok() + ); + + assert_eq!( + request + .headers() + .get(HeaderName::from_static("test-header")) + .expect("header to be set"), + HeaderValue::from_static("test-header-value") + ) + } + + #[test] + #[cfg(feature = "__h3")] + fn test_new_verify_h3() { + let cx = RequestContext { + version: Version::Http3, + server_name: Arc::from("ns.example.com"), + query_path: Arc::from("/dns-query"), + set_headers: None, + }; + + let request = cx.build(512).expect("error converting to http"); + assert!( + verify( + Version::Http3, + Some("ns.example.com"), + "/dns-query", + &request + ) + .is_ok() + ); + } + + impl SetHeaders for Vec<(HeaderName, HeaderValue)> { + fn set_headers(&self, map: &mut HeaderMap) -> Result<(), NetError> { + for (name, value) in self.iter() { + map.insert(name.clone(), value.clone()); + } + Ok(()) + } + } + + #[tokio::test] + async fn fetch_body_accumulates_chunks() { + let mut body = chunks(&[512, 512, 200]); + let bytes = fetch_body(&mut body, None).await.unwrap(); + assert_eq!(bytes.len(), 1224); + } + + #[tokio::test] + async fn fetch_body_stops_at_content_length() { + let mut body = chunks(&[512, 512]); + assert!(fetch_body(&mut body, Some(512)).await.is_err()); + } + + #[tokio::test] + async fn fetch_body_short_body_errors() { + let mut body = chunks(&[512]); + assert!(fetch_body(&mut body, Some(1024)).await.is_err()); + } + + #[tokio::test] + async fn fetch_body_single_oversized_chunk() { + // a single chunk exceeding the limit is rejected without being buffered + let mut body = chunks(&[MAX_REQUEST_SIZE + 1]); + assert!(matches!( + fetch_body(&mut body, None).await, + Err(NetError::RequestTooLarge) + )); + } + + #[tokio::test] + async fn fetch_body_accumulated_oversize_rejected() { + // no single chunk is too large, but together they exceed the limit + let half = MAX_REQUEST_SIZE / 2 + 1; + let mut body = chunks(&[half, half]); + assert!(matches!( + fetch_body(&mut body, None).await, + Err(NetError::RequestTooLarge) + )); + } + + #[tokio::test] + async fn fetch_body_at_limit_ok() { + // a body exactly at the limit is still accepted + let mut body = chunks(&[MAX_REQUEST_SIZE]); + let bytes = fetch_body(&mut body, None).await.unwrap(); + assert_eq!(bytes.len(), MAX_REQUEST_SIZE); + } + + fn chunks(lengths: &[usize]) -> impl Stream> + Unpin { + stream::iter( + lengths + .iter() + .map(|&len| Ok(Bytes::from(vec![0u8; len]))) + .collect::>(), + ) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/lib.rs b/vendor/hickory-dns/hickory-net/src/lib.rs new file mode 100644 index 00000000..b219afac --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/lib.rs @@ -0,0 +1,40 @@ +//! Networking library for Hickory DNS + +#![warn(clippy::dbg_macro, clippy::print_stdout, missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub use hickory_proto as proto; + +pub mod client; + +#[cfg(feature = "__dnssec")] +pub mod dnssec; + +mod error; +pub use error::{DnsError, ForwardNSData, NetError, NoRecords}; + +#[cfg(feature = "__https")] +pub mod h2; +#[cfg(feature = "__h3")] +pub mod h3; +#[cfg(any(feature = "__https", feature = "__h3"))] +pub mod http; +#[cfg(feature = "mdns")] +pub mod multicast; +#[cfg(all(feature = "__quic", feature = "tokio"))] +pub mod quic; +pub mod runtime; +pub mod tcp; +#[cfg(feature = "__tls")] +pub mod tls; +pub mod udp; +pub mod xfer; + +#[doc(hidden)] +pub use crate::xfer::BufDnsStreamHandle; +#[doc(hidden)] +pub use crate::xfer::dns_handle::{DnsHandle, DnsStreamHandle}; +#[doc(hidden)] +pub use crate::xfer::dns_multiplexer::DnsMultiplexer; +#[doc(hidden)] +pub use crate::xfer::retry_dns_handle::RetryDnsHandle; diff --git a/vendor/hickory-dns/hickory-net/src/multicast/mdns_client_stream.rs b/vendor/hickory-dns/hickory-net/src/multicast/mdns_client_stream.rs new file mode 100644 index 00000000..d9cf19bb --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/multicast/mdns_client_stream.rs @@ -0,0 +1,110 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::net::{Ipv4Addr, SocketAddr}; +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::future::Future; + +use futures_util::stream::{Stream, StreamExt, TryStreamExt}; + +use crate::BufDnsStreamHandle; +use crate::error::NetError; +use crate::multicast::mdns_stream::{MDNS_IPV4, MDNS_IPV6}; +use crate::multicast::{MdnsQueryType, MdnsStream}; +use crate::proto::op::SerialMessage; +use crate::runtime::TokioTime; +use crate::xfer::DnsClientStream; + +/// A UDP client stream of DNS binary packets +#[must_use = "futures do nothing unless polled"] +pub struct MdnsClientStream { + mdns_stream: MdnsStream, +} + +impl MdnsClientStream { + /// associates the socket to the well-known ipv4 multicast address + pub fn new_ipv4( + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ) -> ( + impl Future>, + BufDnsStreamHandle, + ) { + Self::new(*MDNS_IPV4, mdns_query_type, packet_ttl, ipv4_if, None) + } + + /// associates the socket to the well-known ipv6 multicast address + pub fn new_ipv6( + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv6_if: Option, + ) -> ( + impl Future>, + BufDnsStreamHandle, + ) { + Self::new(*MDNS_IPV6, mdns_query_type, packet_ttl, None, ipv6_if) + } + + /// it is expected that the resolver wrapper will be responsible for creating and managing + /// new UdpClients such that each new client would have a random port (reduce chance of cache + /// poisoning) + /// + /// # Return + /// + /// a tuple of a Future Stream which will handle sending and receiving messages, and a + /// handle which can be used to send messages into the stream. + #[allow(clippy::new_ret_no_self)] + pub fn new( + mdns_addr: SocketAddr, + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ipv6_if: Option, + ) -> ( + impl Future>, + BufDnsStreamHandle, + ) { + let (stream_future, sender) = + MdnsStream::new(mdns_addr, mdns_query_type, packet_ttl, ipv4_if, ipv6_if); + + ( + async { + Ok(Self { + mdns_stream: stream_future.await?, + }) + }, + sender, + ) + } +} + +impl DnsClientStream for MdnsClientStream { + type Time = TokioTime; + + fn name_server_addr(&self) -> SocketAddr { + self.mdns_stream.multicast_addr() + } +} + +impl Stream for MdnsClientStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mdns_stream = &mut self.as_mut().mdns_stream; + mdns_stream.map_err(NetError::from).poll_next_unpin(cx) + // match ready!(self.mdns_stream.poll_next_unpin(cx).map_err(ProtoError::from)) { + // Some(serial_message) => { + // // TODO: for mDNS queries could come from anywhere. It's not clear that there is anything + // // we can validate in this case. + // Poll::Ready(Some(Ok(serial_message))) + // } + // None => Poll::Ready(None), + // } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/multicast/mdns_stream.rs b/vendor/hickory-dns/hickory-net/src/multicast/mdns_stream.rs new file mode 100644 index 00000000..57026be3 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/multicast/mdns_stream.rs @@ -0,0 +1,735 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::future::Future; +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::io; +use std::sync::{Arc, LazyLock}; + +use futures_util::{ + FutureExt, + future::{self, BoxFuture}, + ready, + stream::{Stream, StreamExt}, +}; +use socket2::{self, Socket}; +use tokio::net::UdpSocket; +use tracing::{debug, trace}; + +use crate::BufDnsStreamHandle; +use crate::multicast::MdnsQueryType; +use crate::proto::op::SerialMessage; +use crate::runtime::TokioRuntimeProvider; +use crate::udp::UdpStream; + +pub(crate) const MDNS_PORT: u16 = 5353; +/// mDNS ipv4 address, see [multicast-addresses](https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml) +pub static MDNS_IPV4: LazyLock = + LazyLock::new(|| SocketAddr::new(Ipv4Addr::new(224, 0, 0, 251).into(), MDNS_PORT)); +/// link-local mDNS ipv6 address, see [ipv6-multicast-addresses](https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml) +pub static MDNS_IPV6: LazyLock = LazyLock::new(|| { + SocketAddr::new( + Ipv6Addr::new(0xFF02, 0, 0, 0, 0, 0, 0, 0x00FB).into(), + MDNS_PORT, + ) +}); + +/// A UDP stream of DNS binary packets +#[must_use = "futures do nothing unless polled"] +pub struct MdnsStream { + /// Multicast address used for mDNS queries + multicast_addr: SocketAddr, + /// This is used for sending and (directly) receiving messages + datagram: Option>, + // FIXME: like UdpStream, this Arc is unnecessary, only needed for temp async/await capture below + /// In one-shot multicast, this will not join the multicast group + multicast: Option>, + /// Receiving portion of the MdnsStream + rcving_mcast: Option>>, +} + +impl MdnsStream { + /// associates the socket to the well-known ipv4 multicast address + pub fn new_ipv4( + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ) -> ( + BoxFuture<'static, Result>, + BufDnsStreamHandle, + ) { + Self::new(*MDNS_IPV4, mdns_query_type, packet_ttl, ipv4_if, None) + } + + /// associates the socket to the well-known ipv6 multicast address + pub fn new_ipv6( + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv6_if: Option, + ) -> ( + BoxFuture<'static, Result>, + BufDnsStreamHandle, + ) { + Self::new(*MDNS_IPV6, mdns_query_type, packet_ttl, None, ipv6_if) + } + + /// Returns the address of the multicast network in use + pub fn multicast_addr(&self) -> SocketAddr { + self.multicast_addr + } + + /// This method is available for specifying a custom Multicast address to use. + /// + /// In general this operates nearly identically to UDP, except that it automatically joins + /// the default multicast DNS addresses. See + /// for details. + /// + /// When sending ipv6 multicast packets, the interface being used is required, + /// this will panic if the interface is not specified for all MdnsQueryType except Passive + /// (which does not allow sending data) + /// + /// # Arguments + /// + /// * `multicast_addr` - address to use for multicast requests + /// * `mdns_query_type` - true if the querier using this socket will only perform standard DNS queries over multicast. + /// * `ipv4_if` - Address to bind to for sending multicast packets, defaults to `0.0.0.0` if not specified (not relevant for ipv6) + /// * `ipv6_if` - Interface index for the interface to be used when sending ipv6 packets. + /// + /// # Return + /// + /// a tuple of a Future Stream which will handle sending and receiving messages, and a + /// handle which can be used to send messages into the stream. + pub fn new( + multicast_addr: SocketAddr, + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ipv6_if: Option, + ) -> ( + BoxFuture<'static, Result>, + BufDnsStreamHandle, + ) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(multicast_addr); + let multicast_socket = match Self::join_multicast(&multicast_addr, mdns_query_type) { + Ok(socket) => socket, + Err(err) => return (Box::pin(future::err(err)), message_sender), + }; + + // TODO: allow the bind address to be specified... + // constructs a future for getting the next randomly bound port to a UdpSocket + let next_socket = Self::next_bound_local_address( + &multicast_addr, + mdns_query_type, + packet_ttl, + ipv4_if, + ipv6_if, + ); + + // while 0 is meant to keep the packet on localhost, linux regards this as an error, + // while macOS (BSD?) and Windows allow it. + if let Some(ttl) = packet_ttl { + assert!(ttl > 0, "TTL must be greater than 0"); + } + + // This set of futures collapses the next udp socket into a stream which can be used for + // sending and receiving udp packets. + let stream = Box::pin(async move { + let datagram = if let Some(socket) = next_socket.await? { + socket.set_nonblocking(true)?; + Some(UdpStream::from_parts( + UdpSocket::from_std(socket)?, + outbound_messages, + )) + } else { + None + }; + + let multicast = if let Some(multicast_socket) = multicast_socket { + Some(Arc::new(UdpSocket::from_std(multicast_socket)?)) + } else { + None + }; + + Ok(Self { + multicast_addr, + datagram, + multicast, + rcving_mcast: None, + }) + }); + + (stream, message_sender) + } + + /// On Windows, unlike all Unix variants, it is improper to bind to the multicast address + /// + /// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms737550(v=vs.85).aspx + #[cfg(windows)] + fn bind_multicast(socket: &Socket, multicast_addr: &SocketAddr) -> io::Result<()> { + let multicast_addr = match multicast_addr { + SocketAddr::V4(addr) => SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), addr.port()), + SocketAddr::V6(addr) => SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), addr.port()), + }; + socket.bind(&socket2::SockAddr::from(multicast_addr)) + } + + /// On unixes we bind to the multicast address, which causes multicast packets to be filtered + #[cfg(unix)] + fn bind_multicast(socket: &Socket, multicast_addr: &SocketAddr) -> io::Result<()> { + socket.bind(&socket2::SockAddr::from(*multicast_addr)) + } + + /// Returns a socket joined to the multicast address + fn join_multicast( + multicast_addr: &SocketAddr, + mdns_query_type: MdnsQueryType, + ) -> Result, io::Error> { + if !mdns_query_type.join_multicast() { + return Ok(None); + } + + let ip_addr = multicast_addr.ip(); + // it's an error to not use a proper mDNS address + if !ip_addr.is_multicast() { + return Err(io::Error::other(format!( + "expected multicast address for binding: {ip_addr}" + ))); + } + + // binding the UdpSocket to the multicast address tells the OS to filter all packets on this socket to just this + // multicast address + // TODO: allow the binding interface to be specified + let socket = match &ip_addr { + IpAddr::V4(mdns_v4) => { + let socket = Socket::new( + socket2::Domain::IPV4, + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + socket.join_multicast_v4(mdns_v4, &Ipv4Addr::UNSPECIFIED)?; + socket + } + IpAddr::V6(mdns_v6) => { + let socket = Socket::new( + socket2::Domain::IPV6, + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + + socket.set_only_v6(true)?; + socket.join_multicast_v6(mdns_v6, 0)?; + socket + } + }; + + socket.set_nonblocking(true)?; + socket.set_reuse_address(true)?; + #[cfg(unix)] // this is currently restricted to Unix's in socket2 + socket.set_reuse_port(true)?; + Self::bind_multicast(&socket, multicast_addr)?; + + debug!("joined {multicast_addr}"); + Ok(Some(std::net::UdpSocket::from(socket))) + } + + /// Creates a future for randomly binding to a local socket address for client connections. + fn next_bound_local_address( + multicast_addr: &SocketAddr, + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ipv6_if: Option, + ) -> NextRandomUdpSocket { + let bind_address: IpAddr = match multicast_addr { + SocketAddr::V4(..) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + SocketAddr::V6(..) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), + }; + + NextRandomUdpSocket { + bind_address, + mdns_query_type, + packet_ttl, + ipv4_if, + ipv6_if, + } + } +} + +impl Stream for MdnsStream { + type Item = io::Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + assert!(self.datagram.is_some() || self.multicast.is_some()); + + // we poll the datagram socket first, if available, since it's a direct response or direct request + if let Some(datagram) = self.as_mut().datagram.as_mut() { + match datagram.poll_next_unpin(cx) { + Poll::Ready(ready) => return Poll::Ready(ready), + Poll::Pending => (), // drop through + } + } + + loop { + let msg = if let Some(receiving) = self.rcving_mcast.as_mut() { + // TODO: should we drop this packet if it's not from the same src as dest? + let msg = ready!(receiving.as_mut().poll_unpin(cx))?; + + Some(Poll::Ready(Some(Ok(msg)))) + } else { + None + }; + + self.rcving_mcast = None; + + if let Some(msg) = msg { + return msg; + } + + // let socket = Arc::clone(socket); + if let Some(socket) = &self.multicast { + let socket = Arc::clone(socket); + let receive_future = async { + let socket = socket; + let mut buf = [0u8; 2_048]; + let (len, src) = socket.recv_from(&mut buf).await?; + + Ok(SerialMessage::new( + buf.iter().take(len).cloned().collect(), + src, + )) + }; + + self.rcving_mcast = Some(Box::pin(receive_future)); + } + } + } +} + +#[must_use = "futures do nothing unless polled"] +struct NextRandomUdpSocket { + bind_address: IpAddr, + mdns_query_type: MdnsQueryType, + packet_ttl: Option, + ipv4_if: Option, + ipv6_if: Option, +} + +impl NextRandomUdpSocket { + fn prepare_sender(&self, socket: std::net::UdpSocket) -> io::Result { + let addr = socket.local_addr()?; + debug!("preparing sender on: {addr}"); + + let socket = Socket::from(socket); + + // TODO: TTL doesn't work on ipv6 + match addr { + SocketAddr::V4(..) => { + socket.set_multicast_loop_v4(true)?; + socket.set_multicast_if_v4(&self.ipv4_if.unwrap_or(Ipv4Addr::UNSPECIFIED))?; + if let Some(ttl) = self.packet_ttl { + socket.set_ttl_v4(ttl)?; + socket.set_multicast_ttl_v4(ttl)?; + } + } + SocketAddr::V6(..) => { + let ipv6_if = self.ipv6_if.unwrap_or_else(|| { + panic!("for ipv6 multicasting the interface must be specified") + }); + + socket.set_multicast_loop_v6(true)?; + socket.set_multicast_if_v6(ipv6_if)?; + if let Some(ttl) = self.packet_ttl { + socket.set_unicast_hops_v6(ttl)?; + socket.set_multicast_hops_v6(ttl)?; + } + } + } + + Ok(std::net::UdpSocket::from(socket)) + } +} + +impl Future for NextRandomUdpSocket { + // TODO: clean this up, the RandomUdpSocket shouldn't care about the query type + type Output = io::Result>; + + /// polls until there is an available next random UDP port. + /// + /// if there is no port available after 10 attempts, returns NotReady + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // non-one-shot, i.e. continuous, always use one of the well-known mdns ports and bind to the multicast addr + if !self.mdns_query_type.sender() { + debug!("skipping sending stream"); + Poll::Ready(Ok(None)) + } else if self.mdns_query_type.bind_on_5353() { + let addr = SocketAddr::new(self.bind_address, MDNS_PORT); + debug!("binding sending stream to {}", addr); + let socket = std::net::UdpSocket::bind(addr)?; + let socket = self.prepare_sender(socket)?; + + Poll::Ready(Ok(Some(socket))) + } else { + // TODO: this is basically identical to UdpStream from here... share some code? (except for the port restriction) + // one-shot queries look very similar to UDP socket, but can't listen on 5353 + + for attempt in 0..10 { + // Per RFC 6056 Section 2.1: + // + // The dynamic port range defined by IANA consists of the 49152-65535 + // range, and is meant for the selection of ephemeral ports. + let port = rand::random_range(49152_u16..=u16::MAX); + + // see one_shot usage info: https://tools.ietf.org/html/rfc6762#section-5 + // the MDNS_PORT is used to signal to remote processes that this is capable of receiving multicast packets + // i.e. is joined to the multicast address. + if port == MDNS_PORT { + trace!("unlucky, got MDNS_PORT"); + continue; + } + + let addr = SocketAddr::new(self.bind_address, port); + debug!("binding sending stream to {}", addr); + + match std::net::UdpSocket::bind(addr) { + Ok(socket) => { + let socket = self.prepare_sender(socket)?; + return Poll::Ready(Ok(Some(socket))); + } + Err(err) => debug!("unable to bind port, attempt: {}: {}", attempt, err), + } + } + + debug!("could not get next random port, delaying"); + + // TODO: this replaced a task::current().notify, is it correct? + cx.waker().wake_by_ref(); + Poll::Pending + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use futures_util::future::Either; + use std::println; + use std::sync::LazyLock; + use test_support::subscribe; + use tokio::runtime; + + use super::*; + use crate::xfer::dns_handle::DnsStreamHandle; + + // TODO: is there a better way? + const BASE_TEST_PORT: u16 = 5379; + + /// 250 appears to be unused/unregistered + static TEST_MDNS_IPV4: LazyLock = + LazyLock::new(|| Ipv4Addr::new(224, 0, 0, 250).into()); + /// FA appears to be unused/unregistered + static TEST_MDNS_IPV6: LazyLock = + LazyLock::new(|| Ipv6Addr::new(0xFF02, 0, 0, 0, 0, 0, 0, 0x00FA).into()); + + // one_shot tests are basically clones from the udp tests + #[tokio::test] + #[allow(unused_qualifications)] // https://github.com/rust-lang/rust/issues/149873 + async fn test_next_random_socket() { + subscribe(); + + let (stream, _) = MdnsStream::new( + SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT), + MdnsQueryType::OneShot, + Some(1), + None, + None, + ); + let result = stream.await; + + if let Err(error) = result { + println!("Random address error: {error:#?}"); + panic!("failed to get next random address"); + } + } + + // FIXME: reenable after breakage in async/await + #[ignore] + #[test] + fn test_one_shot_mdns_ipv4() { + subscribe(); + one_shot_mdns_test(SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 1)); + } + + #[test] + #[ignore] + fn test_one_shot_mdns_ipv6() { + subscribe(); + one_shot_mdns_test(SocketAddr::new(*TEST_MDNS_IPV6, BASE_TEST_PORT + 2)); + } + + // as there are probably unexpected responses coming on the standard addresses + fn one_shot_mdns_test(mdns_addr: SocketAddr) { + use core::time::Duration; + + let client_done = Arc::new(core::sync::atomic::AtomicBool::new(false)); + + let test_bytes: &'static [u8; 8] = b"DEADBEEF"; + let send_recv_times = 10; + let client_done_clone = client_done.clone(); + + // an in and out server + let server_handle = std::thread::Builder::new() + .name("test_one_shot_mdns:server".to_string()) + .spawn(move || { + let server_loop = runtime::Runtime::new().unwrap(); + let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + + // TTLs are 0 so that multicast test packets never leave the test host... + // FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases... + let (server_stream_future, mut server_sender) = MdnsStream::new( + mdns_addr, + MdnsQueryType::OneShotJoin, + Some(1), + None, + Some(5), + ); + + // For one-shot responses we are competing with a system mDNS responder, we will respond from a different port... + let mut server_stream = server_loop + .block_on(server_stream_future) + .expect("could not create mDNS listener") + .into_future(); + + for _ in 0..=send_recv_times { + if client_done_clone.load(core::sync::atomic::Ordering::Relaxed) { + return; + } + // wait for some bytes... + match server_loop.block_on( + future::lazy(|_| future::select(server_stream, timeout)).flatten(), + ) { + Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => { + let (buffer_and_addr, stream_tmp): ( + Option>, + MdnsStream, + ) = buffer_and_addr_stream_tmp; + + server_stream = stream_tmp.into_future(); + timeout = timeout_tmp; + let (buffer, addr) = buffer_and_addr + .expect("no msg received") + .expect("error receiving msg") + .into_parts(); + + assert_eq!(&buffer, test_bytes); + //println!("server got data! {}", addr); + + // bounce them right back... + server_sender + .send(SerialMessage::new(test_bytes.to_vec(), addr)) + .expect("could not send to client"); + } + Either::Right(((), buffer_and_addr_stream_tmp)) => { + server_stream = buffer_and_addr_stream_tmp; + timeout = + future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + } + } + + // let the server turn for a bit... send the message + server_loop.block_on(tokio::time::sleep(Duration::from_millis(100))); + } + }) + .unwrap(); + + // setup the client, which is going to run on the testing thread... + let io_loop = runtime::Runtime::new().unwrap(); + + // FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases... + let (stream, mut sender) = + MdnsStream::new(mdns_addr, MdnsQueryType::OneShot, Some(1), None, Some(5)); + let mut stream = io_loop.block_on(stream).unwrap().into_future(); + let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + let mut successes = 0; + + for _ in 0..send_recv_times { + // test once + sender + .send(SerialMessage::new(test_bytes.to_vec(), mdns_addr)) + .unwrap(); + + println!("client sending data!"); + + // TODO: this lazy isn't needed is it? + match io_loop.block_on(future::lazy(|_| future::select(stream, timeout)).flatten()) { + Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => { + let (buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp; + stream = stream_tmp.into_future(); + timeout = timeout_tmp; + + let (buffer, _addr) = buffer_and_addr + .expect("no msg received") + .expect("error receiving msg") + .into_parts(); + println!("client got data!"); + + assert_eq!(&buffer, test_bytes); + successes += 1; + } + Either::Right(((), buffer_and_addr_stream_tmp)) => { + stream = buffer_and_addr_stream_tmp; + timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + } + } + } + + client_done.store(true, core::sync::atomic::Ordering::Relaxed); + println!("successes: {successes}"); + assert!(successes >= 1); + server_handle.join().expect("server thread failed"); + } + + // FIXME: reenable after breakage in async/await + #[ignore] + #[test] + fn test_passive_mdns() { + subscribe(); + passive_mdns_test( + MdnsQueryType::Passive, + SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 3), + ) + } + + // FIXME: reenable after breakage in async/await + #[ignore] + #[test] + fn test_oneshot_join_mdns() { + subscribe(); + passive_mdns_test( + MdnsQueryType::OneShotJoin, + SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 4), + ) + } + + // as there are probably unexpected responses coming on the standard addresses + fn passive_mdns_test(mdns_query_type: MdnsQueryType, mdns_addr: SocketAddr) { + use core::time::Duration; + + let server_got_packet = Arc::new(core::sync::atomic::AtomicBool::new(false)); + + let test_bytes: &'static [u8; 8] = b"DEADBEEF"; + let send_recv_times = 10; + let server_got_packet_clone = server_got_packet.clone(); + + // an in and out server + let _server_handle = std::thread::Builder::new() + .name("test_one_shot_mdns:server".to_string()) + .spawn(move || { + let io_loop = runtime::Runtime::new().unwrap(); + let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + + // TTLs are 0 so that multicast test packets never leave the test host... + // FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases... + let (server_stream_future, _server_sender) = + MdnsStream::new(mdns_addr, mdns_query_type, Some(1), None, Some(5)); + + // For one-shot responses we are competing with a system mDNS responder, we will respond from a different port... + let mut server_stream = io_loop + .block_on(server_stream_future) + .expect("could not create mDNS listener") + .into_future(); + + for _ in 0..=send_recv_times { + // wait for some bytes... + match io_loop.block_on( + future::lazy(|_| future::select(server_stream, timeout)).flatten(), + ) { + Either::Left((_buffer_and_addr_stream_tmp, _timeout_tmp)) => { + // let (buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp; + + // server_stream = stream_tmp.into_future(); + // timeout = timeout_tmp; + // let (buffer, addr) = buffer_and_addr.expect("no buffer received"); + + // assert_eq!(&buffer, test_bytes); + // println!("server got data! {}", addr); + + server_got_packet_clone + .store(true, core::sync::atomic::Ordering::Relaxed); + return; + } + Either::Right(((), buffer_and_addr_stream_tmp)) => { + server_stream = buffer_and_addr_stream_tmp; + timeout = + future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + } + } + + // let the server turn for a bit... send the message + io_loop.block_on(tokio::time::sleep(Duration::from_millis(100))); + } + }) + .unwrap(); + + // setup the client, which is going to run on the testing thread... + let io_loop = runtime::Runtime::new().unwrap(); + // FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases... + let (stream, mut sender) = + MdnsStream::new(mdns_addr, MdnsQueryType::OneShot, Some(1), None, Some(5)); + let mut stream = io_loop.block_on(stream).unwrap().into_future(); + let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + + for _ in 0..send_recv_times { + // test once + sender + .send(SerialMessage::new(test_bytes.to_vec(), mdns_addr)) + .unwrap(); + + println!("client sending data!"); + + // TODO: this lazy is probably unnecessary? + let run_result = + io_loop.block_on(future::lazy(|_| future::select(stream, timeout)).flatten()); + + if server_got_packet.load(core::sync::atomic::Ordering::Relaxed) { + return; + } + + match run_result { + Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => { + let (_buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp; + stream = stream_tmp.into_future(); + timeout = timeout_tmp; + } + Either::Right(((), buffer_and_addr_stream_tmp)) => { + stream = buffer_and_addr_stream_tmp; + timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100))) + .flatten() + .boxed(); + } + } + } + + panic!("server never got packet."); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/multicast/mod.rs b/vendor/hickory-dns/hickory-net/src/multicast/mod.rs new file mode 100644 index 00000000..ee768b70 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/multicast/mod.rs @@ -0,0 +1,72 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Multicast protocol related components for DNS + +#[cfg(feature = "tokio")] +mod mdns_client_stream; +#[cfg(feature = "tokio")] +mod mdns_stream; + +#[cfg(feature = "tokio")] +pub use self::mdns_client_stream::MdnsClientStream; +#[cfg(feature = "tokio")] +pub use self::mdns_stream::{MDNS_IPV4, MDNS_IPV6, MdnsStream}; + +/// See [rfc6762](https://tools.ietf.org/html/rfc6762#section-5) details on these different types. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MdnsQueryType { + /// The querier using this socket will only perform standard DNS queries over multicast. (clients only) + /// + /// Effectively treats mDNS as essentially no different than any other DNS query; one request followed by one response. + /// Only one UDP socket will be created. + OneShot, + /// The querier is fully compliant with [rfc6762](https://tools.ietf.org/html/rfc6762#section-5). (servers, clients) + /// + /// mDNS capable clients will sent messages with many queries, and they will expect many responses. Two UDP sockets will be + /// created, one for receiving multicast traffic, the other used for sending queries and direct responses. This requires + /// port 5353 to be available on the system (many modern OSes already have mDNSResponders running taking this port). + Continuous, + /// The querier operates under the OneShot semantics, but also joins the multicast group. (non-compliant servers, clients) + /// + /// This is not defined in the mDNS RFC, but allows for a multicast client to join the group, receiving all multicast network + /// traffic. This is useful where listening for all mDNS traffic is of interest, but because another mDNS process may have + /// already taken the known port, 5353. Query responses will come from and to the standard UDP socket with a random port, + /// multicast traffic will come from the multicast socket. This will create two sockets. + OneShotJoin, + /// The querier operates under the OneShot semantics, but also joins the multicast group. (servers) + /// + /// Not defined in the RFC, allows for a passive listener to receive all mDNS traffic. + Passive, +} + +impl MdnsQueryType { + /// This will be sending packets, i.e. a standard UDP socket will be created + pub fn sender(self) -> bool { + match self { + Self::Passive => false, + Self::OneShot | Self::OneShotJoin => true, + Self::Continuous => true, + } + } + + /// Returns true if this process can bind to *:5353 + pub fn bind_on_5353(self) -> bool { + match self { + Self::OneShot | Self::OneShotJoin | Self::Passive => false, + Self::Continuous => true, + } + } + + /// Returns true if this mDNS client should join, listen, on the multicast address + pub fn join_multicast(self) -> bool { + match self { + Self::OneShot => false, + Self::Continuous | Self::OneShotJoin | Self::Passive => true, + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/quic/mod.rs b/vendor/hickory-dns/hickory-net/src/quic/mod.rs new file mode 100644 index 00000000..91327b67 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! QUIC protocol related components for DNS over QUIC (DoQ) + +mod quic_client_stream; +mod quic_config; +mod quic_server; +mod quic_stream; + +#[cfg(feature = "__h3")] +pub(crate) use self::quic_client_stream::connect_quic; +pub use self::quic_client_stream::{QuicClientStream, QuicClientStreamBuilder}; +pub use self::quic_server::{QuicServer, QuicStreams}; +pub use self::quic_stream::{DoqErrorCode, QuicStream}; + +#[cfg(test)] +mod tests; diff --git a/vendor/hickory-dns/hickory-net/src/quic/quic_client_stream.rs b/vendor/hickory-dns/hickory-net/src/quic/quic_client_stream.rs new file mode 100644 index 00000000..ee44b0fd --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/quic_client_stream.rs @@ -0,0 +1,407 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::{ + fmt::{self, Display}, + future::Future, + net::SocketAddr, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use std::io; +use std::sync::Arc; + +use futures_util::stream::Stream; +use quinn::{ + ClientConfig, Connection, Endpoint, TransportConfig, VarInt, crypto::rustls::QuicClientConfig, +}; +use tokio::time::timeout; + +use crate::{ + error::NetError, + proto::op::{DnsRequest, DnsResponse}, + quic::quic_stream::{DoqErrorCode, QuicStream}, + runtime::{RuntimeProvider, Spawn}, + tls::client_config, + udp::UdpSocket, + xfer::{CONNECT_TIMEOUT, DnsExchange, DnsRequestSender, DnsResponseStream}, +}; + +use super::{quic_config, quic_stream}; + +/// A DNS client connection for DNS-over-QUIC +#[must_use = "futures do nothing unless polled"] +#[derive(Clone)] +pub struct QuicClientStream { + quic_connection: Connection, + server_name: Arc, + name_server: SocketAddr, + is_shutdown: bool, + request_timeout: Option, +} + +impl Display for QuicClientStream { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(formatter, "QUIC({},{})", self.name_server, self.server_name) + } +} + +impl QuicClientStream { + /// Builder for QuicClientStream + pub fn builder() -> QuicClientStreamBuilder { + QuicClientStreamBuilder::default() + } + + async fn inner_send( + connection: Connection, + message: DnsRequest, + ) -> Result { + let (send_stream, recv_stream) = connection.open_bi().await.map_err(NetError::from)?; + + // RFC: The mapping specified here requires that the client selects a separate + // QUIC stream for each query. The server then uses the same stream to provide all the response messages for that query. + let mut stream = QuicStream::new(send_stream, recv_stream); + + stream.send(message.into_parts().0).await?; + + // The client MUST send the DNS query over the selected stream, + // and MUST indicate through the STREAM FIN mechanism that no further data will be sent on that stream. + stream.finish().await?; + + stream.receive().await + } +} + +impl DnsRequestSender for QuicClientStream { + /// The send loop for QUIC in DNS stipulates that a new QUIC "stream" should be opened and use for sending data. + /// + /// It should be closed after receiving the response. TODO: AXFR/IXFR support... + /// + /// ```text + /// RFC 9250 DNS over Dedicated QUIC Connections + /// + /// 4.2. Stream Mapping and Usage + /// + /// The mapping of DNS traffic over QUIC streams takes advantage of the + /// QUIC stream features detailed in Section 2 of [RFC9000], the QUIC + /// transport specification. + /// + /// DNS query/response traffic [RFC1034] [RFC1035] follows a simple + /// pattern in which the client sends a query, and the server provides + /// one or more responses (multiple responses can occur in zone + /// transfers). + /// + /// The mapping specified here requires that the client select a separate + /// QUIC stream for each query. The server then uses the same stream to + /// provide all the response messages for that query. In order for + /// multiple responses to be parsed, a 2-octet length field is used in + /// exactly the same way as the 2-octet length field defined for DNS over + /// TCP [RFC1035]. The practical result of this is that the content of + /// each QUIC stream is exactly the same as the content of a TCP + /// connection that would manage exactly one query. + /// + /// All DNS messages (queries and responses) sent over DoQ connections + /// MUST be encoded as a 2-octet length field followed by the message + /// content as specified in [RFC1035]. + /// + /// The client MUST select the next available client-initiated + /// bidirectional stream for each subsequent query on a QUIC connection, + /// in conformance with the QUIC transport specification [RFC9000]. + /// Packet losses and other network events might cause queries to arrive + /// in a different order. Servers SHOULD process queries as they arrive, + /// as not doing so would cause unnecessary delays. + /// + /// The client MUST send the DNS query over the selected stream and MUST + /// indicate through the STREAM FIN mechanism that no further data will + /// be sent on that stream. + /// + /// The server MUST send the response(s) on the same stream and MUST + /// indicate, after the last response, through the STREAM FIN mechanism + /// that no further data will be sent on that stream. + /// + /// Therefore, a single DNS transaction consumes a single bidirectional + /// client-initiated stream. This means that the client's first query + /// occurs on QUIC stream 0, the second on 4, and so on (see Section 2.1 + /// of [RFC9000]). + /// + /// Servers MAY defer processing of a query until the STREAM FIN has been + /// indicated on the stream selected by the client. + /// + /// Servers and clients MAY monitor the number of "dangling" streams. + /// These are open streams where the following events have not occurred + /// after implementation-defined timeouts: + /// + /// * the expected queries or responses have not been received or, + /// + /// * the expected queries or responses have been received but not the + /// STREAM FIN + /// + /// Implementations MAY impose a limit on the number of such dangling + /// streams. If limits are encountered, implementations MAY close the + /// connection. + /// + /// 4.2.1. DNS Message IDs + /// + /// When sending queries over a QUIC connection, the DNS Message ID MUST + /// be set to 0. The stream mapping for DoQ allows for unambiguous + /// correlation of queries and responses, so the Message ID field is not + /// required. + /// + /// This has implications for proxying DoQ messages to and from other + /// transports. For example, proxies may have to manage the fact that + /// DoQ can support a larger number of outstanding queries on a single + /// connection than, for example, DNS over TCP, because DoQ is not + /// limited by the Message ID space. This issue already exists for DoH, + /// where a Message ID of 0 is recommended. + /// + /// When forwarding a DNS message from DoQ over another transport, a DNS + /// Message ID MUST be generated according to the rules of the protocol + /// that is in use. When forwarding a DNS message from another transport + /// over DoQ, the Message ID MUST be set to 0. + /// ``` + fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream { + if self.is_shutdown { + panic!("can not send messages after stream is shutdown") + } + + let send_fut = Self::inner_send(self.quic_connection.clone(), request); + + let Some(request_timeout) = self.request_timeout else { + return Box::pin(send_fut).into(); + }; + + Box::pin(async move { + timeout(request_timeout, send_fut) + .await + .map_err(|_| NetError::Timeout)? + }) + .into() + } + + fn shutdown(&mut self) { + self.is_shutdown = true; + self.quic_connection + .close(DoqErrorCode::NoError.into(), b"Shutdown"); + } + + fn is_shutdown(&self) -> bool { + self.is_shutdown + } +} + +impl Stream for QuicClientStream { + type Item = Result<(), NetError>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + if self.is_shutdown { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(()))) + } + } +} + +/// A QUIC connection builder for DNS-over-QUIC +#[derive(Clone)] +pub struct QuicClientStreamBuilder { + crypto_config: Option, + transport_config: Arc, + bind_addr: Option, + request_timeout: Option, +} + +impl QuicClientStreamBuilder { + /// Constructs a new TlsStreamBuilder with the associated ClientConfig + pub fn crypto_config(mut self, crypto_config: rustls::ClientConfig) -> Self { + self.crypto_config = Some(crypto_config); + self + } + + /// Sets the address to connect from. + pub fn bind_addr(mut self, bind_addr: SocketAddr) -> Self { + self.bind_addr = Some(bind_addr); + self + } + + /// Set the per-request timeout applied to each DNS send over the QUIC connection. + /// + /// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with + /// [`NetError::Timeout`] if no response arrives within this duration. + pub fn request_timeout(mut self, timeout: Duration) -> Self { + self.request_timeout = Some(timeout); + self + } + + /// Creates a new QuicStream to the specified name_server + /// + /// # Arguments + /// + /// * `name_server` - IP and Port for the remote DNS resolver + /// * `server_name` - The DNS name associated with a certificate + pub fn build( + self, + name_server: SocketAddr, + server_name: Arc, + ) -> impl Future> + Send + 'static { + self.connect(name_server, server_name) + } + + /// Creates a new [`DnsExchange`] wrapping the [`QuicClientStream`] from this builder + pub async fn exchange( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + provider: P, + ) -> Result, NetError> { + let stream = self + .connect_with_future(socket, name_server, server_name) + .await?; + let (exchange, bg) = DnsExchange::from_stream(stream); + provider.create_handle().spawn_bg(bg); + Ok(exchange) + } + + /// Create a QuicStream with existing connection + pub fn build_with_future( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + ) -> impl Future> + Send + 'static { + self.connect_with_future(socket, name_server, server_name) + } + + async fn connect_with_future( + self, + socket: Arc, + name_server: SocketAddr, + server_name: Arc, + ) -> Result { + let endpoint_config = quic_config::endpoint(); + let endpoint = Endpoint::new_with_abstract_socket( + endpoint_config, + None, + socket, + Arc::new(quinn::TokioRuntime), + )?; + self.connect_inner(endpoint, name_server, server_name).await + } + + async fn connect( + self, + name_server: SocketAddr, + server_name: Arc, + ) -> Result { + let connect = if let Some(bind_addr) = self.bind_addr { + ::connect_with_bind(name_server, bind_addr) + } else { + ::connect(name_server) + }; + + let socket = connect.await?; + let socket = socket.into_std()?; + let endpoint_config = quic_config::endpoint(); + let endpoint = Endpoint::new(endpoint_config, None, socket, Arc::new(quinn::TokioRuntime))?; + self.connect_inner(endpoint, name_server, server_name).await + } + + async fn connect_inner( + self, + endpoint: Endpoint, + name_server: SocketAddr, + server_name: Arc, + ) -> Result { + // ensure the ALPN protocol is set correctly + let crypto_config = if let Some(crypto_config) = self.crypto_config { + crypto_config + } else { + client_config().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("failed to initialize default TLS client config: {e}"), + ) + })? + }; + + let quic_connection = connect_quic( + name_server, + server_name.clone(), + quic_stream::DOQ_ALPN, + crypto_config, + self.transport_config, + endpoint, + ) + .await?; + + Ok(QuicClientStream { + quic_connection, + server_name, + name_server, + is_shutdown: false, + request_timeout: self.request_timeout, + }) + } +} + +pub(crate) async fn connect_quic( + addr: SocketAddr, + server_name: Arc, + protocol: &[u8], + mut crypto_config: rustls::ClientConfig, + transport_config: Arc, + mut endpoint: Endpoint, +) -> Result { + if crypto_config.alpn_protocols.is_empty() { + crypto_config.alpn_protocols = vec![protocol.to_vec()]; + } + let early_data_enabled = crypto_config.enable_early_data; + + let mut client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(crypto_config)?)); + client_config.transport_config(transport_config.clone()); + + endpoint.set_default_client_config(client_config); + + let connecting = endpoint.connect(addr, &server_name)?; + // TODO: for Client/Dynamic update, don't use RTT, for queries, do use it. + + Ok(if early_data_enabled { + match connecting.into_0rtt() { + Ok((new_connection, _)) => new_connection, + Err(connecting) => connect_with_timeout(connecting).await?, + } + } else { + connect_with_timeout(connecting).await? + }) +} + +async fn connect_with_timeout(connecting: quinn::Connecting) -> Result { + match timeout(CONNECT_TIMEOUT, connecting).await { + Ok(Ok(connection)) => Ok(connection), + Ok(Err(e)) => Err(e.into()), + Err(_) => Err(io::Error::new( + io::ErrorKind::TimedOut, + format!("QUIC handshake timed out after {CONNECT_TIMEOUT:?}",), + )), + } +} + +impl Default for QuicClientStreamBuilder { + fn default() -> Self { + let mut transport_config = quic_config::transport(); + // clients never accept new bidirectional streams + transport_config.max_concurrent_bidi_streams(VarInt::from_u32(0)); + + Self { + crypto_config: None, + transport_config: Arc::new(transport_config), + bind_addr: None, + request_timeout: None, + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/quic/quic_config.rs b/vendor/hickory-dns/hickory-net/src/quic/quic_config.rs new file mode 100644 index 00000000..f16cd685 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/quic_config.rs @@ -0,0 +1,33 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use quinn::{EndpointConfig, TransportConfig, VarInt}; + +/// Returns a default endpoint configuration for DNS-over-QUIC +pub(crate) fn endpoint() -> EndpointConfig { + // set some better EndpointConfig defaults for DoQ + let mut endpoint_config = EndpointConfig::default(); + + // all DNS packets have a maximum size of u16 due to DoQ and 1035 rfc + // TODO: the RFC claims max == u16::max, but this matches the max in some test servers. + endpoint_config + .max_udp_payload_size(0x45ac) + .expect("max udp payload size exceeded"); + + endpoint_config +} + +/// Returns a default endpoint configuration for DNS-over-QUIC +pub(crate) fn transport() -> TransportConfig { + let mut transport_config = TransportConfig::default(); + + transport_config.max_concurrent_uni_streams(VarInt::from_u32(0)); + transport_config.datagram_receive_buffer_size(None); + transport_config.datagram_send_buffer_size(0); + + transport_config +} diff --git a/vendor/hickory-dns/hickory-net/src/quic/quic_server.rs b/vendor/hickory-dns/hickory-net/src/quic/quic_server.rs new file mode 100644 index 00000000..bcda72df --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/quic_server.rs @@ -0,0 +1,124 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::net::SocketAddr; +use std::io; +use std::sync::Arc; +use std::time::Duration; + +use quinn::crypto::rustls::QuicServerConfig; +use quinn::{Connection, Endpoint, ServerConfig}; +use rustls::server::ResolvesServerCert; +use rustls::server::ServerConfig as TlsServerConfig; +use rustls::version::TLS13; +use tokio::time::timeout; + +use crate::{error::NetError, tls::default_provider, udp::UdpSocket}; + +use super::{ + quic_config, + quic_stream::{self, QuicStream}, +}; + +/// A DNS-over-QUIC Server, see QuicClientStream for the client counterpart +pub struct QuicServer { + endpoint: Endpoint, +} + +impl QuicServer { + /// Construct the new Acceptor with the associated pkcs12 data + pub async fn new( + name_server: SocketAddr, + server_cert_resolver: Arc, + ) -> Result { + // setup a new socket for the server to use + let socket = ::bind(name_server).await?; + Self::with_socket(socket, server_cert_resolver) + } + + /// Construct the new server with an existing socket and a default TLS configuration + pub fn with_socket( + socket: tokio::net::UdpSocket, + server_cert_resolver: Arc, + ) -> Result { + let mut config = TlsServerConfig::builder_with_provider(Arc::new(default_provider())) + .with_protocol_versions(&[&TLS13]) + .unwrap() // The ring default provider is guaranteed to support TLS 1.3 + .with_no_client_auth() + .with_cert_resolver(server_cert_resolver); + + config.alpn_protocols = vec![quic_stream::DOQ_ALPN.to_vec()]; + + Self::with_socket_and_tls_config(socket, Arc::new(config)) + } + + /// Construct the new server with an existing socket and a custom TLS configuration + /// + /// The caller must ensure the `TlsServerConfig` has the appropriate DoQ ALPN protocol enabled. + pub fn with_socket_and_tls_config( + socket: tokio::net::UdpSocket, + tls_config: Arc, + ) -> Result { + let mut server_config = + ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(tls_config)?)); + server_config.transport = Arc::new(quic_config::transport()); + + let socket = socket.into_std()?; + + let endpoint_config = quic_config::endpoint(); + let endpoint = Endpoint::new( + endpoint_config, + Some(server_config), + socket, + Arc::new(quinn::TokioRuntime), + )?; + + Ok(Self { endpoint }) + } + + /// Get the next incoming stream + /// + /// # Returns + /// + /// A remote connection that could have many potential bi-directional streams and the remote socket address + pub async fn next(&mut self) -> Result, NetError> { + let Some(connecting) = self.endpoint.accept().await else { + return Ok(None); + }; + + let remote_addr = connecting.remote_address(); + let connection = timeout(Duration::from_secs(5), connecting) + .await + .map_err(|_| NetError::from("quic timeout expired during handshake"))??; + Ok(Some((QuicStreams { connection }, remote_addr))) + } + + /// Returns the address this server is listening on + /// + /// This can be useful in tests, where a random port can be associated with the server by binding on `127.0.0.1:0` and then getting the + /// associated port address with this function. + pub fn local_addr(&self) -> Result { + self.endpoint.local_addr() + } +} + +/// A stream of bi-directional QUIC streams +pub struct QuicStreams { + connection: Connection, +} + +impl QuicStreams { + /// Get the next bi directional stream from the client + pub async fn next(&mut self) -> Option> { + match self.connection.accept_bi().await { + Ok((send_stream, receive_stream)) => { + Some(Ok(QuicStream::new(send_stream, receive_stream))) + } + Err(e) => Some(Err(NetError::from(e))), + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/quic/quic_stream.rs b/vendor/hickory-dns/hickory-net/src/quic/quic_stream.rs new file mode 100644 index 00000000..5a1d810f --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/quic_stream.rs @@ -0,0 +1,236 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use bytes::{BufMut, Bytes, BytesMut}; +use quinn::{RecvStream, SendStream, VarInt}; +use tracing::debug; + +use crate::{ + error::NetError, + proto::{ + ProtoError, + op::{DnsResponse, Message}, + }, +}; + +/// ```text +/// 4.1. Connection Establishment +/// +/// DoQ connections are established as described in the QUIC transport +/// specification [RFC9000]. During connection establishment, DoQ +/// support is indicated by selecting the Application-Layer Protocol +/// Negotiation (ALPN) token "doq" in the crypto handshake. +/// ``` +pub(crate) const DOQ_ALPN: &[u8] = b"doq"; + +/// [RFC 9250](https://www.rfc-editor.org/rfc/rfc9250.html#name-doq-error-codes) +/// ```text +/// 4.3. DoQ Error Codes +/// +/// The following error codes are defined for use when abruptly +/// terminating streams, for use as application protocol error codes when +/// aborting reading of streams, or for immediately closing connections: +/// +/// DOQ_NO_ERROR (0x0): No error. This is used when the connection or +/// stream needs to be closed, but there is no error to signal. +/// +/// DOQ_INTERNAL_ERROR (0x1): The DoQ implementation encountered an +/// internal error and is incapable of pursuing the transaction or the +/// connection. +/// +/// DOQ_PROTOCOL_ERROR (0x2): The DoQ implementation encountered a +/// protocol error and is forcibly aborting the connection. +/// +/// DOQ_REQUEST_CANCELLED (0x3): A DoQ client uses this to signal that +/// it wants to cancel an outstanding transaction. +/// +/// DOQ_EXCESSIVE_LOAD (0x4): A DoQ implementation uses this to signal +/// when closing a connection due to excessive load. +/// +/// DOQ_UNSPECIFIED_ERROR (0x5): A DoQ implementation uses this in the +/// absence of a more specific error code. +/// +/// DOQ_ERROR_RESERVED (0xd098ea5e): An alternative error code used for +/// tests. +/// ``` +#[derive(Clone, Copy)] +pub enum DoqErrorCode { + /// No error. This is used when the connection or stream needs to be closed, but there is no error to signal. + NoError, + /// The DoQ implementation encountered an internal error and is incapable of pursuing the transaction or the connection. + InternalError, + /// The DoQ implementation encountered a protocol error and is forcibly aborting the connection. + ProtocolError, + /// A DoQ client uses this to signal that it wants to cancel an outstanding transaction. + RequestCancelled, + /// A DoQ implementation uses this to signal when closing a connection due to excessive load. + ExcessiveLoad, + /// An alternative error code used for tests. + ErrorReserved, + /// Unknown Error code + Unknown(u32), +} + +// not using repr(u32) above because of the Unknown +const NO_ERROR: u32 = 0x0; +const INTERNAL_ERROR: u32 = 0x1; +const PROTOCOL_ERROR: u32 = 0x2; +const REQUEST_CANCELLED: u32 = 0x3; +const EXCESSIVE_LOAD: u32 = 0x4; +const ERROR_RESERVED: u32 = 0xd098ea5e; + +impl From for VarInt { + fn from(doq_error: DoqErrorCode) -> Self { + use DoqErrorCode::*; + + match doq_error { + NoError => Self::from_u32(NO_ERROR), + InternalError => Self::from_u32(INTERNAL_ERROR), + ProtocolError => Self::from_u32(PROTOCOL_ERROR), + RequestCancelled => Self::from_u32(REQUEST_CANCELLED), + ExcessiveLoad => Self::from_u32(EXCESSIVE_LOAD), + ErrorReserved => Self::from_u32(ERROR_RESERVED), + Unknown(code) => Self::from_u32(code), + } + } +} + +impl From for DoqErrorCode { + fn from(doq_error: VarInt) -> Self { + let code: u32 = if let Ok(code) = doq_error.into_inner().try_into() { + code + } else { + return Self::ProtocolError; + }; + + match code { + NO_ERROR => Self::NoError, + INTERNAL_ERROR => Self::InternalError, + PROTOCOL_ERROR => Self::ProtocolError, + REQUEST_CANCELLED => Self::RequestCancelled, + EXCESSIVE_LOAD => Self::ExcessiveLoad, + ERROR_RESERVED => Self::ErrorReserved, + _ => Self::Unknown(code), + } + } +} + +/// A single bi-directional stream +pub struct QuicStream { + send_stream: SendStream, + receive_stream: RecvStream, +} + +impl QuicStream { + pub(crate) fn new(send_stream: SendStream, receive_stream: RecvStream) -> Self { + Self { + send_stream, + receive_stream, + } + } + + /// Send the DNS message to the other side + pub async fn send(&mut self, mut message: Message) -> Result<(), NetError> { + // RFC: When sending queries over a QUIC connection, the DNS Message ID MUST be set to 0. + // The stream mapping for DoQ allows for unambiguous correlation of queries and responses, + // so the Message ID field is not required. + + message.metadata.id = 0; + + let bytes = Bytes::from(message.to_vec()?); + + self.send_bytes(bytes).await + } + + /// Send pre-encoded bytes, warning, QUIC requires the message id to be 0. + pub async fn send_bytes(&mut self, bytes: Bytes) -> Result<(), NetError> { + // In order for multiple responses to be parsed, a 2-octet length field is used in exactly + // the same way as the 2-octet length field defined for DNS over TCP [RFC1035]. The + // practical result of this is that the content of each QUIC stream is exactly the same as + // the content of a TCP connection that would manage exactly one query. + // + // All DNS messages (queries and responses) sent over DoQ connections MUST be encoded as a + // 2-octet length field followed by the message content as specified in [RFC1035]. + let bytes_len = u16::try_from(bytes.len()) + .map_err(|_e| NetError::from(ProtoError::MaxBufferSizeExceeded(bytes.len())))?; + let len = bytes_len.to_be_bytes().to_vec(); + let len = Bytes::from(len); + + debug!("received packet len: {} bytes: {:x?}", bytes_len, bytes); + self.send_stream.write_all_chunks(&mut [len, bytes]).await?; + Ok(()) + } + + /// finishes the send stream, i.e. there will be no more data sent to the remote + pub async fn finish(&mut self) -> Result<(), NetError> { + self.send_stream.finish()?; + Ok(()) + } + + /// Receive a single packet + pub async fn receive(&mut self) -> Result { + let bytes = self.receive_bytes().await?; + let message = Message::from_vec(&bytes)?; + + // assert that the message id is 0, this is a bad dns-over-quic packet if not + if message.id != 0 { + if let Err(error) = self.reset(DoqErrorCode::ProtocolError) { + debug!(%error, "stream already closed"); + } + return Err(NetError::QuicMessageIdNot0(message.id)); + } + + Ok(DnsResponse::from_buffer(bytes.to_vec())?) + } + + // TODO: we should change the protocol handlers to work with Messages since some require things like 0 for the Message ID. + /// Receive a single packet as raw bytes + pub async fn receive_bytes(&mut self) -> Result { + // following above, the data should be first the length, followed by the message(s) + let mut len = [0u8; 2]; + self.receive_stream.read_exact(&mut len).await?; + let len = u16::from_be_bytes(len) as usize; + + // RFC: DoQ queries and responses are sent on QUIC streams, which in theory can carry up to + // 2^62 bytes. However, DNS messages are restricted in practice to a maximum size of 65535 + // bytes. This maximum size is enforced by the use of a 2-octet message length field in DNS + // over TCP [RFC1035] and DoT [RFC7858], and by the definition of the + // "application/dns-message" for DoH [RFC8484]. DoQ enforces the same restriction. + let mut bytes = BytesMut::with_capacity(512); + let mut left = len; + while left > 0 { + match self.receive_stream.read_chunk(left, true).await { + Ok(Some(chunk)) => { + left -= chunk.bytes.len(); + bytes.put(chunk.bytes); + } + Ok(None) => { + return Err(NetError::QuinnReadError( + quinn::ReadExactError::FinishedEarly(bytes.len()), + )); + } + Err(e) => return Err(NetError::QuinnReadError(e.into())), + } + } + + Ok(bytes) + } + + /// Reset the sending stream due to some error + pub fn reset(&mut self, code: DoqErrorCode) -> Result<(), NetError> { + self.send_stream + .reset(code.into()) + .map_err(|_| NetError::QuinnUnknownStreamError) + } + + /// Stop the receiving stream due to some error + pub fn stop(&mut self, code: DoqErrorCode) -> Result<(), NetError> { + self.receive_stream + .stop(code.into()) + .map_err(|_| NetError::QuinnUnknownStreamError) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/quic/tests.rs b/vendor/hickory-dns/hickory-net/src/quic/tests.rs new file mode 100644 index 00000000..ced5b830 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/quic/tests.rs @@ -0,0 +1,139 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +#![allow(clippy::print_stdout)] // this is a test module + +use core::{net::SocketAddr, str::FromStr}; +use std::sync::Arc; +use std::{env, path::Path, println}; + +use futures_util::StreamExt; +use rustls::{ + ClientConfig, KeyLogFile, + pki_types::{ + CertificateDer, PrivateKeyDer, + pem::{self, PemObject}, + }, + sign::{CertifiedKey, SingleCertAndKey}, +}; +use test_support::subscribe; + +use crate::{ + proto::{ + op::{Message, Query}, + rr::{Name, RecordType}, + }, + quic::QuicClientStreamBuilder, + tls::default_provider, + xfer::DnsRequestSender, +}; + +use super::quic_server::QuicServer; + +async fn server_responder(mut server: QuicServer) { + while let Some((mut conn, addr)) = server + .next() + .await + .expect("failed to get next quic session") + { + println!("received client request {addr}"); + + while let Some(stream) = conn.next().await { + let mut stream = stream.expect("new client stream failed"); + + let bytes = stream.receive_bytes().await.expect("failed to receive"); + let client_message = Message::from_vec(&bytes).expect("failed to parse message"); + + // just respond with the same message converted to a response. + stream + .send(client_message.into_response()) + .await + .expect("failed to send response") + } + } +} + +#[tokio::test] +async fn test_quic_stream() { + subscribe(); + + let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned()); + println!("using server src path: {server_path}"); + + let ca = read_certs(format!("{server_path}/tests/test-data/ca.pem")).unwrap(); + let cert_chain = read_certs(format!("{server_path}/tests/test-data/cert.pem")).unwrap(); + + let key = + PrivateKeyDer::from_pem_file(format!("{server_path}/tests/test-data/cert.key")).unwrap(); + + let certificate_and_key = SingleCertAndKey::from( + CertifiedKey::from_der(cert_chain, key, &default_provider()).unwrap(), + ); + + // All testing is only done on local addresses, construct the server + let quic_ns = QuicServer::new( + SocketAddr::from(([127, 0, 0, 1], 0)), + Arc::new(certificate_and_key), + ) + .await + .expect("failed to initialize QuicServer"); + + // kick off the server + let server_addr = quic_ns.local_addr().expect("no address"); + println!("testing quic on: {server_addr}"); + let server_join = tokio::spawn(server_responder(quic_ns)); + + // now construct the client + let mut roots = rustls::RootCertStore::empty(); + let (_, ignored) = roots.add_parsable_certificates(ca.into_iter()); + assert_eq!(ignored, 0); + + let mut client_config = ClientConfig::builder_with_provider(Arc::new(default_provider())) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(roots) + .with_no_client_auth(); + + client_config.key_log = Arc::new(KeyLogFile::new()); + + println!("starting quic connect"); + let builder = QuicClientStreamBuilder::default().crypto_config(client_config); + let mut client_stream = builder + .build(server_addr, Arc::from("ns.example.com")) + .await + .expect("failed to connect"); + + println!("connected client to server"); + + // create a test message, send and then receive... + let mut message = Message::query(); + message.add_query(Query::query( + Name::from_str("www.example.test.").unwrap(), + RecordType::AAAA, + )); + message.metadata.id = 0; // RFC: DNS over QUIC requires the Message ID to be 0 + + // TODO: we should make the finalizer easier to call so this round-trip serialization isn't necessary. + let bytes = message.to_vec().unwrap(); + let message = Message::from_vec(&bytes).unwrap(); + + let response = client_stream + .send_message(message.clone().into()) + .next() + .await + .expect("no response received") + .expect("failed to read response"); + + assert_eq!(*response, message.into_response()); + + // and finally kill the server + server_join.abort(); +} + +fn read_certs(cert_path: impl AsRef) -> Result>, pem::Error> { + CertificateDer::pem_file_iter(cert_path)?.collect::, _>>() +} diff --git a/vendor/hickory-dns/hickory-net/src/runtime.rs b/vendor/hickory-dns/hickory-net/src/runtime.rs new file mode 100644 index 00000000..b07fc9a3 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/runtime.rs @@ -0,0 +1,396 @@ +//! Abstractions to deal with different async runtimes. + +use core::future::Future; +use core::marker::Send; +use core::net::SocketAddr; +use core::pin::Pin; +use core::time::Duration; +#[cfg(feature = "__quic")] +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + future::poll_fn, + io, + task::{Context, Poll}, +}; + +use async_trait::async_trait; +use futures_io::{AsyncRead, AsyncWrite}; +#[cfg(any(test, feature = "tokio"))] +use tokio::runtime::Runtime; +#[cfg(any(test, feature = "tokio"))] +use tokio::task::JoinHandle; + +/// Spawn a background task, if it was present +#[cfg(any(test, feature = "tokio"))] +pub fn spawn_bg + Send + 'static, R: Send + 'static>( + runtime: &Runtime, + background: F, +) -> JoinHandle { + runtime.spawn(background) +} + +#[cfg(feature = "tokio")] +#[doc(hidden)] +pub mod iocompat { + use core::pin::Pin; + use core::task::{Context, Poll}; + use std::io::{self, IoSlice}; + + use futures_io::{AsyncRead, AsyncWrite}; + use tokio::io::{AsyncRead as TokioAsyncRead, AsyncWrite as TokioAsyncWrite, ReadBuf}; + + /// Conversion from `tokio::io::{AsyncRead, AsyncWrite}` to `std::io::{AsyncRead, AsyncWrite}` + pub struct AsyncIoTokioAsStd(pub T); + + impl Unpin for AsyncIoTokioAsStd {} + impl AsyncRead for AsyncIoTokioAsStd { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut buf = ReadBuf::new(buf); + let polled = Pin::new(&mut self.0).poll_read(cx, &mut buf); + + polled.map_ok(|_| buf.filled().len()) + } + } + + impl AsyncWrite for AsyncIoTokioAsStd { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + Pin::new(&mut self.0).poll_write_vectored(cx, bufs) + } + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0).poll_flush(cx) + } + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + } + + /// Conversion from `std::io::{AsyncRead, AsyncWrite}` to `tokio::io::{AsyncRead, AsyncWrite}` + pub struct AsyncIoStdAsTokio(pub T); + + impl Unpin for AsyncIoStdAsTokio {} + impl TokioAsyncRead for AsyncIoStdAsTokio { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.get_mut().0) + .poll_read(cx, buf.initialized_mut()) + .map_ok(|len| buf.advance(len)) + } + } + + impl TokioAsyncWrite for AsyncIoStdAsTokio { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.get_mut().0).poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().0).poll_flush(cx) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.get_mut().0).poll_close(cx) + } + } +} + +#[cfg(feature = "tokio")] +#[allow(unreachable_pub)] +mod tokio_runtime { + use std::sync::Arc; + use std::sync::Mutex; + + #[cfg(feature = "__quic")] + use quinn::Runtime; + use tokio::net::{TcpSocket, TcpStream, UdpSocket as TokioUdpSocket}; + use tokio::task::JoinSet; + use tokio::time::timeout; + use tracing::debug; + + use super::iocompat::AsyncIoTokioAsStd; + use super::*; + use crate::xfer::CONNECT_TIMEOUT; + + /// A handle to the Tokio runtime + #[derive(Clone, Default)] + pub struct TokioHandle { + join_set: Arc>>, + } + + impl Spawn for TokioHandle { + fn spawn_bg(&mut self, future: impl Future + Send + 'static) { + let mut join_set = self.join_set.lock().unwrap(); + join_set.spawn(future); + reap_tasks(&mut join_set); + } + } + + /// The Tokio Runtime for async execution + #[derive(Clone, Default)] + pub struct TokioRuntimeProvider(TokioHandle); + + impl TokioRuntimeProvider { + /// Create a Tokio runtime + pub fn new() -> Self { + Self::default() + } + } + + impl RuntimeProvider for TokioRuntimeProvider { + type Handle = TokioHandle; + type Timer = TokioTime; + type Udp = TokioUdpSocket; + type Tcp = AsyncIoTokioAsStd; + + fn create_handle(&self) -> Self::Handle { + self.0.clone() + } + + fn connect_tcp( + &self, + server_addr: SocketAddr, + bind_addr: Option, + wait_for: Option, + ) -> Pin>>> { + Box::pin(async move { + let socket = match server_addr { + SocketAddr::V4(_) => TcpSocket::new_v4(), + SocketAddr::V6(_) => TcpSocket::new_v6(), + }?; + + if let Some(bind_addr) = bind_addr { + socket.bind(bind_addr)?; + } + + socket.set_nodelay(true)?; + let future = socket.connect(server_addr); + let wait_for = wait_for.unwrap_or(CONNECT_TIMEOUT); + match timeout(wait_for, future).await { + Ok(Ok(socket)) => Ok(AsyncIoTokioAsStd(socket)), + Ok(Err(e)) => Err(e), + Err(_) => { + debug!(%server_addr, "TCP connect timeout"); + Err(io::Error::new( + io::ErrorKind::TimedOut, + "TCP connect timed out", + )) + } + } + }) + } + + fn bind_udp( + &self, + local_addr: SocketAddr, + _server_addr: SocketAddr, + ) -> Pin>>> { + Box::pin(async move { tokio::net::UdpSocket::bind(local_addr).await }) + } + + #[cfg(feature = "__quic")] + fn quic_binder(&self) -> Option<&dyn QuicSocketBinder> { + Some(&TokioQuicSocketBinder) + } + } + + /// Reap finished tasks from a `JoinSet`, without awaiting or blocking. + fn reap_tasks(join_set: &mut JoinSet<()>) { + while join_set.try_join_next().is_some() {} + } + + #[cfg(feature = "__quic")] + struct TokioQuicSocketBinder; + + #[cfg(feature = "__quic")] + impl QuicSocketBinder for TokioQuicSocketBinder { + fn bind_quic( + &self, + local_addr: SocketAddr, + _server_addr: SocketAddr, + ) -> Result, io::Error> { + let socket = std::net::UdpSocket::bind(local_addr)?; + quinn::TokioRuntime.wrap_udp_socket(socket) + } + } +} + +#[cfg(feature = "tokio")] +pub use tokio_runtime::{TokioHandle, TokioRuntimeProvider}; + +/// RuntimeProvider defines which async runtime that handles IO and timers. +pub trait RuntimeProvider: Clone + Send + Sync + Unpin + 'static { + /// Handle to the executor; + type Handle: Clone + Send + Spawn + Sync + Unpin; + + /// Timer + type Timer: Time; + + /// UdpSocket + type Udp: DnsUdpSocket; + + /// TcpStream + type Tcp: DnsTcpStream; + + /// Create a runtime handle + fn create_handle(&self) -> Self::Handle; + + /// Create a TCP connection with custom configuration. + fn connect_tcp( + &self, + server_addr: SocketAddr, + bind_addr: Option, + timeout: Option, + ) -> Pin>>>; + + /// Create a UDP socket bound to `local_addr`. The returned value should **not** be connected to `server_addr`. + /// *Notice: the future should be ready once returned at best effort. Otherwise UDP DNS may need much more retries.* + fn bind_udp( + &self, + local_addr: SocketAddr, + server_addr: SocketAddr, + ) -> Pin>>>; + + /// Yields an object that knows how to bind a QUIC socket. + // + // Use some indirection here to avoid exposing the `quinn` crate in the public API + // even for runtimes that might not (want to) provide QUIC support. + fn quic_binder(&self) -> Option<&dyn QuicSocketBinder> { + None + } +} + +/// Trait for DnsUdpSocket +#[async_trait] +pub trait DnsUdpSocket +where + Self: Send + Sync + Sized + Unpin, +{ + /// Time implementation used for this type + type Time: Time; + + /// Poll once Receive data from the socket and returns the number of bytes read and the address from + /// where the data came on success. + fn poll_recv_from( + &self, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll>; + + /// Receive data from the socket and returns the number of bytes read and the address from + /// where the data came on success. + async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { + poll_fn(|cx| self.poll_recv_from(cx, buf)).await + } + + /// Poll once to send data to the given address. + fn poll_send_to( + &self, + cx: &mut Context<'_>, + buf: &[u8], + target: SocketAddr, + ) -> Poll>; + + /// Send data to the given address. + async fn send_to(&self, buf: &[u8], target: SocketAddr) -> io::Result { + poll_fn(|cx| self.poll_send_to(cx, buf, target)).await + } +} + +/// Noop trait for when the `quinn` dependency is not available. +#[cfg(not(feature = "__quic"))] +pub trait QuicSocketBinder {} + +/// Create a UDP socket for QUIC usage. +/// This trait is designed for customization. +#[cfg(feature = "__quic")] +pub trait QuicSocketBinder { + /// Create a UDP socket for QUIC usage. + fn bind_quic( + &self, + _local_addr: SocketAddr, + _server_addr: SocketAddr, + ) -> Result, io::Error>; +} + +/// Trait for TCP connection +pub trait DnsTcpStream: AsyncRead + AsyncWrite + Unpin + Send + Sync + Sized + 'static { + /// Timer type to use with this TCP stream type + type Time: Time; +} + +/// A type defines the Handle which can spawn future. +pub trait Spawn { + /// Spawn a future in the background + fn spawn_bg(&mut self, future: impl Future + Send + 'static); +} + +/// Generic Time for Delay and Timeout. +// This trait is created to allow to use different types of time systems. It's used in Fuchsia OS, please be mindful when update it. +#[async_trait] +pub trait Time: Send + Sync + Unpin { + /// Return a type that implements `Future` that will wait until the specified duration has + /// elapsed. + async fn delay_for(duration: Duration); + + /// Return a type that implement `Future` to complete before the specified duration has elapsed. + async fn timeout( + duration: Duration, + future: F, + ) -> Result; + + /// Get the current time as a Unix timestamp. + /// + /// This returns the number of seconds since the Unix epoch. + fn current_time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } +} + +/// New type which is implemented using tokio::time::{Delay, Timeout} +#[cfg(any(test, feature = "tokio"))] +#[derive(Clone, Copy, Debug)] +pub struct TokioTime; + +#[cfg(any(test, feature = "tokio"))] +#[async_trait] +impl Time for TokioTime { + async fn delay_for(duration: Duration) { + tokio::time::sleep(duration).await + } + + async fn timeout( + duration: Duration, + future: F, + ) -> Result { + tokio::time::timeout(duration, future) + .await + .map_err(move |_| io::Error::new(io::ErrorKind::TimedOut, "future timed out")) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/tcp/mod.rs b/vendor/hickory-dns/hickory-net/src/tcp/mod.rs new file mode 100644 index 00000000..7fdaff05 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/tcp/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2015 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! TCP protocol related components for DNS +mod tcp_client_stream; +mod tcp_stream; +#[cfg(test)] +#[allow(clippy::print_stdout)] +mod tests; + +pub use self::tcp_client_stream::TcpClientStream; +pub use self::tcp_stream::TcpStream; diff --git a/vendor/hickory-dns/hickory-net/src/tcp/tcp_client_stream.rs b/vendor/hickory-dns/hickory-net/src/tcp/tcp_client_stream.rs new file mode 100644 index 00000000..92a30a03 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/tcp/tcp_client_stream.rs @@ -0,0 +1,161 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::net::SocketAddr; +use core::pin::Pin; +use core::task::{Context, Poll, ready}; +use core::time::Duration; +use std::future::Future; + +use futures_util::{StreamExt, stream::Stream}; +use tracing::warn; + +use crate::error::NetError; +use crate::proto::op::SerialMessage; +#[cfg(feature = "tokio")] +use crate::runtime::TokioTime; +#[cfg(feature = "tokio")] +use crate::runtime::iocompat::AsyncIoTokioAsStd; +use crate::runtime::{DnsTcpStream, RuntimeProvider, Spawn}; +use crate::tcp::TcpStream; +use crate::xfer::{DnsClientStream, DnsExchange}; +use crate::{BufDnsStreamHandle, DnsMultiplexer}; + +/// Tcp client stream +/// +/// Use with `hickory_client::client::DnsMultiplexer` impls +#[must_use = "futures do nothing unless polled"] +pub struct TcpClientStream +where + S: DnsTcpStream, +{ + tcp_stream: TcpStream, +} + +impl TcpClientStream { + /// Create a new [`DnsExchange`] wrapped around a multiplexed [`TcpClientStream`] + /// + /// # Arguments + /// + /// * `remote_addr` - Address of the remote nameserver + /// * `bind_addr` - Optional local address to bind to + /// * `timeout` - Timeout for requests + /// * `max_active_requests` - Optional limit on concurrent in-flight requests. + /// If `None`, uses the default (32). + /// * `provider` - Runtime provider for spawning background tasks + pub async fn exchange>( + remote_addr: SocketAddr, + bind_addr: Option, + timeout: Duration, + max_active_requests: Option, + provider: P, + ) -> Result, NetError> { + let mut handle = provider.create_handle(); + let (future, sender) = Self::new(remote_addr, bind_addr, Some(timeout), provider); + + // TODO: need config for Signer... + let mut multiplexer = DnsMultiplexer::new(future.await?, sender).with_timeout(timeout); + if let Some(max) = max_active_requests { + multiplexer = multiplexer.with_max_active_requests(max); + } + let (exchange, bg) = DnsExchange::from_stream(multiplexer); + handle.spawn_bg(bg); + Ok(exchange) + } + + /// Create a new TcpClientStream + pub fn new>( + peer_addr: SocketAddr, + bind_addr: Option, + timeout: Option, + provider: P, + ) -> ( + impl Future> + Send + 'static, + BufDnsStreamHandle, + ) { + let (sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr); + ( + async move { + let tcp = provider.connect_tcp(peer_addr, bind_addr, timeout).await?; + Ok(Self::from_stream(TcpStream::from_stream_with_receiver( + tcp, + peer_addr, + outbound_messages, + ))) + }, + sender, + ) + } + + /// Wraps the TcpStream in TcpClientStream + pub fn from_stream(tcp_stream: TcpStream) -> Self { + Self { tcp_stream } + } +} + +impl DnsClientStream for TcpClientStream { + type Time = S::Time; + + fn name_server_addr(&self) -> SocketAddr { + self.tcp_stream.peer_addr() + } +} + +impl Stream for TcpClientStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let message = match ready!(self.tcp_stream.poll_next_unpin(cx)) { + Some(Ok(t)) => t, + Some(Err(e)) => return Poll::Ready(Some(Err(NetError::from(e)))), + None => return Poll::Ready(None), + }; + + // this is busted if the tcp connection doesn't have a peer + let peer = self.tcp_stream.peer_addr(); + if message.addr() != peer { + // TODO: this should be an error, right? + warn!("{} does not match name_server: {}", message.addr(), peer) + } + + Poll::Ready(Some(Ok(message))) + } +} + +#[cfg(feature = "tokio")] +impl DnsTcpStream for AsyncIoTokioAsStd +where + T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + Sized + 'static, +{ + type Time = TokioTime; +} + +#[cfg(test)] +#[cfg(feature = "tokio")] +mod tests { + use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use test_support::subscribe; + + use crate::runtime::TokioRuntimeProvider; + use crate::tcp::tests::tcp_client_stream_test; + #[tokio::test] + async fn test_tcp_stream_ipv4() { + subscribe(); + tcp_client_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await; + } + + #[tokio::test] + async fn test_tcp_stream_ipv6() { + subscribe(); + tcp_client_stream_test( + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + TokioRuntimeProvider::new(), + ) + .await; + } +} diff --git a/vendor/hickory-dns/hickory-net/src/tcp/tcp_stream.rs b/vendor/hickory-dns/hickory-net/src/tcp/tcp_stream.rs new file mode 100644 index 00000000..c928edb6 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/tcp/tcp_stream.rs @@ -0,0 +1,421 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! This module contains all the TCP structures for demuxing TCP into streams of DNS packets. + +use core::mem; +use core::net::SocketAddr; +use core::pin::Pin; +use core::task::{Context, Poll}; +use core::time::Duration; +use std::io; + +use futures_io::IoSlice; +use futures_util::stream::Stream; +use futures_util::{self, future::Future, ready}; +use tracing::{debug, trace}; + +use crate::proto::op::SerialMessage; +use crate::runtime::{DnsTcpStream, Time}; +use crate::xfer::{BufDnsStreamHandle, StreamReceiver}; + +/// Current state while writing to the remote of the TCP connection +enum WriteTcpState { + /// Currently writing the length of bytes to of the buffer. + LenBytes { + /// Current position in the length buffer being written + pos: usize, + /// Length of the buffer + length: [u8; 2], + /// Buffer to write after the length + bytes: Vec, + }, + /// Currently writing the buffer to the remote + Bytes { + /// Current position in the buffer written + pos: usize, + /// Buffer to write to the remote + bytes: Vec, + }, + /// Currently flushing the bytes to the remote + Flushing, +} + +/// Current state of a TCP stream as it's being read. +pub(crate) enum ReadTcpState { + /// Currently reading the length of the TCP packet + LenBytes { + /// Current position in the buffer + pos: usize, + /// Buffer of the length to read + bytes: [u8; 2], + }, + /// Currently reading the bytes of the DNS packet + Bytes { + /// Current position while reading the buffer + pos: usize, + /// buffer being read into + bytes: Vec, + }, +} + +/// A Stream used for sending data to and from a remote DNS endpoint (client or server). +#[must_use = "futures do nothing unless polled"] +pub struct TcpStream { + socket: S, + outbound_messages: StreamReceiver, + send_state: Option, + read_state: ReadTcpState, + peer_addr: SocketAddr, +} + +impl TcpStream { + /// Returns the address of the peer connection. + pub fn peer_addr(&self) -> SocketAddr { + self.peer_addr + } + + fn pollable_split( + &mut self, + ) -> ( + &mut S, + &mut StreamReceiver, + &mut Option, + &mut ReadTcpState, + ) { + ( + &mut self.socket, + &mut self.outbound_messages, + &mut self.send_state, + &mut self.read_state, + ) + } + + /// Initializes a TcpStream with an established connection. + /// + /// Uses the default buffer size (32) for the outbound message queue. + /// + /// # Arguments + /// + /// * `stream` - the established IO stream for communication + /// * `peer_addr` - address of the remote peer + pub fn from_stream(stream: S, peer_addr: SocketAddr) -> (Self, BufDnsStreamHandle) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr); + let stream = Self::from_stream_with_receiver(stream, peer_addr, outbound_messages); + (stream, message_sender) + } + + /// Initializes a TcpStream with an established connection and explicit buffer size. + /// + /// Use this when you need a larger buffer to handle high message rates without + /// dropping messages due to backpressure. + /// + /// # Arguments + /// + /// * `stream` - the established IO stream for communication + /// * `peer_addr` - address of the remote peer + /// * `buffer_size` - maximum number of messages that can be queued for sending + pub fn from_stream_with_buffer_size( + stream: S, + peer_addr: SocketAddr, + buffer_size: usize, + ) -> (Self, BufDnsStreamHandle) { + let (message_sender, outbound_messages) = + BufDnsStreamHandle::with_buffer_size(peer_addr, buffer_size); + let stream = Self::from_stream_with_receiver(stream, peer_addr, outbound_messages); + (stream, message_sender) + } + + /// Wraps a stream where a sender and receiver have already been established + pub fn from_stream_with_receiver( + socket: S, + peer_addr: SocketAddr, + outbound_messages: StreamReceiver, + ) -> Self { + Self { + socket, + outbound_messages, + send_state: None, + read_state: ReadTcpState::LenBytes { + pos: 0, + bytes: [0u8; 2], + }, + peer_addr, + } + } + + /// Creates a new future of the eventually establish a IO stream connection or fail trying + /// + /// # Arguments + /// + /// * `future` - underlying stream future which this tcp stream relies on + /// * `name_server` - the IP and Port of the DNS server to connect to + /// * `timeout` - connection timeout + pub fn with_future> + Send + 'static>( + future: F, + name_server: SocketAddr, + timeout: Duration, + ) -> ( + impl Future> + Send, + BufDnsStreamHandle, + ) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(name_server); + let stream_fut = Self::connect_with_future(future, name_server, timeout, outbound_messages); + + (stream_fut, message_sender) + } + + async fn connect_with_future> + Send + 'static>( + future: F, + name_server: SocketAddr, + timeout: Duration, + outbound_messages: StreamReceiver, + ) -> Result { + let socket = S::Time::timeout(timeout, future).await??; + debug!("TCP connection established to: {}", name_server); + Ok(Self { + socket, + outbound_messages, + send_state: None, + read_state: ReadTcpState::LenBytes { + pos: 0, + bytes: [0u8; 2], + }, + peer_addr: name_server, + }) + } +} + +impl Stream for TcpStream { + type Item = io::Result; + + #[allow(clippy::cognitive_complexity)] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let peer = self.peer_addr; + let (socket, outbound_messages, send_state, read_state) = self.pollable_split(); + let mut socket = Pin::new(socket); + let mut outbound_messages = Pin::new(outbound_messages); + + // this will not accept incoming data while there is data to send + // makes this self throttling. + // TODO: it might be interesting to try and split the sending and receiving futures. + loop { + // in the case we are sending, send it all? + if send_state.is_some() { + // sending... + match send_state { + Some(WriteTcpState::LenBytes { pos, length, bytes }) => { + let wrote = ready!(socket.as_mut().poll_write_vectored( + cx, + &[IoSlice::new(&length[*pos..]), IoSlice::new(bytes)] + ))?; + *pos += wrote; + } + Some(WriteTcpState::Bytes { pos, bytes }) => { + let wrote = ready!(socket.as_mut().poll_write(cx, &bytes[*pos..]))?; + *pos += wrote; + } + Some(WriteTcpState::Flushing) => { + ready!(socket.as_mut().poll_flush(cx))?; + } + _ => (), + } + + // get current state + let current_state = send_state.take(); + + // switch states + match current_state { + Some(WriteTcpState::LenBytes { pos, length, bytes }) => { + if pos < length.len() { + *send_state = Some(WriteTcpState::LenBytes { pos, length, bytes }); + } else if pos < length.len() + bytes.len() { + *send_state = Some(WriteTcpState::Bytes { + pos: pos - length.len(), + bytes, + }); + } else { + *send_state = Some(WriteTcpState::Flushing); + } + } + Some(WriteTcpState::Bytes { pos, bytes }) => { + if pos < bytes.len() { + *send_state = Some(WriteTcpState::Bytes { pos, bytes }); + } else { + // At this point we successfully delivered the entire message. + // flush + *send_state = Some(WriteTcpState::Flushing); + } + } + Some(WriteTcpState::Flushing) => { + // At this point we successfully delivered the entire message. + send_state.take(); + } + None => (), + }; + } else { + // then see if there is more to send + match outbound_messages.as_mut().poll_next(cx) + // .map_err(|()| io::Error::new(io::ErrorKind::Other, "unknown"))? + { + // already handled above, here to make sure the poll() pops the next message + Poll::Ready(Some(message)) => { + // if there is no peer, this connection should die... + let (buffer, dst) = message.into(); + + // This is an error if the destination is not our peer (this is TCP after all) + // This will kill the connection... + if peer != dst { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("mismatched peer: {peer} and dst: {dst}"), + )))); + } + + // will return if the socket will block + // the length is 16 bits + let len = u16::to_be_bytes(buffer.len() as u16); + + debug!("sending message len: {} to: {}", buffer.len(), dst); + *send_state = Some(WriteTcpState::LenBytes { + pos: 0, + length: len, + bytes: buffer, + }); + } + // now we get to drop through to the receives... + // TODO: should we also return None if there are no more messages to send? + Poll::Pending => break, + Poll::Ready(None) => { + debug!("no messages to send"); + break; + } + } + } + } + + let mut ret_buf = None; + + // this will loop while there is data to read, or the data has been read, or an IO + // event would block + while ret_buf.is_none() { + // Evaluates the next state. If None is the result, then no state change occurs, + // if Some(_) is returned, then that will be used as the next state. + let new_state: Option = match read_state { + ReadTcpState::LenBytes { pos, bytes } => { + // debug!("reading length {}", bytes.len()); + let read = ready!(socket.as_mut().poll_read(cx, &mut bytes[*pos..]))?; + if read == 0 { + // the Stream was closed! + debug!("zero bytes read, stream closed?"); + //try!(self.socket.shutdown(Shutdown::Both)); // TODO: add generic shutdown function + + if *pos == 0 { + // Since this is the start of the next message, we have a clean end + return Poll::Ready(None); + } else { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "closed while reading length", + )))); + } + } + trace!("in ReadTcpState::LenBytes: {}", pos); + *pos += read; + + if *pos < bytes.len() { + trace!("remain ReadTcpState::LenBytes: {}", pos); + None + } else { + let length = u16::from_be_bytes(*bytes); + trace!("got length: {}", length); + let mut bytes = vec![0; length as usize]; + bytes.resize(length as usize, 0); + + trace!("move ReadTcpState::Bytes: {}", bytes.len()); + Some(ReadTcpState::Bytes { pos: 0, bytes }) + } + } + ReadTcpState::Bytes { pos, bytes } => { + let read = ready!(socket.as_mut().poll_read(cx, &mut bytes[*pos..]))?; + if read == 0 { + // the Stream was closed! + trace!("zero bytes read for message, stream closed?"); + + // Since this is the start of the next message, we have a clean end + // try!(self.socket.shutdown(Shutdown::Both)); // TODO: add generic shutdown function + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "closed while reading message", + )))); + } + + trace!("in ReadTcpState::Bytes: {}", bytes.len()); + *pos += read; + + if *pos < bytes.len() { + trace!("remain ReadTcpState::Bytes: {}", bytes.len()); + None + } else { + trace!("reset ReadTcpState::LenBytes: {}", 0); + Some(ReadTcpState::LenBytes { + pos: 0, + bytes: [0u8; 2], + }) + } + } + }; + + // this will move to the next state, + // if it was a completed receipt of bytes, then it will move out the bytes + if let Some(state) = new_state { + if let ReadTcpState::Bytes { pos, bytes } = mem::replace(read_state, state) { + assert_eq!(pos, bytes.len()); + ret_buf = Some(bytes); + } + } + } + + // if the buffer is ready, return it, if not we're Pending + if let Some(buffer) = ret_buf { + let src_addr = self.peer_addr; + Poll::Ready(Some(Ok(SerialMessage::new(buffer, src_addr)))) + } else { + debug!("bottomed out"); + // at a minimum the outbound_messages should have been polled, + // which will wake this future up later... + Poll::Pending + } + } +} + +#[cfg(test)] +#[cfg(feature = "tokio")] +mod tests { + use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use test_support::subscribe; + + use crate::runtime::TokioRuntimeProvider; + use crate::tcp::tests::tcp_stream_test; + + #[tokio::test] + async fn test_tcp_stream_ipv4() { + subscribe(); + tcp_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await; + } + + #[tokio::test] + async fn test_tcp_stream_ipv6() { + subscribe(); + tcp_stream_test( + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + TokioRuntimeProvider::new(), + ) + .await; + } +} diff --git a/vendor/hickory-dns/hickory-net/src/tcp/tests.rs b/vendor/hickory-dns/hickory-net/src/tcp/tests.rs new file mode 100644 index 00000000..b45c6057 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/tcp/tests.rs @@ -0,0 +1,148 @@ +use core::net::{IpAddr, SocketAddr}; +use core::sync::atomic::AtomicBool; +use std::io::{Read, Write}; +use std::println; +use std::sync::Arc; + +use futures_util::stream::StreamExt; + +use crate::proto::op::SerialMessage; +use crate::runtime::RuntimeProvider; +use crate::tcp::TcpStream; +use crate::xfer::dns_handle::DnsStreamHandle; + +const TEST_BYTES: &[u8; 8] = b"DEADBEEF"; +const TEST_BYTES_LEN: usize = 8; +const SEND_RECV_TIMES: usize = 4; + +fn tcp_server_setup( + server_name: &str, + server_addr: IpAddr, +) -> (Arc, std::thread::JoinHandle<()>, SocketAddr) { + let succeeded = Arc::new(AtomicBool::new(false)); + let succeeded_clone = succeeded.clone(); + std::thread::Builder::new() + .name("thread_killer".to_string()) + .spawn(move || { + let succeeded = succeeded_clone; + for _ in 0..15 { + std::thread::sleep(core::time::Duration::from_secs(1)); + if succeeded.load(core::sync::atomic::Ordering::Relaxed) { + return; + } + } + + println!("Thread Killer has been awoken, killing process"); + std::process::exit(-1); + }) + .expect("Thread spawning failed"); + + // TODO: need a timeout on listen + let server = std::net::TcpListener::bind(SocketAddr::new(server_addr, 0)) + .expect("Unable to bind a TCP socket"); + let server_addr = server.local_addr().unwrap(); + + // an in and out server + let server_handle = std::thread::Builder::new() + .name(server_name.to_string()) + .spawn(move || { + let (mut socket, _) = server.accept().expect("accept failed"); + + socket + .set_read_timeout(Some(core::time::Duration::from_secs(5))) + .unwrap(); // should receive something within 5 seconds... + socket + .set_write_timeout(Some(core::time::Duration::from_secs(5))) + .unwrap(); // should receive something within 5 seconds... + + for _ in 0..SEND_RECV_TIMES { + // wait for some bytes... + let mut len_bytes = [0_u8; 2]; + socket + .read_exact(&mut len_bytes) + .expect("SERVER: receive failed"); + let length = + (u16::from(len_bytes[0]) << 8) & 0xFF00 | u16::from(len_bytes[1]) & 0x00FF; + assert_eq!(length as usize, TEST_BYTES_LEN); + + let mut buffer = [0_u8; TEST_BYTES_LEN]; + socket.read_exact(&mut buffer).unwrap(); + + // println!("read bytes iter: {}", i); + assert_eq!(&buffer, TEST_BYTES); + + // bounce them right back... + socket + .write_all(&len_bytes) + .expect("SERVER: send length failed"); + socket + .write_all(&buffer) + .expect("SERVER: send buffer failed"); + // println!("wrote bytes iter: {}", i); + std::thread::yield_now(); + } + }) + .unwrap(); + (succeeded, server_handle, server_addr) +} + +/// Test tcp_stream. +pub(super) async fn tcp_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) { + let (succeeded, server_handle, server_addr) = + tcp_server_setup("test_tcp_stream:server", server_addr); + + // setup the client, which is going to run on the testing thread... + + let tcp = provider + .connect_tcp(server_addr, None, None) + .await + .expect("connect failed"); + let (mut stream, mut sender) = TcpStream::from_stream(tcp, server_addr); + + for _ in 0..SEND_RECV_TIMES { + // test once + sender + .send(SerialMessage::new(TEST_BYTES.to_vec(), server_addr)) + .expect("send failed"); + + let (buffer, stream_tmp) = stream.into_future().await; + stream = stream_tmp; + let message = buffer + .expect("no buffer received") + .expect("error receiving buffer"); + assert_eq!(message.bytes(), TEST_BYTES); + } + + succeeded.store(true, core::sync::atomic::Ordering::Relaxed); + server_handle.join().expect("server thread failed"); +} + +/// Test tcp_client_stream. +pub(super) async fn tcp_client_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) { + let (succeeded, server_handle, server_addr) = + tcp_server_setup("test_tcp_client_stream:server", server_addr); + + // setup the client, which is going to run on the testing thread... + + let tcp = provider + .connect_tcp(server_addr, None, None) + .await + .expect("connect failed"); + let (mut stream, mut sender) = TcpStream::from_stream(tcp, server_addr); + + for _ in 0..SEND_RECV_TIMES { + // test once + sender + .send(SerialMessage::new(TEST_BYTES.to_vec(), server_addr)) + .expect("send failed"); + let (buffer, stream_tmp) = stream.into_future().await; + stream = stream_tmp; + let buffer = buffer + .expect("no buffer received") + .expect("error receiving buffer"); + assert_eq!(buffer.bytes(), TEST_BYTES); + } + + succeeded.store(true, core::sync::atomic::Ordering::Relaxed); + server_handle.join().expect("server thread failed"); +} diff --git a/vendor/hickory-dns/hickory-net/src/tls.rs b/vendor/hickory-dns/hickory-net/src/tls.rs new file mode 100644 index 00000000..890a7c22 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/tls.rs @@ -0,0 +1,287 @@ +// Copyright 2015-2021 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! TLS protocol related components for DNS over TLS + +use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration}; + +use futures_util::future::BoxFuture; +#[cfg(not(feature = "rustls-platform-verifier"))] +use rustls::RootCertStore; +use rustls::{ + ClientConfig, + crypto::{self, CryptoProvider}, + pki_types::ServerName, +}; +#[cfg(feature = "rustls-platform-verifier")] +use rustls_platform_verifier::BuilderVerifierExt; +use tokio::time::timeout; +use tokio_rustls::TlsConnector; +use tracing::debug; + +use crate::{ + error::NetError, + runtime::{ + DnsTcpStream, RuntimeProvider, Spawn, + iocompat::{AsyncIoStdAsTokio, AsyncIoTokioAsStd}, + }, + tcp::{TcpClientStream, TcpStream}, + xfer::{BufDnsStreamHandle, CONNECT_TIMEOUT, DnsExchange, DnsMultiplexer, StreamReceiver}, +}; + +/// Type of TlsClientStream used with Rustls +pub type TlsClientStream = + TcpClientStream>>>; + +/// Create a new [`DnsExchange`] wrapped around a multiplexed [`TlsClientStream`] +/// +/// # Arguments +/// +/// * `remote_addr` - Address of the remote nameserver +/// * `server_name` - TLS server name for certificate validation +/// * `config` - TLS client configuration +/// * `timeout` - Timeout for requests +/// * `max_active_requests` - Optional limit on concurrent in-flight requests. +/// If `None`, uses the default (32). +/// * `provider` - Runtime provider for spawning background tasks +pub async fn tls_exchange, S: DnsTcpStream>( + remote_addr: SocketAddr, + server_name: ServerName<'static>, + config: ClientConfig, + timeout: Duration, + max_active_requests: Option, + provider: P, +) -> Result, NetError> { + tls_exchange_with_bind_addr( + remote_addr, + None, + server_name, + config, + timeout, + max_active_requests, + provider, + ) + .await +} + +/// Create a new [`DnsExchange`] wrapped around a multiplexed [`TlsClientStream`], +/// optionally binding the underlying TCP socket to a local address. +/// +/// # Arguments +/// +/// * `remote_addr` - Address of the remote nameserver +/// * `bind_addr` - Local address to bind the outgoing TCP socket to. When `None`, the OS picks. +/// * `server_name` - TLS server name for certificate validation +/// * `config` - TLS client configuration +/// * `timeout` - Timeout for requests +/// * `max_active_requests` - Optional limit on concurrent in-flight requests. +/// If `None`, uses the default (32). +/// * `provider` - Runtime provider for spawning background tasks +#[allow(clippy::too_many_arguments)] +pub async fn tls_exchange_with_bind_addr, S: DnsTcpStream>( + remote_addr: SocketAddr, + bind_addr: Option, + server_name: ServerName<'static>, + mut config: ClientConfig, + timeout: Duration, + max_active_requests: Option, + provider: P, +) -> Result, NetError> { + // The port (853) of DOT is for dns dedicated, SNI is unnecessary. (ISP block by the SNI name) + config.enable_sni = false; + + let stream = provider + .connect_tcp(remote_addr, bind_addr, Some(timeout)) + .await?; + let (future, sender) = tls_client_connect_with_future( + stream, + remote_addr, + server_name.to_owned(), + Arc::new(config), + ); + + let mut multiplexer = DnsMultiplexer::new(future.await?, sender).with_timeout(timeout); + if let Some(max) = max_active_requests { + multiplexer = multiplexer.with_max_active_requests(max); + } + let (exchange, bg) = DnsExchange::

::from_stream(multiplexer); + provider.create_handle().spawn_bg(bg); + Ok(exchange) +} + +/// Creates a new TlsStream to the specified name_server +/// +/// # Arguments +/// +/// * `name_server` - IP and Port for the remote DNS resolver +/// * `bind_addr` - IP and port to connect from +/// * `dns_name` - The DNS name associated with a certificate +#[allow(clippy::type_complexity)] +pub fn tls_client_connect( + name_server: SocketAddr, + server_name: ServerName<'static>, + client_config: Arc, + provider: P, +) -> ( + BoxFuture<'static, Result, NetError>>, + BufDnsStreamHandle, +) { + tls_client_connect_with_bind_addr(name_server, None, server_name, client_config, provider) +} + +/// Creates a new TlsStream to the specified name_server connecting from a specific address. +/// +/// # Arguments +/// +/// * `name_server` - IP and Port for the remote DNS resolver +/// * `bind_addr` - IP and port to connect from +/// * `dns_name` - The DNS name associated with a certificate +#[allow(clippy::type_complexity)] +pub fn tls_client_connect_with_bind_addr( + name_server: SocketAddr, + bind_addr: Option, + server_name: ServerName<'static>, + client_config: Arc, + provider: P, +) -> ( + BoxFuture<'static, Result, NetError>>, + BufDnsStreamHandle, +) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(name_server); + let early_data_enabled = client_config.enable_early_data; + let tls_connector = TlsConnector::from(client_config).early_data(early_data_enabled); + + // This set of futures collapses the next tcp socket into a stream which can be used for + // sending and receiving tcp packets. + let stream = async move { + let tcp = provider.connect_tcp(name_server, bind_addr, None).await?; + connect_tls_stream( + tls_connector, + tcp, + name_server, + server_name, + outbound_messages, + ) + .await + }; + + let new_future = Box::pin(async { Ok(TcpClientStream::from_stream(stream.await?)) }); + + (new_future, message_sender) +} + +/// Creates a new TlsStream to the specified name_server connecting from a specific address. +/// +/// # Arguments +/// +/// * `future` - A future producing DnsTcpStream +/// * `dns_name` - The DNS name associated with a certificate +fn tls_client_connect_with_future( + stream: S, + socket_addr: SocketAddr, + server_name: ServerName<'static>, + client_config: Arc, +) -> ( + impl Future, NetError>> + Send + 'static, + BufDnsStreamHandle, +) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(socket_addr); + let early_data_enabled = client_config.enable_early_data; + let tls_connector = TlsConnector::from(client_config).early_data(early_data_enabled); + + // This set of futures collapses the next tcp socket into a stream which can be used for + // sending and receiving tcp packets. + let stream = async move { + connect_tls_stream( + tls_connector, + stream, + socket_addr, + server_name, + outbound_messages, + ) + .await + }; + + ( + async move { Ok(TcpClientStream::from_stream(stream.await?)) }, + message_sender, + ) +} + +pub(super) async fn connect_tls_stream( + tls_connector: TlsConnector, + stream: S, + name_server: SocketAddr, + server_name: ServerName<'static>, + outbound_messages: StreamReceiver, +) -> Result>>, NetError> { + let stream = AsyncIoStdAsTokio(stream); + let s = match timeout(CONNECT_TIMEOUT, tls_connector.connect(server_name, stream)).await { + Ok(Ok(s)) => s, + Ok(Err(e)) => return Err(NetError::from(e)), + Err(_) => { + debug!(%name_server, "TLS connect timeout"); + return Err(NetError::Timeout); + } + }; + + Ok(TcpStream::from_stream_with_receiver( + AsyncIoTokioAsStd(s), + name_server, + outbound_messages, + )) +} + +/// Initializes a TlsStream with an existing tokio_tls::TlsStream. +/// +/// This is intended for use with a TlsListener and Incoming connections +pub fn tls_from_stream( + stream: S, + peer_addr: SocketAddr, +) -> (TlsStream, BufDnsStreamHandle) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr); + let stream = TcpStream::from_stream_with_receiver(stream, peer_addr, outbound_messages); + (stream, message_sender) +} + +/// Make a new [`ClientConfig`] with the default settings +pub fn client_config() -> Result { + let builder = ClientConfig::builder_with_provider(Arc::new(default_provider())) + .with_safe_default_protocol_versions() + .unwrap(); + + #[cfg(feature = "rustls-platform-verifier")] + let builder = builder.with_platform_verifier()?; + #[cfg(not(feature = "rustls-platform-verifier"))] + let builder = builder.with_root_certificates({ + #[cfg_attr(not(feature = "webpki-roots"), allow(unused_mut))] + let mut root_store = RootCertStore::empty(); + #[cfg(feature = "webpki-roots")] + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + root_store + }); + + Ok(builder.with_no_client_auth()) +} + +/// Instantiate a new [`CryptoProvider`] for use with rustls +#[cfg(all(feature = "tls-aws-lc-rs", not(feature = "tls-ring")))] +pub fn default_provider() -> CryptoProvider { + crypto::aws_lc_rs::default_provider() +} + +/// Instantiate a new [`CryptoProvider`] for use with rustls +#[cfg(feature = "tls-ring")] +pub fn default_provider() -> CryptoProvider { + crypto::ring::default_provider() +} + +/// Predefined type for abstracting the base I/O TlsStream with TokioTls +pub type TlsStream = TcpStream; + +/// Predefined type for abstracting the TlsClientStream with TokioTls +pub type TokioTlsClientStream = tokio_rustls::client::TlsStream>; diff --git a/vendor/hickory-dns/hickory-net/src/udp/mod.rs b/vendor/hickory-dns/hickory-net/src/udp/mod.rs new file mode 100644 index 00000000..e961510c --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/udp/mod.rs @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! UDP protocol related components for DNS + +#[cfg(test)] +#[allow(clippy::print_stdout)] +mod tests; +mod udp_client_stream; +mod udp_stream; + +pub use self::udp_client_stream::{UdpClientStream, UdpClientStreamBuilder}; +pub use self::udp_stream::{UdpSocket, UdpStream}; + +/// Max size for the UDP receive buffer as recommended by +/// [RFC6891](https://datatracker.ietf.org/doc/html/rfc6891#section-6.2.5). +pub const MAX_RECEIVE_BUFFER_SIZE: usize = 4_096; diff --git a/vendor/hickory-dns/hickory-net/src/udp/tests.rs b/vendor/hickory-dns/hickory-net/src/udp/tests.rs new file mode 100644 index 00000000..2ca395b9 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/udp/tests.rs @@ -0,0 +1,302 @@ +use core::net::{IpAddr, Ipv4Addr, SocketAddr}; +use core::str::FromStr; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::time::Duration; +use std::net::{ToSocketAddrs, UdpSocket}; +use std::sync::Arc; +use std::{println, process, thread}; + +use futures_util::stream::StreamExt; +use tracing::debug; + +use crate::NetError; +use crate::proto::op::{DnsRequest, DnsRequestOptions, DnsResponse, Message, Query, SerialMessage}; +use crate::proto::rr::rdata::NULL; +use crate::proto::rr::{Name, RData, Record, RecordType}; +use crate::runtime::RuntimeProvider; +use crate::udp::{UdpClientStream, UdpStream}; +use crate::xfer::dns_handle::DnsStreamHandle; +use crate::xfer::{DnsRequestSender, FirstAnswer}; + +/// Test next random udpsocket. +pub(super) async fn next_random_socket_test(provider: impl RuntimeProvider) { + let (stream, _) = UdpStream::new( + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 52), + None, + None, + false, + provider, + ); + drop(stream.await.expect("failed to get next socket address")); +} + +/// Test udp_stream. +pub(super) async fn udp_stream_test(server_addr: IpAddr, provider: P) { + let stop_thread_killer = start_thread_killer(); + + let server = UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); // should receive something within 5 seconds... + server + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); // should receive something within 5 seconds... + let server_addr = server.local_addr().unwrap(); + println!("server listening on: {server_addr}"); + + let test_bytes: &'static [u8; 8] = b"DEADBEEF"; + let send_recv_times = 4u32; + + // an in and out server + let server_handle = thread::Builder::new() + .name("test_udp_stream_ipv4:server".to_string()) + .spawn(move || { + let mut buffer = [0_u8; 512]; + + for _ in 0..send_recv_times { + // wait for some bytes... + //println!("receiving message: {}", _i); + let (len, addr) = server.recv_from(&mut buffer).expect("receive failed"); + + assert_eq!(&buffer[0..len], test_bytes); + + //println!("sending message len back: {}", len); + // bounce them right back... + assert_eq!( + server.send_to(&buffer[0..len], addr).expect("send failed"), + len + ); + } + }) + .unwrap(); + + // setup the client, which is going to run on the testing thread... + // the tests should run within 5 seconds... right? + // TODO: add timeout here, so that test never hangs... + let client_addr = match server_addr { + SocketAddr::V4(_) => "127.0.0.1:0", + SocketAddr::V6(_) => "[::1]:0", + }; + + println!("binding client socket"); + let socket = provider + .bind_udp( + client_addr.to_socket_addrs().unwrap().next().unwrap(), + server_addr, + ) + .await + .expect("could not create socket"); // some random address... + println!("bound client socket"); + + let (mut stream, mut sender) = UdpStream::

::with_bound(socket, server_addr); + + for _i in 0..send_recv_times { + // test once + sender + .send(SerialMessage::new(test_bytes.to_vec(), server_addr)) + .unwrap(); + //println!("blocking on client stream: {}", _i); + let buffer_and_addr = stream.next().await; + //println!("got message"); + let message = buffer_and_addr.expect("no message").expect("io error"); + assert_eq!(message.bytes(), test_bytes); + assert_eq!(message.addr(), server_addr); + } + + stop_thread_killer.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread failed"); +} + +/// Test udp_client_stream. +#[allow(clippy::print_stdout)] +pub(super) async fn udp_client_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) { + udp_client_stream_test_inner( + server_addr, + provider, + "udp_client_stream", + 4, + 1, + |_, _| {}, + |response| match response { + Ok(response) => { + let response = Message::from(response); + if let RData::NULL(null) = &response.answers[0].data { + assert_eq!(null.anything, b"DEADBEEF"); + true + } else { + panic!("not a NULL response"); + } + } + Err(_) => false, + }, + ) + .await; +} + +/// Test udp_client_stream handling of bad response IDs +#[allow(clippy::print_stdout)] +pub(super) async fn udp_client_stream_bad_id_test( + server_addr: IpAddr, + provider: impl RuntimeProvider, +) { + udp_client_stream_test_inner( + server_addr, + provider, + "udp_client_stream_bad_id", + 1, + 1, + |idx, message| { + // Mutate the first response to have the wrong ID + if idx == 0 { + message.metadata.id = message.id.wrapping_add(1); + } + }, + |response| { + // The test should pass when we see a timeout after waiting for a response + // with a correct transaction ID + matches!(response, Err(NetError::Timeout)) + }, + ) + .await; +} + +pub(super) async fn udp_client_stream_empty_question_section_test( + server_addr: IpAddr, + provider: impl RuntimeProvider, +) { + udp_client_stream_test_inner( + server_addr, + provider, + "udp_client_stream_empty_question_section_test", + 1, + 1, + // Send a response with an empty question section. + |_, message| message.queries.clear(), + // We expect to see a timeout after waiting for a response with a matching question section. + |response_res| matches!(response_res, Err(NetError::Timeout)), + ) + .await; +} + +async fn udp_client_stream_test_inner( + server_addr: IpAddr, + provider: impl RuntimeProvider, + test_name: &str, + request_count: usize, + response_count: usize, + response_mutator: impl Fn(usize, &mut Message) + Send + 'static, + accept_response: impl Fn(Result) -> bool, +) { + let stop_thread_killer = start_thread_killer(); + + let server = UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); // should receive something within 5 seconds... + server + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); // should write something within 5 seconds... + let server_addr = server.local_addr().unwrap(); + + let mut query = Message::query(); + let query_name = Name::from_str("dead.beef.").unwrap(); + query.add_query(Query::query(query_name.clone(), RecordType::NULL)); + let test_bytes: &'static [u8; 8] = b"DEADBEEF"; + + // Keep the server socket alive until the client is done, so that retry + // attempts don't hit a closed port. On Windows, sending UDP to a closed + // port triggers ICMP "port unreachable" which causes `recv_from()` to return + // a connection reset error instead of blocking until timeout. + let server_done = stop_thread_killer.clone(); + let test_name_server = query_name; + let server_handle = thread::Builder::new() + .name(format!("{test_name}:server")) + .spawn(move || { + let mut buffer = [0_u8; 512]; + + for i in 0..request_count { + // wait for some bytes... + debug!("server receiving request {}", i); + let (len, addr) = server.recv_from(&mut buffer).expect("receive failed"); + debug!("server received request {} from: {}", i, addr); + + let request = Message::from_vec(&buffer[0..len]).expect("failed parse of request"); + assert_eq!(*request.queries[0].name(), test_name_server.clone()); + assert_eq!(request.queries[0].query_type(), RecordType::NULL); + + for response_idx in 0..response_count { + let mut message = request.clone().into_response(); + message.add_answer(Record::from_rdata( + test_name_server.clone(), + 0, + RData::NULL(NULL::with(test_bytes.to_vec())), + )); + + response_mutator(response_idx, &mut message); + + // bounce them right back... + let bytes = message.to_vec().unwrap(); + server.send_to(&bytes, addr).expect("send failed"); + debug!("server sent response {response_idx} for request {i}"); + } + thread::yield_now(); + } + + while !server_done.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(10)); + } + }) + .unwrap(); + + // setup the client, which is going to run on the testing thread... + + // the tests should run within 5 seconds... right? + // TODO: add timeout here, so that test never hangs... + // let timeout = Timeout::new(Duration::from_secs(5)); + let mut stream = UdpClientStream::builder(server_addr, provider) + .with_timeout(Some(Duration::from_millis(500))) + .build(); + let mut worked_once = false; + + for i in 0..request_count { + let response_stream = + stream.send_message(DnsRequest::new(query.clone(), DnsRequestOptions::default())); + println!("client sending request {i}"); + let response = response_stream.first_answer().await; + println!("client got response {i}"); + + if accept_response(response) { + worked_once = true; + } + } + + stop_thread_killer.store(true, Ordering::Relaxed); + server_handle.join().expect("server thread failed"); + + assert!(worked_once); +} + +/// Spawn a thread that wil kill the process after 15 seconds unless stopped. +/// +/// The thread killer can be stopped when the testing logic is complete by writing +/// a true value to the returned atomic bool. +fn start_thread_killer() -> Arc { + let succeeded = Arc::new(AtomicBool::new(false)); + let succeeded_clone = succeeded.clone(); + thread::Builder::new() + .name("thread_killer".to_string()) + .spawn(move || { + let succeeded = succeeded_clone; + for _ in 0..15 { + thread::sleep(Duration::from_secs(1)); + if succeeded.load(Ordering::Relaxed) { + return; + } + } + + println!("Thread Killer has been awoken, killing process"); + process::exit(-1); + }) + .unwrap(); + succeeded +} diff --git a/vendor/hickory-dns/hickory-net/src/udp/udp_client_stream.rs b/vendor/hickory-dns/hickory-net/src/udp/udp_client_stream.rs new file mode 100644 index 00000000..904c1ac6 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/udp/udp_client_stream.rs @@ -0,0 +1,745 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::fmt::{self, Display}; +use core::net::SocketAddr; +use core::pin::Pin; +use core::task::{Context, Poll}; +use core::time::Duration; +use std::collections::HashSet; +use std::sync::Arc; + +use futures_util::{FutureExt, Stream, StreamExt, pin_mut, stream::FuturesUnordered}; +use tracing::{debug, trace, warn}; + +use crate::error::NetError; +use crate::proto::op::{DEFAULT_RETRY_FLOOR, DnsRequest, DnsResponse, Message, SerialMessage}; +#[cfg(feature = "__dnssec")] +use crate::proto::rr::TSigner; +use crate::runtime::{DnsUdpSocket, RuntimeProvider, Spawn, Time}; +use crate::udp::MAX_RECEIVE_BUFFER_SIZE; +use crate::udp::udp_stream::NextRandomUdpSocket; +use crate::xfer::{DnsExchange, DnsRequestSender, DnsResponseStream}; + +/// A UDP client stream of DNS binary packets. +/// +/// It is expected that the resolver wrapper will be responsible for creating and managing a new UDP +/// client stream such that each request would have a random port. This is to avoid potential cache +/// poisoning due to UDP spoofing attacks. +#[must_use = "futures do nothing unless polled"] +pub struct UdpClientStream

{ + name_server: SocketAddr, + timeout: Duration, + is_shutdown: bool, + #[cfg(feature = "__dnssec")] + signer: Option, + bind_addr: Option, + avoid_local_ports: Arc>, + os_port_selection: bool, + provider: P, + max_retries: u8, + retry_interval_floor: Duration, +} + +impl UdpClientStream

{ + /// Construct a new [`UdpClientStream`] via a [`UdpClientStreamBuilder`]. + pub fn builder(name_server: SocketAddr, provider: P) -> UdpClientStreamBuilder

{ + UdpClientStreamBuilder { + name_server, + timeout: None, + #[cfg(feature = "__dnssec")] + signer: None, + bind_addr: None, + avoid_local_ports: Arc::default(), + os_port_selection: false, + provider, + max_retries: 3, + // This is the default value to use for the retry interval floor, which acts as a lower + // bound on the retry interval. + retry_interval_floor: DEFAULT_RETRY_FLOOR, + } + } +} + +impl

Display for UdpClientStream

{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(formatter, "UDP({})", self.name_server) + } +} + +impl DnsRequestSender for UdpClientStream

{ + fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream { + if self.is_shutdown { + panic!("can not send messages after stream is shutdown") + } + + let retry_interval_time = request.options().retry_interval; + let request = UdpRequest::new(request, self); + + let max_retries = self.max_retries; + let retry_interval = if retry_interval_time < self.retry_interval_floor { + self.retry_interval_floor + } else { + retry_interval_time + }; + + P::Timer::timeout( + self.timeout, + retry::

(request, retry_interval, max_retries.into()), + ) + .into() + } + + fn shutdown(&mut self) { + self.is_shutdown = true; + } + + fn is_shutdown(&self) -> bool { + self.is_shutdown + } +} + +// TODO: is this impl necessary? there's nothing being driven here... +impl

Stream for UdpClientStream

{ + type Item = Result<(), NetError>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + // Technically the Stream doesn't actually do anything. + if self.is_shutdown { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(()))) + } + } +} + +/// Request context for data send_udp_message needs via the retry handler closure +struct UdpRequest

{ + avoid_local_ports: Arc>, + name_server: SocketAddr, + request: DnsRequest, + provider: P, + #[cfg(feature = "__dnssec")] + signer: Option, + #[cfg(feature = "__dnssec")] + now: u64, + bind_addr: Option, + os_port_selection: bool, + case_randomization: bool, + recv_buf_size: usize, +} + +impl UdpRequest

{ + fn new(request: DnsRequest, stream: &UdpClientStream

) -> Self { + Self { + avoid_local_ports: stream.avoid_local_ports.clone(), + recv_buf_size: MAX_RECEIVE_BUFFER_SIZE.min(request.max_payload() as usize), + case_randomization: request.options().case_randomization, + name_server: stream.name_server, + // Only smuggle in the signer if we are going to use it. + #[cfg(feature = "__dnssec")] + signer: match &stream.signer { + Some(signer) if signer.should_sign_message(&request) => stream.signer.clone(), + _ => None, + }, + request, + provider: stream.provider.clone(), + #[cfg(feature = "__dnssec")] + now: P::Timer::current_time(), + bind_addr: stream.bind_addr, + os_port_selection: stream.os_port_selection, + } + } +} + +impl Request for UdpRequest

{ + async fn send(&self) -> Result { + let original_query = self.request.original_query(); + #[cfg_attr(not(feature = "__dnssec"), expect(unused_mut))] + let mut request = self.request.clone(); + + #[cfg(feature = "__dnssec")] + let mut verifier = None; + #[cfg(feature = "__dnssec")] + if let Some(signer) = &self.signer { + match request.finalize(signer, self.now) { + Ok(answer_verifier) => verifier = answer_verifier, + Err(e) => { + debug!("could not sign message: {}", e); + return Err(e.into()); + } + } + } + + let request_bytes = match request.to_vec() { + Ok(bytes) => bytes, + Err(err) => return Err(err.into()), + }; + + let msg_id = request.id; + let msg = SerialMessage::new(request_bytes, self.name_server); + let addr = msg.addr(); + let final_message = match msg.to_message() { + Ok(m) => m, + Err(e) => return Err(e.into()), + }; + debug!(%final_message, "final message"); + + let socket = NextRandomUdpSocket::new( + addr, + self.bind_addr, + self.avoid_local_ports.clone(), + self.os_port_selection, + self.provider.clone(), + ) + .await?; + + let bytes = msg.bytes(); + let len_sent: usize = socket.send_to(bytes, addr).await?; + + if bytes.len() != len_sent { + return Err(NetError::from(format!( + "Not all bytes of message sent, {} of {}", + len_sent, + bytes.len() + ))); + } + + // Create the receive buffer. + trace!( + recv_buf_size = self.recv_buf_size, + "creating UDP receive buffer" + ); + let mut recv_buf = vec![0; self.recv_buf_size]; + + // Wait until we receive a response to our query. Note that outer layers are responsible for + // retries and timeouts. + loop { + let (len, src) = socket.recv_from(&mut recv_buf).await?; + + // Copy the slice of read bytes. + let response_bytes = &recv_buf[0..len]; + let response_buffer = Vec::from(response_bytes); + + // compare expected src to received packet + let request_target = msg.addr(); + + // We need to check the source port and the message ID before doing anything else, + // including fallible message parsing. These two checks are our first screens against + // spoofed requests, accounting for about 32 bits of entropy. Any code path that would + // break out of the receive loop earlier would allow for a low-effort DoS attack. + // + // Compare the IP and port directly, as internal information about the link may be + // stored in the SocketAddr. See https://github.com/hickory-dns/hickory-dns/issues/2081. + if src.ip().to_canonical() != request_target.ip().to_canonical() + || src.port() != request_target.port() + { + warn!( + "ignoring response from {}:{} because it does not match name_server: {}:{}.", + src.ip().to_canonical(), + src.port(), + request_target.ip().to_canonical(), + request_target.port(), + ); + + // await an answer from the correct NameServer + continue; + } + + // Parse the message ID. + let Some(id_bytes) = response_buffer.first_chunk::<2>() else { + warn!(length = response_buffer.len(), "ignoring short message"); + continue; + }; + let response_id = u16::from_be_bytes(*id_bytes); + + // Validate the message id in the response matches the value chosen for the query. + if msg_id != response_id { + // on wrong id, attempted poison? + warn!( + expected_id = msg_id, + received_id = response_id, + "ignoring response with wrong message id", + ); + + // await an answer with the correct message id + continue; + } + + let mut response = DnsResponse::from_buffer(response_buffer)?; + + // Validate the returned query name. + // + // This currently checks that each response query name was present in the original query, but not that + // every original question is present. + // + // References: + // + // RFC 1035 7.3: + // + // The next step is to match the response to a current resolver request. + // The recommended strategy is to do a preliminary matching using the ID + // field in the domain header, and then to verify that the question section + // corresponds to the information currently desired. + // + // RFC 1035 7.4: + // + // In general, we expect a resolver to cache all data which it receives in + // responses since it may be useful in answering future client requests. + // However, there are several types of data which should not be cached: + // + // ... + // + // - RR data in responses of dubious reliability. When a resolver + // receives unsolicited responses or RR data other than that + // requested, it should discard it without caching it. + let request_message = Message::from_vec(msg.bytes())?; + let request_queries = &request_message.queries; + let response_queries = &mut response.queries; + + let question_matches = request_queries.len() == response_queries.len() + && response_queries + .iter() + .all(|elem| request_queries.contains(elem)); + if self.case_randomization + && question_matches + && !response_queries.iter().all(|elem| { + request_queries + .iter() + .any(|req_q| req_q == elem && req_q.name().eq_case(elem.name())) + }) + { + warn!( + "case of question section did not match: we expected '{request_queries:?}', but received '{response_queries:?}' from server {src}" + ); + return Err(NetError::QueryCaseMismatch); + } + if !question_matches { + warn!( + "detected forged question section: we expected '{request_queries:?}', but received '{response_queries:?}' from server {src}" + ); + continue; + } + + // overwrite the query with the original query if case randomization may have been used + if self.case_randomization { + if let Some(original_query) = original_query { + for response_query in response_queries.iter_mut() { + if response_query == original_query { + *response_query = original_query.clone(); + } + } + } + } + + debug!("received message id: {}", response.id); + #[cfg(feature = "__dnssec")] + if let Some(mut verifier) = verifier { + return Ok(verifier.verify(response_bytes)?); + } + return Ok(response); + } + } +} + +/// A builder to create a UDP client stream. +/// +/// This is created by [`UdpClientStream::builder`]. +pub struct UdpClientStreamBuilder

{ + name_server: SocketAddr, + timeout: Option, + #[cfg(feature = "__dnssec")] + signer: Option, + bind_addr: Option, + avoid_local_ports: Arc>, + os_port_selection: bool, + provider: P, + max_retries: u8, + retry_interval_floor: Duration, +} + +impl UdpClientStreamBuilder

{ + /// Sets the connection timeout. + pub fn with_timeout(mut self, timeout: Option) -> Self { + self.timeout = timeout; + self + } + + /// Sets the message finalizer to be applied to queries. + #[cfg(feature = "__dnssec")] + pub fn with_signer(self, signer: Option) -> Self { + Self { + name_server: self.name_server, + timeout: self.timeout, + signer, + bind_addr: self.bind_addr, + avoid_local_ports: self.avoid_local_ports, + os_port_selection: self.os_port_selection, + provider: self.provider, + max_retries: self.max_retries, + retry_interval_floor: self.retry_interval_floor, + } + } + + /// Sets the local socket address to connect from. + /// + /// If the port number is 0, a random port number will be chosen to defend against spoofing + /// attacks. If the port number is nonzero, it will be used instead. + pub fn with_bind_addr(mut self, bind_addr: Option) -> Self { + self.bind_addr = bind_addr; + self + } + + /// Configures a list of local UDP ports that should not be used when making outgoing + /// connections. + pub fn avoid_local_ports(mut self, avoid_local_ports: Arc>) -> Self { + self.avoid_local_ports = avoid_local_ports; + self + } + + /// Configures that OS should provide the ephemeral port, not the Hickory DNS + pub fn with_os_port_selection(mut self, os_port_selection: bool) -> Self { + self.os_port_selection = os_port_selection; + self + } + + /// Sets the maximum number of retries for a single request + pub fn with_max_retries(mut self, max_retries: u8) -> Self { + self.max_retries = max_retries; + self + } + + /// Sets the retry interval floor + pub fn with_retry_interval_floor(mut self, floor: u64) -> Self { + self.retry_interval_floor = Duration::from_millis(floor); + self + } + + /// Wrap a [`DnsExchange`] around the built [`UdpClientStream`] + pub fn exchange(self) -> DnsExchange

{ + let mut handle = self.provider.create_handle(); + let stream = self.build(); + let (exchange, bg) = DnsExchange::from_stream(stream); + handle.spawn_bg(bg); + exchange + } + + /// Construct a new UDP client stream. + /// + /// Returns a future that outputs the client stream. + pub fn build(self) -> UdpClientStream

{ + UdpClientStream { + name_server: self.name_server, + timeout: self.timeout.unwrap_or(Duration::from_secs(5)), + is_shutdown: false, + #[cfg(feature = "__dnssec")] + signer: self.signer, + bind_addr: self.bind_addr, + avoid_local_ports: self.avoid_local_ports.clone(), + os_port_selection: self.os_port_selection, + provider: self.provider, + max_retries: self.max_retries, + retry_interval_floor: self.retry_interval_floor, + } + } +} + +/// This implements a retry handler for tasks that might not complete successfully (e.g., +/// DNS requests made via UDP.) It starts a task future immediately, then every +/// retry_interval_time period up to a maximum of max_tasks. It will immediately return +/// the first task that completes successfully, or an error if no tasks succeed. +/// It does not implement an overall timeout to bound the work. +async fn retry( + request: impl Request, + retry_interval_time: Duration, + max_tasks: usize, +) -> Result { + let mut futures = FuturesUnordered::new(); + + let retry_timer = Provider::Timer::delay_for(retry_interval_time).fuse(); + pin_mut!(retry_timer); + + futures.push(request.send()); + let mut tasks = 1; + + loop { + futures_util::select! { + result = futures.next() => { + match result { + Some(result) => return result, + None => return Err(NetError::from("no tasks successful")), + } + } + _ = &mut retry_timer => { + if tasks < max_tasks { + tasks += 1; + futures.push(request.send()); + retry_timer.set(Provider::Timer::delay_for(retry_interval_time).fuse()); + } + } + } + } +} + +trait Request { + async fn send(&self) -> Result; +} + +#[cfg(all(test, feature = "tokio"))] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use core::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, + sync::atomic::{AtomicU8, Ordering}, + }; + use std::io; + + use tokio::{net::UdpSocket, select, spawn, sync::oneshot, time::sleep}; + + use test_support::subscribe; + + use super::*; + use crate::{ + proto::{ + op::{DnsRequestOptions, Query, ResponseCode}, + rr::{Name, RData, Record, RecordType, rdata::NULL}, + }, + runtime::{TokioRuntimeProvider, TokioTime}, + udp::tests::{ + udp_client_stream_bad_id_test, udp_client_stream_empty_question_section_test, + udp_client_stream_test, + }, + xfer::FirstAnswer, + }; + + #[tokio::test] + async fn test_udp_client_stream_ipv4() { + subscribe(); + udp_client_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await; + } + + #[tokio::test] + async fn test_udp_client_stream_ipv4_bad_id() { + subscribe(); + udp_client_stream_bad_id_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()) + .await; + } + + #[tokio::test] + async fn test_udp_client_stream_ipv4_empty_question_section() { + subscribe(); + udp_client_stream_empty_question_section_test( + IpAddr::V4(Ipv4Addr::LOCALHOST), + TokioRuntimeProvider::new(), + ) + .await; + } + + #[tokio::test] + async fn test_udp_client_stream_ipv6() { + subscribe(); + udp_client_stream_test( + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + TokioRuntimeProvider::new(), + ) + .await; + } + + #[tokio::test] + async fn test_udp_client_stream_ipv6_bad_id() { + subscribe(); + udp_client_stream_bad_id_test( + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + TokioRuntimeProvider::new(), + ) + .await; + } + + #[tokio::test] + async fn test_udp_client_stream_ipv6_empty_question_section() { + subscribe(); + udp_client_stream_empty_question_section_test( + IpAddr::V6(Ipv6Addr::LOCALHOST), + TokioRuntimeProvider::new(), + ) + .await; + } + + #[tokio::test(start_paused = true)] + async fn retry_handler_test() -> Result<(), NetError> { + let mut message = Message::query().into_response(); + message.metadata.response_code = ResponseCode::NoError; + + let ret = retry::( + FixedResponse { + response: DnsResponse::from_message(message.clone())?, + }, + Duration::from_millis(200), + 5, + ) + .await?; + assert_eq!(ret.response_code, ResponseCode::NoError); + + // test: retry timer doesn't fire extra tasks before the retry interval + let (req, tries) = DelayedResponse::new( + DnsResponse::from_message(message.clone()).unwrap(), + Duration::from_millis(100), + Arc::new(AtomicU8::new(0)), + ); + retry::(req, Duration::from_millis(200), 5).await?; + assert_eq!(tries.load(Ordering::Relaxed), 1); + + // test: retry timer does fire extra tasks after the retry interval + let (req, tries) = DelayedResponse::new( + DnsResponse::from_message(message.clone()).unwrap(), + Duration::from_millis(1500), + Arc::new(AtomicU8::new(0)), + ); + retry::(req, Duration::from_millis(200), 5).await?; + assert_eq!(tries.load(Ordering::Relaxed), 5); + + // test: retry timer tasks when nested under a Time::timer + let (req, tries) = DelayedResponse::new( + DnsResponse::from_message(message.clone()).unwrap(), + Duration::from_millis(1000), + Arc::new(AtomicU8::new(0)), + ); + let timer_ret = TokioTime::timeout( + Duration::from_millis(500), + retry::(req, Duration::from_millis(200), 5), + ) + .await; + + if let Err(e) = timer_ret { + assert_eq!(e.kind(), io::ErrorKind::TimedOut); + } else { + panic!("timer did not timeout"); + } + + assert_eq!(tries.load(Ordering::Relaxed), 3); + + Ok(()) + } + + struct FixedResponse { + response: DnsResponse, + } + + impl Request for FixedResponse { + async fn send(&self) -> Result { + Ok(self.response.clone()) + } + } + + struct DelayedResponse { + response: DnsResponse, + delay: Duration, + counter: Arc, + } + + impl DelayedResponse { + fn new( + response: DnsResponse, + delay: Duration, + counter: Arc, + ) -> (Self, Arc) { + ( + Self { + response, + delay, + counter: counter.clone(), + }, + counter, + ) + } + } + + impl Request for DelayedResponse { + async fn send(&self) -> Result { + let _ = self.counter.fetch_add(1, Ordering::Relaxed); + sleep(self.delay).await; + Ok(self.response.clone()) + } + } + + /// This tests one DoS variant of the TuDoor attack. + /// + /// + #[tokio::test] + async fn test_ignore_invalid_message() { + subscribe(); + + let provider = TokioRuntimeProvider::new(); + + // Set up server. + let server_socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap(); + let server_addr = server_socket.local_addr().unwrap(); + let (shutdown_sender, mut shutdown_receiver) = oneshot::channel(); + let server_handle = spawn(async move { + loop { + let mut buffer = [0u8; 4096]; + let result = select! { + result = server_socket.recv_from(&mut buffer) => result, + _ = &mut shutdown_receiver => break, + }; + let (len, addr) = result.unwrap(); + + let request = Message::from_vec(&buffer[..len]).unwrap(); + + // Attacker sends datagrams with invalid DNS packets first. + for id in [ + request.id ^ 1, + request.id ^ 2, + request.id ^ 4, + request.id ^ 8, + ] { + server_socket + .send_to(&id.to_be_bytes(), addr) + .await + .unwrap(); + } + + // Name server sends honest response next. + let query_name = request.queries[0].name.clone(); + let mut message = request.into_response(); + message.add_answer(Record::from_rdata( + query_name, + 0, + RData::NULL(NULL::with(b"DEADBEEF".to_vec())), + )); + let bytes = message.to_vec().unwrap(); + server_socket.send_to(&bytes, addr).await.unwrap(); + } + }); + + // Set up client. + let mut stream = UdpClientStream::builder(server_addr, provider) + .with_timeout(Some(Duration::from_millis(500))) + .build(); + + let mut query_message = Message::query(); + query_message.add_query(Query::query( + Name::from_str("dead.beef.").unwrap(), + RecordType::NULL, + )); + let response_stream = + stream.send_message(DnsRequest::new(query_message, DnsRequestOptions::default())); + let response = response_stream + .first_answer() + .await + .expect("failed to read response in the presence of spoofed invalid messages"); + let RData::NULL(rdata) = &response.answers[0].data else { + panic!("unexpected record type: {response:?}"); + }; + assert_eq!(rdata.anything, b"DEADBEEF"); + + shutdown_sender.send(()).unwrap(); + server_handle.await.unwrap(); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/udp/udp_stream.rs b/vendor/hickory-dns/hickory-net/src/udp/udp_stream.rs new file mode 100644 index 00000000..3168ca1f --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/udp/udp_stream.rs @@ -0,0 +1,398 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::collections::HashSet; +use std::io; +use std::sync::Arc; + +use async_trait::async_trait; +use futures_util::{ + future::{BoxFuture, Future}, + ready, + stream::Stream, +}; +use tracing::{debug, trace, warn}; + +use crate::error::NetError; +use crate::proto::op::SerialMessage; +use crate::runtime::{DnsUdpSocket, RuntimeProvider}; +use crate::udp::MAX_RECEIVE_BUFFER_SIZE; +use crate::xfer::{BufDnsStreamHandle, StreamReceiver}; + +/// Trait for UdpSocket +#[async_trait] +pub trait UdpSocket: DnsUdpSocket { + /// setups up a "client" udp connection that will only receive packets from the associated address + async fn connect(addr: SocketAddr) -> io::Result; + + /// same as connect, but binds to the specified local address for sending address + async fn connect_with_bind(addr: SocketAddr, bind_addr: SocketAddr) -> io::Result; + + /// a "server" UDP socket, that bind to the local listening address, and unbound remote address (can receive from anything) + async fn bind(addr: SocketAddr) -> io::Result; +} + +/// A UDP stream of DNS binary packets +#[must_use = "futures do nothing unless polled"] +pub struct UdpStream { + socket: P::Udp, + outbound_messages: StreamReceiver, +} + +impl UdpStream

{ + /// This method is intended for client connections, see [`Self::with_bound`] for a method better + /// for straight listening. It is expected that the resolver wrapper will be responsible for + /// creating and managing new UdpStreams such that each new client would have a random port + /// (reduce chance of cache poisoning). This will return a randomly assigned local port, unless + /// a nonzero port number is specified in `bind_addr`. + /// + /// # Arguments + /// + /// * `remote_addr` - socket address for the remote connection (used to determine IPv4 or IPv6) + /// * `bind_addr` - optional local socket address to connect from (if a nonzero port number is + /// specified, it will be used instead of randomly selecting a port) + /// * `os_port_selection` - Boolean parameter to specify whether to use the operating system's + /// standard UDP port selection logic instead of Hickory's logic to + /// securely select a random source port. We do not recommend using + /// this option unless absolutely necessary, as the operating system + /// may select ephemeral ports from a smaller range than Hickory, which + /// can make response poisoning attacks easier to conduct. Some + /// operating systems (notably, Windows) might display a user-prompt to + /// allow a Hickory-specified port to be used, and setting this option + /// will prevent those prompts from being displayed. If os_port_selection + /// is true, avoid_local_udp_ports will be ignored. + /// * `provider` - async runtime provider, for I/O and timers + /// + /// # Return + /// + /// A tuple of a Future of a Stream which will handle sending and receiving messages, and a + /// handle which can be used to send messages into the stream. + pub fn new( + remote_addr: SocketAddr, + bind_addr: Option, + avoid_local_ports: Option>>, + os_port_selection: bool, + provider: P, + ) -> ( + BoxFuture<'static, Result>, + BufDnsStreamHandle, + ) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(remote_addr); + + // constructs a future for getting the next randomly bound port to a UdpSocket + let next_socket = NextRandomUdpSocket::new( + remote_addr, + bind_addr, + avoid_local_ports.unwrap_or_default(), + os_port_selection, + provider, + ); + + // This set of futures collapses the next udp socket into a stream which can be used for + // sending and receiving udp packets. + let stream = Box::pin(async { + Ok(Self { + socket: next_socket.await?, + outbound_messages, + }) + }); + + (stream, message_sender) + } +} + +impl UdpStream

{ + /// Initialize the Stream with an already bound socket. Generally this should be only used for + /// server listening sockets. See [`Self::new`] for a client oriented socket. Specifically, + /// this requires there is already a bound socket, whereas `new` makes sure to randomize ports + /// for additional cache poison prevention. + /// + /// # Arguments + /// + /// * `socket` - an already bound UDP socket + /// * `remote_addr` - remote side of this connection + /// + /// # Return + /// + /// A tuple of a Stream which will handle sending and receiving messages, and a handle which can + /// be used to send messages into the stream. + pub fn with_bound(socket: P::Udp, remote_addr: SocketAddr) -> (Self, BufDnsStreamHandle) { + let (message_sender, outbound_messages) = BufDnsStreamHandle::new(remote_addr); + let stream = Self { + socket, + outbound_messages, + }; + + (stream, message_sender) + } + + #[cfg(all(feature = "tokio", feature = "mdns"))] + pub(crate) fn from_parts(socket: P::Udp, outbound_messages: StreamReceiver) -> Self { + Self { + socket, + outbound_messages, + } + } +} + +impl UdpStream

{ + fn pollable_split(&mut self) -> (&mut P::Udp, &mut StreamReceiver) { + (&mut self.socket, &mut self.outbound_messages) + } +} + +impl Stream for UdpStream

{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let (socket, outbound_messages) = self.pollable_split(); + let socket = Pin::new(socket); + let mut outbound_messages = Pin::new(outbound_messages); + + // this will not accept incoming data while there is data to send + // makes this self throttling. + while let Poll::Ready(Some(message)) = outbound_messages.as_mut().poll_peek(cx) { + // first try to send + let addr = message.addr(); + + // this will return if not ready, + // meaning that sending will be preferred over receiving... + + // TODO: shouldn't this return the error to send to the sender? + if let Err(e) = ready!(socket.poll_send_to(cx, message.bytes(), addr)) { + // Drop the UDP packet and continue + warn!( + "error sending message to {} on udp_socket, dropping response: {}", + addr, e + ); + } + + // message sent, need to pop the message + assert!(outbound_messages.as_mut().poll_next(cx).is_ready()); + } + + // For QoS, this will only accept one message and output that + // receive all inbound messages + + // TODO: this should match edns settings + let mut buf = [0u8; MAX_RECEIVE_BUFFER_SIZE]; + let (len, src) = ready!(socket.poll_recv_from(cx, &mut buf))?; + + let serial_message = SerialMessage::new(buf.iter().take(len).cloned().collect(), src); + Poll::Ready(Some(Ok(serial_message))) + } +} + +#[must_use = "futures do nothing unless polled"] +pub(crate) struct NextRandomUdpSocket { + name_server: SocketAddr, + bind_address: SocketAddr, + provider: P, + /// Number of unsuccessful attempts to pick a port. + attempted: usize, + #[allow(clippy::type_complexity)] + future: Option>>>>, + avoid_local_ports: Arc>, + os_port_selection: bool, +} + +impl NextRandomUdpSocket

{ + /// Creates a future for randomly binding to a local socket address for client connections, + /// if no port is specified. + /// + /// If a port is specified in the bind address it is used. + pub(crate) fn new( + name_server: SocketAddr, + bind_addr: Option, + avoid_local_ports: Arc>, + os_port_selection: bool, + provider: P, + ) -> Self { + let bind_address = match bind_addr { + Some(ba) => ba, + None => match name_server { + SocketAddr::V4(..) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + SocketAddr::V6(..) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), + }, + }; + + Self { + name_server, + bind_address, + provider, + attempted: 0, + future: None, + avoid_local_ports, + os_port_selection, + } + } +} + +impl Future for NextRandomUdpSocket

{ + type Output = Result; + + /// polls until there is an available next random UDP port, + /// if no port has been specified in bind_addr. + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + loop { + this.future = match this.future.take() { + Some(mut future) => match future.as_mut().poll(cx) { + Poll::Ready(Ok(socket)) => { + debug!("created socket successfully"); + return Poll::Ready(Ok(socket)); + } + Poll::Ready(Err(NetError::Io(io))) + if matches!( + io.kind(), + io::ErrorKind::PermissionDenied | io::ErrorKind::AddrInUse + ) && this.attempted < ATTEMPT_RANDOM + 1 => + { + debug!("unable to bind port, attempt: {}: {io}", this.attempted); + this.attempted += 1; + None + } + Poll::Ready(Err(err)) => { + debug!("failed to bind port: {err}"); + return Poll::Ready(Err(err)); + } + Poll::Pending => { + debug!("unable to bind port, attempt: {}", this.attempted); + this.future = Some(future); + return Poll::Pending; + } + }, + None => { + let mut bind_addr = this.bind_address; + + if !this.os_port_selection && bind_addr.port() == 0 { + while this.attempted < ATTEMPT_RANDOM { + // Per RFC 6056 Section 3.2: + // + // As mentioned in Section 2.1, the dynamic ports consist of the range + // 49152-65535. However, ephemeral port selection algorithms should use + // the whole range 1024-65535. + let port = rand::random_range(1024..=u16::MAX); + if this.avoid_local_ports.contains(&port) { + // Count this against the total number of attempts to pick a port. + // RFC 6056 Section 3.3.2 notes that this algorithm should find a + // suitable port in one or two attempts with high probability in + // common scenarios. If `avoid_local_ports` is pathologically large, + // then incrementing the counter here will prevent an infinite loop. + this.attempted += 1; + continue; + } else { + bind_addr = SocketAddr::new(bind_addr.ip(), port); + break; + } + } + } + + trace!(port = bind_addr.port(), "binding UDP socket"); + let future = this.provider.bind_udp(bind_addr, this.name_server); + Some(Box::pin(async move { Ok(future.await?) })) + } + } + } + } +} + +const ATTEMPT_RANDOM: usize = 10; + +#[cfg(feature = "tokio")] +#[async_trait] +impl UdpSocket for tokio::net::UdpSocket { + /// sets up up a "client" udp connection that will only receive packets from the associated address + /// + /// if the addr is ipv4 then it will bind local addr to 0.0.0.0:0, ipv6 \[::\]0 + async fn connect(addr: SocketAddr) -> io::Result { + let bind_addr: SocketAddr = match addr { + SocketAddr::V4(_addr) => (Ipv4Addr::UNSPECIFIED, 0).into(), + SocketAddr::V6(_addr) => (Ipv6Addr::UNSPECIFIED, 0).into(), + }; + + Self::connect_with_bind(addr, bind_addr).await + } + + /// same as connect, but binds to the specified local address for sending address + async fn connect_with_bind(_addr: SocketAddr, bind_addr: SocketAddr) -> io::Result { + let socket = Self::bind(bind_addr).await?; + + // TODO: research connect more, it appears to break UDP receiving tests, etc... + // socket.connect(addr).await?; + + Ok(socket) + } + + async fn bind(addr: SocketAddr) -> io::Result { + Self::bind(addr).await + } +} + +#[cfg(feature = "tokio")] +#[async_trait] +impl DnsUdpSocket for tokio::net::UdpSocket { + type Time = crate::runtime::TokioTime; + + fn poll_recv_from( + &self, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut buf = tokio::io::ReadBuf::new(buf); + let addr = ready!(Self::poll_recv_from(self, cx, &mut buf))?; + let len = buf.filled().len(); + + Poll::Ready(Ok((len, addr))) + } + + fn poll_send_to( + &self, + cx: &mut Context<'_>, + buf: &[u8], + target: SocketAddr, + ) -> Poll> { + Self::poll_send_to(self, cx, buf, target) + } +} + +#[cfg(test)] +#[cfg(feature = "tokio")] +mod tests { + use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use test_support::subscribe; + + use crate::{ + runtime::TokioRuntimeProvider, + udp::tests::{next_random_socket_test, udp_stream_test}, + }; + + #[tokio::test] + async fn test_next_random_socket() { + subscribe(); + let provider = TokioRuntimeProvider::new(); + next_random_socket_test(provider).await; + } + + #[tokio::test] + async fn test_udp_stream_ipv4() { + subscribe(); + let provider = TokioRuntimeProvider::new(); + udp_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), provider).await; + } + + #[tokio::test] + async fn test_udp_stream_ipv6() { + subscribe(); + let provider = TokioRuntimeProvider::new(); + udp_stream_test(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), provider).await; + } +} diff --git a/vendor/hickory-dns/hickory-net/src/xfer/dns_exchange.rs b/vendor/hickory-dns/hickory-net/src/xfer/dns_exchange.rs new file mode 100644 index 00000000..c9be751e --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/xfer/dns_exchange.rs @@ -0,0 +1,193 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! This module contains all the types for demuxing DNS oriented streams. + +use core::future::Future; +use core::marker::PhantomData; +use core::pin::Pin; +use core::task::{Context, Poll}; + +use futures_channel::mpsc; +use futures_util::stream::{Peekable, Stream, StreamExt}; +use tracing::debug; + +use crate::error::NetError; +use crate::proto::op::{DnsRequest, DnsResponse}; +use crate::runtime::RuntimeProvider; +use crate::runtime::Time; +use crate::xfer::dns_handle::DnsHandle; +use crate::xfer::{ + BufDnsRequestStreamHandle, DEFAULT_STREAM_BUFFER_SIZE, DnsRequestSender, DnsResponseReceiver, + OneshotDnsRequest, +}; + +/// This is a generic Exchange implemented over multiplexed DNS connection providers. +/// +/// The underlying `DnsRequestSender` is expected to multiplex any I/O connections. +/// DnsExchange assumes that the underlying stream is responsible for this. +#[must_use = "futures do nothing unless polled"] +pub struct DnsExchange

{ + sender: BufDnsRequestStreamHandle

, +} + +impl DnsExchange

{ + /// Wraps a [`DnsRequestSender`] in a buffered, cloneable [`DnsHandle`]. + /// + /// Returns a `DnsExchange` handle and a background future that must be spawned + /// to drive I/O. The handle can be cloned and shared across tasks; requests are + /// buffered in an internal channel and forwarded to the underlying stream by the + /// background task. + /// + /// # Arguments + /// + /// * `stream` - Any [`DnsRequestSender`] (e.g., + /// [`UdpClientStream`][crate::udp::UdpClientStream], + /// [`TcpClientStream`][crate::tcp::TcpClientStream]) + pub fn from_stream( + stream: S, + ) -> (Self, DnsExchangeBackground) { + let (sender, outbound_messages) = mpsc::channel(DEFAULT_STREAM_BUFFER_SIZE); + ( + Self { + sender: BufDnsRequestStreamHandle { + sender, + _phantom: PhantomData, + }, + }, + DnsExchangeBackground { + io_stream: stream, + outbound_messages: outbound_messages.peekable(), + marker: PhantomData, + }, + ) + } +} + +impl Clone for DnsExchange

{ + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + } + } +} + +impl DnsHandle for DnsExchange

{ + type Response = DnsExchangeSend

; + type Runtime = P; + + fn send(&self, request: DnsRequest) -> Self::Response { + DnsExchangeSend { + result: self.sender.send(request), + _sender: self.sender.clone(), // TODO: this shouldn't be necessary, currently the presence of Senders is what allows the background to track current users, it generally is dropped right after send, this makes sure that there is at least one active after send + } + } +} + +/// A Stream that will resolve to Responses after sending the request +#[must_use = "futures do nothing unless polled"] +pub struct DnsExchangeSend

{ + result: DnsResponseReceiver, + _sender: BufDnsRequestStreamHandle

, +} + +impl Stream for DnsExchangeSend

{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // as long as there is no result, poll the exchange + self.result.poll_next_unpin(cx) + } +} + +/// This background future is responsible for driving all network operations for the DNS protocol. +/// +/// It must be spawned before any DNS messages are sent. +#[must_use = "futures do nothing unless polled"] +pub struct DnsExchangeBackground { + io_stream: S, + outbound_messages: Peekable>, + marker: PhantomData, +} + +impl DnsExchangeBackground { + fn pollable_split(&mut self) -> (&mut S, &mut Peekable>) { + (&mut self.io_stream, &mut self.outbound_messages) + } +} + +impl Future for DnsExchangeBackground { + type Output = (); + + #[allow(clippy::unused_unit)] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let (io_stream, outbound_messages) = self.pollable_split(); + let mut io_stream = Pin::new(io_stream); + let mut outbound_messages = Pin::new(outbound_messages); + + // this will not accept incoming data while there is data to send + // makes this self throttling. + loop { + // poll the underlying stream, to drive it... + match io_stream.as_mut().poll_next(cx) { + // The stream is ready + Poll::Ready(Some(Ok(()))) => (), + Poll::Pending => { + if io_stream.is_shutdown() { + // the io_stream is in a shutdown state, we are only waiting for final results... + return Poll::Pending; + } + + // NotReady and not shutdown, see if there are more messages to send + () + } // underlying stream is complete. + Poll::Ready(None) => { + debug!("io_stream is done, shutting down"); + return Poll::Ready(()); + } + Poll::Ready(Some(Err(error))) => { + debug!( + %error, + "io_stream hit an error, shutting down" + ); + + return Poll::Ready(()); + } + } + + // then see if there is more to send + match outbound_messages.as_mut().poll_next(cx) { + // already handled above, here to make sure the poll() pops the next message + Poll::Ready(Some(dns_request)) => { + // if there is no peer, this connection should die... + let (dns_request, serial_response): (DnsRequest, _) = dns_request.into_parts(); + + // Try to forward the `DnsResponseStream` to the requesting task. If we fail, + // it must be because the requesting task has gone away / is no longer + // interested. In that case, we can just log a warning, but there's no need + // to take any more serious measures (such as shutting down this task). + match serial_response.send_response(io_stream.send_message(dns_request)) { + Ok(()) => (), + Err(_) => { + debug!("failed to associate send_message response to the sender"); + } + } + } + // On not ready, this is our time to return... + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => { + // if there is nothing that can use this connection to send messages, then this is done... + io_stream.shutdown(); + + // now we'll await the stream to shutdown... see io_stream poll above + } + } + + // else we loop to poll on the outbound_messages + } + } +} diff --git a/vendor/hickory-dns/hickory-net/src/xfer/dns_handle.rs b/vendor/hickory-dns/hickory-net/src/xfer/dns_handle.rs new file mode 100644 index 00000000..00cdae23 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/xfer/dns_handle.rs @@ -0,0 +1,66 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! `DnsHandle` types perform conversions of the raw DNS messages before sending the messages on the specified streams. + +use futures_util::stream::Stream; +use tracing::debug; + +use crate::{ + error::NetError, + proto::op::{DnsRequest, DnsRequestOptions, DnsResponse, Query, SerialMessage}, + runtime::RuntimeProvider, +}; + +/// Implementations of Sinks for sending DNS messages +pub trait DnsStreamHandle: 'static + Send { + /// Sends a message to the Handle for delivery to the server. + fn send(&mut self, buffer: SerialMessage) -> Result<(), NetError>; +} + +/// A trait for implementing high level functions of DNS. +pub trait DnsHandle: 'static + Clone + Send + Sync + Unpin { + /// The associated response from the response stream, this should resolve to the Response messages + type Response: Stream> + Send + Unpin + 'static; + + /// The asynchronous runtime in use. + type Runtime: RuntimeProvider; + + /// Only returns true if and only if this DNS handle is validating DNSSEC. + /// + /// If the DnsHandle impl is wrapping other clients, then the correct option is to delegate the question to the wrapped client. + fn is_verifying_dnssec(&self) -> bool { + false + } + + /// Allow for disabling EDNS + fn is_using_edns(&self) -> bool { + true + } + + /// Send a message via the channel in the client + /// + /// # Arguments + /// + /// * `request` - the fully constructed Message to send, note that most implementations of + /// will most likely be required to rewrite the QueryId, do no rely on that as + /// being stable. + fn send(&self, request: DnsRequest) -> Self::Response; + + /// A *classic* DNS query + /// + /// This is identical to `query`, but instead takes a `Query` object. + /// + /// # Arguments + /// + /// * `query` - the query to lookup + /// * `options` - options to use when constructing the message + fn lookup(&self, query: Query, options: DnsRequestOptions) -> Self::Response { + debug!("querying: {} {:?}", query.name(), query.query_type()); + self.send(DnsRequest::from_query(query, options)) + } +} diff --git a/vendor/hickory-dns/hickory-net/src/xfer/dns_multiplexer.rs b/vendor/hickory-dns/hickory-net/src/xfer/dns_multiplexer.rs new file mode 100644 index 00000000..8cd00f66 --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/xfer/dns_multiplexer.rs @@ -0,0 +1,634 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! `DnsMultiplexer` and associated types implement the state machines for sending DNS messages while using the underlying streams. + +use core::{ + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use std::collections::{HashMap, hash_map::Entry}; +use std::io; + +use futures_channel::mpsc; +use futures_util::{ + FutureExt, + future::BoxFuture, + stream::{Stream, StreamExt}, +}; +use rand::RngExt; +use tracing::debug; + +use super::{ + BufDnsStreamHandle, DnsClientStream, DnsRequestSender, DnsResponseStream, ignore_send, +}; +use crate::proto::op::{DnsRequest, DnsResponse, SerialMessage}; +#[cfg(feature = "__dnssec")] +use crate::proto::rr::{TSigVerifier, TSigner}; +use crate::{DnsStreamHandle, error::NetError, runtime::Time}; + +struct ActiveRequest { + // the completion is the channel for a response to the original request + completion: mpsc::Sender>, + request_id: u16, + timeout: BoxFuture<'static, ()>, + #[cfg(feature = "__dnssec")] + verifier: Option, +} + +impl ActiveRequest { + fn new( + completion: mpsc::Sender>, + request_id: u16, + timeout: BoxFuture<'static, ()>, + #[cfg(feature = "__dnssec")] verifier: Option, + ) -> Self { + Self { + completion, + request_id, + // request, + timeout, + #[cfg(feature = "__dnssec")] + verifier, + } + } + + /// polls the timeout and converts the error + fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Poll<()> { + self.timeout.poll_unpin(cx) + } + + /// Returns true of the other side canceled the request + fn is_canceled(&self) -> bool { + self.completion.is_closed() + } + + /// the request id of the message that was sent + fn request_id(&self) -> u16 { + self.request_id + } + + /// Sends an error + fn complete_with_error(mut self, error: NetError) { + ignore_send(self.completion.try_send(Err(error))); + } +} + +/// A DNS Client implemented over futures-rs. +/// +/// This Client is generic and capable of wrapping UDP, TCP, and other underlying DNS protocol +/// implementations. This should be used for underlying protocols that do not natively support +/// multiplexed sessions. +#[must_use = "futures do nothing unless polled"] +pub struct DnsMultiplexer { + stream: S, + timeout_duration: Duration, + stream_handle: BufDnsStreamHandle, + active_requests: HashMap, + max_active_requests: usize, + #[cfg(feature = "__dnssec")] + signer: Option, + is_shutdown: bool, +} + +impl DnsMultiplexer { + /// Spawns a new DnsMultiplexer Stream. + /// + /// This uses a default timeout of 5 seconds for all requests unless changed with + /// [`Self::with_timeout()`]. At most 32 in-flight requests are allowed unless + /// changed with [`Self::with_max_active_requests()`]. + /// + /// # Arguments + /// + /// * `stream` - A stream of bytes that can be used to send/receive DNS messages + /// (see TcpClientStream or UdpClientStream) + /// * `stream_handle` - The handle for the `stream` on which bytes can be sent/received. + pub fn new(stream: S, stream_handle: BufDnsStreamHandle) -> Self { + Self { + stream, + timeout_duration: Duration::from_secs(5), + stream_handle, + active_requests: HashMap::default(), + max_active_requests: 32, + #[cfg(feature = "__dnssec")] + signer: None, + is_shutdown: false, + } + } + + /// Change the default timeout of the DnsMultiplexer stream. + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout_duration = timeout; + self + } + + /// Set the maximum number of active (in-flight) requests. + /// + /// This limits how many DNS queries can be simultaneously pending on this + /// multiplexed connection. When the limit is reached, new requests will + /// return [`NetError::Busy`]. + pub fn with_max_active_requests(mut self, max: usize) -> Self { + self.max_active_requests = max; + self + } + + /// Specify an optional signer to TSIG authenticate requests. + #[cfg(feature = "__dnssec")] + pub fn with_signer(mut self, signer: TSigner) -> Self { + self.signer = Some(signer); + self + } + + /// loop over active_requests and remove cancelled requests + /// this should free up space if we already had 4096 active requests + fn drop_cancelled(&mut self, cx: &mut Context<'_>) { + let mut canceled = HashMap::::new(); + for (&id, active_req) in &mut self.active_requests { + if active_req.is_canceled() { + canceled.insert(id, NetError::from("requestor canceled")); + } + + // check for timeouts... + match active_req.poll_timeout(cx) { + Poll::Ready(()) => { + debug!("request timed out: {}", id); + canceled.insert(id, NetError::Timeout); + } + Poll::Pending => (), + } + } + + // drop all the canceled requests + for (id, error) in canceled { + if let Some(active_request) = self.active_requests.remove(&id) { + // complete the request, it's failed... + active_request.complete_with_error(error); + } + } + } + + /// creates random query_id, validates against all active queries + fn next_random_query_id(&self) -> Result { + let mut rand = rand::rng(); + + for _ in 0..100 { + let id: u16 = rand.random(); // the range is [0 ... u16::max] + + if !self.active_requests.contains_key(&id) { + return Ok(id); + } + } + + Err(NetError::from( + "id space exhausted, consider filing an issue", + )) + } + + /// Closes all outstanding completes with a closed stream error + fn stream_closed_close_all(&mut self, error: NetError) { + debug!(%error, addr = %self.stream.name_server_addr()); + for (_, active_request) in self.active_requests.drain() { + // complete the request, it's failed... + active_request.complete_with_error(error.clone()); + } + } +} + +impl DnsRequestSender for DnsMultiplexer { + fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream { + if self.is_shutdown { + panic!("can not send messages after stream is shutdown") + } + + if self.active_requests.len() >= self.max_active_requests { + return NetError::Busy.into(); + } + + let query_id = match self.next_random_query_id() { + Ok(id) => id, + Err(e) => return e.into(), + }; + + let (mut request, _) = request.into_parts(); + request.metadata.id = query_id; + + #[cfg(feature = "__dnssec")] + let mut verifier = None; + #[cfg(feature = "__dnssec")] + if let Some(signer) = &self.signer { + if signer.should_sign_message(&request) { + match request.finalize(signer, S::Time::current_time()) { + Ok(answer_verifier) => verifier = answer_verifier, + Err(e) => { + debug!("could not sign message: {}", e); + return NetError::from(e).into(); + } + } + } + } + + // store a Timeout for this message before sending + let timeout = S::Time::delay_for(self.timeout_duration); + + let (complete, receiver) = mpsc::channel(QUERY_RESPONSE_BUFFER_SIZE); + + // send the message + let active_request = ActiveRequest::new( + complete, + request.id, + timeout, + #[cfg(feature = "__dnssec")] + verifier, + ); + + match request.to_vec() { + Ok(buffer) => { + debug!(id = %active_request.request_id(), "sending message"); + let serial_message = SerialMessage::new(buffer, self.stream.name_server_addr()); + + debug!( + "final message: {}", + serial_message + .to_message() + .expect("bizarre we just made this message") + ); + + // add to the map -after- the client send b/c we don't want to put it in the map if + // we ended up returning an error from the send. + match self.stream_handle.send(serial_message) { + Ok(()) => self + .active_requests + .insert(active_request.request_id(), active_request), + Err(err) => return err.into(), + }; + } + Err(error) => { + debug!( + id = %active_request.request_id(), + %error, + "error message" + ); + // complete with the error, don't add to the map of active requests + return NetError::from(error).into(); + } + } + + receiver.into() + } + + fn shutdown(&mut self) { + self.is_shutdown = true; + } + + fn is_shutdown(&self) -> bool { + self.is_shutdown + } +} + +impl Stream for DnsMultiplexer { + type Item = Result<(), NetError>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Always drop the cancelled queries first + self.drop_cancelled(cx); + + if self.is_shutdown && self.active_requests.is_empty() { + debug!("stream is done: {}", self.stream.name_server_addr()); + return Poll::Ready(None); + } + + // Collect all inbound requests, max 100 at a time for QoS + // by having a max we will guarantee that the client can't be DOSed in this loop + // TODO: make the QoS configurable + let mut messages_received = 0; + for i in 0..QOS_MAX_RECEIVE_MSGS { + match self.stream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(buffer))) => { + messages_received = i; + + // deserialize or log decode_error + match DnsResponse::from_buffer(buffer.into_parts().0) { + Ok(response) => match self.active_requests.entry(response.id) { + Entry::Occupied(mut request_entry) => { + // send the response, complete the request... + let active_request = request_entry.get_mut(); + #[cfg(feature = "__dnssec")] + if let Some(verifier) = &mut active_request.verifier { + ignore_send( + active_request.completion.try_send( + verifier + .verify(response.as_buffer()) + .map_err(NetError::from), + ), + ); + } else { + ignore_send(active_request.completion.try_send(Ok(response))); + } + #[cfg(not(feature = "__dnssec"))] + ignore_send(active_request.completion.try_send(Ok(response))); + } + Entry::Vacant(..) => debug!("unexpected request_id: {}", response.id), + }, + // TODO: return src address for diagnostics + Err(error) => debug!(%error, "error decoding message"), + } + } + Poll::Ready(err) => { + let err = match err { + Some(Err(e)) => e, + None => NetError::from(io::Error::new( + io::ErrorKind::UnexpectedEof, + "stream closed", + )), + _ => unreachable!(), + }; + + self.stream_closed_close_all(err); + self.is_shutdown = true; + return Poll::Ready(None); + } + Poll::Pending => break, + } + } + + // If still active, then if the qos (for _ in 0..100 loop) limit + // was hit then "yield". This'll make sure that the future is + // woken up immediately on the next turn of the event loop. + if messages_received == QOS_MAX_RECEIVE_MSGS { + // FIXME: this was a task::current().notify(); is this right? + cx.waker().wake_by_ref(); + } + + // Finally, return not ready to keep the 'driver task' alive. + Poll::Pending + } +} + +const QOS_MAX_RECEIVE_MSGS: usize = 100; // max number of messages to receive from the UDP socket + +/// Buffer size for per-query response channels. +/// +/// Each outgoing DNS query gets its own channel to receive responses. Standard +/// DNS queries receive exactly one response so a small buffer is sufficient. +const QUERY_RESPONSE_BUFFER_SIZE: usize = 8; + +#[cfg(test)] +mod test { + use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; + + use futures_util::{ + future::{self, BoxFuture}, + ready, + stream::TryStreamExt, + }; + use test_support::subscribe; + + use super::*; + use crate::proto::op::{DnsRequestOptions, Message, Query}; + use crate::proto::rr::rdata::{NS, SOA}; + use crate::proto::rr::{DNSClass, Name, RData, Record, RecordType}; + use crate::proto::serialize::binary::BinEncodable; + use crate::xfer::{DnsClientStream, StreamReceiver}; + + struct MockClientStream { + messages: Vec, + addr: SocketAddr, + id: Option, + receiver: Option, + } + + impl MockClientStream { + fn new( + mut messages: Vec, + addr: SocketAddr, + ) -> BoxFuture<'static, Result> { + messages.reverse(); // so we can pop() and get messages in order + Box::pin(future::ok(Self { + messages, + addr, + id: None, + receiver: None, + })) + } + } + + impl Stream for MockClientStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let id = if let Some(id) = self.id { + id + } else { + let serial = ready!( + self.receiver + .as_mut() + .expect("should only be polled after receiver has been set") + .poll_next_unpin(cx) + ); + let message = serial.unwrap().to_message().unwrap(); + self.id = Some(message.id); + message.id + }; + + if let Some(mut message) = self.messages.pop() { + message.metadata.id = id; + Poll::Ready(Some(Ok(SerialMessage::new( + message.to_bytes().unwrap(), + self.addr, + )))) + } else { + Poll::Pending + } + } + } + + impl DnsClientStream for MockClientStream { + type Time = crate::runtime::TokioTime; + + fn name_server_addr(&self) -> SocketAddr { + self.addr + } + } + + async fn get_mocked_multiplexer( + mock_response: Vec, + ) -> DnsMultiplexer { + let addr = SocketAddr::from(([127, 0, 0, 1], 1234)); + let mock_response = MockClientStream::new(mock_response, addr).await.unwrap(); + let (handler, receiver) = BufDnsStreamHandle::new(addr); + let mut multiplexer = + DnsMultiplexer::new(mock_response, handler).with_timeout(Duration::from_millis(100)); + + multiplexer.stream.receiver = Some(receiver); // so it can get the correct request id + + multiplexer + } + + fn a_query_answer() -> (DnsRequest, Vec) { + let name = Name::from_ascii("www.example.com.").unwrap(); + + let mut request = Message::query(); + request.metadata.recursion_desired = true; + request.add_query({ + let mut q = Query::query(name.clone(), RecordType::A); + q.set_query_class(DNSClass::IN); + q + }); + + let mut response = request.clone().into_response(); + response.add_answer(Record::from_rdata( + name, + 86400, + RData::A(Ipv4Addr::new(93, 184, 215, 14).into()), + )); + ( + DnsRequest::new(request, DnsRequestOptions::default()), + vec![response], + ) + } + + fn axfr_query() -> Message { + let name = Name::from_ascii("example.com.").unwrap(); + + let mut msg = Message::query(); + msg.metadata.recursion_desired = true; + msg.add_query({ + let mut query = Query::query(name, RecordType::AXFR); + query.set_query_class(DNSClass::IN); + query + }); + msg + } + + fn axfr_response() -> Vec { + let origin = Name::from_ascii("example.com.").unwrap(); + let soa = Record::from_rdata( + origin.clone(), + 3600, + RData::SOA(SOA::new( + Name::parse("sns.dns.icann.org.", None).unwrap(), + Name::parse("noc.dns.icann.org.", None).unwrap(), + 2015082403, + 7200, + 3600, + 1209600, + 3600, + )), + ); + + vec![ + soa.clone(), + Record::from_rdata( + origin.clone(), + 86400, + RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())), + ), + Record::from_rdata( + origin.clone(), + 86400, + RData::NS(NS(Name::parse("b.iana-servers.net.", None).unwrap())), + ), + Record::from_rdata( + origin.clone(), + 86400, + RData::A(Ipv4Addr::new(93, 184, 215, 14).into()), + ), + Record::from_rdata( + origin, + 86400, + RData::AAAA( + Ipv6Addr::new( + 0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c, + ) + .into(), + ), + ), + soa, + ] + } + + fn axfr_query_answer() -> (DnsRequest, Vec) { + let msg = axfr_query(); + + let mut response = msg.clone().into_response(); + response.insert_answers(axfr_response()); + ( + DnsRequest::new(msg, DnsRequestOptions::default()), + vec![response], + ) + } + + fn axfr_query_answer_multi() -> (DnsRequest, Vec) { + let base = axfr_query(); + + let query = base.clone(); + let mut rr = axfr_response(); + let rr2 = rr.split_off(3); + let mut msg1 = base.clone().into_response(); + msg1.insert_answers(rr); + let mut msg2 = base.into_response(); + msg2.insert_answers(rr2); + ( + DnsRequest::new(query, DnsRequestOptions::default()), + vec![msg1, msg2], + ) + } + + #[tokio::test] + async fn test_multiplexer_a() { + subscribe(); + let (query, answer) = a_query_answer(); + let mut multiplexer = get_mocked_multiplexer(answer).await; + let response = multiplexer.send_message(query); + let response = tokio::select! { + _ = multiplexer.next() => { + // polling multiplexer to make it run + panic!("should never end") + }, + r = response.try_collect::>() => r.unwrap(), + }; + assert_eq!(response.len(), 1); + } + + #[tokio::test] + async fn test_multiplexer_axfr() { + subscribe(); + let (query, answer) = axfr_query_answer(); + let mut multiplexer = get_mocked_multiplexer(answer).await; + let response = multiplexer.send_message(query); + let response = tokio::select! { + _ = multiplexer.next() => { + // polling multiplexer to make it run + panic!("should never end") + }, + r = response.try_collect::>() => r.unwrap(), + }; + assert_eq!(response.len(), 1); + assert_eq!(response[0].answers.len(), axfr_response().len()); + } + + #[tokio::test] + async fn test_multiplexer_axfr_multi() { + subscribe(); + let (query, answer) = axfr_query_answer_multi(); + let mut multiplexer = get_mocked_multiplexer(answer).await; + let response = multiplexer.send_message(query); + let response = tokio::select! { + _ = multiplexer.next() => { + // polling multiplexer to make it run + panic!("should never end") + }, + r = response.try_collect::>() => r.unwrap(), + }; + assert_eq!(response.len(), 2); + assert_eq!( + response.iter().map(|m| m.answers.len()).sum::(), + axfr_response().len() + ); + } +} diff --git a/vendor/hickory-dns/hickory-net/src/xfer/mod.rs b/vendor/hickory-dns/hickory-net/src/xfer/mod.rs new file mode 100644 index 00000000..62cfb90c --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/xfer/mod.rs @@ -0,0 +1,484 @@ +//! DNS high level transit implementations. +//! +//! Primarily there are two types in this module of interest, the `DnsMultiplexer` type and the `DnsHandle` type. `DnsMultiplexer` can be thought of as the state machine responsible for sending and receiving DNS messages. `DnsHandle` is the type given to API users of the `hickory-proto` library to send messages into the `DnsMultiplexer` for delivery. Finally there is the `DnsRequest` type. This allows for customizations, through `DnsRequestOptions`, to the delivery of messages via a `DnsMultiplexer`. +//! +//! TODO: this module needs some serious refactoring and normalization. + +use core::fmt::Display; +use core::fmt::{self, Debug}; +use core::future::Future; +use core::marker::PhantomData; +use core::net::SocketAddr; +use core::pin::Pin; +use core::task::{Context, Poll}; +use core::time::Duration; +use std::io; + +use futures_channel::mpsc; +use futures_channel::oneshot; +use futures_util::future::BoxFuture; +use futures_util::ready; +use futures_util::stream::{Fuse, Peekable}; +use futures_util::stream::{Stream, StreamExt}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; + +use crate::error::NetError; +use crate::proto::ProtoError; +use crate::proto::op::{DnsRequest, DnsResponse, SerialMessage}; +use crate::runtime::{RuntimeProvider, Time}; + +mod dns_exchange; +pub use dns_exchange::{DnsExchange, DnsExchangeBackground, DnsExchangeSend}; + +pub mod dns_handle; +pub use dns_handle::{DnsHandle, DnsStreamHandle}; + +pub mod dns_multiplexer; +pub use dns_multiplexer::DnsMultiplexer; + +pub mod retry_dns_handle; +pub use retry_dns_handle::RetryDnsHandle; + +/// A stream returning DNS responses +pub struct DnsResponseStream { + inner: DnsResponseStreamInner, + done: bool, +} + +impl DnsResponseStream { + fn new(inner: DnsResponseStreamInner) -> Self { + Self { inner, done: false } + } +} + +impl Stream for DnsResponseStream { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use DnsResponseStreamInner::*; + + // if the standard futures are done, don't poll again + if self.done { + return Poll::Ready(None); + } + + // split mutable refs to Self + let Self { inner, done } = self.get_mut(); + + let result = match inner { + Timeout(fut) => { + let x = match ready!(fut.as_mut().poll(cx)) { + Ok(x) => x, + Err(e) => Err(e.into()), + }; + *done = true; + x + } + Receiver(fut) => match ready!(Pin::new(fut).poll_next(cx)) { + Some(Ok(x)) => Ok(x), + Some(Err(e)) => Err(e), + None => return Poll::Ready(None), + }, + Error(err) => { + *done = true; + Err(err.take().expect("cannot poll after complete")) + } + Boxed(fut) => { + let x = ready!(fut.as_mut().poll(cx)); + *done = true; + x + } + }; + + match result { + Err(NetError::Timeout) => Poll::Ready(None), + r => Poll::Ready(Some(r)), + } + } +} + +impl From for DnsResponseStream { + fn from(f: TimeoutFuture) -> Self { + Self::new(DnsResponseStreamInner::Timeout(f)) + } +} + +impl From>> for DnsResponseStream { + fn from(receiver: mpsc::Receiver>) -> Self { + Self::new(DnsResponseStreamInner::Receiver(receiver)) + } +} + +impl From for DnsResponseStream { + fn from(e: NetError) -> Self { + Self::new(DnsResponseStreamInner::Error(Some(e))) + } +} + +impl From>> for DnsResponseStream +where + F: Future> + Send + 'static, +{ + fn from(f: Pin>) -> Self { + Self::new(DnsResponseStreamInner::Boxed(f)) + } +} + +enum DnsResponseStreamInner { + Timeout(TimeoutFuture), + Receiver(mpsc::Receiver>), + Error(Option), + Boxed(BoxFuture<'static, Result>), +} + +type TimeoutFuture = BoxFuture<'static, Result, io::Error>>; + +/// Ignores the result of a send operation and logs and ignores errors +fn ignore_send(result: Result>) { + if let Err(error) = result { + if error.is_disconnected() { + debug!("ignoring send error on disconnected stream"); + return; + } + + warn!("error notifying wait, possible future leak: {:?}", error); + } +} + +/// A non-multiplexed stream of Serialized DNS messages +pub trait DnsClientStream: + Stream> + Unpin + Send + 'static +{ + /// Time implementation for this impl + type Time: Time; + + /// The remote name server address + fn name_server_addr(&self) -> SocketAddr; +} + +/// Receiver handle for peekable fused SerialMessage channel +pub type StreamReceiver = Peekable>>; + +/// A buffered channel for outbound DNS messages on a connection. +/// +/// Used to queue messages for sending over a DNS connection. On the client/resolver side, +/// this buffers outbound queries to nameservers. On the server side, this buffers outbound +/// responses to clients. +#[derive(Clone)] +pub struct BufDnsStreamHandle { + remote_addr: SocketAddr, + sender: mpsc::Sender, +} + +impl BufDnsStreamHandle { + /// Constructs a new buffered stream handle with the default buffer size (32). + /// + /// # Arguments + /// + /// * `remote_addr` - address of the DNS peer (nameserver for clients, client for servers) + pub fn new(remote_addr: SocketAddr) -> (Self, StreamReceiver) { + Self::with_buffer_size(remote_addr, DEFAULT_STREAM_BUFFER_SIZE) + } + + /// Constructs a new buffered stream handle with an explicit buffer size. + /// + /// Use this when you need a larger buffer to handle high message rates without + /// dropping messages due to backpressure. + /// + /// # Arguments + /// + /// * `remote_addr` - address of the DNS peer (nameserver for clients, client for servers) + /// * `buffer_size` - maximum number of messages that can be queued for sending + pub fn with_buffer_size(remote_addr: SocketAddr, buffer_size: usize) -> (Self, StreamReceiver) { + let (sender, receiver) = mpsc::channel(buffer_size); + let receiver = receiver.fuse().peekable(); + + let this = Self { + remote_addr, + sender, + }; + + (this, receiver) + } + + /// Associates a different remote address for any responses. + /// + /// This is mainly useful in server use cases where the incoming address is only known after receiving a packet. + pub fn with_remote_addr(&self, remote_addr: SocketAddr) -> Self { + Self { + remote_addr, + sender: self.sender.clone(), + } + } +} + +impl DnsStreamHandle for BufDnsStreamHandle { + fn send(&mut self, buffer: SerialMessage) -> Result<(), NetError> { + let sender: &mut _ = &mut self.sender; + sender + .try_send(SerialMessage::new(buffer.into_parts().0, self.remote_addr)) + .map_err(|e| NetError::from(format!("mpsc::SendError {e}"))) + } +} + +/// Types that implement this are capable of sending a serialized DNS message on a stream +/// +/// The underlying Stream implementation should yield `Some(())` whenever it is ready to send a message, +/// NotReady, if it is not ready to send a message, and `Err` or `None` in the case that the stream is +/// done, and should be shutdown. +pub trait DnsRequestSender: Stream> + Send + Unpin + 'static { + /// Send a message, and return a stream of response + /// + /// # Return + /// + /// A stream which will resolve to SerialMessage responses + fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream; + + /// Allows the upstream user to inform the underling stream that it should shutdown. + /// + /// After this is called, the next time `poll` is called on the stream it would be correct to return `Poll::Ready(Ok(()))`. This is not required though, if there are say outstanding requests that are not yet complete, then it would be correct to first wait for those results. + fn shutdown(&mut self); + + /// Returns true if the stream has been shutdown with `shutdown` + fn is_shutdown(&self) -> bool; +} + +/// Used for associating a name_server to a DnsRequestStreamHandle +#[derive(Clone)] + +pub struct BufDnsRequestStreamHandle

{ + sender: mpsc::Sender, + _phantom: PhantomData

, +} + +impl DnsHandle for BufDnsRequestStreamHandle

{ + type Response = DnsResponseReceiver; + type Runtime = P; + + fn send(&self, request: DnsRequest) -> Self::Response { + debug!( + "enqueueing message:{}:{:?}", + request.op_code, request.queries + ); + + let (request, oneshot) = OneshotDnsRequest::oneshot(request); + let mut sender = self.sender.clone(); + let try_send = sender.try_send(request).map_err(|_| { + debug!("unable to enqueue message"); + NetError::Busy + }); + + match try_send { + Ok(val) => val, + Err(err) => return DnsResponseReceiver::Err(Some(err)), + } + + DnsResponseReceiver::Receiver(oneshot) + } +} + +// TODO: this future should return the origin message in the response on errors +/// A OneshotDnsRequest creates a channel for a response to message +pub struct OneshotDnsRequest { + dns_request: DnsRequest, + sender_for_response: oneshot::Sender, +} + +impl OneshotDnsRequest { + fn oneshot(dns_request: DnsRequest) -> (Self, oneshot::Receiver) { + let (sender_for_response, receiver) = oneshot::channel(); + + ( + Self { + dns_request, + sender_for_response, + }, + receiver, + ) + } + + fn into_parts(self) -> (DnsRequest, OneshotDnsResponse) { + ( + self.dns_request, + OneshotDnsResponse(self.sender_for_response), + ) + } +} + +struct OneshotDnsResponse(oneshot::Sender); + +impl OneshotDnsResponse { + fn send_response(self, serial_response: DnsResponseStream) -> Result<(), DnsResponseStream> { + self.0.send(serial_response) + } +} + +/// A Stream that wraps a [`oneshot::Receiver`] and resolves to items in the inner Stream +pub enum DnsResponseReceiver { + /// The receiver + Receiver(oneshot::Receiver), + /// The stream once received + Received(DnsResponseStream), + /// Error during the send operation + Err(Option), +} + +impl Stream for DnsResponseReceiver { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + loop { + *self = match &mut *self { + Self::Receiver(receiver) => { + let receiver = Pin::new(receiver); + let future = ready!( + receiver + .poll(cx) + .map_err(|_| NetError::from("receiver was canceled")) + )?; + Self::Received(future) + } + Self::Received(stream) => { + return stream.poll_next_unpin(cx); + } + Self::Err(err) => return Poll::Ready(err.take().map(Err)), + }; + } + } +} + +/// Helper trait to convert a Stream of dns response into a Future +pub trait FirstAnswer>: Stream> + Unpin + Sized { + /// Convert a Stream of dns response into a Future yielding the first answer, + /// discarding others if any. + fn first_answer(self) -> FirstAnswerFuture { + FirstAnswerFuture { stream: Some(self) } + } +} + +impl FirstAnswer for S +where + S: Stream> + Unpin + Sized, + E: From, +{ +} + +/// See [FirstAnswer::first_answer] +#[derive(Debug)] +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct FirstAnswerFuture { + stream: Option, +} + +impl> + Unpin, T> Future for FirstAnswerFuture +where + S: Stream> + Unpin + Sized, +{ + type Output = S::Item; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let s = self + .stream + .as_mut() + .expect("polling FirstAnswerFuture twice"); + let item = match ready!(s.poll_next_unpin(cx)) { + Some(r) => r, + None => Err(NetError::Timeout), + }; + self.stream.take(); + Poll::Ready(item) + } +} + +/// The protocol on which a NameServer should be communicated with +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "lowercase") +)] +#[non_exhaustive] +pub enum Protocol { + /// UDP is the traditional DNS port, this is generally the correct choice + Udp, + /// TCP can be used for large queries, but not all NameServers support it + Tcp, + /// Tls for DNS over TLS + #[cfg(feature = "__tls")] + Tls, + /// Https for DNS over HTTPS + #[cfg(feature = "__https")] + Https, + /// QUIC for DNS over QUIC + #[cfg(feature = "__quic")] + Quic, + /// HTTP/3 for DNS over HTTP/3 + #[cfg(feature = "__h3")] + H3, +} + +impl Protocol { + /// Returns true if this is a datagram oriented protocol, e.g. UDP + pub fn is_datagram(self) -> bool { + matches!(self, Self::Udp) + } + + /// Returns true if this is a stream oriented protocol, e.g. TCP + pub fn is_stream(self) -> bool { + !self.is_datagram() + } + + /// Is this an encrypted protocol, i.e. TLS or HTTPS + pub fn is_encrypted(self) -> bool { + match self { + Self::Udp => false, + Self::Tcp => false, + #[cfg(feature = "__tls")] + Self::Tls => true, + #[cfg(feature = "__https")] + Self::Https => true, + #[cfg(feature = "__quic")] + Self::Quic => true, + #[cfg(feature = "__h3")] + Self::H3 => true, + } + } + + /// The string name of the protocol. + pub fn as_str(&self) -> &'static str { + match self { + Self::Udp => "udp", + Self::Tcp => "tcp", + #[cfg(feature = "__tls")] + Self::Tls => "tls", + #[cfg(feature = "__https")] + Self::Https => "https", + #[cfg(feature = "__quic")] + Self::Quic => "quic", + #[cfg(feature = "__h3")] + Self::H3 => "http3", + } + } +} + +impl Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +#[allow(unused)] // May be unused depending on features +pub(crate) const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); + +/// Default buffer size for outbound DNS message streams. +/// +/// This controls the maximum number of DNS messages that can be queued for sending on a +/// single connection. The buffer is used in two contexts: +/// +/// - **Server side**: buffers outbound responses to clients +/// - **Client/resolver side**: buffers outbound queries to nameservers +/// +/// Under high load, a larger buffer prevents messages from being dropped due to backpressure. +const DEFAULT_STREAM_BUFFER_SIZE: usize = 32; diff --git a/vendor/hickory-dns/hickory-net/src/xfer/retry_dns_handle.rs b/vendor/hickory-dns/hickory-net/src/xfer/retry_dns_handle.rs new file mode 100644 index 00000000..826ff24f --- /dev/null +++ b/vendor/hickory-dns/hickory-net/src/xfer/retry_dns_handle.rs @@ -0,0 +1,181 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! `RetryDnsHandle` allows for DnsQueries to be reattempted on failure + +use core::pin::Pin; +use core::task::{Context, Poll}; + +use futures_util::stream::{Stream, StreamExt}; + +use crate::xfer::{DnsHandle, DnsRequest, DnsResponse}; +use crate::{DnsError, NetError}; + +/// Can be used to reattempt queries if they fail +/// +/// Note: this does not reattempt queries that fail with a negative response. +/// For example, if a query gets a `NODATA` response from a name server, the +/// query will not be retried. It only reattempts queries that effectively +/// failed to get a response, such as queries that resulted in IO or timeout +/// errors. +/// +/// *note* Current value of this is not clear, it may be removed +#[derive(Clone)] +#[must_use = "queries can only be sent through a ClientHandle"] +#[allow(dead_code)] +pub struct RetryDnsHandle { + handle: H, + attempts: usize, +} + +impl RetryDnsHandle { + /// Creates a new Client handler for reattempting requests on failures. + /// + /// # Arguments + /// + /// * `handle` - handle to the dns connection + /// * `attempts` - number of attempts before failing + pub fn new(handle: H, attempts: usize) -> Self { + Self { handle, attempts } + } +} + +impl DnsHandle for RetryDnsHandle { + type Response = Pin> + Send + Unpin>>; + type Runtime = H::Runtime; + + fn send(&self, request: DnsRequest) -> Self::Response { + // need to clone here so that the retry can resend if necessary... + // obviously it would be nice to be lazy about this... + let stream = self.handle.send(request.clone()); + + Box::pin(RetrySendStream { + request, + handle: self.handle.clone(), + stream, + remaining_attempts: self.attempts, + }) + } +} + +/// A stream for retrying (on failure, for the remaining number of times specified) +struct RetrySendStream { + request: DnsRequest, + handle: H, + stream: ::Response, + remaining_attempts: usize, +} + +impl Stream for RetrySendStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // loop over the stream, on errors, spawn a new stream + // on ready and not ready return. + loop { + let err = match self.stream.poll_next_unpin(cx) { + Poll::Ready(Some(Err(e))) => e, + poll => return poll, + }; + + match (self.remaining_attempts, err) { + // No attempts left, return the error + (0, err) => return Poll::Ready(Some(Err(err))), + // Don't retry some kinds of errors + ( + _, + err @ NetError::NoConnections + | err @ NetError::Dns(DnsError::NoRecordsFound(_)) + | err @ NetError::Dns(DnsError::DnssecBogus), + ) => return Poll::Ready(Some(Err(err))), + // Don't count `Busy` as an attempt + (_, NetError::Busy) => {} + // Try again and count this as one attempt + (_, _) => self.remaining_attempts -= 1, + } + + // TODO: if the "sent" Message is part of the error result, + // then we can just reuse it... and no clone necessary + let request = self.request.clone(); + self.stream = self.handle.send(request); + } + } +} + +#[cfg(all(test, feature = "tokio"))] +mod test { + use core::sync::atomic::{AtomicU16, Ordering}; + use std::sync::Arc; + + use futures_executor::block_on; + use futures_util::future::{err, ok}; + use futures_util::stream::{Stream, once}; + + use super::*; + use crate::proto::op::Message; + use crate::runtime::TokioRuntimeProvider; + use crate::xfer::{DnsHandle, DnsRequest, DnsResponse, FirstAnswer}; + use test_support::subscribe; + + #[derive(Clone)] + struct TestClient { + last_succeed: bool, + retries: u16, + attempts: Arc, + } + + impl DnsHandle for TestClient { + type Response = Box> + Send + Unpin>; + type Runtime = TokioRuntimeProvider; + + fn send(&self, _: DnsRequest) -> Self::Response { + let i = self.attempts.load(Ordering::SeqCst); + + if (i > self.retries || self.retries - i == 0) && self.last_succeed { + let mut message = Message::query(); + message.metadata.id = i; + return Box::new(once(ok( + DnsResponse::from_message(message.into_response()).unwrap() + ))); + } + + self.attempts.fetch_add(1, Ordering::SeqCst); + Box::new(once(err(NetError::from("last retry set to fail")))) + } + } + + #[test] + fn test_retry() { + subscribe(); + let handle = RetryDnsHandle::new( + TestClient { + last_succeed: true, + retries: 1, + attempts: Arc::new(AtomicU16::new(0)), + }, + 2, + ); + let test1 = DnsRequest::from(Message::query()); + let result = block_on(handle.send(test1).first_answer()).expect("should have succeeded"); + assert_eq!(result.id, 1); // this is checking the number of iterations the TestClient ran + } + + #[test] + fn test_error() { + subscribe(); + let client = RetryDnsHandle::new( + TestClient { + last_succeed: false, + retries: 1, + attempts: Arc::new(AtomicU16::new(0)), + }, + 2, + ); + let test1 = DnsRequest::from(Message::query()); + assert!(block_on(client.send(test1).first_answer()).is_err()); + } +} diff --git a/vendor/hickory-dns/hickory-proto/Cargo.toml b/vendor/hickory-dns/hickory-proto/Cargo.toml new file mode 100644 index 00000000..18226417 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/Cargo.toml @@ -0,0 +1,249 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.88" +name = "hickory-proto" +version = "0.26.3" +authors = ["The contributors to Hickory DNS"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = """ +hickory-proto is a safe and secure low-level DNS library. This is the foundational DNS protocol +library used by the other higher-level Hickory DNS crates. +""" +homepage = "https://hickory-dns.org/" +documentation = "https://docs.rs/hickory-proto" +readme = "README.md" +keywords = [ + "dns", + "dnssec", +] +categories = ["network-programming"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/hickory-dns/hickory-dns" + +[package.metadata.docs.rs] +all-features = true +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.cargo-all-features] +skip_optional_dependencies = true +max_combination_size = 2 +denylist = [ + "__tls", + "__https", + "__quic", + "__h3", + "__dnssec", +] + +[features] +__dnssec = [ + "dep:bitflags", + "dep:rustls-pki-types", + "dep:time", + "std", +] +access-control = [ + "std", + "dep:prefix-trie", +] +default = ["std"] +dnssec-aws-lc-rs = [ + "dep:aws-lc-rs", + "aws-lc-rs/aws-lc-sys", + "aws-lc-rs/ring-io", + "__dnssec", +] +dnssec-ring = [ + "dep:ring", + "__dnssec", +] +mdns = [] +no-std-rand = [ + "once_cell/critical-section", + "dep:critical-section", +] +serde = [ + "dep:serde", + "std", + "url/serde", +] +std = [ + "data-encoding/std", + "ipnet/std", + "rand/std", + "rand/thread_rng", + "ring?/std", + "thiserror/std", + "tracing/std", + "url/std", +] +testing = ["std"] +wasm-bindgen = [ + "dep:wasm-bindgen-crate", + "dep:js-sys", +] + +[lib] +name = "hickory_proto" +path = "src/lib.rs" + +[[test]] +name = "integration" +path = "tests/integration/main.rs" + +[[bench]] +name = "lib" +path = "benches/lib.rs" + +[[bench]] +name = "lower_name_benches" +path = "benches/lower_name_benches.rs" + +[[bench]] +name = "name_benches" +path = "benches/name_benches.rs" + +[dependencies.aws-lc-rs] +version = "1.12.3" +features = ["prebuilt-nasm"] +optional = true +default-features = false + +[dependencies.bitflags] +version = "2.4.1" +optional = true + +[dependencies.critical-section] +version = "1.1.1" +optional = true + +[dependencies.data-encoding] +version = "2.2.0" +features = ["alloc"] +default-features = false + +[dependencies.idna] +version = "1.0.3" +features = [ + "alloc", + "compiled_data", +] +default-features = false + +[dependencies.ipnet] +version = "2.11.0" +default-features = false + +[dependencies.js-sys] +version = "0.3.44" +optional = true + +[dependencies.once_cell] +version = "1.20.0" +features = ["critical-section"] +default-features = false + +[dependencies.prefix-trie] +version = "0.10" +optional = true + +[dependencies.rand] +version = "0.10.0" +features = [ + "alloc", + "std_rng", +] +default-features = false + +[dependencies.ring] +version = "0.17" +features = ["std"] +optional = true + +[dependencies.rustls-pki-types] +version = "1.10" +optional = true + +[dependencies.serde] +version = "1.0" +features = ["derive"] +optional = true + +[dependencies.thiserror] +version = "2" +default-features = false + +[dependencies.time] +version = "0.3" +optional = true + +[dependencies.tinyvec] +version = "1.1.1" +features = ["alloc"] + +[dependencies.tracing] +version = "0.1.30" +default-features = false + +[dependencies.url] +version = "2.5.4" +default-features = false + +[dependencies.wasm-bindgen-crate] +version = "0.2.107" +optional = true +package = "wasm-bindgen" + +[dev-dependencies] + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.22.1" + +[lints.clippy] +bool_to_int_with_if = "allow" +default_trait_access = "warn" +manual_let_else = "warn" +or_fun_call = "warn" +single_component_path_imports = "allow" +unimplemented = "warn" +upper_case_acronyms = "allow" +use_self = "warn" + +[lints.rust] +non_snake_case = "warn" +non_upper_case_globals = "warn" +rust_2018_idioms = "warn" +unnameable_types = "warn" +unreachable_pub = "warn" +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = ["cfg(nightly)"] diff --git a/vendor/hickory-dns/hickory-proto/LICENSE-APACHE b/vendor/hickory-dns/hickory-proto/LICENSE-APACHE new file mode 100644 index 00000000..d5dd862b --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/hickory-dns/hickory-proto/LICENSE-MIT b/vendor/hickory-dns/hickory-proto/LICENSE-MIT new file mode 100644 index 00000000..cc6341a3 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright (c) 2015 The Hickory DNS Developers +Copyright (c) 2017 Google LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/hickory-dns/hickory-proto/README.md b/vendor/hickory-dns/hickory-proto/README.md new file mode 100644 index 00000000..3a770022 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/README.md @@ -0,0 +1,37 @@ +# Overview + +`hickory-proto` is a safe and secure low-level DNS library. +This is the foundational DNS protocol library used by the other higher-level Hickory DNS crates. + +Unless you want to manipulate the DNS packets directly, it is likely not the library you want. +For higher-level interfaces, refer to the [`hickory-server`], [`hickory-resolver`] library crates +instead. + +[`hickory-net`]: ../net +[`hickory-server`]: ../server +[`hickory-resolver`]: ../resolver + +## Cryptography provider + +Features requiring cryptography require selecting a specific cryptography +provider. See the [project README] for more information. + +[project README]: ../../README.md#Cryptography-provider + +## DNSSEC + +In order to use DNSSEC you must enable a cargo feature corresponding to your +desired cryptography provider: + +* `dnssec-aws-lc-rs` +* `dnssec-ring` + +## Other crate features + +* `text-parsing` - support for reading text-based zone files. +* `serde` - enable serde serialization support. +* `std` - disable for no-std support. +* `no-std-rand` - enables a custom random function backed by a no_std compatible mutex. +* `mdns` (experimental) - enable experimental mDNS support. +* `wasm-bindgen` - support for WASM. +* `access-control` - enable data structures useful for blocklists and access control. diff --git a/vendor/hickory-dns/hickory-proto/benches/lib.rs b/vendor/hickory-dns/hickory-proto/benches/lib.rs new file mode 100644 index 00000000..130e1040 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/benches/lib.rs @@ -0,0 +1,171 @@ +#![cfg(nightly)] +#![feature(test)] + +extern crate test; + +use hickory_proto::op::{ + Header, HeaderCounts, Message, MessageType, Metadata, OpCode, ResponseCode, +}; +use hickory_proto::rr::rdata::PTR; +use hickory_proto::rr::{Name, RData, Record, RecordType}; +use hickory_proto::serialize::binary::{BinDecodable, BinDecoder, BinEncodable, BinEncoder}; + +use test::Bencher; + +#[bench] +fn bench_emit_header(b: &mut Bencher) { + let header = Header { + metadata: Metadata::new(10, MessageType::Query, OpCode::Query), + counts: HeaderCounts::default(), + }; + b.iter(|| { + // we need to create the vector here, otherwise its length is already big enough and the + // encoder does not need to resize it + let mut bytes = Vec::with_capacity(512); + let mut encoder = BinEncoder::new(&mut bytes); + header.emit(&mut encoder) + }) +} + +// TODO: +// This is a bit silly, because everywhere in the codebase, we reserve 512 bytes for the buffer. +// But what we want to measure here is the cost of reserving more space, which can happen for big +// messages exceeding 512 bytes. A better benchmark would be to emit such a big message. +#[bench] +fn bench_parse_header_no_reservation(b: &mut Bencher) { + let header = Header { + metadata: Metadata::new(10, MessageType::Query, OpCode::Query), + counts: HeaderCounts::default(), + }; + b.iter(|| { + let mut bytes = Vec::with_capacity(0); + let mut encoder = BinEncoder::new(&mut bytes); + header.emit(&mut encoder) + }) +} + +#[bench] +fn bench_parse_header(b: &mut Bencher) { + let byte_vec = vec![ + 0x01, 0x10, 0xAA, 0x83, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, + ]; + b.iter(|| { + let mut decoder = BinDecoder::new(&byte_vec); + Header::read(&mut decoder) + }) +} + +#[bench] +fn bench_emit_message(b: &mut Bencher) { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.authentic_data = true; + message.metadata.checking_disabled = true; + message.metadata.response_code = ResponseCode::ServFail; + message.add_answer(stub_record()); + message.add_authority(stub_record()); + message.add_additional(stub_record()); + b.iter(|| { + let mut byte_vec: Vec = Vec::with_capacity(512); + let mut encoder = BinEncoder::new(&mut byte_vec); + message.emit(&mut encoder) + }) +} + +#[bench] +fn bench_emit_message_no_reservation(b: &mut Bencher) { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.authentic_data = true; + message.metadata.checking_disabled = true; + message.metadata.response_code = ResponseCode::ServFail; + message.add_answer(stub_record()); + message.add_authority(stub_record()); + message.add_additional(stub_record()); + b.iter(|| { + let mut byte_vec: Vec = Vec::with_capacity(0); + let mut encoder = BinEncoder::new(&mut byte_vec); + message.emit(&mut encoder) + }) +} + +#[bench] +fn bench_emit_message_cve_2024_8508(b: &mut Bencher) { + let mut message = Message::response(10, OpCode::Update); + for i in 0..20 { + for j in 0..10 { + for k in 0..10 { + let record = Record::from_rdata( + Name::parse(&format!("l{i}.l{j}.l{k}.example."), None).unwrap(), + 86400, + RData::PTR(PTR( + Name::parse(&format!("l{i}.l{j}.l{k}.test."), None).unwrap() + )), + ); + message.add_answer(record); + } + } + } + let mut buffer = Vec::with_capacity(u16::MAX as usize); + b.iter(|| { + let mut encoder = BinEncoder::new(&mut buffer); + message.emit(&mut encoder).unwrap(); + buffer.clear(); + }) +} + +#[bench] +fn bench_parse_message(b: &mut Bencher) { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.authentic_data = true; + message.metadata.checking_disabled = true; + message.metadata.response_code = ResponseCode::ServFail; + + message.add_answer(stub_record()); + message.add_authority(stub_record()); + message.add_additional(stub_record()); + let mut byte_vec: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut byte_vec); + message.emit(&mut encoder).unwrap(); + } + b.iter(|| { + let mut decoder = BinDecoder::new(&byte_vec); + Message::read(&mut decoder) + }) +} + +fn stub_record() -> Record { + Record::update0(Name::new(), 0, RecordType::NULL) +} + +#[bench] +fn bench_parse_real_message(b: &mut Bencher) { + let bytes = [ + 145, 188, 129, 128, 0, 1, 0, 6, 0, 0, 0, 0, 5, 118, 105, 100, 101, 111, 5, 116, 119, 105, + 109, 103, 3, 99, 111, 109, 0, 0, 1, 0, 1, 192, 12, 0, 5, 0, 1, 0, 0, 0, 245, 0, 11, 8, 118, + 105, 100, 101, 111, 45, 97, 107, 192, 18, 192, 45, 0, 5, 0, 1, 0, 0, 12, 213, 0, 24, 5, + 118, 105, 100, 101, 111, 5, 116, 119, 105, 109, 103, 6, 97, 107, 97, 100, 110, 115, 3, 110, + 101, 116, 0, 192, 68, 0, 5, 0, 1, 0, 0, 0, 57, 0, 28, 5, 118, 105, 100, 101, 111, 5, 116, + 119, 105, 109, 103, 3, 99, 111, 109, 9, 97, 107, 97, 109, 97, 105, 122, 101, 100, 192, 87, + 192, 104, 0, 5, 0, 1, 0, 0, 2, 194, 0, 22, 5, 118, 105, 100, 101, 111, 5, 116, 119, 105, + 109, 103, 3, 99, 111, 109, 3, 101, 105, 112, 192, 80, 192, 144, 0, 5, 0, 1, 0, 0, 0, 43, 0, + 35, 8, 101, 105, 112, 45, 116, 97, 116, 97, 5, 118, 105, 100, 101, 111, 5, 116, 119, 105, + 109, 103, 3, 99, 111, 109, 7, 97, 107, 97, 104, 111, 115, 116, 192, 87, 192, 178, 0, 1, 0, + 1, 0, 0, 0, 23, 0, 4, 184, 31, 3, 236, + ]; + b.iter(|| { + let mut decoder = BinDecoder::new(&bytes[..]); + assert!(Message::read(&mut decoder).is_ok()); + }) +} diff --git a/vendor/hickory-dns/hickory-proto/benches/lower_name_benches.rs b/vendor/hickory-dns/hickory-proto/benches/lower_name_benches.rs new file mode 100644 index 00000000..d56282ca --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/benches/lower_name_benches.rs @@ -0,0 +1,77 @@ +// Copyright 2015-2017 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +#![cfg(nightly)] +#![feature(test)] + +extern crate test; + +use std::str::FromStr; + +use test::Bencher; + +use hickory_proto::rr::{LowerName, Name}; + +#[bench] +fn name_cmp_short(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("com").unwrap()); + let name2 = LowerName::new(&Name::from_str("COM").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} + +#[bench] +fn name_cmp_short_case(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("com").unwrap()); + let name2 = LowerName::new(&Name::from_str("com").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} + +#[bench] +fn name_cmp_medium(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("www.example.com").unwrap()); + let name2 = LowerName::new(&Name::from_str("www.EXAMPLE.com").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} + +#[bench] +fn name_cmp_medium_case(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("www.example.com").unwrap()); + let name2 = LowerName::new(&Name::from_str("www.example.com").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} + +#[bench] +fn name_cmp_long(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("a.crazy.really.long.example.com").unwrap()); + let name2 = LowerName::new(&Name::from_str("a.crazy.really.long.EXAMPLE.com").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} + +#[bench] +fn name_cmp_long_case(b: &mut Bencher) { + let name1 = LowerName::new(&Name::from_str("a.crazy.really.long.example.com").unwrap()); + let name2 = LowerName::new(&Name::from_str("a.crazy.really.long.example.com").unwrap()); + + b.iter(|| { + assert_eq!(name1, name2); + }); +} diff --git a/vendor/hickory-dns/hickory-proto/benches/name_benches.rs b/vendor/hickory-dns/hickory-proto/benches/name_benches.rs new file mode 100644 index 00000000..84ef8b81 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/benches/name_benches.rs @@ -0,0 +1,160 @@ +#![cfg(nightly)] +#![feature(test)] + +extern crate test; + +use std::cmp::Ordering; + +use test::Bencher; + +use hickory_proto::rr::*; + +#[bench] +fn name_cmp_short(b: &mut Bencher) { + let name1 = Name::from_ascii("com").unwrap(); + let name2 = Name::from_ascii("COM").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_short_not_eq(b: &mut Bencher) { + let name1 = Name::from_ascii("com").unwrap(); + let name2 = Name::from_ascii("COM").unwrap(); + + b.iter(|| { + assert_ne!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_short_case(b: &mut Bencher) { + let name1 = Name::from_ascii("com").unwrap(); + let name2 = Name::from_ascii("com").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_medium(b: &mut Bencher) { + let name1 = Name::from_ascii("www.example.com").unwrap(); + let name2 = Name::from_ascii("www.EXAMPLE.com").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_medium_not_eq(b: &mut Bencher) { + let name1 = Name::from_ascii("www.example.com").unwrap(); + let name2 = Name::from_ascii("www.EXAMPLE.com").unwrap(); + + b.iter(|| { + assert_ne!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_medium_case(b: &mut Bencher) { + let name1 = Name::from_ascii("www.example.com").unwrap(); + let name2 = Name::from_ascii("www.example.com").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_long(b: &mut Bencher) { + let name1 = Name::from_ascii("a.crazy.really.long.example.com").unwrap(); + let name2 = Name::from_ascii("a.crazy.really.long.EXAMPLE.com").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_long_not_eq(b: &mut Bencher) { + let name1 = Name::from_ascii("a.crazy.really.long.example.com").unwrap(); + let name2 = Name::from_ascii("a.crazy.really.long.EXAMPLE.com").unwrap(); + + b.iter(|| { + assert_ne!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_cmp_long_case(b: &mut Bencher) { + let name1 = Name::from_ascii("a.crazy.really.long.example.com").unwrap(); + let name2 = Name::from_ascii("a.crazy.really.long.example.com").unwrap(); + + b.iter(|| { + assert_eq!(name1.cmp_case(&name2), Ordering::Equal); + }); +} + +#[bench] +fn name_to_lower_short(b: &mut Bencher) { + let name1 = Name::from_ascii("COM").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 1); + }); +} + +#[bench] +fn name_to_lower_medium(b: &mut Bencher) { + let name1 = Name::from_ascii("example.COM").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 2); + }); +} + +#[bench] +fn name_to_lower_long(b: &mut Bencher) { + let name1 = Name::from_ascii("www.EXAMPLE.com").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 3); + }); +} + +#[bench] +fn name_no_lower_short(b: &mut Bencher) { + let name1 = Name::from_ascii("com").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 1); + }); +} + +#[bench] +fn name_no_lower_medium(b: &mut Bencher) { + let name1 = Name::from_ascii("example.com").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 2); + }); +} + +#[bench] +fn name_no_lower_long(b: &mut Bencher) { + let name1 = Name::from_ascii("www.example.com").unwrap(); + + b.iter(|| { + let lower = name1.to_lowercase(); + assert_eq!(lower.num_labels(), 3); + }); +} diff --git a/vendor/hickory-dns/hickory-proto/src/access_control.rs b/vendor/hickory-dns/hickory-proto/src/access_control.rs new file mode 100644 index 00000000..40fcc16a --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/access_control.rs @@ -0,0 +1,391 @@ +//! Utility functions that are used in multiple crates +use core::net::IpAddr; + +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +use prefix_trie::PrefixSet; +use tracing::debug; + +use crate::ProtoError; + +/// A builder interface for constructing an [`AccessControlSet`]. +pub struct AccessControlSetBuilder(AccessControlSet); + +impl<'a> AccessControlSetBuilder { + /// Construct a builder for an access control set with the given `name`. + /// + /// The `name` is used to contextualize logging when an [`IpAddr`] is denied. + pub fn new(name: &'static str) -> Self { + Self(AccessControlSet::new(name)) + } + + /// Override the [`Self::deny()`] list for the provided IP networks, allowing access. + /// + /// Existing networks allowed by prior [`Self::allow()`] calls are not removed. + /// + /// See [`AccessControlSet`] for more information on the access semantics. + pub fn allow(mut self, allow: impl Iterator) -> Self { + for network in allow { + debug!(name = self.0.name, ?network, "appending to allow list"); + match network { + IpNet::V4(network) => { + self.0.v4_allow.insert(*network); + } + IpNet::V6(network) => { + self.0.v6_allow.insert(*network); + } + } + } + self + } + + /// Deny clients from the provided IP networks, unless present in the [`Self::allow()`] list. + /// + /// Existing networks denied by prior [`Self::deny()`] calls are not removed. + /// + /// See [`AccessControlSet`] for more information on the access semantics. + pub fn deny(mut self, deny: impl Iterator) -> Self { + for network in deny { + debug!(name = self.0.name, ?network, "appending to deny list"); + match network { + IpNet::V4(network) => { + self.0.v4_deny.insert(*network); + } + IpNet::V6(network) => { + self.0.v6_deny.insert(*network); + } + } + } + self + } + + /// Clear all IP networks previous allowed with [`Self::allow()`]. + pub fn clear_allow(mut self) -> Self { + self.0.v4_allow.clear(); + self.0.v6_allow.clear(); + self + } + + /// Clear all IP networks previously denied with [`Self::deny()`]. + pub fn clear_deny(mut self) -> Self { + self.0.v4_deny.clear(); + self.0.v6_deny.clear(); + self + } + + /// Consume the builder and produce an [`AccessControlSet`]. + /// + /// Returns an error if [`Self::allow()`] was used to add deny list override networks + /// without using [`Self::deny()`] to specify one or more denied networks. + pub fn build(self) -> Result { + let deny_empty = self.0.v4_deny.is_empty() && self.0.v6_deny.is_empty(); + let allowed_count = self.0.v4_allow.iter().count() + self.0.v6_allow.iter().count(); + if deny_empty && allowed_count != 0 { + return Err(format!( + "access control set {name:?} has {allowed_count} allowed overrides, but no denied networks to override", + name = self.0.name + ).into()); + } + Ok(self.0) + } +} + +/// An IPv4/IPv6 access control set. +/// +/// Use [`AccessControlSetBuilder`] to construct an instance. +/// When determining if an [`IpAddr`] is denied with [`Self::denied()`], the deny list +/// is considered first, and the allow list may override the deny +/// decision. +/// +/// The full access semantics are: +/// +/// | Present in deny list | Present in allow list | Result | +/// |-----------------------|----------------------|----------| +/// | true | false | denied | +/// | false | false | allowed | +/// | true | true | allowed | +/// | false | true | allowed | +#[derive(Clone, Debug)] +pub struct AccessControlSet { + name: &'static str, + v4_allow: PrefixSet, + v4_deny: PrefixSet, + v6_allow: PrefixSet, + v6_deny: PrefixSet, +} + +impl AccessControlSet { + /// Construct an access control set with the given `name`. + /// + /// The name is used to contextualize logging when an [`IpAddr`] is denied. + fn new(name: &'static str) -> Self { + Self { + name, + v4_allow: PrefixSet::new(), + v4_deny: PrefixSet::new(), + v6_allow: PrefixSet::new(), + v6_deny: PrefixSet::new(), + } + } + + /// Construct an empty access control set with the given `name` that allows all networks. + pub fn empty(name: &'static str) -> Self { + Self::new(name) + } + + /// Returns true if the access control set allows all addresses. + /// + /// This is true when no IPv4 or IPv6 networks are denied. + pub fn allows_all(&self) -> bool { + self.v4_deny.is_empty() && self.v6_deny.is_empty() + } + + /// Check if the IP address `ip` should be denied. + /// + /// If the IP address is in a network previously denied by [`AccessControlSetBuilder::deny()`] + /// that wasn't explicitly allowed with [`AccessControlSetBuilder::allow()`], this function + /// will return true. + /// + /// All other combinations will return false (i.e., [`AccessControlSetBuilder::allow()`] acts + /// like an exception list for [`AccessControlSetBuilder::deny()`]) + pub fn denied(&self, ip: IpAddr) -> bool { + // If both deny lists are empty, short-circuit. There's nothing to consider. + if self.allows_all() { + return false; + } + match ip.to_canonical() { + IpAddr::V4(ip) => { + self.v4_allow.get_spm(&ip.into()).is_none() + && self.v4_deny.get_spm(&ip.into()).is_some() + } + IpAddr::V6(ip) => { + self.v6_allow.get_spm(&ip.into()).is_none() + && self.v6_deny.get_spm(&ip.into()).is_some() + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::access_control::{AccessControlSet, AccessControlSetBuilder}; + + #[test] + fn access_control_set_networks_test() { + let acs = AccessControlSetBuilder::new("test acs") + .deny( + [ + "10.0.0.0/8".parse().unwrap(), + "172.16.0.0/12".parse().unwrap(), + "192.168.0.0/16".parse().unwrap(), + "fe80::/10".parse().unwrap(), + ] + .iter(), + ) + .allow( + [ + "10.1.0.3/29".parse().unwrap(), + "192.168.1.10/32".parse().unwrap(), + "fe80::200/128".parse().unwrap(), + ] + .iter(), + ) + .build() + .unwrap(); + + // 10.1.0.3/29 above should cause 10.1.0.0/29 to be placed into the allow list; validate the + // address before and after are blocked, and addresses within the subnet are allowed + assert!(acs.denied([10, 0, 254, 254].into())); + assert!(!acs.denied([10, 1, 0, 0].into())); + assert!(!acs.denied([10, 1, 0, 3].into())); + assert!(!acs.denied([10, 1, 0, 7].into())); + assert!(acs.denied([10, 1, 0, 8].into())); + + assert!(acs.denied([192, 168, 1, 1].into())); + assert!(!acs.denied([192, 168, 1, 10].into())); + + assert!(!acs.denied([0xfe80, 0, 0, 0, 0, 0, 0, 0x200].into())); + assert!(acs.denied([0xfe80, 0, 0, 0, 0, 0, 0, 1].into())); + } + + // Test the access control semantics described in the Rustdoc of `AccessControlSet` + #[test] + fn access_control_semantics_test() { + struct TestCase { + name: &'static str, + in_deny: bool, + in_allow: bool, + expected_build_err: bool, + expected_denied: bool, + } + + let test_cases = [ + TestCase { + name: "deny=true, allow=false -> denied", + in_deny: true, + in_allow: false, + expected_build_err: false, + expected_denied: true, + }, + TestCase { + name: "deny=false, allow=false -> allowed", + in_deny: false, + in_allow: false, + expected_build_err: false, + expected_denied: false, + }, + TestCase { + name: "deny=true, allow=true -> allowed", + in_deny: true, + in_allow: true, + expected_build_err: false, + expected_denied: false, + }, + TestCase { + name: "deny=false, allow=true -> allowed", + in_deny: false, + in_allow: true, + expected_build_err: true, + expected_denied: false, + }, + ]; + + let test_v4 = [192, 0, 2, 1].into(); + let test_v4_net = "192.0.2.0/24".parse().unwrap(); + let test_v6 = [0x2001, 0xdb8, 0, 0, 0, 0, 0, 1].into(); + let test_v6_net = "2001:db8::/32".parse().unwrap(); + + for tc in &test_cases { + let mut builder = AccessControlSetBuilder::new(tc.name); + if tc.in_deny { + builder = builder.deny([test_v4_net, test_v6_net].iter()); + } + if tc.in_allow { + builder = builder.allow([test_v4_net, test_v6_net].iter()); + } + + let Ok(acs) = builder.build() else { + match tc.expected_build_err { + true => continue, + false => panic!("unexpected builder error"), + } + }; + assert_eq!( + acs.denied(test_v4), + tc.expected_denied, + "IPv4 case '{}' failed", + tc.name + ); + assert_eq!( + acs.denied(test_v6), + tc.expected_denied, + "IPv6 case '{}' failed", + tc.name + ); + } + } + + #[test] + fn allows_all_test() { + let empty = AccessControlSet::empty("empty"); + assert!(empty.allows_all()); + + let v4_only = AccessControlSetBuilder::new("v4 only") + .deny(["10.0.0.0/8".parse().unwrap()].iter()) + .build() + .unwrap(); + assert!(!v4_only.allows_all()); + + let v6_only = AccessControlSetBuilder::new("v6 only") + .deny(["fe80::/10".parse().unwrap()].iter()) + .build() + .unwrap(); + assert!(!v6_only.allows_all()); + + let both = AccessControlSetBuilder::new("both") + .deny(["10.0.0.0/8".parse().unwrap(), "fe80::/10".parse().unwrap()].iter()) + .build() + .unwrap(); + assert!(!both.allows_all()); + } + + #[test] + fn v4_only_deny_test() { + let acs = AccessControlSetBuilder::new("v4 only deny") + .deny(["10.0.0.0/8".parse().unwrap()].iter()) + .build() + .unwrap(); + + assert!(!acs.allows_all()); + assert!(acs.denied([10, 0, 0, 1].into())); + assert!(acs.denied([10, 255, 255, 255].into())); + assert!(!acs.denied([11, 0, 0, 1].into())); + assert!(!acs.denied([0xfe80, 0, 0, 0, 0, 0, 0, 1].into())); + } + + #[test] + fn v6_only_deny_test() { + let acs = AccessControlSetBuilder::new("v6 only deny") + .deny(["fe80::/10".parse().unwrap()].iter()) + .build() + .unwrap(); + + assert!(!acs.allows_all()); + assert!(acs.denied([0xfe80, 0, 0, 0, 0, 0, 0, 1].into())); + assert!( + acs.denied( + [ + 0xfebf, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff + ] + .into() + ) + ); + assert!(!acs.denied([0xfec0, 0, 0, 0, 0, 0, 0, 1].into())); + assert!(!acs.denied([10, 0, 0, 1].into())); + } + + // `::ffff:a.b.c.d` (RFC 4291 §2.5.5.2 IPv4-mapped IPv6) names the IPv4 node + // `a.b.c.d` on a dual-stack kernel. It must match the same v4 deny prefix. + #[test] + fn v4_mapped_v6_matches_v4_deny() { + let acs = AccessControlSetBuilder::new("v4 mapped deny") + .deny( + [ + "127.0.0.0/8".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + "192.168.0.0/16".parse().unwrap(), + ] + .iter(), + ) + .build() + .unwrap(); + + assert!(acs.denied("127.0.0.1".parse().unwrap())); + assert!(acs.denied("10.2.3.4".parse().unwrap())); + assert!(acs.denied("192.168.1.1".parse().unwrap())); + + assert!(acs.denied("::ffff:127.0.0.1".parse().unwrap())); + assert!(acs.denied("::ffff:10.2.3.4".parse().unwrap())); + assert!(acs.denied("::ffff:192.168.1.1".parse().unwrap())); + + assert!(!acs.denied("8.8.8.8".parse().unwrap())); + assert!(!acs.denied("::ffff:8.8.8.8".parse().unwrap())); + assert!(!acs.denied("2001:db8::1".parse().unwrap())); + } + + // The v4-mapped form must also pick up v4 allow-list overrides, otherwise + // canonicalising would deny mapped traffic the operator explicitly allowed. + #[test] + fn v4_mapped_v6_honours_v4_allow_override() { + let acs = AccessControlSetBuilder::new("v4 mapped allow override") + .deny(["10.0.0.0/8".parse().unwrap()].iter()) + .allow(["10.1.2.3/32".parse().unwrap()].iter()) + .build() + .unwrap(); + + assert!(acs.denied("10.2.3.4".parse().unwrap())); + assert!(!acs.denied("10.1.2.3".parse().unwrap())); + + assert!(acs.denied("::ffff:10.2.3.4".parse().unwrap())); + assert!(!acs.denied("::ffff:10.1.2.3".parse().unwrap())); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/algorithm.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/algorithm.rs new file mode 100644 index 00000000..cd0318e7 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/algorithm.rs @@ -0,0 +1,306 @@ +// Copyright 2015-2022 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +// needed for the derive statements on algorithm +// this issue in rustc would help narrow the statement: https://github.com/rust-lang/rust/issues/62398 +#![allow(deprecated, clippy::use_self)] + +use alloc::fmt; +use core::fmt::{Display, Formatter}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::error::*; +use crate::serialize::binary::*; + +/// DNSSEC signing and validation algorithms. +/// +/// For [reference](https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml) +/// the iana documents have all the officially registered algorithms. +/// +/// [RFC 6944](https://tools.ietf.org/html/rfc6944), DNSSEC DNSKEY Algorithm Status, April 2013 +/// +/// ```text +/// +/// 2.2. Algorithm Implementation Status Assignment Rationale +/// +/// RSASHA1 has an implementation status of Must Implement, consistent +/// with [RFC4034]. RSAMD5 has an implementation status of Must Not +/// Implement because of known weaknesses in MD5. +/// +/// The status of RSASHA1-NSEC3-SHA1 is set to Recommended to Implement +/// as many deployments use NSEC3. The status of RSA/SHA-256 and RSA/ +/// SHA-512 are also set to Recommended to Implement as major deployments +/// (such as the root zone) use these algorithms [ROOTDPS]. It is +/// believed that RSA/SHA-256 or RSA/SHA-512 algorithms will replace +/// older algorithms (e.g., RSA/SHA-1) that have a perceived weakness. +/// +/// Likewise, ECDSA with the two identified curves (ECDSAP256SHA256 and +/// ECDSAP384SHA384) is an algorithm that may see widespread use due to +/// the perceived similar level of security offered with smaller key size +/// compared to the key sizes of algorithms such as RSA. Therefore, +/// ECDSAP256SHA256 and ECDSAP384SHA384 are Recommended to Implement. +/// +/// All other algorithms used in DNSSEC specified without an +/// implementation status are currently set to Optional. +/// +/// 2.3. DNSSEC Implementation Status Table +/// +/// The DNSSEC algorithm implementation status table is listed below. +/// Only the algorithms already specified for use with DNSSEC at the time +/// of writing are listed. +/// +/// +------------+------------+-------------------+-------------------+ +/// | Must | Must Not | Recommended | Optional | +/// | Implement | Implement | to Implement | | +/// +------------+------------+-------------------+-------------------+ +/// | | | | | +/// | RSASHA1 | RSAMD5 | RSASHA256 | Any | +/// | | | RSASHA1-NSEC3 | registered | +/// | | | -SHA1 | algorithm | +/// | | | RSASHA512 | not listed in | +/// | | | ECDSAP256SHA256 | this table | +/// | | | ECDSAP384SHA384 | | +/// +------------+------------+-------------------+-------------------+ +/// +/// This table does not list the Reserved values in the IANA registry +/// table or the values for INDIRECT (252), PRIVATE (253), and PRIVATEOID +/// (254). These values may relate to more than one algorithm and are +/// therefore up to the implementer's discretion. As noted, any +/// algorithm not listed in the table is Optional. As of this writing, +/// the Optional algorithms are DSASHA1, DH, DSA-NSEC3-SHA1, and GOST- +/// ECC, but in general, anything not explicitly listed is Optional. +/// +/// 2.4. Specifying New Algorithms and Updating the Status of Existing +/// Entries +/// +/// [RFC6014] establishes a parallel procedure for adding a registry +/// entry for a new algorithm other than a standards track document. +/// Because any algorithm not listed in the foregoing table is Optional, +/// algorithms entered into the registry using the [RFC6014] procedure +/// are automatically Optional. +/// +/// It has turned out to be useful for implementations to refer to a +/// single document that specifies the implementation status of every +/// algorithm. Accordingly, when a new algorithm is to be registered +/// with a status other than Optional, this document shall be made +/// obsolete by a new document that adds the new algorithm to the table +/// in Section 2.3. Similarly, if the status of any algorithm in the +/// table in Section 2.3 changes, a new document shall make this document +/// obsolete; that document shall include a replacement of the table in +/// Section 2.3. This way, the goal of having one authoritative document +/// to specify all the status values is achieved. +/// +/// This document cannot be updated, only made obsolete and replaced by a +/// successor document. +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +#[non_exhaustive] +pub enum Algorithm { + /// DO NOT USE, MD5 is a compromised hashing function, it is here for backward compatibility + #[deprecated( + note = "this is a compromised hashing function, it is here for backward compatibility" + )] + RSAMD5, + /// DO NOT USE, DSA is a compromised hashing function, it is here for backward compatibility + #[deprecated( + note = "this is a compromised hashing function, it is here for backward compatibility" + )] + DSA, + /// DO NOT USE, SHA1 is a compromised hashing function, it is here for backward compatibility + #[deprecated( + note = "this is a compromised hashing function, it is here for backward compatibility" + )] + RSASHA1, + /// DO NOT USE, SHA1 is a compromised hashing function, it is here for backward compatibility + #[deprecated( + note = "this is a compromised hashing function, it is here for backward compatibility" + )] + #[cfg_attr(feature = "serde", serde(rename = "RSASHA1-NSEC3-SHA1"))] + RSASHA1NSEC3SHA1, + /// RSA public key with SHA256 hash + RSASHA256, + /// RSA public key with SHA512 hash + RSASHA512, + /// [rfc6605](https://tools.ietf.org/html/rfc6605) + ECDSAP256SHA256, + /// [rfc6605](https://tools.ietf.org/html/rfc6605) + ECDSAP384SHA384, + /// [RFC 8080](https://datatracker.ietf.org/doc/html/rfc8080) + ED25519, + /// An unknown algorithm identifier + Unknown(u8), +} + +impl Algorithm { + /// + pub fn from_u8(value: u8) -> Self { + #[allow(deprecated)] + match value { + 1 => Self::RSAMD5, + 3 => Self::DSA, + 5 => Self::RSASHA1, + 7 => Self::RSASHA1NSEC3SHA1, + 8 => Self::RSASHA256, + 10 => Self::RSASHA512, + 13 => Self::ECDSAP256SHA256, + 14 => Self::ECDSAP384SHA384, + 15 => Self::ED25519, + _ => Self::Unknown(value), + } + } + + /// Whether this algorithm is supported by hickory's build settings + pub fn is_supported(&self) -> bool { + matches!( + self, + Algorithm::ECDSAP256SHA256 + | Algorithm::ECDSAP384SHA384 + | Algorithm::ED25519 + | Algorithm::RSASHA1 + | Algorithm::RSASHA1NSEC3SHA1 + | Algorithm::RSASHA256 + | Algorithm::RSASHA512 + ) + } + + /// length in bytes that the hash portion of this function will produce + pub fn hash_len(self) -> Option { + match self { + Self::RSAMD5 => Some(16), // 128 bits + Self::DSA | Self::RSASHA1 | Self::RSASHA1NSEC3SHA1 => Some(20), // 160 bits + Self::RSASHA256 | Self::ECDSAP256SHA256 | Self::ED25519 => Some(32), // 256 bits + Self::ECDSAP384SHA384 => Some(48), + Self::RSASHA512 => Some(64), // 512 bites + Self::Unknown(_) => None, + } + } + + /// Convert to string form + #[deprecated(note = "use as_str instead")] + pub fn to_str(self) -> &'static str { + self.as_str() + } + + /// Convert to string form + pub fn as_str(self) -> &'static str { + match self { + Self::RSAMD5 => "RSAMD5", + Self::DSA => "DSA", + Self::RSASHA1 => "RSASHA1", + Self::RSASHA256 => "RSASHA256", + Self::RSASHA1NSEC3SHA1 => "RSASHA1-NSEC3-SHA1", + Self::RSASHA512 => "RSASHA512", + Self::ECDSAP256SHA256 => "ECDSAP256SHA256", + Self::ECDSAP384SHA384 => "ECDSAP384SHA384", + Self::ED25519 => "ED25519", + Self::Unknown(_) => "Unknown", + } + } +} + +impl BinEncodable for Algorithm { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit(u8::from(*self)) + } +} + +impl<'r> BinDecodable<'r> for Algorithm { + // https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml + fn read(decoder: &mut BinDecoder<'r>) -> Result { + let algorithm_id = + decoder.read_u8()?.unverified(/*Algorithm is verified as safe in processing this*/); + Ok(Self::from_u8(algorithm_id)) + } +} + +impl From for &'static str { + fn from(a: Algorithm) -> &'static str { + a.as_str() + } +} + +impl From for u8 { + fn from(a: Algorithm) -> Self { + match a { + Algorithm::RSAMD5 => 1, + Algorithm::DSA => 3, + Algorithm::RSASHA1 => 5, + Algorithm::RSASHA1NSEC3SHA1 => 7, + Algorithm::RSASHA256 => 8, + Algorithm::RSASHA512 => 10, + Algorithm::ECDSAP256SHA256 => 13, + Algorithm::ECDSAP384SHA384 => 14, + Algorithm::ED25519 => 15, + Algorithm::Unknown(v) => v, + } + } +} + +impl Display for Algorithm { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str(self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_into() { + for algorithm in &[ + Algorithm::RSAMD5, + Algorithm::DSA, + Algorithm::RSASHA1, + Algorithm::RSASHA256, + Algorithm::RSASHA1NSEC3SHA1, + Algorithm::RSASHA512, + Algorithm::ECDSAP256SHA256, + Algorithm::ECDSAP384SHA384, + Algorithm::ED25519, + ] { + assert_eq!(*algorithm, Algorithm::from_u8(Into::::into(*algorithm))) + } + } + + #[test] + fn test_order() { + let mut algorithms = [ + Algorithm::RSAMD5, + Algorithm::DSA, + Algorithm::RSASHA1, + Algorithm::RSASHA256, + Algorithm::RSASHA1NSEC3SHA1, + Algorithm::RSASHA512, + Algorithm::ECDSAP256SHA256, + Algorithm::ECDSAP384SHA384, + Algorithm::ED25519, + ]; + + algorithms.sort(); + + for (got, expect) in algorithms.iter().zip( + [ + Algorithm::RSAMD5, + Algorithm::DSA, + Algorithm::RSASHA1, + Algorithm::RSASHA1NSEC3SHA1, + Algorithm::RSASHA256, + Algorithm::RSASHA512, + Algorithm::ECDSAP256SHA256, + Algorithm::ECDSAP384SHA384, + Algorithm::ED25519, + ] + .iter(), + ) { + assert_eq!(got, expect); + } + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/crypto.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/crypto.rs new file mode 100644 index 00000000..3db4c8c5 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/crypto.rs @@ -0,0 +1,590 @@ +use alloc::{borrow::Cow, boxed::Box, sync::Arc, vec::Vec}; + +use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer}; + +use super::{ + Algorithm, DigestType, DnsSecError, DnsSecResult, PublicKey, PublicKeyBuf, SigningKey, TBS, + ec_public_key::ECPublicKey, + ring_like::{ + ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, ED25519_PUBLIC_KEY_LEN, + EcdsaKeyPair, Ed25519KeyPair, KeyPair, PublicKeyComponents, RSA_PKCS1_SHA256, + RSA_PKCS1_SHA512, RsaKeyPair, SystemRandom, digest, signature, + }, + rsa_public_key::RSAPublicKey, +}; +use crate::{ProtoError, error::ProtoResult, serialize::binary::DecodeError}; + +/// Decode private key +pub fn signing_key_from_der( + key_der: &PrivateKeyDer<'_>, + algorithm: Algorithm, +) -> DnsSecResult> { + #[allow(deprecated)] + match algorithm { + Algorithm::Unknown(v) => Err(format!("unknown algorithm: {v}").into()), + Algorithm::RSASHA256 | Algorithm::RSASHA512 => { + Ok(Box::new(RsaSigningKey::from_key_der(key_der, algorithm)?)) + } + Algorithm::ECDSAP256SHA256 | Algorithm::ECDSAP384SHA384 => { + Ok(Box::new(EcdsaSigningKey::from_key_der(key_der, algorithm)?)) + } + Algorithm::ED25519 => Ok(Box::new(Ed25519SigningKey::from_key_der(key_der)?)), + e => Err(format!("unsupported SigningKey algorithm for ring: {e:?}").into()), + } +} + +pub(super) fn decode_public_key<'a>( + public_key: &'a [u8], + algorithm: Algorithm, +) -> ProtoResult> { + // try to keep this and `Algorithm::is_supported` in sync + debug_assert!(algorithm.is_supported()); + + #[allow(deprecated)] + match algorithm { + Algorithm::ECDSAP256SHA256 | Algorithm::ECDSAP384SHA384 => Ok(Arc::new( + ECPublicKey::from_public_bytes(public_key, algorithm)?, + )), + Algorithm::ED25519 => Ok(Arc::new(Ed25519::from_public_bytes(public_key.into())?)), + Algorithm::RSASHA1 + | Algorithm::RSASHA1NSEC3SHA1 + | Algorithm::RSASHA256 + | Algorithm::RSASHA512 => Ok(Arc::new(Rsa::from_public_bytes(public_key, algorithm)?)), + _ => Err("public key algorithm not supported".into()), + } +} + +/// An ECDSA signing key pair (backed by ring). +pub struct EcdsaSigningKey { + inner: EcdsaKeyPair, + algorithm: Algorithm, +} + +impl EcdsaSigningKey { + /// Decode signing key pair from DER. + /// + /// Errors unless the given algorithm is one of the following: + /// + /// - [`Algorithm::ECDSAP256SHA256`] + /// - [`Algorithm::ECDSAP384SHA384`] + pub fn from_key_der(key: &PrivateKeyDer<'_>, algorithm: Algorithm) -> DnsSecResult { + match key { + PrivateKeyDer::Pkcs8(key) => Self::from_pkcs8(key, algorithm), + _ => Err("unsupported key format (only PKCS#8 supported)".into()), + } + } + + /// Decode signing key pair from DER-encoded PKCS#8 bytes. + /// + /// Errors unless the given algorithm is one of the following: + /// + /// - [`Algorithm::ECDSAP256SHA256`] + /// - [`Algorithm::ECDSAP384SHA384`] + pub fn from_pkcs8(key: &PrivatePkcs8KeyDer<'_>, algorithm: Algorithm) -> DnsSecResult { + let ring_algorithm = if algorithm == Algorithm::ECDSAP256SHA256 { + &ECDSA_P256_SHA256_FIXED_SIGNING + } else if algorithm == Algorithm::ECDSAP384SHA384 { + &ECDSA_P384_SHA384_FIXED_SIGNING + } else { + return Err(DnsSecError::Message("unsupported algorithm")); + }; + + #[cfg(all(feature = "dnssec-aws-lc-rs", not(feature = "dnssec-ring")))] + let inner = EcdsaKeyPair::from_pkcs8(ring_algorithm, key.secret_pkcs8_der())?; + + #[cfg(feature = "dnssec-ring")] + let inner = + EcdsaKeyPair::from_pkcs8(ring_algorithm, key.secret_pkcs8_der(), &SystemRandom::new())?; + + Ok(Self { inner, algorithm }) + } + + /// Creates an ECDSA key pair with ring. + pub fn from_ecdsa(inner: EcdsaKeyPair, algorithm: Algorithm) -> Self { + Self { inner, algorithm } + } + + /// Generate signing key pair and return the DER-encoded PKCS#8 bytes. + /// + /// Errors unless the given algorithm is one of the following: + /// + /// - [`Algorithm::ECDSAP256SHA256`] + /// - [`Algorithm::ECDSAP384SHA384`] + pub fn generate_pkcs8(algorithm: Algorithm) -> DnsSecResult> { + let rng = SystemRandom::new(); + let alg = if algorithm == Algorithm::ECDSAP256SHA256 { + &ECDSA_P256_SHA256_FIXED_SIGNING + } else if algorithm == Algorithm::ECDSAP384SHA384 { + &ECDSA_P384_SHA384_FIXED_SIGNING + } else { + return Err(DnsSecError::Message("unsupported algorithm")); + }; + + let pkcs8 = EcdsaKeyPair::generate_pkcs8(alg, &rng)?; + Ok(PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec())) + } +} + +impl SigningKey for EcdsaSigningKey { + fn sign(&self, tbs: &TBS) -> DnsSecResult> { + let rng = SystemRandom::new(); + Ok(self.inner.sign(&rng, tbs.as_ref())?.as_ref().to_vec()) + } + + fn to_public_key(&self) -> DnsSecResult { + let mut bytes = self.inner.public_key().as_ref().to_vec(); + bytes.remove(0); + Ok(PublicKeyBuf::new(bytes, self.algorithm)) + } + + fn algorithm(&self) -> Algorithm { + self.algorithm + } +} + +/// An Ed25519 signing key pair (backed by ring). +pub struct Ed25519SigningKey { + inner: Ed25519KeyPair, +} + +impl Ed25519SigningKey { + /// Decode signing key pair from DER. + pub fn from_key_der(key: &PrivateKeyDer<'_>) -> DnsSecResult { + match key { + PrivateKeyDer::Pkcs8(key) => Self::from_pkcs8(key), + _ => Err("unsupported key format (only PKCS#8 supported)".into()), + } + } + + /// Decode signing key pair from DER-encoded PKCS#8 bytes. + pub fn from_pkcs8(key: &PrivatePkcs8KeyDer<'_>) -> DnsSecResult { + Ok(Self { + inner: Ed25519KeyPair::from_pkcs8(key.secret_pkcs8_der())?, + }) + } + + /// Creates an Ed25519 keypair. + pub fn from_ed25519(inner: Ed25519KeyPair) -> Self { + Self { inner } + } + + /// Generate signing key pair and return the DER-encoded PKCS#8 bytes. + pub fn generate_pkcs8() -> DnsSecResult> { + let rng = SystemRandom::new(); + let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)?; + Ok(PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec())) + } +} + +impl SigningKey for Ed25519SigningKey { + fn sign(&self, tbs: &TBS) -> DnsSecResult> { + Ok(self.inner.sign(tbs.as_ref()).as_ref().to_vec()) + } + + fn to_public_key(&self) -> DnsSecResult { + Ok(PublicKeyBuf::new( + self.inner.public_key().as_ref().to_vec(), + Algorithm::ED25519, + )) + } + + fn algorithm(&self) -> Algorithm { + Algorithm::ED25519 + } +} + +/// Ed25519 Public key +pub struct Ed25519<'k> { + raw: Cow<'k, [u8]>, +} + +impl<'k> Ed25519<'k> { + /// ```text + /// RFC 8080 EdDSA for DNSSEC February 2017 + /// + /// An Ed25519 public key consists of a 32-octet value, which is encoded + /// into the Public Key field of a DNSKEY resource record as a simple bit + /// string. The generation of a public key is defined in Section 5.1.5 + /// of [RFC8032]. + /// ``` + pub fn from_public_bytes(public_key: Cow<'k, [u8]>) -> ProtoResult { + if public_key.len() != ED25519_PUBLIC_KEY_LEN { + return Err(format!( + "expected {} byte public_key: {}", + ED25519_PUBLIC_KEY_LEN, + public_key.len() + ) + .into()); + } + + Ok(Self { raw: public_key }) + } +} + +impl PublicKey for Ed25519<'_> { + // TODO: just store reference to public key bytes in ctor... + fn public_bytes(&self) -> &[u8] { + self.raw.as_ref() + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> ProtoResult<()> { + let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, self.raw.as_ref()); + public_key + .verify(message, signature) + .map_err(|_| ProtoError::Crypto("ED25519 signature verification failed")) + } + + fn algorithm(&self) -> Algorithm { + Algorithm::ED25519 + } +} + +/// Rsa public key +pub struct Rsa<'k> { + raw: &'k [u8], + pkey: RSAPublicKey<'k>, + algorithm: Algorithm, +} + +impl<'k> Rsa<'k> { + /// ```text + /// RFC 3110 RSA SIGs and KEYs in the DNS May 2001 + /// + /// 2. RSA Public KEY Resource Records + /// + /// RSA public keys are stored in the DNS as KEY RRs using algorithm + /// number 5 [RFC2535]. The structure of the algorithm specific portion + /// of the RDATA part of such RRs is as shown below. + /// + /// Field Size + /// ----- ---- + /// exponent length 1 or 3 octets (see text) + /// exponent as specified by length field + /// modulus remaining space + /// + /// For interoperability, the exponent and modulus are each limited to + /// 4096 bits in length. The public key exponent is a variable length + /// unsigned integer. Its length in octets is represented as one octet + /// if it is in the range of 1 to 255 and by a zero octet followed by a + /// two octet unsigned length if it is longer than 255 bytes. The public + /// key modulus field is a multiprecision unsigned integer. The length + /// of the modulus can be determined from the RDLENGTH and the preceding + /// RDATA fields including the exponent. Leading zero octets are + /// prohibited in the exponent and modulus. + /// + /// Note: KEY RRs for use with RSA/SHA1 DNS signatures MUST use this + /// algorithm number (rather than the algorithm number specified in the + /// obsoleted RFC 2537). + /// + /// Note: This changes the algorithm number for RSA KEY RRs to be the + /// same as the new algorithm number for RSA/SHA1 SIGs. + /// ``` + pub fn from_public_bytes(raw: &'k [u8], algorithm: Algorithm) -> ProtoResult { + let pkey = RSAPublicKey::try_from(raw)?; + Ok(Self { + raw, + pkey, + algorithm, + }) + } +} + +impl PublicKey for Rsa<'_> { + fn public_bytes(&self) -> &[u8] { + self.raw + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> ProtoResult<()> { + #[allow(deprecated)] + let alg = match self.algorithm { + Algorithm::RSASHA256 => &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, + Algorithm::RSASHA512 => &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY, + // RFC 5155 §2: algorithm 7 uses the same RSA/SHA-1 signature scheme as algorithm 5; + // the distinct identifier only signals NSEC3 capability of the signer. + Algorithm::RSASHA1 | Algorithm::RSASHA1NSEC3SHA1 => { + &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY + } + _ => unreachable!("non-RSA algorithm passed to RSA verify()"), + }; + let public_key = signature::RsaPublicKeyComponents { + n: self.pkey.n(), + e: self.pkey.e(), + }; + public_key + .verify(alg, message, signature) + .map_err(|_| ProtoError::Crypto("RSA signature verification failed")) + } + + fn algorithm(&self) -> Algorithm { + self.algorithm + } +} + +/// An RSA signing key pair (backed by ring). +pub struct RsaSigningKey { + inner: RsaKeyPair, + algorithm: Algorithm, +} + +impl RsaSigningKey { + /// Decode signing key pair from DER. + pub fn from_key_der(key: &PrivateKeyDer<'_>, algorithm: Algorithm) -> DnsSecResult { + match key { + PrivateKeyDer::Pkcs8(key) => Self::from_pkcs8(key, algorithm), + PrivateKeyDer::Pkcs1(key) => Self::from_pkcs1(key, algorithm), + _ => Err("unsupported key format (only PKCS#8 supported)".into()), + } + } + + /// Decode signing key pair from DER-encoded PKCS#8 bytes. + pub fn from_pkcs8(key: &PrivatePkcs8KeyDer<'_>, algorithm: Algorithm) -> DnsSecResult { + match algorithm { + #[allow(deprecated)] + Algorithm::RSASHA1 | Algorithm::RSASHA1NSEC3SHA1 => { + return Err("unsupported Algorithm (insecure): {algorithm:?}".into()); + } + Algorithm::RSASHA256 | Algorithm::RSASHA512 => {} + _ => return Err("unsupported Algorithm: {algorithm:?}".into()), + } + + Ok(Self { + inner: RsaKeyPair::from_pkcs8(key.secret_pkcs8_der())?, + algorithm, + }) + } + + /// Decode signing key pair from DER-encoded PKCS#1 bytes. + pub fn from_pkcs1(key: &PrivatePkcs1KeyDer<'_>, algorithm: Algorithm) -> DnsSecResult { + match algorithm { + #[allow(deprecated)] + Algorithm::RSASHA1 | Algorithm::RSASHA1NSEC3SHA1 => { + return Err("unsupported Algorithm (insecure): {algorithm:?}".into()); + } + Algorithm::RSASHA256 | Algorithm::RSASHA512 => {} + _ => return Err("unsupported Algorithm: {algorithm:?}".into()), + } + + Ok(Self { + inner: RsaKeyPair::from_der(key.secret_pkcs1_der())?, + algorithm, + }) + } +} + +impl SigningKey for RsaSigningKey { + fn sign(&self, tbs: &TBS) -> DnsSecResult> { + let encoding = match self.algorithm { + Algorithm::RSASHA256 => &RSA_PKCS1_SHA256, + Algorithm::RSASHA512 => &RSA_PKCS1_SHA512, + _ => unreachable!(), + }; + + let rng = SystemRandom::new(); + let mut signature = vec![0; self.inner.public_key().modulus_len()]; + self.inner + .sign(encoding, &rng, tbs.as_ref(), &mut signature)?; + Ok(signature) + } + + fn to_public_key(&self) -> DnsSecResult { + let components = PublicKeyComponents::>::from(self.inner.public_key()); + + let mut buf = Vec::with_capacity(components.e.len() + components.n.len()); + if components.e.len() > 255 { + buf.push(0); + buf.push((components.e.len() >> 8) as u8); + } + + buf.push(components.e.len() as u8); + buf.extend(&components.e); + buf.extend(&components.n); + Ok(PublicKeyBuf::new(buf, self.algorithm)) + } + + fn algorithm(&self) -> Algorithm { + self.algorithm + } +} + +/// Hashing wrapper type. +#[derive(Clone, Copy, Debug)] +pub struct Digest(digest::Digest); + +impl Digest { + /// Hashes the given `data` `iterations` times with the given `salt` and `r#type`. + pub fn iterated( + salt: &[u8], + bytes: &[u8], + r#type: DigestType, + mut iterations: u16, + ) -> Result { + let alg = r#type.try_into()?; + let mut cur = hash_iter([bytes, salt], alg); + while iterations > 0 { + cur = hash_iter([cur.as_ref(), salt], alg); + iterations -= 1; + } + Ok(Self(cur)) + } + + /// Hashes the data from the `bytes` iterator with the given `r#type`. + pub fn from_iter<'a>( + bytes: impl IntoIterator, + r#type: DigestType, + ) -> Result { + Ok(Self(hash_iter(bytes, r#type.try_into()?))) + } + + /// Hashes + pub fn new(bytes: &[u8], r#type: DigestType) -> Result { + Ok(Self(digest::digest(r#type.try_into()?, bytes))) + } +} + +fn hash_iter<'a>( + iter: impl IntoIterator, + alg: &'static digest::Algorithm, +) -> digest::Digest { + let mut ctx = digest::Context::new(alg); + for d in iter { + ctx.update(d); + } + ctx.finish() +} + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl TryFrom for &'static digest::Algorithm { + type Error = DecodeError; + + fn try_from(value: DigestType) -> Result<&'static digest::Algorithm, Self::Error> { + match value { + DigestType::SHA1 => Ok(&digest::SHA1_FOR_LEGACY_USE_ONLY), + DigestType::SHA256 => Ok(&digest::SHA256), + DigestType::SHA384 => Ok(&digest::SHA384), + DigestType::Unknown(other) => Err(DecodeError::UnknownDigestAlgorithm(other)), + } + } +} + +#[cfg(test)] +mod tests { + use rustls_pki_types::pem::PemObject; + + use super::*; + use crate::dnssec::test_utils::{hash_test, public_key_test}; + + #[test] + fn test_ec_p256_pkcs8() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let key = signing_key_from_der(&PrivateKeyDer::from(pkcs8), algorithm).unwrap(); + public_key_test(&*key); + + let neg_pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let neg = signing_key_from_der(&PrivateKeyDer::from(neg_pkcs8), algorithm).unwrap(); + hash_test(&*key, &*neg); + } + + #[test] + fn test_ec_p384_pkcs8() { + let algorithm = Algorithm::ECDSAP384SHA384; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let key = signing_key_from_der(&PrivateKeyDer::from(pkcs8), algorithm).unwrap(); + public_key_test(&*key); + + let neg_pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let neg = signing_key_from_der(&PrivateKeyDer::from(neg_pkcs8), algorithm).unwrap(); + hash_test(&*key, &*neg); + } + + #[test] + fn test_ed25519() { + let algorithm = Algorithm::ED25519; + let pkcs8 = Ed25519SigningKey::generate_pkcs8().unwrap(); + let key = signing_key_from_der(&PrivateKeyDer::from(pkcs8), algorithm).unwrap(); + public_key_test(&*key); + + let neg_pkcs8 = Ed25519SigningKey::generate_pkcs8().unwrap(); + let neg = signing_key_from_der(&PrivateKeyDer::from(neg_pkcs8), algorithm).unwrap(); + hash_test(&*key, &*neg); + } + + #[test] + fn test_rsa() { + // ring currently does not support RSA key generation support. + // Generated per the documentation from https://docs.rs/ring/latest/ring/rsa/struct.KeyPair.html#method.from_pkcs8. + const KEY_1: &[u8] = include_bytes!("../../tests/test-data/rsa-2048-private-key-1.pk8"); + const KEY_2: &[u8] = include_bytes!("../../tests/test-data/rsa-2048-private-key-2.pk8"); + + let algorithm = Algorithm::RSASHA256; + let key_der = PrivateKeyDer::try_from(KEY_1).unwrap(); + let key = signing_key_from_der(&key_der, algorithm).unwrap(); + public_key_test(&*key); + + let key_der = PrivateKeyDer::try_from(KEY_2).unwrap(); + let neg = signing_key_from_der(&key_der, algorithm).unwrap(); + hash_test(&*key, &*neg); + } + + #[test] + fn test_ec_encode_decode_pkcs8() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + signing_key_from_der(&PrivateKeyDer::from(pkcs8), algorithm).unwrap(); + } + + #[test] + fn test_ed25519_encode_decode_pkcs8() { + let pkcs8 = Ed25519SigningKey::generate_pkcs8().unwrap(); + signing_key_from_der(&PrivateKeyDer::from(pkcs8), Algorithm::ED25519).unwrap(); + } + + #[test] + fn test_rsasha256_encode_decode_pkcs8() { + // ring currently does not support RSA key generation support. + // Generated per the documentation from https://docs.rs/ring/latest/ring/rsa/struct.KeyPair.html#method.from_pkcs8. + const KEY: &[u8] = include_bytes!("../../tests/test-data/rsa-2048-private-key-1.pk8"); + let key_der = PrivateKeyDer::try_from(KEY).unwrap(); + signing_key_from_der(&key_der, Algorithm::RSASHA256).unwrap(); + } + + #[test] + fn test_rsasha256_decode_pkcs1() { + const KEY: &[u8] = include_bytes!("../../tests/test-data/rsa-2048-pkcs1.pem"); + let key_der = PrivateKeyDer::from_pem_slice(KEY).unwrap(); + assert!(matches!(key_der, PrivateKeyDer::Pkcs1(_))); + signing_key_from_der(&key_der, Algorithm::RSASHA256).unwrap(); + } + + /// Regression test for https://github.com/hickory-dns/hickory-dns/issues/3690. + /// + /// Per RFC 5155 §2, DNSSEC algorithm 7 (RSASHA1NSEC3SHA1) uses the same RSA/SHA-1 + /// signature scheme as algorithm 5 (RSASHA1); the distinct identifier only signals + /// that the signer understands NSEC3. `Algorithm::is_supported()` advertises both, + /// so `Rsa::verify` must accept them on equal footing instead of rejecting alg 7. + #[test] + #[allow(deprecated)] + fn test_rsasha1_nsec3sha1_uses_same_crypto_as_rsasha1() { + const KEY: &[u8] = include_bytes!("../../tests/test-data/rsa-2048-private-key-1.pk8"); + let signing = + RsaSigningKey::from_pkcs8(&PrivatePkcs8KeyDer::from(KEY), Algorithm::RSASHA256) + .unwrap(); + let key_bytes = signing.to_public_key().unwrap().into_inner(); + + // Both algorithms must take the same code path. A garbage signature must + // produce a verifier failure, not an algorithm-dispatch failure. + for algorithm in [Algorithm::RSASHA1, Algorithm::RSASHA1NSEC3SHA1] { + let key = Rsa::from_public_bytes(&key_bytes, algorithm).unwrap(); + let err = key.verify(b"message", b"bogus signature").unwrap_err(); + assert!( + matches!(err, ProtoError::Crypto("RSA signature verification failed")), + "unexpected error for {algorithm:?}: {err:?}", + ); + } + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/ec_public_key.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/ec_public_key.rs new file mode 100644 index 00000000..6d65346e --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/ec_public_key.rs @@ -0,0 +1,113 @@ +// Copyright 2017 Brian Smith +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use super::{Algorithm, PublicKey, ring_like::signature}; +use crate::error::{ProtoError, ProtoResult}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) struct ECPublicKey { + buf: [u8; MAX_LEN], + len: usize, + pub(crate) algorithm: Algorithm, +} + +// The length of the longest supported EC public key (P-384). +const MAX_LEN: usize = 1 + (2 * 48); + +impl ECPublicKey { + /// ```text + /// RFC 6605 ECDSA for DNSSEC April 2012 + /// + /// 4. DNSKEY and RRSIG Resource Records for ECDSA + /// + /// ECDSA public keys consist of a single value, called "Q" in FIPS + /// 186-3. In DNSSEC keys, Q is a simple bit string that represents the + /// uncompressed form of a curve point, "x | y". + /// + /// The ECDSA signature is the combination of two non-negative integers, + /// called "r" and "s" in FIPS 186-3. The two integers, each of which is + /// formatted as a simple octet string, are combined into a single longer + /// octet string for DNSSEC as the concatenation "r | s". (Conversion of + /// the integers to bit strings is described in Section C.2 of FIPS + /// 186-3.) For P-256, each integer MUST be encoded as 32 octets; for + /// P-384, each integer MUST be encoded as 48 octets. + /// + /// The algorithm numbers associated with the DNSKEY and RRSIG resource + /// records are fully defined in the IANA Considerations section. They + /// are: + /// + /// o DNSKEY and RRSIG RRs signifying ECDSA with the P-256 curve and + /// SHA-256 use the algorithm number 13. + /// + /// o DNSKEY and RRSIG RRs signifying ECDSA with the P-384 curve and + /// SHA-384 use the algorithm number 14. + /// + /// Conformant implementations that create records to be put into the DNS + /// MUST implement signing and verification for both of the above + /// algorithms. Conformant DNSSEC verifiers MUST implement verification + /// for both of the above algorithms. + /// ``` + pub(super) fn from_public_bytes(public_key: &[u8], algorithm: Algorithm) -> ProtoResult { + Self::from_unprefixed(public_key, algorithm) + } + + // DNSSEC encodes uncompressed EC public keys without the standard 0x04 + // prefix that indicates they are uncompressed, but crypto libraries + // require that prefix. + pub(super) fn from_unprefixed( + without_prefix: &[u8], + algorithm: Algorithm, + ) -> ProtoResult { + let field_len = match algorithm { + Algorithm::ECDSAP256SHA256 => 32, + Algorithm::ECDSAP384SHA384 => 48, + _ => return Err("only ECDSAP256SHA256 and ECDSAP384SHA384 are supported by Ec".into()), + }; + let len = 1 + (2 * field_len); + if len - 1 != without_prefix.len() { + return Err("EC public key is the wrong length".into()); + } + let mut buf = [0x04u8; MAX_LEN]; + buf[1..len].copy_from_slice(without_prefix); + Ok(Self { + buf, + len, + algorithm, + }) + } + + pub(super) fn prefixed_bytes(&self) -> &[u8] { + &self.buf[..self.len] + } + + pub(super) fn unprefixed_bytes(&self) -> &[u8] { + &self.buf[1..self.len] + } +} + +impl PublicKey for ECPublicKey { + fn public_bytes(&self) -> &[u8] { + self.unprefixed_bytes() + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> ProtoResult<()> { + // TODO: assert_eq!(algorithm, self.algorithm); once *ring* allows this. + let alg = match self.algorithm { + Algorithm::ECDSAP256SHA256 => &signature::ECDSA_P256_SHA256_FIXED, + Algorithm::ECDSAP384SHA384 => &signature::ECDSA_P384_SHA384_FIXED, + _ => return Err("only ECDSAP256SHA256 and ECDSAP384SHA384 are supported by Ec".into()), + }; + let public_key = signature::UnparsedPublicKey::new(alg, self.prefixed_bytes()); + public_key + .verify(message, signature) + .map_err(|_| ProtoError::Crypto("ECDSA signature verification failed")) + } + + fn algorithm(&self) -> Algorithm { + self.algorithm + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/mod.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/mod.rs new file mode 100644 index 00000000..87707166 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/mod.rs @@ -0,0 +1,636 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! dns security extension related modules + +use alloc::string::String; +use alloc::vec::Vec; +use core::slice; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::dnssec::crypto::Digest; +use crate::error::ProtoError; +use crate::rr::{Name, RData, Record, RecordType, rdata::tsig::TsigAlgorithm}; +use crate::serialize::binary::{BinEncodable, BinEncoder, DecodeError, NameEncoding}; + +mod algorithm; +pub use algorithm::Algorithm; + +/// Cryptographic backend implementations of DNSSEC traits. +pub mod crypto; + +mod ec_public_key; + +mod proof; +pub use proof::{Proof, ProofFlags, Proven}; + +mod public_key; +pub use public_key::{PublicKey, PublicKeyBuf}; + +pub mod rdata; + +mod rsa_public_key; + +mod signer; +pub use signer::DnssecSigner; + +mod supported_algorithm; +pub use supported_algorithm::SupportedAlgorithms; + +mod tbs; +pub use tbs::TBS; + +mod trust_anchor; +pub use trust_anchor::TrustAnchors; + +mod verifier; +pub use verifier::Verifier; + +/// An iterator over record data with all data wrapped in a Proven type for dnssec validation +pub struct DnssecIter<'a>(slice::Iter<'a, Record>); + +impl<'a> DnssecIter<'a> { + /// Create a new DnssecIter from any iterator of Record references + pub fn new(records: &'a [Record]) -> Self { + Self(records.iter()) + } +} + +impl<'a> Iterator for DnssecIter<'a> { + type Item = Proven<&'a Record>; + + fn next(&mut self) -> Option { + self.0.next().map(Proven::from) + } +} + +/// ```text +/// RFC 5155 NSEC3 March 2008 +/// +/// 11. IANA Considerations +/// +/// Although the NSEC3 and NSEC3PARAM RR formats include a hash algorithm +/// parameter, this document does not define a particular mechanism for +/// safely transitioning from one NSEC3 hash algorithm to another. When +/// specifying a new hash algorithm for use with NSEC3, a transition +/// mechanism MUST also be defined. +/// +/// This document updates the IANA registry "DOMAIN NAME SYSTEM +/// PARAMETERS" (https://www.iana.org/assignments/dns-parameters) in sub- +/// registry "TYPES", by defining two new types. Section 3 defines the +/// NSEC3 RR type 50. Section 4 defines the NSEC3PARAM RR type 51. +/// +/// This document updates the IANA registry "DNS SECURITY ALGORITHM +/// NUMBERS -- per [RFC4035]" +/// (https://www.iana.org/assignments/dns-sec-alg-numbers). Section 2 +/// defines the aliases DSA-NSEC3-SHA1 (6) and RSASHA1-NSEC3-SHA1 (7) for +/// respectively existing registrations DSA and RSASHA1 in combination +/// with NSEC3 hash algorithm SHA1. +/// +/// Since these algorithm numbers are aliases for existing DNSKEY +/// algorithm numbers, the flags that exist for the original algorithm +/// are valid for the alias algorithm. +/// +/// This document creates a new IANA registry for NSEC3 flags. This +/// registry is named "DNSSEC NSEC3 Flags". The initial contents of this +/// registry are: +/// +/// 0 1 2 3 4 5 6 7 +/// +---+---+---+---+---+---+---+---+ +/// | | | | | | | |Opt| +/// | | | | | | | |Out| +/// +---+---+---+---+---+---+---+---+ +/// +/// bit 7 is the Opt-Out flag. +/// +/// bits 0 - 6 are available for assignment. +/// +/// Assignment of additional NSEC3 Flags in this registry requires IETF +/// Standards Action [RFC2434]. +/// +/// This document creates a new IANA registry for NSEC3PARAM flags. This +/// registry is named "DNSSEC NSEC3PARAM Flags". The initial contents of +/// this registry are: +/// +/// 0 1 2 3 4 5 6 7 +/// +---+---+---+---+---+---+---+---+ +/// | | | | | | | | 0 | +/// +---+---+---+---+---+---+---+---+ +/// +/// bit 7 is reserved and must be 0. +/// +/// bits 0 - 6 are available for assignment. +/// +/// Assignment of additional NSEC3PARAM Flags in this registry requires +/// IETF Standards Action [RFC2434]. +/// +/// Finally, this document creates a new IANA registry for NSEC3 hash +/// algorithms. This registry is named "DNSSEC NSEC3 Hash Algorithms". +/// The initial contents of this registry are: +/// +/// 0 is Reserved. +/// +/// 1 is SHA-1. +/// +/// 2-255 Available for assignment. +/// +/// Assignment of additional NSEC3 hash algorithms in this registry +/// requires IETF Standards Action [RFC2434]. +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Default)] +pub enum Nsec3HashAlgorithm { + /// Hash for the Nsec3 records + #[default] + #[cfg_attr(feature = "serde", serde(rename = "SHA-1"))] + SHA1, +} + +impl Nsec3HashAlgorithm { + /// ```text + /// Laurie, et al. Standards Track [Page 14] + /// + /// RFC 5155 NSEC3 March 2008 + /// + /// Define H(x) to be the hash of x using the Hash Algorithm selected by + /// the NSEC3 RR, k to be the number of Iterations, and || to indicate + /// concatenation. Then define: + /// + /// IH(salt, x, 0) = H(x || salt), and + /// + /// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 + /// + /// Then the calculated hash of an owner name is + /// + /// IH(salt, owner name, iterations), + /// + /// where the owner name is in the canonical form, defined as: + /// + /// The wire format of the owner name where: + /// + /// 1. The owner name is fully expanded (no DNS name compression) and + /// fully qualified; + /// + /// 2. All uppercase US-ASCII letters are replaced by the corresponding + /// lowercase US-ASCII letters; + /// + /// 3. If the owner name is a wildcard name, the owner name is in its + /// original unexpanded form, including the "*" label (no wildcard + /// substitution); + /// ``` + pub fn hash(self, salt: &[u8], name: &Name, iterations: u16) -> Result { + match self { + // if there ever is more than just SHA1 support, this should be a genericized method + Self::SHA1 => { + let mut buf: Vec = Vec::new(); + { + let mut encoder = BinEncoder::new(&mut buf); + let mut encoder = + encoder.with_name_encoding(NameEncoding::UncompressedLowercase); + name.emit(&mut encoder)?; + } + + Ok(Digest::iterated(salt, &buf, DigestType::SHA1, iterations)?) + } + } + } +} + +impl TryFrom for Nsec3HashAlgorithm { + type Error = DecodeError; + + /// + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::SHA1), + // TODO: where/when is SHA2? + _ => Err(DecodeError::UnknownNsec3HashAlgorithm(value)), + } + } +} + +impl From for u8 { + fn from(a: Nsec3HashAlgorithm) -> Self { + match a { + Nsec3HashAlgorithm::SHA1 => 1, + } + } +} + +/// DNSSEC Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms +/// +/// [IANA Registry](https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml) +/// ```text +/// Value Description Status Reference +/// 0 Reserved - [RFC3658] +/// 1 SHA-1 MANDATORY [RFC3658] +/// 2 SHA-256 MANDATORY [RFC4509] +/// 3 GOST R 34.11-94 DEPRECATED [RFC5933][Change the status of GOST Signature Algorithms in DNSSEC in the IETF stream to Historic] +/// 4 SHA-384 OPTIONAL [RFC6605] +/// 5 GOST R 34.11-2012 OPTIONAL [RFC9558] +/// 6 SM3 OPTIONAL [RFC9563] +/// ``` +/// +/// +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +#[non_exhaustive] +pub enum DigestType { + /// [RFC 3658](https://tools.ietf.org/html/rfc3658) + #[cfg_attr(feature = "serde", serde(rename = "SHA-1"))] + SHA1, + /// [RFC 4509](https://tools.ietf.org/html/rfc4509) + #[cfg_attr(feature = "serde", serde(rename = "SHA-256"))] + SHA256, + /// [RFC 6605](https://tools.ietf.org/html/rfc6605) + #[cfg_attr(feature = "serde", serde(rename = "SHA-384"))] + SHA384, + /// An unknown digest type + Unknown(u8), +} + +impl DigestType { + /// Whether this is a supported digest type + pub fn is_supported(&self) -> bool { + !matches!(self, Self::Unknown(_)) + } +} + +impl From for DigestType { + fn from(value: u8) -> Self { + match value { + 1 => Self::SHA1, + 2 => Self::SHA256, + 4 => Self::SHA384, + _ => Self::Unknown(value), + } + } +} + +impl From for u8 { + fn from(a: DigestType) -> Self { + match a { + DigestType::SHA1 => 1, + DigestType::SHA256 => 2, + DigestType::SHA384 => 4, + DigestType::Unknown(other) => other, + } + } +} + +/// A key that can be used to sign records. +pub trait SigningKey: Send + Sync + 'static { + /// Sign DNS records. + /// + /// # Return value + /// + /// The signature, ready to be stored in an `RData::RRSIG`. + fn sign(&self, tbs: &TBS) -> DnsSecResult>; + + /// Returns a [`PublicKeyBuf`] for this [`SigningKey`]. + fn to_public_key(&self) -> DnsSecResult; + + /// Returns the algorithm of the key. + fn algorithm(&self) -> Algorithm; +} + +/// The format of the binary key +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum KeyFormat { + /// A der encoded key + Der, + /// A pem encoded key, the default of OpenSSL + Pem, + /// Pkcs8, a pkcs8 formatted private key + Pkcs8, +} + +/// An alias for dnssec results returned by functions of this crate +pub type DnsSecResult = ::core::result::Result; + +/// The error kind for dnssec errors that get returned in the crate +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum DnsSecError { + /// An HMAC failed to verify + #[error("hmac validation failure")] + HmacInvalid, + + /// An error with an arbitrary message, referenced as &'static str + #[error("{0}")] + Message(&'static str), + + /// An error with an arbitrary message, stored as String + #[error("{0}")] + Msg(String), + + // foreign + /// An error got returned by the hickory-proto crate + #[error("proto error: {0}")] + Proto(#[from] ProtoError), + + /// A ring error + #[error("ring error: {0}")] + RingKeyRejected(#[from] ring_like::KeyRejected), + + /// A ring error + #[error("ring error: {0}")] + RingUnspecified(#[from] ring_like::Unspecified), + + /// Tsig unsupported mac algorithm + /// Supported algorithm documented in `TsigAlgorithm::supported` function. + #[error("Tsig unsupported mac algorithm")] + TsigUnsupportedMacAlgorithm(TsigAlgorithm), + + /// Tsig key verification failed + #[error("Tsig key wrong key error")] + TsigWrongKey, +} + +impl From for DnsSecError { + fn from(msg: String) -> Self { + Self::Msg(msg) + } +} + +impl From<&'static str> for DnsSecError { + fn from(msg: &'static str) -> Self { + Self::Message(msg) + } +} + +impl Clone for DnsSecError { + fn clone(&self) -> Self { + use DnsSecError::*; + match self { + HmacInvalid => HmacInvalid, + Message(msg) => Message(msg), + Msg(msg) => Msg(msg.clone()), + // foreign + Proto(proto) => Proto(proto.clone()), + RingKeyRejected(r) => Msg(format!("Ring rejected key: {r}")), + RingUnspecified(_r) => RingUnspecified(ring_like::Unspecified), + TsigUnsupportedMacAlgorithm(alg) => TsigUnsupportedMacAlgorithm(alg.clone()), + TsigWrongKey => TsigWrongKey, + } + } +} + +/// DNSSEC status of an answer +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DnssecSummary { + /// All records have been DNSSEC validated + Secure, + /// At least one record is in the Bogus state + Bogus, + /// Insecure / Indeterminate (e.g. "Island of security") + Insecure, +} + +impl DnssecSummary { + /// Whether the records have been DNSSEC validated or not + /// + /// RRSIGs are skipped, since only the RRSIG used for verification carries the RRset's proof. + pub fn from_records<'a>(records: impl Iterator) -> Self { + let mut all_secure = None; + for record in records { + if record.record_type() == RecordType::RRSIG { + continue; + } + + match &record.proof { + Proof::Secure => { + all_secure.get_or_insert(true); + } + Proof::Bogus => return Self::Bogus, + _ => all_secure = Some(false), + } + } + + if all_secure.unwrap_or(false) { + Self::Secure + } else { + Self::Insecure + } + } +} + +#[cfg(all(feature = "dnssec-aws-lc-rs", not(feature = "dnssec-ring")))] +pub(crate) use aws_lc_rs_impl as ring_like; +#[cfg(feature = "dnssec-ring")] +pub(crate) use ring_impl as ring_like; + +#[cfg(feature = "dnssec-aws-lc-rs")] +#[cfg_attr(feature = "dnssec-ring", allow(unused_imports))] +pub(crate) mod aws_lc_rs_impl { + pub(crate) use aws_lc_rs::{ + digest, + error::{KeyRejected, Unspecified}, + hmac, + rand::SystemRandom, + rsa::PublicKeyComponents, + signature::{ + self, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, + ED25519_PUBLIC_KEY_LEN, EcdsaKeyPair, Ed25519KeyPair, KeyPair, RSA_PKCS1_SHA256, + RSA_PKCS1_SHA512, RsaKeyPair, + }, + }; +} + +#[cfg(feature = "dnssec-ring")] +pub(crate) mod ring_impl { + pub(crate) use ring::{ + digest, + error::{KeyRejected, Unspecified}, + hmac, + rand::SystemRandom, + rsa::PublicKeyComponents, + signature::{ + self, ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, + ED25519_PUBLIC_KEY_LEN, EcdsaKeyPair, Ed25519KeyPair, KeyPair, RSA_PKCS1_SHA256, + RSA_PKCS1_SHA512, RsaKeyPair, + }, + }; +} + +#[cfg(test)] +mod test_utils { + use rdata::DNSKEY; + + use super::*; + + pub(super) fn public_key_test(key: &dyn SigningKey) { + let pk = key.to_public_key().unwrap(); + + let tbs = TBS::from(&b"www.example.com"[..]); + let mut sig = key.sign(&tbs).unwrap(); + assert!( + pk.verify(tbs.as_ref(), &sig).is_ok(), + "public_key_test() failed to verify (algorithm: {:?})", + key.algorithm(), + ); + sig[10] = !sig[10]; + assert!( + pk.verify(tbs.as_ref(), &sig).is_err(), + "algorithm: {:?} (public key, neg)", + key.algorithm(), + ); + } + + pub(super) fn hash_test(key: &dyn SigningKey, neg: &dyn SigningKey) { + let tbs = TBS::from(&b"www.example.com"[..]); + + // TODO: convert to stored keys... + let pub_key = key.to_public_key().unwrap(); + let neg_pub_key = neg.to_public_key().unwrap(); + + let sig = key.sign(&tbs).unwrap(); + assert!( + pub_key.verify(tbs.as_ref(), &sig).is_ok(), + "algorithm: {:?}", + key.algorithm(), + ); + + let pub_key = key.to_public_key().unwrap(); + let dns_key = DNSKEY::from_key(&pub_key); + assert!( + dns_key.verify(tbs.as_ref(), &sig).is_ok(), + "algorithm: {:?} (dnskey)", + pub_key.algorithm(), + ); + assert!( + neg_pub_key.verify(tbs.as_ref(), &sig).is_err(), + "algorithm: {:?} (neg)", + neg_pub_key.algorithm(), + ); + + let neg_pub_key = neg.to_public_key().unwrap(); + let neg_dns_key = DNSKEY::from_key(&neg_pub_key); + assert!( + neg_dns_key.verify(tbs.as_ref(), &sig).is_err(), + "algorithm: {:?} (dnskey, neg)", + neg_pub_key.algorithm(), + ); + } +} + +#[cfg(test)] +mod tests { + use alloc::vec; + use core::net::Ipv4Addr; + + use super::rdata::{DNSSECRData, RRSIG, SigInput}; + use super::*; + use crate::rr::SerialNumber; + + fn a_record(name: &Name, proof: Proof) -> Record { + let mut record = Record::from_rdata( + name.clone(), + 3600, + RData::A(Ipv4Addr::new(192, 0, 2, 1).into()), + ); + record.proof = proof; + record + } + + fn rrsig_record(name: &Name, algorithm: Algorithm, proof: Proof) -> Record { + let input = SigInput { + type_covered: RecordType::A, + algorithm, + num_labels: 2, + original_ttl: 3600, + sig_expiration: SerialNumber(0), + sig_inception: SerialNumber(0), + key_tag: 0, + signer_name: Name::root(), + }; + let mut record = Record::from_rdata( + name.clone(), + 3600, + RData::DNSSEC(DNSSECRData::RRSIG(RRSIG::from_sig(input, vec![]))), + ); + record.proof = proof; + record + } + + /// Only the RRSIG used for verification is marked, the other one stays Indeterminate. + #[test] + fn summary_ignores_unused_rrsig() { + let name = Name::from_ascii("www.example.").unwrap(); + let records = [ + a_record(&name, Proof::Secure), + rrsig_record(&name, Algorithm::ECDSAP256SHA256, Proof::Secure), + rrsig_record(&name, Algorithm::RSASHA256, Proof::Indeterminate), + ]; + + assert_eq!( + DnssecSummary::from_records(records.iter()), + DnssecSummary::Secure + ); + } + + #[test] + fn summary_follows_rrset_proof() { + let name = Name::from_ascii("www.example.").unwrap(); + for (proof, expected) in [ + (Proof::Secure, DnssecSummary::Secure), + (Proof::Insecure, DnssecSummary::Insecure), + (Proof::Indeterminate, DnssecSummary::Insecure), + (Proof::Bogus, DnssecSummary::Bogus), + ] { + let records = [ + a_record(&name, proof), + rrsig_record(&name, Algorithm::ECDSAP256SHA256, proof), + ]; + assert_eq!(DnssecSummary::from_records(records.iter()), expected); + } + } + + /// The proof of an RRSIG must not override the proof of the records it covers. + #[test] + fn summary_ignores_rrsig_proof() { + let name = Name::from_ascii("www.example.").unwrap(); + let records = [ + a_record(&name, Proof::Bogus), + rrsig_record(&name, Algorithm::ECDSAP256SHA256, Proof::Secure), + ]; + assert_eq!( + DnssecSummary::from_records(records.iter()), + DnssecSummary::Bogus + ); + + let records = [ + a_record(&name, Proof::Insecure), + rrsig_record(&name, Algorithm::ECDSAP256SHA256, Proof::Secure), + ]; + assert_eq!( + DnssecSummary::from_records(records.iter()), + DnssecSummary::Insecure + ); + } + + #[test] + fn summary_of_rrsigs_only_is_insecure() { + let name = Name::from_ascii("www.example.").unwrap(); + let records = [rrsig_record( + &name, + Algorithm::ECDSAP256SHA256, + Proof::Secure, + )]; + assert_eq!( + DnssecSummary::from_records(records.iter()), + DnssecSummary::Insecure + ); + assert_eq!( + DnssecSummary::from_records([].iter()), + DnssecSummary::Insecure + ); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/proof.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/proof.rs new file mode 100644 index 00000000..3b7a9010 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/proof.rs @@ -0,0 +1,231 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! DNSSEC related Proof of record authenticity + +use core::{fmt, ops::BitOr}; + +use bitflags::bitflags; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Represents the status of a DNSSEC verified record. +/// +/// see [RFC 4035, DNSSEC Protocol Modifications, March 2005](https://datatracker.ietf.org/doc/html/rfc4035#section-4.3) +/// ```text +/// 4.3. Determining Security Status of Data +/// +/// A security-aware resolver MUST be able to determine whether it should +/// expect a particular RRset to be signed. More precisely, a +/// security-aware resolver must be able to distinguish between four +/// cases: +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[must_use = "Proof is a flag on Record data, it should be interrogated before using a record"] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +#[repr(u8)] +pub enum Proof { + /// An RRset for which the resolver is able to build a chain of + /// signed DNSKEY and DS RRs from a trusted security anchor to the + /// RRset. In this case, the RRset should be signed and is subject to + /// signature validation, as described above. + Secure = 3, + + /// An RRset for which the resolver knows that it has no chain + /// of signed DNSKEY and DS RRs from any trusted starting point to the + /// RRset. This can occur when the target RRset lies in an unsigned + /// zone or in a descendent of an unsigned zone. In this case, the + /// RRset may or may not be signed, but the resolver will not be able + /// to verify the signature. + Insecure = 2, + + /// An RRset for which the resolver believes that it ought to be + /// able to establish a chain of trust but for which it is unable to + /// do so, either due to signatures that for some reason fail to + /// validate or due to missing data that the relevant DNSSEC RRs + /// indicate should be present. This case may indicate an attack but + /// may also indicate a configuration error or some form of data + /// corruption. + Bogus = 1, + + /// An RRset for which the resolver is not able to + /// determine whether the RRset should be signed, as the resolver is + /// not able to obtain the necessary DNSSEC RRs. This can occur when + /// the security-aware resolver is not able to contact security-aware + /// name servers for the relevant zones. + #[default] + Indeterminate = 0, +} + +impl Proof { + /// Returns true if this Proof represents a validated DNSSEC record + #[inline] + pub fn is_secure(&self) -> bool { + *self == Self::Secure + } + + /// Returns true if this Proof represents a validated to be insecure DNSSEC record, + /// meaning the zone is known to be not signed + #[inline] + pub fn is_insecure(&self) -> bool { + *self == Self::Insecure + } + + /// Returns true if this Proof represents a DNSSEC record that failed validation, + /// meaning that the DNSSEC is bad, or other DNSSEC records are incorrect + #[inline] + pub fn is_bogus(&self) -> bool { + *self == Self::Bogus + } + + /// Either the record has not been verified or there were network issues fetching DNSSEC records + #[inline] + pub fn is_indeterminate(&self) -> bool { + *self == Self::Indeterminate + } +} + +impl fmt::Display for Proof { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::Secure => "Secure", + Self::Insecure => "Insecure", + Self::Bogus => "Bogus", + Self::Indeterminate => "Indeterminate", + }; + + f.write_str(s) + } +} + +impl core::error::Error for Proof {} + +#[test] +fn test_order() { + assert!(Proof::Secure > Proof::Insecure); + assert!(Proof::Insecure > Proof::Bogus); + assert!(Proof::Bogus > Proof::Indeterminate); +} + +bitflags! { + /// Represents a set of flags. + pub struct ProofFlags: u32 { + /// Represents Proof::Secure + const SECURE = 1 << Proof::Secure as u8; + /// Represents Proof::Insecure + const INSECURE = 1 << Proof::Insecure as u8; + /// Represents Proof::Bogus + const BOGUS = 1 << Proof::Bogus as u8; + /// Represents Proof::Indeterminate + const INDETERMINATE = 1 << Proof::Indeterminate as u8; + } +} + +impl From for ProofFlags { + fn from(proof: Proof) -> Self { + match proof { + Proof::Secure => Self::SECURE, + Proof::Insecure => Self::INSECURE, + Proof::Bogus => Self::BOGUS, + Proof::Indeterminate => Self::INDETERMINATE, + } + } +} + +impl BitOr for Proof { + type Output = ProofFlags; + + // rhs is the "right-hand side" of the expression `a | b` + fn bitor(self, rhs: Self) -> Self::Output { + ProofFlags::from(self) | ProofFlags::from(rhs) + } +} + +/// A wrapper type to ensure that the state of a DNSSEC proof is evaluated before use +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Proven { + proof: Proof, + value: T, +} + +impl Proven { + /// Wrap the value with the given proof + pub fn new(proof: Proof, value: T) -> Self { + Self { proof, value } + } + + /// Get the associated proof + pub fn proof(&self) -> Proof { + self.proof + } + + /// Attempts to borrow the value only if it matches flags, returning the associated proof on failure + /// + /// ``` + /// use hickory_proto::dnssec::{Proof, Proven}; + /// + /// let proven = Proven::new(Proof::Bogus, 42u32); + /// + /// assert_eq!(*proven.require_as_ref(Proof::Bogus).unwrap(), 42_u32); + /// assert_eq!(*proven.require_as_ref(Proof::Bogus | Proof::Indeterminate).unwrap(), 42_u32); + /// assert_eq!(proven.require_as_ref(Proof::Secure | Proof::Insecure).unwrap_err(), Proof::Bogus); + /// ``` + pub fn require_as_ref>(&self, flags: I) -> Result<&T, Proof> { + if flags.into().contains(ProofFlags::from(self.proof)) { + Ok(&self.value) + } else { + Err(self.proof) + } + } + + /// Attempts to take the value only if it matches flags, returning the associated proof on failure + /// + /// ``` + /// use hickory_proto::dnssec::{Proof, Proven}; + /// + /// let proven = Proven::new(Proof::Bogus, 42u32); + /// + /// assert_eq!(proven.clone().require(Proof::Bogus).unwrap(), 42_u32); + /// assert_eq!(proven.clone().require(Proof::Bogus | Proof::Indeterminate).unwrap(), 42_u32); + /// assert!(proven.require(Proof::Secure | Proof::Insecure).is_err()); + /// ``` + pub fn require>(self, flags: I) -> Result { + if flags.into().contains(ProofFlags::from(self.proof)) { + Ok(self.value) + } else { + Err(self) + } + } + + /// Map the value with the associated function, carrying forward the proof + pub fn map(self, f: F) -> Proven + where + F: FnOnce(T) -> U, + { + Proven { + proof: self.proof, + value: f(self.value), + } + } + + /// Unwraps the Proven type into it's parts + pub fn into_parts(self) -> (Proof, T) { + let Self { proof, value } = self; + + (proof, value) + } +} + +impl Proven> { + /// If the inner type is an Option this will transpose them so that it's an option wrapped Proven + pub fn transpose(self) -> Option> { + Some(Proven { + proof: self.proof, + value: self.value?, + }) + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/public_key.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/public_key.rs new file mode 100644 index 00000000..a42ca8f1 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/public_key.rs @@ -0,0 +1,75 @@ +// Copyright 2015-2016 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Public Key implementations for supported key types + +use alloc::vec::Vec; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::{Algorithm, crypto::decode_public_key}; +use crate::error::ProtoResult; + +/// PublicKeys implement the ability to ideally be zero copy abstractions over public keys for verifying signed content. +/// +/// In DNS the KEY and DNSKEY types are generally the RData types which store public key material. +pub trait PublicKey { + /// Returns the public bytes of the public key, in DNS format + fn public_bytes(&self) -> &[u8]; + + /// Verifies the hash matches the signature with the current `key`. + /// + /// # Arguments + /// + /// * `message` - the message to be validated, see `hash_rrset` + /// * `signature` - the signature to use to verify the hash, extracted from an `RData::RRSIG` + /// for example. + /// + /// # Return value + /// + /// True if and only if the signature is valid for the hash. This will always return + /// false if the `key`. + fn verify(&self, message: &[u8], signature: &[u8]) -> ProtoResult<()>; + + /// The algorithm associated with this key. + fn algorithm(&self) -> Algorithm; +} + +/// An owned variant of PublicKey +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct PublicKeyBuf { + key_buf: Vec, + algorithm: Algorithm, +} + +impl PublicKeyBuf { + /// Constructs a new PublicKey from the specified bytes, these should be in DNSKEY form. + pub fn new(key_buf: Vec, algorithm: Algorithm) -> Self { + Self { key_buf, algorithm } + } + + /// Extract the inner buffer of public key bytes. + pub fn into_inner(self) -> Vec { + self.key_buf + } +} + +impl PublicKey for PublicKeyBuf { + fn public_bytes(&self) -> &[u8] { + &self.key_buf + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> ProtoResult<()> { + decode_public_key(&self.key_buf, self.algorithm)?.verify(message, signature) + } + + fn algorithm(&self) -> Algorithm { + self.algorithm + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cdnskey.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cdnskey.rs new file mode 100644 index 00000000..45ae7d06 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cdnskey.rs @@ -0,0 +1,263 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! CDNSKEY type and related implementations + +use alloc::vec::Vec; +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + dnssec::{Algorithm, PublicKeyBuf}, + error::ProtoResult, + rr::{RData, RecordData, RecordDataDecodable, RecordType}, + serialize::binary::{ + BinDecoder, BinEncodable, BinEncoder, DecodeError, Restrict, RestrictedMath, + }, +}; + +use super::DNSSECRData; + +/// Child DNSKEY. See RFC 8078. +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct CDNSKEY { + flags: u16, + algorithm: Option, + public_key: Vec, +} + +impl CDNSKEY { + /// Construct a new CDNSKEY RData + /// + /// # Arguments + /// + /// * `zone_key` - this key is used to sign Zone resource records + /// * `secure_entry_point` - this key is used to sign DNSKeys that sign the Zone records + /// * `revoke` - this key has been revoked + /// * `algorithm` - the key's algorithm, or `None` to request deletion + /// * `public_key` - the public key encoded as a byte array + /// + /// # Return + /// + /// A new CDNSKEY RData for use in a Resource Record + pub fn new( + zone_key: bool, + secure_entry_point: bool, + revoke: bool, + algorithm: Option, + public_key: Vec, + ) -> Self { + let mut flags: u16 = 0; + if zone_key { + flags |= 0b0000_0001_0000_0000; + } + if secure_entry_point { + flags |= 0b0000_0000_0000_0001; + } + if revoke { + flags |= 0b0000_0000_1000_0000; + } + Self::with_flags(flags, algorithm, public_key) + } + + /// Construct a new CDNSKEY RData + /// + /// # Arguments + /// + /// * `flags` - flags associated with this key + /// * `algorithm` - the key's algorithm, or `None` to request deletion + /// * `public_key` - the public key encoded as a byte array + /// + /// # Return + /// + /// A new CDNSKEY RData for use in a Resource Record + pub fn with_flags(flags: u16, algorithm: Option, public_key: Vec) -> Self { + Self { + flags, + algorithm, + public_key, + } + } + + /// Returns the value of the Zone Key flag + pub fn zone_key(&self) -> bool { + self.flags & 0b0000_0001_0000_0000 != 0 + } + + /// Returns the value of the Secure Entry Point flag + pub fn secure_entry_point(&self) -> bool { + self.flags & 0b0000_0000_0000_0001 != 0 + } + + /// Returns the value of the Revoke flag. + pub fn revoke(&self) -> bool { + self.flags & 0b0000_0000_1000_0000 != 0 + } + + /// Returns the Algorithm field. This is `None` if deletion is requested, or the key's algorithm + /// if an update is requested. + pub fn algorithm(&self) -> Option { + self.algorithm + } + + /// Returns whether this record is requesting deletion of the DS RRset. + pub fn is_delete(&self) -> bool { + self.algorithm.is_none() + } + + /// Returns the public key, or `None` if deletion is requested. + pub fn public_key(&self) -> Option { + Some(PublicKeyBuf::new(self.public_key.clone(), self.algorithm?)) + } + + /// Returns the Flags field + pub fn flags(&self) -> u16 { + self.flags + } +} + +impl From for RData { + fn from(value: CDNSKEY) -> Self { + Self::DNSSEC(DNSSECRData::CDNSKEY(value)) + } +} + +impl BinEncodable for CDNSKEY { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16(self.flags())?; + encoder.emit(3)?; + match self.algorithm() { + Some(algorithm) => algorithm.emit(encoder)?, + None => encoder.emit_u8(0)?, + } + encoder.emit_vec(&self.public_key)?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for CDNSKEY { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + let flags = decoder.read_u16()?.unverified(/* used as a bitfield, this is safe */); + + // protocol is defined to only be '3' right now + let _protocol = decoder + .read_u8()? + .verify_unwrap(|protocol| *protocol == 3) + .map_err(DecodeError::DnsKeyProtocolNot3)?; + + let algorithm_value = decoder.read_u8()?.unverified(/* no further validation required */); + let algorithm = match algorithm_value { + 0 => None, + _ => Some(Algorithm::from_u8(algorithm_value)), + }; + + // The public key is the remaining bytes, excluding the first four bytes for the above + // fields. This subtraction is safe, as the first three fields must have been in the RDATA, + // otherwise there would have been an earlier return. + let key_len = length + .map(|u| u as usize) + .checked_sub(4) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { read: 4, len })? + .unverified(/* used only as length safely */); + let public_key = decoder + .read_vec(key_len)? + .unverified(/* signature verification will fail if the public key is invalid */); + + Ok(Self::with_flags(flags, algorithm, public_key)) + } +} + +impl RecordData for CDNSKEY { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::CDNSKEY(cdnskey)) => Some(cdnskey), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::CDNSKEY + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::CDNSKEY(self)) + } +} + +impl fmt::Display for CDNSKEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{flags} 3 {alg} {key}", + flags = self.flags, + alg = self.algorithm.map(u8::from).unwrap_or(0), + key = data_encoding::BASE64.encode(&self.public_key) + ) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use alloc::vec::Vec; + use std::println; + + use crate::{ + dnssec::Algorithm, + rr::RecordDataDecodable, + serialize::binary::{BinDecoder, BinEncodable, BinEncoder, Restrict}, + }; + + use super::CDNSKEY; + + #[test] + fn test() { + let rdata = CDNSKEY::new( + true, + true, + false, + Some(Algorithm::ECDSAP256SHA256), + vec![1u8, 2u8, 3u8, 4u8], + ); + + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + rdata.emit(&mut encoder).expect("error encoding"); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder = BinDecoder::new(bytes); + let read_rdata = CDNSKEY::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("error decoding"); + + assert_eq!(rdata, read_rdata); + } + + #[test] + fn test_delete() { + let rdata = CDNSKEY::with_flags(0, None, vec![0u8]); + + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + rdata.emit(&mut encoder).expect("error encoding"); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder = BinDecoder::new(bytes); + let read_rdata = CDNSKEY::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("error decoding"); + + assert_eq!(rdata, read_rdata); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cds.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cds.rs new file mode 100644 index 00000000..8b8906d4 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/cds.rs @@ -0,0 +1,224 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! CDS type and related implementations + +use alloc::vec::Vec; +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + dnssec::{Algorithm, DigestType}, + error::ProtoResult, + rr::{RData, RecordData, RecordDataDecodable, RecordType}, + serialize::binary::{ + BinDecoder, BinEncodable, BinEncoder, DecodeError, Restrict, RestrictedMath, + }, +}; + +use super::DNSSECRData; + +/// Child DS. See RFC 8078. +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct CDS { + key_tag: u16, + /// The algorithm of the desired DS record if requesting an update, or `None` if requesting + /// deletion. + algorithm: Option, + digest_type: DigestType, + digest: Vec, +} + +impl CDS { + /// Constructs a new CDS RData + /// + /// # Arguments + /// + /// * `key_tag` - the key tag associated to the DNSKEY + /// * `algorithm` - algorithm as specified in the DNSKEY, or None to request DS RRset deletion + /// * `digest_type` - hash algorithm used to validate the DNSKEY + /// * `digest` - hash of the DNSKEY + /// + /// # Returns + /// + /// the CDS RDATA for use in a Resource Record + pub fn new( + key_tag: u16, + algorithm: Option, + digest_type: DigestType, + digest: Vec, + ) -> Self { + Self { + key_tag, + algorithm, + digest_type, + digest, + } + } + + /// Returns the Key Tag field + pub fn key_tag(&self) -> u16 { + self.key_tag + } + + /// Returns the Algorithm field. This is `None` if deletion is requested, or the key's algorithm + /// if an update is requested. + pub fn algorithm(&self) -> Option { + self.algorithm + } + + /// Returns whether this record is requesting deletion of the DS RRset. + pub fn is_delete(&self) -> bool { + self.algorithm.is_none() + } + + /// Returns the Digest Type field. + pub fn digest_type(&self) -> DigestType { + self.digest_type + } + + /// Returns the Digest field. + pub fn digest(&self) -> &[u8] { + &self.digest + } +} + +impl From for RData { + fn from(value: CDS) -> Self { + Self::DNSSEC(DNSSECRData::CDS(value)) + } +} + +impl BinEncodable for CDS { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16(self.key_tag())?; + match self.algorithm() { + Some(algorithm) => algorithm.emit(encoder)?, + None => encoder.emit_u8(0)?, + } + encoder.emit(self.digest_type().into())?; + encoder.emit_vec(self.digest())?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for CDS { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + let start_idx = decoder.index(); + + let key_tag = decoder.read_u16()?.unverified(/* any u16 is a valid key_tag */); + + let algorithm_value = decoder.read_u8()?.unverified(/* no further validation required */); + let algorithm = match algorithm_value { + 0 => None, + _ => Some(Algorithm::from_u8(algorithm_value)), + }; + + let digest_type = + DigestType::from(decoder.read_u8()?.unverified(/* DigestType is verified as safe */)); + + let bytes_read = decoder.index() - start_idx; + let left = length + .map(|u| u as usize) + .checked_sub(bytes_read) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { read: bytes_read, len })? + .unverified(/* used only as length safely */); + let digest = + decoder.read_vec(left)?.unverified(/* this is only compared with other digests */); + + Ok(Self::new(key_tag, algorithm, digest_type, digest)) + } +} + +impl RecordData for CDS { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::CDS(cds)) => Some(cds), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::CDS + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::CDS(self)) + } +} + +impl fmt::Display for CDS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{tag} {alg} {ty} {digest}", + tag = self.key_tag, + alg = self.algorithm.map(u8::from).unwrap_or(0), + ty = u8::from(self.digest_type), + digest = data_encoding::HEXUPPER_PERMISSIVE.encode(&self.digest) + ) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use alloc::vec::Vec; + use std::println; + + use crate::{ + dnssec::{Algorithm, DigestType}, + rr::RecordDataDecodable, + serialize::binary::{BinDecoder, BinEncodable, BinEncoder, Restrict}, + }; + + use super::CDS; + + #[test] + fn test() { + let rdata = CDS::new( + 0xF00F, + Some(Algorithm::RSASHA256), + DigestType::SHA256, + vec![5, 6, 7, 8], + ); + + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + rdata.emit(&mut encoder).expect("error encoding"); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder = BinDecoder::new(bytes); + let read_rdata = CDS::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("error decoding"); + assert_eq!(rdata, read_rdata); + } + + #[test] + fn test_delete() { + let rdata = CDS::new(0, None, DigestType::Unknown(0), vec![0]); + + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + rdata.emit(&mut encoder).expect("error encoding"); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder = BinDecoder::new(bytes); + let read_rdata = CDS::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("error decoding"); + assert_eq!(rdata, read_rdata); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/dnskey.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/dnskey.rs new file mode 100644 index 00000000..65c8a605 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/dnskey.rs @@ -0,0 +1,730 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! public key record data for signing zone records + +use alloc::{borrow::ToOwned, string::String, sync::Arc, vec::Vec}; +use core::{fmt, str::FromStr}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + dnssec::{ + Algorithm, DigestType, PublicKey, PublicKeyBuf, Verifier, + crypto::{Digest, decode_public_key}, + }, + error::ProtoResult, + rr::{Name, RecordData, RecordDataDecodable, RecordType, record_data::RData}, + serialize::{ + binary::{ + BinDecodable, BinDecoder, BinEncodable, BinEncoder, DecodeError, NameEncoding, + Restrict, RestrictedMath, + }, + txt::ParseError, + }, +}; + +use super::DNSSECRData; + +/// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-2), DNSSEC Resource Records, March 2005 +/// +/// ```text +/// 2. The DNSKEY Resource Record +/// +/// DNSSEC uses public key cryptography to sign and authenticate DNS +/// resource record sets (RRsets). The public keys are stored in DNSKEY +/// resource records and are used in the DNSSEC authentication process +/// described in [RFC4035]: A zone signs its authoritative RRsets by +/// using a private key and stores the corresponding public key in a +/// DNSKEY RR. A resolver can then use the public key to validate +/// signatures covering the RRsets in the zone, and thus to authenticate +/// them. +/// +/// The DNSKEY RR is not intended as a record for storing arbitrary +/// public keys and MUST NOT be used to store certificates or public keys +/// that do not directly relate to the DNS infrastructure. +/// +/// The Type value for the DNSKEY RR type is 48. +/// +/// The DNSKEY RR is class independent. +/// +/// The DNSKEY RR has no special TTL requirements. +/// +/// 2.1. DNSKEY RDATA Wire Format +/// +/// The RDATA for a DNSKEY RR consists of a 2 octet Flags Field, a 1 +/// octet Protocol Field, a 1 octet Algorithm Field, and the Public Key +/// Field. +/// +/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Flags | Protocol | Algorithm | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// / / +/// / Public Key / +/// / / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// 2.1.5. Notes on DNSKEY RDATA Design +/// +/// Although the Protocol Field always has value 3, it is retained for +/// backward compatibility with early versions of the KEY record. +/// +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct DNSKEY { + flags: u16, + public_key: PublicKeyBuf, +} + +impl DNSKEY { + /// Create a [`DNSKEY`] record representing a `public_key`. + /// + /// # Arguments + /// + /// * `algorithm` - algorithm of the DNSKEY + /// + /// # Return + /// + /// the DNSKEY record data + pub fn from_key(public_key: &dyn PublicKey) -> Self { + Self::new( + true, + true, + false, + PublicKeyBuf::new(public_key.public_bytes().to_owned(), public_key.algorithm()), + ) + } + + /// Construct a new DNSKey RData + /// + /// # Arguments + /// + /// * `zone_key` - this key is used to sign Zone resource records + /// * `secure_entry_point` - this key is used to sign DNSKeys that sign the Zone records + /// * `revoke` - this key has been revoked + /// * `public_key` - the public key + /// + /// # Return + /// + /// A new DNSKEY RData for use in a Resource Record + pub fn new( + zone_key: bool, + secure_entry_point: bool, + revoke: bool, + public_key: PublicKeyBuf, + ) -> Self { + let mut flags: u16 = 0; + if zone_key { + flags |= 0b0000_0001_0000_0000; + } + if secure_entry_point { + flags |= 0b0000_0000_0000_0001; + } + if revoke { + flags |= 0b0000_0000_1000_0000; + } + Self::with_flags(flags, public_key) + } + + pub(crate) fn from_tokens<'i>( + mut tokens: impl Iterator, + ) -> Result { + let flags_str = tokens + .next() + .ok_or(ParseError::Message("flags not present"))?; + let protocol_str = tokens + .next() + .ok_or(ParseError::Message("protocol not present"))?; + let algorithm_str = tokens + .next() + .ok_or(ParseError::Message("algorithm not present"))?; + + let flags = u16::from_str(flags_str)?; + + let protocol = u8::from_str(protocol_str)?; + + if protocol != 3 { + return Err(ParseError::Message("protocol field must be 3")); + } + + let algorithm = Algorithm::from_u8(algorithm_str.parse()?); + + let public_key_str = tokens.collect::(); + if public_key_str.is_empty() { + return Err(ParseError::Message("public key not present")); + } + + let public_key = data_encoding::BASE64.decode(public_key_str.as_bytes())?; + + Ok(Self::with_flags( + flags, + PublicKeyBuf::new(public_key, algorithm), + )) + } + + /// Construct a new DNSKEY RData + /// + /// # Arguments + /// + /// * `flags` - flags associated with this key + /// * `public_key` - the public key + /// + /// # Return + /// + /// A new DNSKEY RData for use in a Resource Record + pub fn with_flags(flags: u16, public_key: PublicKeyBuf) -> Self { + Self { flags, public_key } + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.1.1) + /// + /// ```text + /// 2.1.1. The Flags Field + /// + /// Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value 1, + /// then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// owner name MUST be the name of a zone. If bit 7 has value 0, then + /// the DNSKEY record holds some other type of DNS public key and MUST + /// NOT be used to verify RRSIGs that cover RRsets. + /// + /// + /// Bits 0-6 and 8-14 are reserved: these bits MUST have value 0 upon + /// creation of the DNSKEY RR and MUST be ignored upon receipt. + /// ``` + pub fn zone_key(&self) -> bool { + self.flags & 0b0000_0001_0000_0000 != 0 + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.1.1) + /// + /// ```text + /// 2.1.1. The Flags Field + /// + /// Bit 15 of the Flags field is the Secure Entry Point flag, described + /// in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// key intended for use as a secure entry point. This flag is only + /// intended to be a hint to zone signing or debugging software as to the + /// intended use of this DNSKEY record; validators MUST NOT alter their + /// behavior during the signature validation process in any way based on + /// the setting of this bit. This also means that a DNSKEY RR with the + /// SEP bit set would also need the Zone Key flag set in order to be able + /// to generate signatures legally. A DNSKEY RR with the SEP set and the + /// Zone Key flag not set MUST NOT be used to verify RRSIGs that cover + /// RRsets. + /// ``` + pub fn secure_entry_point(&self) -> bool { + self.flags & 0b0000_0000_0000_0001 != 0 + } + + /// A KSK has a `flags` value of `257` + pub fn is_key_signing_key(&self) -> bool { + // a flags value of 257 + self.secure_entry_point() && self.zone_key() && !self.revoke() + } + + /// [RFC 5011, Trust Anchor Update, September 2007](https://tools.ietf.org/html/rfc5011#section-3) + /// + /// ```text + /// RFC 5011 Trust Anchor Update September 2007 + /// + /// 7. IANA Considerations + /// + /// The IANA has assigned a bit in the DNSKEY flags field (see Section 7 + /// of [RFC4034]) for the REVOKE bit (8). + /// ``` + pub fn revoke(&self) -> bool { + self.flags & 0b0000_0000_1000_0000 != 0 + } + + /// The [`PublicKeyBuf`] type combines the algorithm and the public key material. + /// + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.1.4) + /// + /// ```text + /// 2.1.3. The Algorithm Field + /// + /// The Algorithm field identifies the public key's cryptographic + /// algorithm and determines the format of the Public Key field. A list + /// of DNSSEC algorithm types can be found in Appendix A.1 + /// + /// 2.1.4. The Public Key Field + /// + /// The Public Key Field holds the public key material. The format + /// depends on the algorithm of the key being stored and is described in + /// separate documents. + /// ``` + pub fn public_key(&self) -> &PublicKeyBuf { + &self.public_key + } + + /// Output the encoded form of the flags + pub fn flags(&self) -> u16 { + self.flags + } + + /// Creates a message digest for this DNSKEY record. + /// + /// ```text + /// 5.1.4. The Digest Field + /// + /// The DS record refers to a DNSKEY RR by including a digest of that + /// DNSKEY RR. + /// + /// The digest is calculated by concatenating the canonical form of the + /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, + /// and then applying the digest algorithm. + /// + /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); + /// + /// "|" denotes concatenation + /// + /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. + /// + /// The size of the digest may vary depending on the digest algorithm and + /// DNSKEY RR size. As of the time of this writing, the only defined + /// digest algorithm is SHA-1, which produces a 20 octet digest. + /// ``` + /// + /// # Arguments + /// + /// * `name` - the label of of the DNSKEY record. + /// * `digest_type` - the `DigestType` with which to create the message digest. + pub fn to_digest(&self, name: &Name, digest_type: DigestType) -> ProtoResult { + let mut buf: Vec = Vec::new(); + { + let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut buf); + encoder.set_name_encoding(NameEncoding::UncompressedLowercase); + if let Err(e) = name + .to_lowercase() + .emit(&mut encoder) + .and_then(|_| self.emit(&mut encoder)) + { + tracing::warn!("error serializing dnskey: {e}"); + return Err(format!("error serializing dnskey: {e}").into()); + } + } + + Ok(Digest::new(&buf, digest_type)?) + } + + /// The key tag is calculated as a hash to more quickly lookup a DNSKEY. + /// + /// [RFC 2535](https://tools.ietf.org/html/rfc2535), Domain Name System Security Extensions, March 1999 + /// + /// ```text + /// RFC 2535 DNS Security Extensions March 1999 + /// + /// 4.1.6 Key Tag Field + /// + /// The "key Tag" is a two octet quantity that is used to efficiently + /// select between multiple keys which may be applicable and thus check + /// that a public key about to be used for the computationally expensive + /// effort to check the signature is possibly valid. For algorithm 1 + /// (MD5/RSA) as defined in [RFC 2537], it is the next to the bottom two + /// octets of the public key modulus needed to decode the signature + /// field. That is to say, the most significant 16 of the least + /// significant 24 bits of the modulus in network (big endian) order. For + /// all other algorithms, including private algorithms, it is calculated + /// as a simple checksum of the KEY RR as described in Appendix C. + /// + /// Appendix C: Key Tag Calculation + /// + /// The key tag field in the SIG RR is just a means of more efficiently + /// selecting the correct KEY RR to use when there is more than one KEY + /// RR candidate available, for example, in verifying a signature. It is + /// possible for more than one candidate key to have the same tag, in + /// which case each must be tried until one works or all fail. The + /// following reference implementation of how to calculate the Key Tag, + /// for all algorithms other than algorithm 1, is in ANSI C. It is coded + /// for clarity, not efficiency. (See section 4.1.6 for how to determine + /// the Key Tag of an algorithm 1 key.) + /// + /// /* assumes int is at least 16 bits + /// first byte of the key tag is the most significant byte of return + /// value + /// second byte of the key tag is the least significant byte of + /// return value + /// */ + /// + /// int keytag ( + /// + /// unsigned char key[], /* the RDATA part of the KEY RR */ + /// unsigned int keysize, /* the RDLENGTH */ + /// ) + /// { + /// long int ac; /* assumed to be 32 bits or larger */ + /// + /// for ( ac = 0, i = 0; i < keysize; ++i ) + /// ac += (i&1) ? key[i] : key[i]<<8; + /// ac += (ac>>16) & 0xFFFF; + /// return ac & 0xFFFF; + /// } + /// ``` + pub fn calculate_key_tag(&self) -> ProtoResult { + // TODO: + let mut bytes: Vec = Vec::with_capacity(512); + { + let mut e = BinEncoder::new(&mut bytes); + self.emit(&mut e)?; + } + Ok(Self::calculate_key_tag_internal(&bytes)) + } + + /// Internal checksum function (used for non-RSAMD5 hashes only, + /// however, RSAMD5 is considered deprecated and not implemented in + /// hickory-dns, anyways). + pub fn calculate_key_tag_internal(bytes: &[u8]) -> u16 { + let mut ac: u32 = 0; + for (i, k) in bytes.iter().enumerate() { + ac += u32::from(*k) << if i & 0x01 != 0 { 0 } else { 8 }; + } + ac += ac >> 16; + (ac & 0xFFFF) as u16 + } +} + +impl From for RData { + fn from(key: DNSKEY) -> Self { + Self::DNSSEC(DNSSECRData::DNSKEY(key)) + } +} + +impl BinEncodable for DNSKEY { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16(self.flags())?; + encoder.emit(3)?; // always 3 for now + self.public_key.algorithm().emit(encoder)?; + encoder.emit_vec(self.public_key.public_bytes())?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for DNSKEY { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + let flags: u16 = decoder.read_u16()?.unverified(/*used as a bitfield, this is safe*/); + + let _protocol: u8 = decoder + .read_u8()? + .verify_unwrap(|protocol| { + // RFC 4034 DNSSEC Resource Records March 2005 + // + // 2.1.2. The Protocol Field + // + // The Protocol Field MUST have value 3, and the DNSKEY RR MUST be + // treated as invalid during signature verification if it is found to be + // some value other than 3. + // + // protocol is defined to only be '3' right now + + *protocol == 3 + }) + .map_err(DecodeError::DnsKeyProtocolNot3)?; + + let algorithm: Algorithm = Algorithm::read(decoder)?; + + // the public key is the left-over bytes minus 4 for the first fields + // this sub is safe, as the first 4 fields must have been in the rdata, otherwise there would have been + // an earlier return. + let key_len = length + .map(|u| u as usize) + .checked_sub(4) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { read: 4, len })? + .unverified(/*used only as length safely*/); + let public_key = + decoder.read_vec(key_len)?.unverified(/*the byte array will fail in usage if invalid*/); + + Ok(Self::with_flags( + flags, + PublicKeyBuf::new(public_key, algorithm), + )) + } +} + +impl RecordData for DNSKEY { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::DNSKEY(csync)) => Some(csync), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::DNSKEY + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::DNSKEY(self)) + } +} + +impl Verifier for DNSKEY { + fn algorithm(&self) -> Algorithm { + self.public_key.algorithm() + } + + fn key(&self) -> ProtoResult> { + decode_public_key(self.public_key.public_bytes(), self.public_key.algorithm()) + } +} + +/// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.2) +/// +/// ```text +/// 2.2. The DNSKEY RR Presentation Format +/// +/// The presentation format of the RDATA portion is as follows: +/// +/// The Flag field MUST be represented as an unsigned decimal integer. +/// Given the currently defined flags, the possible values are: 0, 256, +/// and 257. +/// +/// The Protocol Field MUST be represented as an unsigned decimal integer +/// with a value of 3. +/// +/// The Algorithm field MUST be represented either as an unsigned decimal +/// integer or as an algorithm mnemonic as specified in Appendix A.1. +/// +/// The Public Key field MUST be represented as a Base64 encoding of the +/// Public Key. Whitespace is allowed within the Base64 text. For a +/// definition of Base64 encoding, see [RFC3548]. +/// +/// 2.3. DNSKEY RR Example +/// +/// The following DNSKEY RR stores a DNS zone key for example.com. +/// +/// example.com. 86400 IN DNSKEY 256 3 5 ( AQPSKmynfzW4kyBv015MUG2DeIQ3 +/// Cbl+BBZH4b/0PY1kxkmvHjcZc8no +/// kfzj31GajIQKY+5CptLr3buXA10h +/// WqTkF7H6RfoRqXQeogmMHfpftf6z +/// Mv1LyBUgia7za6ZEzOJBOztyvhjL +/// 742iU/TpPSEDhm2SNKLijfUppn1U +/// aNvv4w== ) +/// +/// The first four text fields specify the owner name, TTL, Class, and RR +/// type (DNSKEY). Value 256 indicates that the Zone Key bit (bit 7) in +/// the Flags field has value 1. Value 3 is the fixed Protocol value. +/// Value 5 indicates the public key algorithm. Appendix A.1 identifies +/// algorithm type 5 as RSA/SHA1 and indicates that the format of the +/// RSA/SHA1 public key field is defined in [RFC3110]. The remaining +/// text is a Base64 encoding of the public key. +/// ``` +impl fmt::Display for DNSKEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{flags} 3 {alg} {key}", + flags = self.flags(), + alg = u8::from(self.public_key.algorithm()), + key = data_encoding::BASE64.encode(self.public_key.public_bytes()) + ) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use alloc::string::ToString; + #[cfg(feature = "std")] + use std::println; + + use rustls_pki_types::PrivateKeyDer; + + use super::*; + use crate::dnssec::{SigningKey, crypto::EcdsaSigningKey}; + + #[test] + fn test() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = + EcdsaSigningKey::from_key_der(&PrivateKeyDer::from(pkcs8), algorithm).unwrap(); + + let rdata = DNSKEY::new( + true, + true, + false, + PublicKeyBuf::new( + signing_key + .to_public_key() + .unwrap() + .public_bytes() + .to_owned(), + algorithm, + ), + ); + + let mut bytes = Vec::new(); + let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes); + assert!(rdata.emit(&mut encoder).is_ok()); + let bytes = encoder.into_bytes(); + + #[cfg(feature = "std")] + println!("bytes: {bytes:?}"); + + let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes); + let read_rdata = DNSKEY::read_data(&mut decoder, Restrict::new(bytes.len() as u16)); + let read_rdata = read_rdata.expect("error decoding"); + + assert_eq!(rdata, read_rdata); + assert!( + rdata + .to_digest( + &Name::parse("www.example.com.", None).unwrap(), + DigestType::SHA256 + ) + .is_ok() + ); + } + + #[test] + fn test_reserved_flags() { + let rdata = + DNSKEY::with_flags(u16::MAX, PublicKeyBuf::new(vec![0u8], Algorithm::RSASHA256)); + + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + rdata.emit(&mut encoder).expect("error encoding"); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder = BinDecoder::new(bytes); + let read_rdata = DNSKEY::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("error decoding"); + + assert_eq!(rdata, read_rdata); + } + + #[test] + fn test_calculate_key_tag_checksum() { + let test_text = "The quick brown fox jumps over the lazy dog"; + let test_vectors = vec![ + (vec![], 0), + (vec![0, 0, 0, 0], 0), + (vec![0xff, 0xff, 0xff, 0xff], 0xffff), + (vec![1, 0, 0, 0], 0x0100), + (vec![0, 1, 0, 0], 0x0001), + (vec![0, 0, 1, 0], 0x0100), + (test_text.as_bytes().to_vec(), 0x8d5b), + ]; + + for (input_data, exp_result) in test_vectors { + let result = DNSKEY::calculate_key_tag_internal(&input_data); + assert_eq!(result, exp_result); + } + } + + const ENCODED: &str = "aGVsbG8="; + + #[test] + fn accepts_real_world_data() { + let trust_anchor = include_str!("../../../tests/test-data/root.key"); + + let mut did_parse = false; + for line in trust_anchor.lines() { + if line.trim_start().starts_with(';') { + // skip comments + continue; + } + + // skip NAME TTL CLASS TYPE + let parts = line.split_whitespace().skip(4); + DNSKEY::from_tokens(parts).expect("could not parse"); + did_parse = true; + } + + assert!(did_parse); + } + + #[cfg(feature = "__dnssec")] + #[test] + fn it_works() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + let public_key = signing_key.to_public_key().unwrap(); + + let encoded = data_encoding::BASE64.encode(public_key.public_bytes()); + let input = format!("256 3 13 {encoded}"); + let expected = DNSKEY::new( + true, + false, + false, + PublicKeyBuf::new( + signing_key.to_public_key().unwrap().public_bytes().to_vec(), + algorithm, + ), + ); + assert_eq!(expected, parse_ok(&input),); + } + + #[cfg(feature = "__dnssec")] + #[test] + fn secure_entry_point() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + let public_key = signing_key.to_public_key().unwrap(); + + let encoded = data_encoding::BASE64.encode(public_key.public_bytes()); + let input = format!("257 3 13 {encoded}"); + let expected = DNSKEY::new( + true, + true, + false, + PublicKeyBuf::new( + signing_key.to_public_key().unwrap().public_bytes().to_vec(), + algorithm, + ), + ); + assert_eq!(expected, parse_ok(&input),); + } + + #[test] + fn incomplete() { + let cases = ["", "256", "256 3", "256 3 8"]; + for case in cases { + let err = parse_err(case); + assert!(err.to_string().contains("not present")) + } + } + + #[test] + fn reserved_flags() { + let public_key = PublicKeyBuf::new(b"hello".to_vec(), Algorithm::RSASHA256); + let expected = DNSKEY::with_flags(2, public_key); + assert_eq!(expected, parse_ok(&format!("2 3 8 {ENCODED}"))); + } + + #[test] + fn bad_protocol() { + let err = parse_err(&format!("256 0 8 {ENCODED}")); + assert!(err.to_string().contains("protocol field")) + } + + #[test] + fn bad_public_key() { + let mut input = format!("256 3 8 {ENCODED}"); + input.pop().unwrap(); // drop trailing '=' + let err = parse_err(&input); + assert!(err.to_string().contains("data encoding error")) + } + + fn parse_ok(input: &str) -> DNSKEY { + DNSKEY::from_tokens(input.split_whitespace()).expect("parsing failed") + } + + fn parse_err(input: &str) -> ParseError { + DNSKEY::from_tokens(input.split_whitespace()).expect_err("parsing did not fail") + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/ds.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/ds.rs new file mode 100644 index 00000000..d0ea6031 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/ds.rs @@ -0,0 +1,567 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! pointer record from parent zone to child zone for dnskey proof + +use alloc::{borrow::ToOwned, string::String, vec::Vec}; +use core::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::DNSSECRData; +use crate::{ + dnssec::{Algorithm, DigestType, DnsSecError, PublicKey, rdata::DNSKEY}, + error::ProtoResult, + rr::{Name, RData, RecordData, RecordDataDecodable, RecordType}, + serialize::{ + binary::{ + BinDecodable, BinDecoder, BinEncodable, BinEncoder, DecodeError, Restrict, + RestrictedMath, + }, + txt::ParseError, + }, +}; + +/// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5) +/// +/// ```text +/// 5.1. DS RDATA Wire Format +/// +/// The RDATA for a DS RR consists of a 2 octet Key Tag field, a 1 octet +/// Algorithm field, a 1 octet Digest Type field, and a Digest field. +/// +/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Key Tag | Algorithm | Digest Type | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// / / +/// / Digest / +/// / / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// 5.2. Processing of DS RRs When Validating Responses +/// +/// The DS RR links the authentication chain across zone boundaries, so +/// the DS RR requires extra care in processing. The DNSKEY RR referred +/// to in the DS RR MUST be a DNSSEC zone key. The DNSKEY RR Flags MUST +/// have Flags bit 7 set. If the DNSKEY flags do not indicate a DNSSEC +/// zone key, the DS RR (and the DNSKEY RR it references) MUST NOT be +/// used in the validation process. +/// +/// 5.3. The DS RR Presentation Format +/// +/// The presentation format of the RDATA portion is as follows: +/// +/// The Key Tag field MUST be represented as an unsigned decimal integer. +/// +/// The Algorithm field MUST be represented either as an unsigned decimal +/// integer or as an algorithm mnemonic specified in Appendix A.1. +/// +/// The Digest Type field MUST be represented as an unsigned decimal +/// integer. +/// +/// The Digest MUST be represented as a sequence of case-insensitive +/// hexadecimal digits. Whitespace is allowed within the hexadecimal +/// text. +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct DS { + key_tag: u16, + algorithm: Algorithm, + digest_type: DigestType, + digest: Vec, +} + +impl DS { + /// Creates a [`DS`] record for the given `public_key` and `name`. + /// + /// # Arguments + /// + /// * `public_key` - the public key to create the DS record for + /// * `name` - name of the DNSKEY record covered by the new DS record + /// * `algorithm` - the algorithm of the DNSKEY + /// * `digest_type` - the digest_type used to + pub fn from_key( + public_key: &dyn PublicKey, + name: &Name, + digest_type: DigestType, + ) -> Result { + let dnskey = DNSKEY::from_key(public_key); + Ok(Self::new( + dnskey.calculate_key_tag()?, + public_key.algorithm(), + digest_type, + dnskey.to_digest(name, digest_type)?.as_ref().to_owned(), + )) + } + + /// Constructs a new DS RData + /// + /// # Arguments + /// + /// * `key_tag` - the key_tag associated to the DNSKEY + /// * `algorithm` - algorithm as specified in the DNSKEY + /// * `digest_type` - hash algorithm used to validate the DNSKEY + /// * `digest` - hash of the DNSKEY + /// + /// # Returns + /// + /// the DS RDATA for use in a Resource Record + pub fn new( + key_tag: u16, + algorithm: Algorithm, + digest_type: DigestType, + digest: Vec, + ) -> Self { + Self { + key_tag, + algorithm, + digest_type, + digest, + } + } + + /// Parse the RData from a set of Tokens + /// + /// [RFC 4034, Resource Records for the DNS Security Extensions](https://datatracker.ietf.org/doc/html/rfc4034#section-5.3) + /// ```text + /// 5.3. The DS RR Presentation Format + /// + /// The presentation format of the RDATA portion is as follows: + /// + /// The Key Tag field MUST be represented as an unsigned decimal integer. + /// + /// The Algorithm field MUST be represented either as an unsigned decimal + /// integer or as an algorithm mnemonic specified in Appendix A.1. + /// + /// The Digest Type field MUST be represented as an unsigned decimal + /// integer. + /// + /// The Digest MUST be represented as a sequence of case-insensitive + /// hexadecimal digits. Whitespace is allowed within the hexadecimal + /// text. + /// ``` + #[allow(deprecated)] + pub(crate) fn from_tokens<'i, I: Iterator>( + mut tokens: I, + ) -> Result { + let tag_str: &str = tokens + .next() + .ok_or(ParseError::Message("key tag not present"))?; + let algorithm_str: &str = tokens + .next() + .ok_or(ParseError::Message("algorithm not present"))?; + let digest_type_str: &str = tokens + .next() + .ok_or(ParseError::Message("digest type not present"))?; + let tag: u16 = tag_str.parse()?; + let algorithm = match algorithm_str { + // Mnemonics from Appendix A.1. + "RSAMD5" => Algorithm::Unknown(1), + "DH" => Algorithm::Unknown(2), + "DSA" => Algorithm::Unknown(3), + "ECC" => Algorithm::Unknown(4), + "RSASHA1" => Algorithm::RSASHA1, + "INDIRECT" => Algorithm::Unknown(252), + "PRIVATEDNS" => Algorithm::Unknown(253), + "PRIVATEOID" => Algorithm::Unknown(254), + _ => Algorithm::from_u8(algorithm_str.parse()?), + }; + let digest_type = DigestType::from(u8::from_str(digest_type_str)?); + let digest_str: String = tokens.collect(); + if digest_str.is_empty() { + return Err(ParseError::Message("digest not present")); + } + let mut digest = Vec::with_capacity(digest_str.len() / 2); + let mut s = digest_str.as_str(); + while s.len() >= 2 { + if !s.is_char_boundary(2) { + return Err(ParseError::Message("digest contains non hexadecimal text")); + } + let (byte_str, rest) = s.split_at(2); + s = rest; + let byte = u8::from_str_radix(byte_str, 16)?; + digest.push(byte); + } + Ok(Self::new(tag, algorithm, digest_type, digest)) + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5.1.1) + /// + /// ```text + /// 5.1.1. The Key Tag Field + /// + /// The Key Tag field lists the key tag of the DNSKEY RR referred to by + /// the DS record, in network byte order. + /// + /// The Key Tag used by the DS RR is identical to the Key Tag used by + /// RRSIG RRs. Appendix B describes how to compute a Key Tag. + /// ``` + pub fn key_tag(&self) -> u16 { + self.key_tag + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5.1.1) + /// + /// ```text + /// 5.1.2. The Algorithm Field + /// + /// The Algorithm field lists the algorithm number of the DNSKEY RR + /// referred to by the DS record. + /// + /// The algorithm number used by the DS RR is identical to the algorithm + /// number used by RRSIG and DNSKEY RRs. Appendix A.1 lists the + /// algorithm number types. + /// ``` + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5.1.1) + /// + /// ```text + /// 5.1.3. The Digest Type Field + /// + /// The DS RR refers to a DNSKEY RR by including a digest of that DNSKEY + /// RR. The Digest Type field identifies the algorithm used to construct + /// the digest. Appendix A.2 lists the possible digest algorithm types. + /// ``` + pub fn digest_type(&self) -> DigestType { + self.digest_type + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5.1.1) + /// + /// ```text + /// 5.1.4. The Digest Field + /// + /// The DS record refers to a DNSKEY RR by including a digest of that + /// DNSKEY RR. + /// + /// The digest is calculated by concatenating the canonical form of the + /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, + /// and then applying the digest algorithm. + /// + /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); + /// + /// "|" denotes concatenation + /// + /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. + /// + /// The size of the digest may vary depending on the digest algorithm and + /// DNSKEY RR size. As of the time of this writing, the only defined + /// digest algorithm is SHA-1, which produces a 20 octet digest. + /// ``` + pub fn digest(&self) -> &[u8] { + &self.digest + } + + /// Validates that a given DNSKEY is covered by the DS record. + /// + /// # Return + /// + /// true if and only if the DNSKEY is covered by the DS record. + pub fn covers(&self, name: &Name, key: &DNSKEY) -> ProtoResult { + key.to_digest(name, self.digest_type()) + .map(|hash| key.zone_key() && hash.as_ref() == self.digest()) + } +} + +impl BinEncodable for DS { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16(self.key_tag())?; + self.algorithm().emit(encoder)?; + encoder.emit(self.digest_type().into())?; + encoder.emit_vec(self.digest())?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for DS { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + let start_idx = decoder.index(); + + let key_tag: u16 = decoder.read_u16()?.unverified(/*key_tag is valid as any u16*/); + let algorithm: Algorithm = Algorithm::read(decoder)?; + let digest_type = + DigestType::from(decoder.read_u8()?.unverified(/*DigestType is verified as safe*/)); + + let bytes_read = decoder.index() - start_idx; + let left: usize = length + .map(|u| u as usize) + .checked_sub(bytes_read) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { read: bytes_read, len })? + .unverified(/*used only as length safely*/); + let digest = + decoder.read_vec(left)?.unverified(/*the byte array will fail in usage if invalid*/); + + Ok(Self::new(key_tag, algorithm, digest_type, digest)) + } +} + +impl RecordData for DS { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::DS(csync)) => Some(csync), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::DS + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::DS(self)) + } +} + +/// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-5.3) +/// +/// ```text +/// 5.3. The DS RR Presentation Format +/// +/// The presentation format of the RDATA portion is as follows: +/// +/// The Key Tag field MUST be represented as an unsigned decimal integer. +/// +/// The Algorithm field MUST be represented either as an unsigned decimal +/// integer or as an algorithm mnemonic specified in Appendix A.1. +/// +/// The Digest Type field MUST be represented as an unsigned decimal +/// integer. +/// +/// The Digest MUST be represented as a sequence of case-insensitive +/// hexadecimal digits. Whitespace is allowed within the hexadecimal +/// text. +/// +/// 5.4. DS RR Example +/// +/// The following example shows a DNSKEY RR and its corresponding DS RR. +/// +/// dskey.example.com. 86400 IN DNSKEY 256 3 5 ( AQOeiiR0GOMYkDshWoSKz9Xz +/// fwJr1AYtsmx3TGkJaNXVbfi/ +/// 2pHm822aJ5iI9BMzNXxeYCmZ +/// DRD99WYwYqUSdjMmmAphXdvx +/// egXd/M5+X7OrzKBaMbCVdFLU +/// Uh6DhweJBjEVv5f2wwjM9Xzc +/// nOf+EPbtG9DMBmADjFDc2w/r +/// ljwvFw== +/// ) ; key id = 60485 +/// +/// dskey.example.com. 86400 IN DS 60485 5 1 ( 2BB183AF5F22588179A53B0A +/// 98631FAD1A292118 ) +/// +/// The first four text fields specify the name, TTL, Class, and RR type +/// (DS). Value 60485 is the key tag for the corresponding +/// "dskey.example.com." DNSKEY RR, and value 5 denotes the algorithm +/// used by this "dskey.example.com." DNSKEY RR. The value 1 is the +/// algorithm used to construct the digest, and the rest of the RDATA +/// text is the digest in hexadecimal. +/// ``` +impl Display for DS { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{tag} {alg} {ty} {digest}", + tag = self.key_tag, + alg = u8::from(self.algorithm), + ty = u8::from(self.digest_type), + digest = data_encoding::HEXUPPER_PERMISSIVE.encode(&self.digest) + ) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + #[cfg(feature = "std")] + use std::println; + + use super::*; + use crate::dnssec::{PublicKeyBuf, SigningKey, crypto::EcdsaSigningKey, rdata::DNSKEY}; + + #[test] + fn test() { + let rdata = DS::new( + 0xF00F, + Algorithm::RSASHA256, + DigestType::SHA256, + vec![5, 6, 7, 8], + ); + + let mut bytes = Vec::new(); + let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes); + assert!(rdata.emit(&mut encoder).is_ok()); + let bytes = encoder.into_bytes(); + + #[cfg(feature = "std")] + println!("bytes: {bytes:?}"); + + let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes); + let restrict = Restrict::new(bytes.len() as u16); + let read_rdata = DS::read_data(&mut decoder, restrict).expect("Decoding error"); + assert_eq!(rdata, read_rdata); + } + + #[test] + fn test_from_key() { + // The key signing key for `no.` and the matching DS record published in the root zone. + let public_key = PublicKeyBuf::new( + data_encoding::BASE64 + .decode(b"m0YDL7YHCgqY5BPOy+UaYc6cqvGYlXA7rTnJPNzHHy7fK65XVWKLyhVpnK0zsw0XSCz4DiGgVKPjHDCFmtnjqQ==") + .unwrap(), + Algorithm::ECDSAP256SHA256, + ); + let name = Name::parse("no.", None).unwrap(); + + let ds = DS::from_key(&public_key, &name, DigestType::SHA256).unwrap(); + assert_eq!(ds.key_tag(), 38032); + assert_eq!( + ds.key_tag(), + DNSKEY::from_key(&public_key).calculate_key_tag().unwrap() + ); + assert_eq!( + ds.digest(), + data_encoding::HEXUPPER + .decode(b"6D374D769D1E4388B1A549A6DA2F7D89371DB6ABAA53D20EEC70844DB4062E51") + .unwrap() + ); + } + + #[test] + fn test_covers() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + + let dnskey_rdata = DNSKEY::new( + true, + true, + false, + PublicKeyBuf::new( + signing_key + .to_public_key() + .unwrap() + .public_bytes() + .to_owned(), + algorithm, + ), + ); + + let name = Name::parse("www.example.com.", None).unwrap(); + let ds_rdata = DS::new( + 0, + algorithm, + DigestType::SHA256, + dnskey_rdata + .to_digest(&name, DigestType::SHA256) + .unwrap() + .as_ref() + .to_owned(), + ); + + assert!(ds_rdata.covers(&name, &dnskey_rdata).unwrap()); + } + + #[test] + fn test_covers_fails_with_non_zone_key() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + + let dnskey_rdata = DNSKEY::new( + false, + true, + false, + PublicKeyBuf::new( + signing_key + .to_public_key() + .unwrap() + .public_bytes() + .to_owned(), + algorithm, + ), + ); + + let name = Name::parse("www.example.com.", None).unwrap(); + let ds_rdata = DS::new( + 0, + algorithm, + DigestType::SHA256, + dnskey_rdata + .to_digest(&name, DigestType::SHA256) + .unwrap() + .as_ref() + .to_owned(), + ); + + assert!(!ds_rdata.covers(&name, &dnskey_rdata).unwrap()); + } + + #[test] + fn test_covers_uppercase() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + + let dnskey_rdata = DNSKEY::new( + true, + true, + false, + PublicKeyBuf::new( + signing_key + .to_public_key() + .unwrap() + .public_bytes() + .to_owned(), + algorithm, + ), + ); + + let name = Name::parse("www.example.com.", None).unwrap(); + let ds_rdata = DS::new( + 0, + algorithm, + DigestType::SHA256, + dnskey_rdata + .to_digest(&name, DigestType::SHA256) + .unwrap() + .as_ref() + .to_owned(), + ); + + let uppercase_name = Name::from_ascii("WWW.EXAMPLE.COM.").unwrap(); + assert!(ds_rdata.covers(&uppercase_name, &dnskey_rdata).unwrap()); + } + + #[test] + #[allow(deprecated)] + fn test_parsing() { + assert_eq!( + DS::from_tokens("60485 5 1 2BB183AF5F22588179A53B0A 98631FAD1A292118".split(' ')) + .unwrap(), + DS::new( + 60485, + Algorithm::RSASHA1, + DigestType::SHA1, + vec![ + 0x2B, 0xB1, 0x83, 0xAF, 0x5F, 0x22, 0x58, 0x81, 0x79, 0xA5, 0x3B, 0x0A, 0x98, + 0x63, 0x1F, 0xAD, 0x1A, 0x29, 0x21, 0x18 + ] + ) + ); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/key.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/key.rs new file mode 100644 index 00000000..953a2e7e --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/key.rs @@ -0,0 +1,953 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! public key record data for signing zone records +#![allow(clippy::use_self)] + +use alloc::{sync::Arc, vec::Vec}; +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::DNSSECRData; +use crate::{ + dnssec::{Algorithm, PublicKey, Verifier, crypto::decode_public_key}, + error::ProtoResult, + rr::{RecordData, RecordDataDecodable, RecordType, record_data::RData}, + serialize::binary::{ + BinDecodable, BinDecoder, BinEncodable, BinEncoder, DecodeError, Restrict, RestrictedMath, + }, +}; + +/// [RFC 2535](https://tools.ietf.org/html/rfc2535#section-3), Domain Name System Security Extensions, March 1999 +/// +/// ```text +/// 3. The KEY Resource Record +/// +/// The KEY resource record (RR) is used to store a public key that is +/// associated with a Domain Name System (DNS) name. This can be the +/// public key of a zone, a user, or a host or other end entity. Security +/// aware DNS implementations MUST be designed to handle at least two +/// simultaneously valid keys of the same type associated with the same +/// name. +/// +/// The type number for the KEY RR is 25. +/// +/// A KEY RR is, like any other RR, authenticated by a SIG RR. KEY RRs +/// must be signed by a zone level key. +/// +/// 3.1 KEY RDATA format +/// +/// The RDATA for a KEY RR consists of flags, a protocol octet, the +/// algorithm number octet, and the public key itself. The format is as +/// follows: +/// +/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | flags | protocol | algorithm | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | / +/// / public key / +/// / / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| +/// +/// The KEY RR is not intended for storage of certificates and a separate +/// certificate RR has been developed for that purpose, defined in [RFC +/// 2538]. +/// +/// The meaning of the KEY RR owner name, flags, and protocol octet are +/// described in Sections 3.1.1 through 3.1.5 below. The flags and +/// algorithm must be examined before any data following the algorithm +/// octet as they control the existence and format of any following data. +/// The algorithm and public key fields are described in Section 3.2. +/// The format of the public key is algorithm dependent. +/// +/// KEY RRs do not specify their validity period but their authenticating +/// SIG RR(s) do as described in Section 4 below. +/// +/// 3.1.1 Object Types, DNS Names, and Keys +/// +/// The public key in a KEY RR is for the object named in the owner name. +/// +/// A DNS name may refer to three different categories of things. For +/// example, foo.host.example could be (1) a zone, (2) a host or other +/// end entity , or (3) the mapping into a DNS name of the user or +/// account foo@host.example. Thus, there are flag bits, as described +/// below, in the KEY RR to indicate with which of these roles the owner +/// name and public key are associated. Note that an appropriate zone +/// KEY RR MUST occur at the apex node of a secure zone and zone KEY RRs +/// occur only at delegation points. +/// +/// 3.1.2 The KEY RR Flag Field +/// +/// In the "flags" field: +/// +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +/// | A/C | Z | XT| Z | Z | NAMTYP| Z | Z | Z | Z | SIG | +/// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +/// +/// Bit 0 and 1 are the key "type" bits whose values have the following +/// meanings: +/// +/// 10: Use of the key is prohibited for authentication. +/// 01: Use of the key is prohibited for confidentiality. +/// 00: Use of the key for authentication and/or confidentiality +/// is permitted. Note that DNS security makes use of keys +/// for authentication only. Confidentiality use flagging is +/// provided for use of keys in other protocols. +/// Implementations not intended to support key distribution +/// for confidentiality MAY require that the confidentiality +/// use prohibited bit be on for keys they serve. +/// 11: If both bits are one, the "no key" value, there is no key +/// information and the RR stops after the algorithm octet. +/// By the use of this "no key" value, a signed KEY RR can +/// authentically assert that, for example, a zone is not +/// secured. See section 3.4 below. +/// +/// Bits 2 is reserved and must be zero. +/// +/// Bits 3 is reserved as a flag extension bit. If it is a one, a second +/// 16 bit flag field is added after the algorithm octet and +/// before the key data. This bit MUST NOT be set unless one or +/// more such additional bits have been defined and are non-zero. +/// +/// Bits 4-5 are reserved and must be zero. +/// +/// Bits 6 and 7 form a field that encodes the name type. Field values +/// have the following meanings: +/// +/// 00: indicates that this is a key associated with a "user" or +/// "account" at an end entity, usually a host. The coding +/// of the owner name is that used for the responsible +/// individual mailbox in the SOA and RP RRs: The owner name +/// is the user name as the name of a node under the entity +/// name. For example, "j_random_user" on +/// host.subdomain.example could have a public key associated +/// through a KEY RR with name +/// j_random_user.host.subdomain.example. It could be used +/// in a security protocol where authentication of a user was +/// desired. This key might be useful in IP or other +/// security for a user level service such a telnet, ftp, +/// rlogin, etc. +/// 01: indicates that this is a zone key for the zone whose name +/// is the KEY RR owner name. This is the public key used +/// for the primary DNS security feature of data origin +/// authentication. Zone KEY RRs occur only at delegation +/// points. +/// 10: indicates that this is a key associated with the non-zone +/// "entity" whose name is the RR owner name. This will +/// commonly be a host but could, in some parts of the DNS +/// tree, be some other type of entity such as a telephone +/// number [RFC 1530] or numeric IP address. This is the +/// public key used in connection with DNS request and +/// transaction authentication services. It could also be +/// used in an IP-security protocol where authentication at +/// the host, rather than user, level was desired, such as +/// routing, NTP, etc. +/// 11: reserved. +/// +/// Bits 8-11 are reserved and must be zero. +/// +/// Bits 12-15 are the "signatory" field. If non-zero, they indicate +/// that the key can validly sign things as specified in DNS +/// dynamic update [RFC 2137]. Note that zone keys (see bits +/// 6 and 7 above) always have authority to sign any RRs in +/// the zone regardless of the value of the signatory field. +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct KEY { + key_trust: KeyTrust, + key_usage: KeyUsage, + signatory: UpdateScope, + protocol: Protocol, + algorithm: Algorithm, + public_key: Vec, +} + +impl KEY { + /// Construct a new KEY RData + /// + /// # Arguments + /// + /// * `key_trust` - declare the security level of this key + /// * `key_usage` - what type of thing is this key associated to + /// * `revoke` - this key has been revoked + /// * `algorithm` - specifies the algorithm which this Key uses to sign records + /// * `public_key` - the public key material, in native endian, the emitter will perform any necessary conversion + /// + /// # Return + /// + /// A new KEY RData for use in a Resource Record + pub fn new( + key_trust: KeyTrust, + key_usage: KeyUsage, + signatory: UpdateScope, + protocol: Protocol, + algorithm: Algorithm, + public_key: Vec, + ) -> Self { + Self { + key_trust, + key_usage, + signatory, + protocol, + algorithm, + public_key, + } + } + + /// Returns the trust level of the key + pub fn key_trust(&self) -> KeyTrust { + self.key_trust + } + + /// Returns the entity type using this key + pub fn key_usage(&self) -> KeyUsage { + self.key_usage + } + + /// Returns the signatory information of the KEY + pub fn signatory(&self) -> UpdateScope { + self.signatory + } + + /// Returns true if the key_trust is DoNotTrust + pub fn revoke(&self) -> bool { + self.key_trust == KeyTrust::DoNotTrust + } + + /// Returns the protocol which this key can be used with + pub fn protocol(&self) -> Protocol { + self.protocol + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.1.3) + /// + /// ```text + /// 2.1.3. The Algorithm Field + /// + /// The Algorithm field identifies the public key's cryptographic + /// algorithm and determines the format of the Public Key field. A list + /// of DNSSEC algorithm types can be found in Appendix A.1 + /// ``` + pub fn algorithm(&self) -> Algorithm { + self.algorithm + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2.1.4) + /// + /// ```text + /// 2.1.4. The Public Key Field + /// + /// The Public Key Field holds the public key material. The format + /// depends on the algorithm of the key being stored and is described in + /// separate documents. + /// ``` + pub fn public_key(&self) -> &[u8] { + &self.public_key + } + + /// Output the encoded form of the flags + pub fn flags(&self) -> u16 { + let mut flags: u16 = 0; + flags |= u16::from(self.key_trust); + flags |= u16::from(self.key_usage); + flags |= u16::from(self.signatory); + + flags + } + + // /// Creates a message digest for this KEY record. + // /// + // /// ```text + // /// 5.1.4. The Digest Field + // /// + // /// The DS record refers to a KEY RR by including a digest of that + // /// KEY RR. + // /// + // /// The digest is calculated by concatenating the canonical form of the + // /// fully qualified owner name of the KEY RR with the KEY RDATA, + // /// and then applying the digest algorithm. + // /// + // /// digest = digest_algorithm( KEY owner name | KEY RDATA); + // /// + // /// "|" denotes concatenation + // /// + // /// KEY RDATA = Flags | Protocol | Algorithm | Public Key. + // /// + // /// The size of the digest may vary depending on the digest algorithm and + // /// KEY RR size. As of the time of this writing, the only defined + // /// digest algorithm is SHA-1, which produces a 20 octet digest. + // /// ``` + // /// + // /// # Arguments + // /// + // /// * `name` - the label of of the KEY record. + // /// * `digest_type` - the `DigestType` with which to create the message digest. + // pub fn to_digest(&self, name: &Name, digest_type: DigestType) -> ProtoResult> { + // let mut buf: Vec = Vec::new(); + // { + // let mut encoder: BinEncoder = BinEncoder::new(&mut buf); + // encoder.set_canonical_names(true); + // if let Err(e) = name.emit(&mut encoder) + // .and_then(|_| emit(&mut encoder, self)) { + // warn!("error serializing KEY: {}", e); + // return Err(format!("error serializing KEY: {}", e).into()); + // } + // } + + // digest_type.hash(&buf).map_err(|e| e.into()) + // } +} + +impl Verifier for KEY { + fn algorithm(&self) -> Algorithm { + self.algorithm() + } + + fn key(&self) -> ProtoResult> { + decode_public_key(&self.public_key, self.algorithm) + } +} + +impl BinEncodable for KEY { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16(self.flags())?; + encoder.emit(u8::from(self.protocol))?; + self.algorithm().emit(encoder)?; + encoder.emit_vec(self.public_key())?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for KEY { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + // | A/C | Z | XT| Z | Z | NAMTYP| Z | Z | Z | Z | SIG | + // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + let flags: u16 = decoder + .read_u16()? + .verify_unwrap(|flags| { + // Bits 2 is reserved and must be zero. + // Bits 4-5 are reserved and must be zero. + // Bits 8-11 are reserved and must be zero. + flags & 0b0010_1100_1111_0000 == 0 + }) + .map_err(DecodeError::KeyFlagsReserved)?; + + let key_trust = KeyTrust::from(flags); + let extended_flags: bool = flags & 0b0001_0000_0000_0000 != 0; + let key_usage = KeyUsage::from(flags); + let signatory = UpdateScope::from(flags); + + if extended_flags { + return Err(DecodeError::ExtendedKeyFlagsUnsupported(flags)); + } + + // TODO: protocol my be infallible + let protocol = + Protocol::from(decoder.read_u8()?.unverified(/*Protocol is verified as safe*/)); + + let algorithm: Algorithm = Algorithm::read(decoder)?; + + // the public key is the left-over bytes minus 4 for the first fields + // TODO: decode the key here? + let key_len = length + .map(|u| u as usize) + .checked_sub(4) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { read: 4, len })? + .unverified(/*used only as length safely*/); + let public_key: Vec = + decoder.read_vec(key_len)?.unverified(/*the byte array will fail in usage if invalid*/); + + Ok(Self::new( + key_trust, key_usage, signatory, protocol, algorithm, public_key, + )) + } +} + +impl RecordData for KEY { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::KEY(csync)) => Some(csync), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::KEY + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::KEY(self)) + } +} + +/// Note that KEY is a deprecated type in DNS +/// +/// [RFC 2535](https://tools.ietf.org/html/rfc2535#section-7.1), Domain Name System Security Extensions, March 1999 +/// +/// ```text +/// 7.1 Presentation of KEY RRs +/// +/// KEY RRs may appear as single logical lines in a zone data master file +/// [RFC 1033]. +/// +/// The flag field is represented as an unsigned integer or a sequence of +/// mnemonics as follows separated by instances of the vertical bar ("|") +/// character: +/// +/// BIT Mnemonic Explanation +/// 0-1 key type +/// NOCONF =1 confidentiality use prohibited +/// NOAUTH =2 authentication use prohibited +/// NOKEY =3 no key present +/// 2 FLAG2 - reserved +/// 3 EXTEND flags extension +/// 4 FLAG4 - reserved +/// 5 FLAG5 - reserved +/// 6-7 name type +/// USER =0 (default, may be omitted) +/// ZONE =1 +/// HOST =2 (host or other end entity) +/// NTYP3 - reserved +/// 8 FLAG8 - reserved +/// 9 FLAG9 - reserved +/// 10 FLAG10 - reserved +/// 11 FLAG11 - reserved +/// 12-15 signatory field, values 0 to 15 +/// can be represented by SIG0, SIG1, ... SIG15 +/// +/// No flag mnemonic need be present if the bit or field it represents is +/// zero. +/// +/// The protocol octet can be represented as either an unsigned integer +/// or symbolically. The following initial symbols are defined: +/// +/// 000 NONE +/// 001 TLS +/// 002 EMAIL +/// 003 DNSSEC +/// 004 IPSEC +/// 255 ALL +/// +/// Note that if the type flags field has the NOKEY value, nothing +/// appears after the algorithm octet. +/// +/// The remaining public key portion is represented in base 64 (see +/// Appendix A) and may be divided up into any number of white space +/// separated substrings, down to single base 64 digits, which are +/// concatenated to obtain the full signature. These substrings can span +/// lines using the standard parenthesis. +/// +/// Note that the public key may have internal sub-fields but these do +/// not appear in the master file representation. For example, with +/// algorithm 1 there is a public exponent size, then a public exponent, +/// and then a modulus. With algorithm 254, there will be an OID size, +/// an OID, and algorithm dependent information. But in both cases only a +/// single logical base 64 string will appear in the master file. +/// ``` +impl fmt::Display for KEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{flags} {proto} {alg} {key}", + flags = self.flags(), + proto = u8::from(self.protocol), + alg = self.algorithm, + key = data_encoding::BASE64.encode(&self.public_key) + ) + } +} + +impl From for RData { + fn from(key: KEY) -> Self { + Self::DNSSEC(DNSSECRData::KEY(key)) + } +} + +/// Specifies in what contexts this key may be trusted for use +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub enum KeyTrust { + /// Use of the key is prohibited for authentication + NotAuth, + /// Use of the key is prohibited for confidentiality + NotPrivate, + /// Use of the key for authentication and/or confidentiality is permitted + #[default] + AuthOrPrivate, + /// If both bits are one, the "no key" value, (revocation?) + DoNotTrust, +} + +impl From for KeyTrust { + fn from(flags: u16) -> Self { + // we only care about the first two bits, zero out the rest + match flags & 0b1100_0000_0000_0000 { + // 10: Use of the key is prohibited for authentication. + 0b1000_0000_0000_0000 => Self::NotAuth, + // 01: Use of the key is prohibited for confidentiality. + 0b0100_0000_0000_0000 => Self::NotPrivate, + // 00: Use of the key for authentication and/or confidentiality + 0b0000_0000_0000_0000 => Self::AuthOrPrivate, + // 11: If both bits are one, the "no key" value, there is no key + 0b1100_0000_0000_0000 => Self::DoNotTrust, + _ => panic!("All other bit fields should have been cleared"), + } + } +} + +impl From for u16 { + fn from(key_trust: KeyTrust) -> Self { + match key_trust { + // 10: Use of the key is prohibited for authentication. + KeyTrust::NotAuth => 0b1000_0000_0000_0000, + // 01: Use of the key is prohibited for confidentiality. + KeyTrust::NotPrivate => 0b0100_0000_0000_0000, + // 00: Use of the key for authentication and/or confidentiality + KeyTrust::AuthOrPrivate => 0b0000_0000_0000_0000, + // 11: If both bits are one, the "no key" value, there is no key + KeyTrust::DoNotTrust => 0b1100_0000_0000_0000, + } + } +} + +/// Declares what this key is for +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub enum KeyUsage { + /// key associated with a "user" or "account" at an end entity, usually a host + Host, + /// zone key for the zone whose name is the KEY RR owner name + #[deprecated = "For Zone signing DNSKEY should be used"] + Zone, + /// associated with the non-zone "entity" whose name is the RR owner name + #[default] + Entity, + /// Reserved + Reserved, +} + +impl From for KeyUsage { + fn from(flags: u16) -> Self { + // we only care about the 6&7 two bits, zero out the rest + match flags & 0b0000_0011_0000_0000 { + // 00: indicates that this is a key associated with a "user" or + 0b0000_0000_0000_0000 => Self::Host, + // 01: indicates that this is a zone key for the zone whose name + 0b0000_0001_0000_0000 => Self::Zone, + // 10: indicates that this is a key associated with the non-zone + 0b0000_0010_0000_0000 => Self::Entity, + // 11: reserved. + 0b0000_0011_0000_0000 => Self::Reserved, + _ => panic!("All other bit fields should have been cleared"), + } + } +} + +impl From for u16 { + fn from(key_usage: KeyUsage) -> Self { + match key_usage { + // 00: indicates that this is a key associated with a "user" or + KeyUsage::Host => 0b0000_0000_0000_0000, + // 01: indicates that this is a zone key for the zone whose name + KeyUsage::Zone => 0b0000_0001_0000_0000, + // 10: indicates that this is a key associated with the non-zone + KeyUsage::Entity => 0b0000_0010_0000_0000, + // 11: reserved. + KeyUsage::Reserved => 0b0000_0011_0000_0000, + } + } +} + +/// [RFC 2137](https://tools.ietf.org/html/rfc2137#section-3.1), Secure Domain Name System Dynamic Update, April 1997 +/// +/// ```text +/// 3.1.1 Update Key Name Scope +/// +/// The owner name of any update authorizing KEY RR must (1) be the same +/// as the owner name of any RRs being added or deleted or (2) a wildcard +/// name including within its extended scope (see section 3.3) the name +/// of any RRs being added or deleted and those RRs must be in the same +/// zone. +/// +/// 3.1.2 Update Key Class Scope +/// +/// The class of any update authorizing KEY RR must be the same as the +/// class of any RR's being added or deleted. +/// +/// 3.1.3 Update Key Signatory Field +/// +/// The four bit "signatory field" (see RFC 2065) of any update +/// authorizing KEY RR must be non-zero. The bits have the meanings +/// described below for non-zone keys (see section 3.2 for zone type +/// keys). +/// +/// UPDATE KEY RR SIGNATORY FIELD BITS +/// +/// 0 1 2 3 +/// +-----------+-----------+-----------+-----------+ +/// | zone | strong | unique | general | +/// +-----------+-----------+-----------+-----------+ +/// +/// Bit 0, zone control - If nonzero, this key is authorized to attach, +/// detach, and move zones by creating and deleting NS, glue A, and +/// zone KEY RR(s). If zero, the key can not authorize any update +/// that would effect such RRs. This bit is meaningful for both +/// type A and type B dynamic secure zones. +/// +/// NOTE: do not confuse the "zone" signatory field bit with the +/// "zone" key type bit. +/// +/// Bit 1, strong update - If nonzero, this key is authorized to add and +/// delete RRs even if there are other RRs with the same owner name +/// and class that are authenticated by a SIG signed with a +/// different dynamic update KEY. If zero, the key can only +/// authorize updates where any existing RRs of the same owner and +/// class are authenticated by a SIG using the same key. This bit +/// is meaningful only for type A dynamic zones and is ignored in +/// type B dynamic zones. +/// +/// Keeping this bit zero on multiple KEY RRs with the same or +/// nested wild card owner names permits multiple entities to exist +/// that can create and delete names but can not effect RRs with +/// different owner names from any they created. In effect, this +/// creates two levels of dynamic update key, strong and weak, where +/// weak keys are limited in interfering with each other but a +/// strong key can interfere with any weak keys or other strong +/// keys. +/// +/// Bit 2, unique name update - If nonzero, this key is authorized to add +/// and update RRs for only a single owner name. If there already +/// exist RRs with one or more names signed by this key, they may be +/// updated but no new name created until the number of existing +/// names is reduced to zero. This bit is meaningful only for mode +/// A dynamic zones and is ignored in mode B dynamic zones. This bit +/// is meaningful only if the owner name is a wildcard. (Any +/// dynamic update KEY with a non-wildcard name is, in effect, a +/// unique name update key.) +/// +/// This bit can be used to restrict a KEY from flooding a zone with +/// new names. In conjunction with a local administratively imposed +/// limit on the number of dynamic RRs with a particular name, it +/// can completely restrict a KEY from flooding a zone with RRs. +/// +/// Bit 3, general update - The general update signatory field bit has no +/// special meaning. If the other three bits are all zero, it must +/// be one so that the field is non-zero to designate that the key +/// is an update key. The meaning of all values of the signatory +/// field with the general bit and one or more other signatory field +/// bits on is reserved. +/// +/// All the signatory bit update authorizations described above only +/// apply if the update is within the name and class scope as per +/// sections 3.1.1 and 3.1.2. +/// ``` +/// +/// [RFC 3007](https://tools.ietf.org/html/rfc3007#section-1.5), Secure Dynamic Update, November 2000 +/// +/// ```text +/// [RFC2535, section 3.1.2] defines the signatory field of a key as the +/// final 4 bits of the flags field, but does not define its value. This +/// proposal leaves this field undefined. Updating [RFC2535], this field +/// SHOULD be set to 0 in KEY records, and MUST be ignored. +/// +/// ``` +#[deprecated = "Deprecated by RFC3007"] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub struct UpdateScope { + /// this key is authorized to attach, + /// detach, and move zones by creating and deleting NS, glue A, and + /// zone KEY RR(s) + pub zone: bool, + /// this key is authorized to add and + /// delete RRs even if there are other RRs with the same owner name + /// and class that are authenticated by a SIG signed with a + /// different dynamic update KEY + pub strong: bool, + /// this key is authorized to add and update RRs for only a single owner name + pub unique: bool, + /// The general update signatory field bit has no special meaning, (true if the others are false) + pub general: bool, +} + +impl From for UpdateScope { + fn from(flags: u16) -> Self { + // we only care about the final four bits, zero out the rest + Self { + // Bit 0, zone control - If nonzero, this key is authorized to attach, + zone: flags & 0b0000_0000_0000_1000 != 0, + // Bit 1, strong update - If nonzero, this key is authorized to add and + strong: flags & 0b0000_0000_0000_0100 != 0, + // Bit 2, unique name update - If nonzero, this key is authorized to add + unique: flags & 0b0000_0000_0000_0010 != 0, + // Bit 3, general update - The general update signatory field bit has no + general: flags & 0b0000_0000_0000_0001 != 0, + } + } +} + +impl From for u16 { + fn from(update_scope: UpdateScope) -> Self { + let mut flags = 0_u16; + + if update_scope.zone { + flags |= 0b0000_0000_0000_1000; + } + + if update_scope.strong { + flags |= 0b0000_0000_0000_0100; + } + + if update_scope.unique { + flags |= 0b0000_0000_0000_0010; + } + + if update_scope.general { + flags |= 0b0000_0000_0000_0001; + } + + flags + } +} + +/// [RFC 2535](https://tools.ietf.org/html/rfc2535#section-3.1.3), Domain Name System Security Extensions, March 1999 +/// +/// ```text +/// 3.1.3 The Protocol Octet +/// +/// It is anticipated that keys stored in DNS will be used in conjunction +/// with a variety of Internet protocols. It is intended that the +/// protocol octet and possibly some of the currently unused (must be +/// zero) bits in the KEY RR flags as specified in the future will be +/// used to indicate a key's validity for different protocols. +/// +/// The following values of the Protocol Octet are reserved as indicated: +/// +/// VALUE Protocol +/// +/// 0 -reserved +/// 1 TLS +/// 2 email +/// 3 dnssec +/// 4 IPSEC +/// 5-254 - available for assignment by IANA +/// 255 All +/// +/// In more detail: +/// 1 is reserved for use in connection with TLS. +/// 2 is reserved for use in connection with email. +/// 3 is used for DNS security. The protocol field SHOULD be set to +/// this value for zone keys and other keys used in DNS security. +/// Implementations that can determine that a key is a DNS +/// security key by the fact that flags label it a zone key or the +/// signatory flag field is non-zero are NOT REQUIRED to check the +/// protocol field. +/// 4 is reserved to refer to the Oakley/IPSEC [RFC 2401] protocol +/// and indicates that this key is valid for use in conjunction +/// with that security standard. This key could be used in +/// connection with secured communication on behalf of an end +/// entity or user whose name is the owner name of the KEY RR if +/// the entity or user flag bits are set. The presence of a KEY +/// resource with this protocol value is an assertion that the +/// host speaks Oakley/IPSEC. +/// 255 indicates that the key can be used in connection with any +/// protocol for which KEY RR protocol octet values have been +/// defined. The use of this value is discouraged and the use of +/// different keys for different protocols is encouraged. +/// ``` +/// +/// [RFC3445](https://tools.ietf.org/html/rfc3445#section-4), Limiting the KEY Resource Record (RR), December 2002 +/// +/// ```text +/// All Protocol Octet values except DNSSEC (3) are eliminated +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Protocol { + /// Not in use + #[deprecated = "Deprecated by RFC3445"] + Reserved, + /// Reserved for use with TLS + #[deprecated = "Deprecated by RFC3445"] + TLS, + /// Reserved for use with email + #[deprecated = "Deprecated by RFC3445"] + Email, + /// Reserved for use with DNSSEC (Hickory DNS only supports DNSKEY with DNSSEC) + #[default] + DNSSEC, + /// Reserved to refer to the Oakley/IPSEC + #[deprecated = "Deprecated by RFC3445"] + IPSec, + /// Undefined + #[deprecated = "Deprecated by RFC3445"] + Other(u8), + /// the key can be used in connection with any protocol + #[deprecated = "Deprecated by RFC3445"] + All, +} + +impl From for Protocol { + fn from(field: u8) -> Self { + match field { + 0 => Self::Reserved, + 1 => Self::TLS, + 2 => Self::Email, + 3 => Self::DNSSEC, + 4 => Self::IPSec, + 255 => Self::All, + _ => Self::Other(field), + } + } +} + +impl From for u8 { + fn from(protocol: Protocol) -> Self { + match protocol { + Protocol::Reserved => 0, + Protocol::TLS => 1, + Protocol::Email => 2, + Protocol::DNSSEC => 3, + Protocol::IPSec => 4, + Protocol::All => 255, + Protocol::Other(field) => field, + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use std::println; + + use super::*; + use crate::dnssec::{SigningKey, crypto::EcdsaSigningKey}; + + #[test] + fn test() { + let algorithm = Algorithm::ECDSAP256SHA256; + let pkcs8 = EcdsaSigningKey::generate_pkcs8(algorithm).unwrap(); + let signing_key = EcdsaSigningKey::from_pkcs8(&pkcs8, algorithm).unwrap(); + + let rdata = KEY::new( + KeyTrust::default(), + KeyUsage::default(), + UpdateScope::default(), + Protocol::default(), + algorithm, + signing_key.to_public_key().unwrap().public_bytes().to_vec(), + ); + + let mut bytes = Vec::new(); + let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes); + assert!(rdata.emit(&mut encoder).is_ok()); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes); + let restrict = Restrict::new(bytes.len() as u16); + let read_rdata = KEY::read_data(&mut decoder, restrict).expect("Decoding error"); + assert_eq!(rdata, read_rdata); + // assert!(rdata + // .to_digest(&Name::parse("www.example.com.", None).unwrap(), + // DigestType::SHA256) + // .is_ok()); + } + + #[test] + fn test_key_usage() { + assert_eq!(KeyUsage::Host, KeyUsage::from(u16::from(KeyUsage::Host))); + assert_eq!(KeyUsage::Zone, KeyUsage::from(u16::from(KeyUsage::Zone))); + assert_eq!( + KeyUsage::Entity, + KeyUsage::from(u16::from(KeyUsage::Entity)) + ); + assert_eq!( + KeyUsage::Reserved, + KeyUsage::from(u16::from(KeyUsage::Reserved)) + ); + } + + #[test] + fn test_update_scope() { + assert_eq!( + UpdateScope::default(), + UpdateScope::from(u16::from(UpdateScope::default())) + ); + + let update_scope = UpdateScope { + zone: true, + strong: true, + unique: true, + general: true, + }; + assert_eq!(update_scope, UpdateScope::from(u16::from(update_scope))); + + let update_scope = UpdateScope { + zone: true, + strong: false, + unique: true, + general: false, + }; + assert_eq!(update_scope, UpdateScope::from(u16::from(update_scope))); + + let update_scope = UpdateScope { + zone: false, + strong: true, + unique: false, + general: true, + }; + assert_eq!(update_scope, UpdateScope::from(u16::from(update_scope))); + + let update_scope = UpdateScope { + zone: false, + strong: true, + unique: true, + general: false, + }; + assert_eq!(update_scope, UpdateScope::from(u16::from(update_scope))); + + let update_scope = UpdateScope { + zone: true, + strong: false, + unique: false, + general: true, + }; + assert_eq!(update_scope, UpdateScope::from(u16::from(update_scope))); + } + + #[test] + fn test_key_trust() { + assert_eq!( + KeyTrust::NotAuth, + KeyTrust::from(u16::from(KeyTrust::NotAuth)) + ); + assert_eq!( + KeyTrust::NotPrivate, + KeyTrust::from(u16::from(KeyTrust::NotPrivate)) + ); + assert_eq!( + KeyTrust::AuthOrPrivate, + KeyTrust::from(u16::from(KeyTrust::AuthOrPrivate)) + ); + assert_eq!( + KeyTrust::DoNotTrust, + KeyTrust::from(u16::from(KeyTrust::DoNotTrust)) + ); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/mod.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/mod.rs new file mode 100644 index 00000000..17e36dfc --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/mod.rs @@ -0,0 +1,569 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! All record data structures and related serialization methods + +use alloc::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +// TODO: these should each be it's own struct, it would make parsing and decoding a little cleaner +// and also a little more ergonomic when accessing. +// each of these module's has the parser for that rdata embedded, to keep the file sizes down... +pub mod cdnskey; +pub mod cds; +pub mod dnskey; +pub mod ds; +#[allow(deprecated)] +pub mod key; +pub mod nsec; +pub mod nsec3; +pub mod nsec3param; +pub mod rrsig; +pub mod sig; + +use tracing::trace; + +use crate::{ + error::*, + rr::{RData, RecordDataDecodable, RecordType, rdata::NULL}, + serialize::binary::{ + BinDecodable, BinDecoder, BinEncodable, BinEncoder, DecodeError, Restrict, + }, +}; + +pub use self::cdnskey::CDNSKEY; +pub use self::cds::CDS; +pub use self::dnskey::DNSKEY; +pub use self::ds::DS; +pub use self::key::KEY; +pub use self::nsec::NSEC; +pub use self::nsec3::NSEC3; +pub use self::nsec3param::NSEC3PARAM; +pub use self::rrsig::RRSIG; +pub use self::sig::{SIG, SigInput}; + +/// Record data enum variants for DNSSEC-specific records. +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Clone, Eq, Hash)] +#[non_exhaustive] +pub enum DNSSECRData { + /// ```text + /// RFC 7344 Delegation Trust Maintenance September 2014 + /// + /// 3.2. CDNSKEY Resource Record Format + /// + /// The wire and presentation format of the CDNSKEY ("Child DNSKEY") + /// resource record is identical to the DNSKEY record. IANA has + /// allocated RR code 60 for the CDNSKEY resource record via Expert + /// Review. The CDNSKEY RR uses the same registries as DNSKEY for its + /// fields. + /// + /// No special processing is performed by authoritative servers or by + /// resolvers, when serving or resolving. For all practical purposes, + /// CDNSKEY is a regular RR type. + /// ``` + CDNSKEY(CDNSKEY), + + /// ```text + /// RFC 7344 Delegation Trust Maintenance September 2014 + /// + /// 3.1. CDS Resource Record Format + /// The wire and presentation format of the Child DS (CDS) resource + /// record is identical to the DS record [RFC4034]. IANA has allocated + /// RR code 59 for the CDS resource record via Expert Review + /// [DNS-TRANSPORT]. The CDS RR uses the same registries as DS for its + /// fields. + /// + /// No special processing is performed by authoritative servers or by + /// resolvers, when serving or resolving. For all practical purposes, + /// CDS is a regular RR type. + /// ``` + CDS(CDS), + + /// ```text + /// RFC 4034 DNSSEC Resource Records March 2005 + /// + /// 2.1. DNSKEY RDATA Wire Format + /// + /// The RDATA for a DNSKEY RR consists of a 2 octet Flags Field, a 1 + /// octet Protocol Field, a 1 octet Algorithm Field, and the Public Key + /// Field. + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Flags | Protocol | Algorithm | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / Public Key / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// 2.1.1. The Flags Field + /// + /// Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value 1, + /// then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// owner name MUST be the name of a zone. If bit 7 has value 0, then + /// the DNSKEY record holds some other type of DNS public key and MUST + /// NOT be used to verify RRSIGs that cover RRsets. + /// + /// Bit 15 of the Flags field is the Secure Entry Point flag, described + /// in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// key intended for use as a secure entry point. This flag is only + /// intended to be a hint to zone signing or debugging software as to the + /// intended use of this DNSKEY record; validators MUST NOT alter their + /// behavior during the signature validation process in any way based on + /// the setting of this bit. This also means that a DNSKEY RR with the + /// SEP bit set would also need the Zone Key flag set in order to be able + /// to generate signatures legally. A DNSKEY RR with the SEP set and the + /// Zone Key flag not set MUST NOT be used to verify RRSIGs that cover + /// RRsets. + /// + /// Bits 0-6 and 8-14 are reserved: these bits MUST have value 0 upon + /// creation of the DNSKEY RR and MUST be ignored upon receipt. + /// + /// RFC 5011 Trust Anchor Update September 2007 + /// + /// 7. IANA Considerations + /// + /// The IANA has assigned a bit in the DNSKEY flags field (see Section 7 + /// of [RFC4034]) for the REVOKE bit (8). + /// ``` + DNSKEY(DNSKEY), + + /// ```text + /// 5.1. DS RDATA Wire Format + /// + /// The RDATA for a DS RR consists of a 2 octet Key Tag field, a 1 octet + /// Algorithm field, a 1 octet Digest Type field, and a Digest field. + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Key Tag | Algorithm | Digest Type | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / Digest / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// 5.1.1. The Key Tag Field + /// + /// The Key Tag field lists the key tag of the DNSKEY RR referred to by + /// the DS record, in network byte order. + /// + /// The Key Tag used by the DS RR is identical to the Key Tag used by + /// RRSIG RRs. Appendix B describes how to compute a Key Tag. + /// + /// 5.1.2. The Algorithm Field + /// + /// The Algorithm field lists the algorithm number of the DNSKEY RR + /// referred to by the DS record. + /// + /// The algorithm number used by the DS RR is identical to the algorithm + /// number used by RRSIG and DNSKEY RRs. Appendix A.1 lists the + /// algorithm number types. + /// + /// 5.1.3. The Digest Type Field + /// + /// The DS RR refers to a DNSKEY RR by including a digest of that DNSKEY + /// RR. The Digest Type field identifies the algorithm used to construct + /// the digest. Appendix A.2 lists the possible digest algorithm types. + /// + /// 5.1.4. The Digest Field + /// + /// The DS record refers to a DNSKEY RR by including a digest of that + /// DNSKEY RR. + /// + /// The digest is calculated by concatenating the canonical form of the + /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, + /// and then applying the digest algorithm. + /// + /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); + /// + /// "|" denotes concatenation + /// + /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. + /// + /// The size of the digest may vary depending on the digest algorithm and + /// DNSKEY RR size. As of the time of this writing, the only defined + /// digest algorithm is SHA-1, which produces a 20 octet digest. + /// ``` + DS(DS), + + /// ```text + /// RFC 2535 DNS Security Extensions March 1999 + /// + /// 3.1 KEY RDATA format + /// + /// The RDATA for a KEY RR consists of flags, a protocol octet, the + /// algorithm number octet, and the public key itself. The format is as + /// follows: + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | flags | protocol | algorithm | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | / + /// / public key / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| + /// + /// The KEY RR is not intended for storage of certificates and a separate + /// certificate RR has been developed for that purpose, defined in [RFC + /// 2538]. + /// + /// The meaning of the KEY RR owner name, flags, and protocol octet are + /// described in Sections 3.1.1 through 3.1.5 below. The flags and + /// algorithm must be examined before any data following the algorithm + /// octet as they control the existence and format of any following data. + /// The algorithm and public key fields are described in Section 3.2. + /// The format of the public key is algorithm dependent. + /// + /// KEY RRs do not specify their validity period but their authenticating + /// SIG RR(s) do as described in Section 4 below. + /// ``` + KEY(KEY), + + /// ```text + /// RFC 4034 DNSSEC Resource Records March 2005 + /// + /// 4.1. NSEC RDATA Wire Format + /// + /// The RDATA of the NSEC RR is as shown below: + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / Next Domain Name / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / Type Bit Maps / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// ``` + NSEC(NSEC), + + /// ```text + /// RFC 5155 NSEC3 March 2008 + /// + /// 3.2. NSEC3 RDATA Wire Format + /// + /// The RDATA of the NSEC3 RR is as shown below: + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Hash Alg. | Flags | Iterations | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Salt Length | Salt / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Hash Length | Next Hashed Owner Name / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / Type Bit Maps / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// Hash Algorithm is a single octet. + /// + /// Flags field is a single octet, the Opt-Out flag is the least + /// significant bit, as shown below: + /// + /// 0 1 2 3 4 5 6 7 + /// +-+-+-+-+-+-+-+-+ + /// | |O| + /// +-+-+-+-+-+-+-+-+ + /// + /// Iterations is represented as a 16-bit unsigned integer, with the most + /// significant bit first. + /// + /// Salt Length is represented as an unsigned octet. Salt Length + /// represents the length of the Salt field in octets. If the value is + /// zero, the following Salt field is omitted. + /// + /// Salt, if present, is encoded as a sequence of binary octets. The + /// length of this field is determined by the preceding Salt Length + /// field. + /// + /// Hash Length is represented as an unsigned octet. Hash Length + /// represents the length of the Next Hashed Owner Name field in octets. + /// + /// The next hashed owner name is not base32 encoded, unlike the owner + /// name of the NSEC3 RR. It is the unmodified binary hash value. It + /// does not include the name of the containing zone. The length of this + /// field is determined by the preceding Hash Length field. + /// + /// 3.2.1. Type Bit Maps Encoding + /// + /// The encoding of the Type Bit Maps field is the same as that used by + /// the NSEC RR, described in [RFC4034]. It is explained and clarified + /// here for clarity. + /// + /// The RR type space is split into 256 window blocks, each representing + /// the low-order 8 bits of the 16-bit RR type space. Each block that + /// has at least one active RR type is encoded using a single octet + /// window number (from 0 to 255), a single octet bitmap length (from 1 + /// to 32) indicating the number of octets used for the bitmap of the + /// window block, and up to 32 octets (256 bits) of bitmap. + /// + /// Blocks are present in the NSEC3 RR RDATA in increasing numerical + /// order. + /// + /// Type Bit Maps Field = ( Window Block # | Bitmap Length | Bitmap )+ + /// + /// where "|" denotes concatenation. + /// + /// Each bitmap encodes the low-order 8 bits of RR types within the + /// window block, in network bit order. The first bit is bit 0. For + /// window block 0, bit 1 corresponds to RR type 1 (A), bit 2 corresponds + /// to RR type 2 (NS), and so forth. For window block 1, bit 1 + /// corresponds to RR type 257, bit 2 to RR type 258. If a bit is set to + /// 1, it indicates that an RRSet of that type is present for the + /// original owner name of the NSEC3 RR. If a bit is set to 0, it + /// indicates that no RRSet of that type is present for the original + /// owner name of the NSEC3 RR. + /// + /// Since bit 0 in window block 0 refers to the non-existing RR type 0, + /// it MUST be set to 0. After verification, the validator MUST ignore + /// the value of bit 0 in window block 0. + /// + /// Bits representing Meta-TYPEs or QTYPEs as specified in Section 3.1 of + /// [RFC2929] or within the range reserved for assignment only to QTYPEs + /// and Meta-TYPEs MUST be set to 0, since they do not appear in zone + /// data. If encountered, they must be ignored upon reading. + /// + /// Blocks with no types present MUST NOT be included. Trailing zero + /// octets in the bitmap MUST be omitted. The length of the bitmap of + /// each block is determined by the type code with the largest numerical + /// value, within that block, among the set of RR types present at the + /// original owner name of the NSEC3 RR. Trailing octets not specified + /// MUST be interpreted as zero octets. + /// ``` + NSEC3(NSEC3), + + /// ```text + /// RFC 5155 NSEC3 March 2008 + /// + /// 4.2. NSEC3PARAM RDATA Wire Format + /// + /// The RDATA of the NSEC3PARAM RR is as shown below: + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Hash Alg. | Flags | Iterations | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Salt Length | Salt / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// Hash Algorithm is a single octet. + /// + /// Flags field is a single octet. + /// + /// Iterations is represented as a 16-bit unsigned integer, with the most + /// significant bit first. + /// + /// Salt Length is represented as an unsigned octet. Salt Length + /// represents the length of the following Salt field in octets. If the + /// value is zero, the Salt field is omitted. + /// + /// Salt, if present, is encoded as a sequence of binary octets. The + /// length of this field is determined by the preceding Salt Length + /// field. + /// ``` + NSEC3PARAM(NSEC3PARAM), + + /// ```text + /// RFC 2535 & 2931 DNS Security Extensions March 1999 + /// RFC 4034 DNSSEC Resource Records March 2005 + /// + /// 3.1. RRSIG RDATA Wire Format + /// + /// The RDATA for an RRSIG RR consists of a 2 octet Type Covered field, a + /// 1 octet Algorithm field, a 1 octet Labels field, a 4 octet Original + /// TTL field, a 4 octet Signature Expiration field, a 4 octet Signature + /// Inception field, a 2 octet Key tag, the Signer's Name field, and the + /// Signature field. + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Type Covered | Algorithm | Labels | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Original TTL | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Signature Expiration | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Signature Inception | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Key Tag | / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Signer's Name / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / Signature / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// ``` + RRSIG(RRSIG), + + /// ```text + /// RFC 2535 & 2931 DNS Security Extensions March 1999 + /// RFC 4034 DNSSEC Resource Records March 2005 + /// + /// 3.1. RRSIG RDATA Wire Format + /// + /// The RDATA for an RRSIG RR consists of a 2 octet Type Covered field, a + /// 1 octet Algorithm field, a 1 octet Labels field, a 4 octet Original + /// TTL field, a 4 octet Signature Expiration field, a 4 octet Signature + /// Inception field, a 2 octet Key tag, the Signer's Name field, and the + /// Signature field. + /// + /// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Type Covered | Algorithm | Labels | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Original TTL | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Signature Expiration | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Signature Inception | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Key Tag | / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Signer's Name / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / Signature / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// ``` + SIG(SIG), + + /// Unknown or unsupported DNSSEC record data + Unknown { + /// RecordType code + code: u16, + /// RData associated to the record + rdata: NULL, + }, +} + +impl DNSSECRData { + pub(crate) fn read( + decoder: &mut BinDecoder<'_>, + record_type: RecordType, + rdata_length: Restrict, + ) -> Result { + match record_type { + RecordType::CDNSKEY => { + trace!("reading CDNSKEY"); + CDNSKEY::read_data(decoder, rdata_length).map(Self::CDNSKEY) + } + RecordType::CDS => { + trace!("reading CDS"); + CDS::read_data(decoder, rdata_length).map(Self::CDS) + } + RecordType::DNSKEY => { + trace!("reading DNSKEY"); + DNSKEY::read_data(decoder, rdata_length).map(Self::DNSKEY) + } + RecordType::DS => { + trace!("reading DS"); + DS::read_data(decoder, rdata_length).map(Self::DS) + } + RecordType::KEY => { + trace!("reading KEY"); + KEY::read_data(decoder, rdata_length).map(Self::KEY) + } + RecordType::NSEC => { + trace!("reading NSEC"); + NSEC::read_data(decoder, rdata_length).map(Self::NSEC) + } + RecordType::NSEC3 => { + trace!("reading NSEC3"); + NSEC3::read_data(decoder, rdata_length).map(Self::NSEC3) + } + RecordType::NSEC3PARAM => { + trace!("reading NSEC3PARAM"); + NSEC3PARAM::read(decoder).map(Self::NSEC3PARAM) + } + RecordType::RRSIG => { + trace!("reading RRSIG"); + RRSIG::read_data(decoder, rdata_length).map(Self::RRSIG) + } + RecordType::SIG => { + trace!("reading SIG"); + SIG::read_data(decoder, rdata_length).map(Self::SIG) + } + r => { + panic!("not a dnssec RecordType: {}", r); + } + } + } + + pub(crate) fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + match self { + Self::CDNSKEY(cdnskey) => cdnskey.emit(encoder), + Self::CDS(cds) => cds.emit(encoder), + Self::DS(ds) => ds.emit(encoder), + Self::KEY(key) => key.emit(encoder), + Self::DNSKEY(dnskey) => dnskey.emit(encoder), + Self::NSEC(nsec) => nsec.emit(encoder), + Self::NSEC3(nsec3) => nsec3.emit(encoder), + Self::NSEC3PARAM(nsec3param) => nsec3param.emit(encoder), + Self::RRSIG(rrsig) => rrsig.emit(encoder), + Self::SIG(sig) => sig.emit(encoder), + Self::Unknown { rdata, .. } => rdata.emit(encoder), + } + } + + pub(crate) fn to_record_type(&self) -> RecordType { + match self { + Self::CDNSKEY(..) => RecordType::CDNSKEY, + Self::CDS(..) => RecordType::CDS, + Self::DS(..) => RecordType::DS, + Self::KEY(..) => RecordType::KEY, + Self::DNSKEY(..) => RecordType::DNSKEY, + Self::NSEC(..) => RecordType::NSEC, + Self::NSEC3(..) => RecordType::NSEC3, + Self::NSEC3PARAM(..) => RecordType::NSEC3PARAM, + Self::SIG(..) => RecordType::SIG, + Self::RRSIG(..) => RecordType::RRSIG, + Self::Unknown { code, .. } => RecordType::Unknown(*code), + } + } +} + +impl fmt::Display for DNSSECRData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fn w(f: &mut fmt::Formatter<'_>, rdata: D) -> Result<(), fmt::Error> { + write!(f, "{rdata}") + } + + match self { + Self::CDNSKEY(key) => w(f, key), + Self::CDS(ds) => w(f, ds), + Self::DS(ds) => w(f, ds), + Self::KEY(key) => w(f, key), + Self::DNSKEY(key) => w(f, key), + Self::NSEC(nsec) => w(f, nsec), + Self::NSEC3(nsec3) => w(f, nsec3), + Self::NSEC3PARAM(nsec3param) => w(f, nsec3param), + Self::SIG(sig) => w(f, sig), + Self::RRSIG(rrsig) => w(f, rrsig), + Self::Unknown { rdata, .. } => w(f, rdata), + } + } +} + +impl From for RData { + fn from(rdata: DNSSECRData) -> Self { + Self::DNSSEC(rdata) + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec.rs new file mode 100644 index 00000000..e0f388a5 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec.rs @@ -0,0 +1,339 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! NSEC record types + +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::error::*; +use crate::rr::{Name, RData, RecordData, RecordDataDecodable, RecordType, RecordTypeSet}; +use crate::serialize::binary::*; + +use super::DNSSECRData; + +/// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-4), DNSSEC Resource Records, March 2005 +/// +/// ```text +/// 4.1. NSEC RDATA Wire Format +/// +/// The RDATA of the NSEC RR is as shown below: +/// +/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// / Next Domain Name / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// / Type Bit Maps / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// 4.1.3. Inclusion of Wildcard Names in NSEC RDATA +/// +/// If a wildcard owner name appears in a zone, the wildcard label ("*") +/// is treated as a literal symbol and is treated the same as any other +/// owner name for the purposes of generating NSEC RRs. Wildcard owner +/// names appear in the Next Domain Name field without any wildcard +/// expansion. [RFC4035] describes the impact of wildcards on +/// authenticated denial of existence. +/// ``` +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct NSEC { + next_domain_name: Name, + type_bit_maps: RecordTypeSet, +} + +impl NSEC { + /// Constructs a new NSEC RData, warning this won't guarantee that the NSEC covers itself + /// which it should at it's own name. + /// + /// # Arguments + /// + /// * `next_domain_name` - the name labels of the next ordered name in the zone + /// * `type_bit_maps` - a bit map of the types that exist at this name + /// + /// # Returns + /// + /// An NSEC RData for use in a Resource Record + pub fn new( + next_domain_name: Name, + type_bit_maps: impl IntoIterator, + ) -> Self { + Self { + next_domain_name, + type_bit_maps: RecordTypeSet::new(type_bit_maps), + } + } + + /// Constructs a new NSEC RData, this will add the NSEC itself and its RRSIG as covered, + /// generally correct for NSEC records generated at their own name + /// + /// # Arguments + /// + /// * `next_domain_name` - the name labels of the next ordered name in the zone + /// * `type_bit_maps` - a bit map of the types that exist at this name + /// + /// # Returns + /// + /// An NSEC RData for use in a Resource Record + pub fn new_cover_self( + next_domain_name: Name, + type_bit_maps: impl IntoIterator, + ) -> Self { + Self::new( + next_domain_name, + type_bit_maps + .into_iter() + .chain([RecordType::NSEC, RecordType::RRSIG]), + ) + } + + /// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-4.1.1), DNSSEC Resource Records, March 2005 + /// + /// ```text + /// 4.1.1. The Next Domain Name Field + /// + /// The Next Domain field contains the next owner name (in the canonical + /// ordering of the zone) that has authoritative data or contains a + /// delegation point NS RRset; see Section 6.1 for an explanation of + /// canonical ordering. The value of the Next Domain Name field in the + /// last NSEC record in the zone is the name of the zone apex (the owner + /// name of the zone's SOA RR). This indicates that the owner name of + /// the NSEC RR is the last name in the canonical ordering of the zone. + /// + /// A sender MUST NOT use DNS name compression on the Next Domain Name + /// field when transmitting an NSEC RR. + /// + /// Owner names of RRsets for which the given zone is not authoritative + /// (such as glue records) MUST NOT be listed in the Next Domain Name + /// unless at least one authoritative RRset exists at the same owner + /// name. + /// ``` + pub fn next_domain_name(&self) -> &Name { + &self.next_domain_name + } + + /// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-4.1.2) + /// + /// ```text + /// 4.1.2. The Type Bit Maps Field + /// + /// The Type Bit Maps field identifies the RRset types that exist at the + /// NSEC RR's owner name. + /// + /// A zone MUST NOT include an NSEC RR for any domain name that only + /// holds glue records. + /// ``` + pub fn type_bit_maps(&self) -> impl Iterator + '_ { + self.type_bit_maps.iter() + } + + /// Returns the RecordTypeSet representing the type bit maps + pub fn type_set(&self) -> &RecordTypeSet { + &self.type_bit_maps + } + + /// Returns true if this NSEC is an ancestor delegation. + /// + /// Per [RFC 6840 §4.1], such an NSEC sits on the parent side of a zone cut + /// (its bitmap has `NS` set with `SOA` clear, or asserts a `DNAME` at the + /// owner) and authenticates only the absence of `DS` at its owner, not the + /// absence of any name below the cut, nor any other type at the owner. + /// + /// [RFC 6840 §4.1]: https://www.rfc-editor.org/rfc/rfc6840#section-4.1 + pub fn is_ancestor_delegation(&self) -> bool { + let types = &self.type_bit_maps; + types.contains(RecordType::DNAME) + || (types.contains(RecordType::NS) && !types.contains(RecordType::SOA)) + } +} + +impl BinEncodable for NSEC { + /// [RFC 6840](https://tools.ietf.org/html/rfc6840#section-6) + /// + /// ```text + /// 5.1. Errors in Canonical Form Type Code List + /// + /// When canonicalizing DNS names (for both ordering and signing), DNS + /// names in the RDATA section of NSEC resource records are not converted + /// to lowercase. DNS names in the RDATA section of RRSIG resource + /// records are converted to lowercase. + /// ``` + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + // See RFC 6840 section 5.1. + let mut encoder = encoder.with_rdata_behavior(RDataEncoding::Other); + + self.next_domain_name().emit(&mut encoder)?; + self.type_bit_maps.emit(&mut encoder)?; + + Ok(()) + } +} + +impl<'r> RecordDataDecodable<'r> for NSEC { + fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict) -> Result { + let start_idx = decoder.index(); + + let next_domain_name = Name::read(decoder)?; + + let offset = u16::try_from(decoder.index() - start_idx).map_err(|_| { + DecodeError::IncorrectRDataLengthRead { + read: decoder.index() - start_idx, + len: u16::MAX as usize, + } + })?; + let bit_map_len = + length + .checked_sub(offset) + .map_err(|len| DecodeError::IncorrectRDataLengthRead { + read: offset as usize, + len: len as usize, + })?; + let type_bit_maps = RecordTypeSet::read_data(decoder, bit_map_len)?; + + Ok(Self { + next_domain_name, + type_bit_maps, + }) + } +} + +impl RecordData for NSEC { + fn try_borrow(data: &RData) -> Option<&Self> { + match data { + RData::DNSSEC(DNSSECRData::NSEC(csync)) => Some(csync), + _ => None, + } + } + + fn record_type(&self) -> RecordType { + RecordType::NSEC + } + + fn into_rdata(self) -> RData { + RData::DNSSEC(DNSSECRData::NSEC(self)) + } +} + +/// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-4.2), DNSSEC Resource Records, March 2005 +/// +/// ```text +/// 4.2. The NSEC RR Presentation Format +/// +/// The presentation format of the RDATA portion is as follows: +/// +/// The Next Domain Name field is represented as a domain name. +/// +/// The Type Bit Maps field is represented as a sequence of RR type +/// mnemonics. When the mnemonic is not known, the TYPE representation +/// described in [RFC3597], Section 5, MUST be used. +/// +/// 4.3. NSEC RR Example +/// +/// The following NSEC RR identifies the RRsets associated with +/// alfa.example.com. and identifies the next authoritative name after +/// alfa.example.com. +/// +/// alfa.example.com. 86400 IN NSEC host.example.com. ( +/// A MX RRSIG NSEC TYPE1234 ) +/// +/// The first four text fields specify the name, TTL, Class, and RR type +/// (NSEC). The entry host.example.com. is the next authoritative name +/// after alfa.example.com. in canonical order. The A, MX, RRSIG, NSEC, +/// and TYPE1234 mnemonics indicate that there are A, MX, RRSIG, NSEC, +/// and TYPE1234 RRsets associated with the name alfa.example.com. +/// +/// Assuming that the validator can authenticate this NSEC record, it +/// could be used to prove that beta.example.com does not exist, or to +/// prove that there is no AAAA record associated with alfa.example.com. +/// Authenticated denial of existence is discussed in [RFC4035]. +/// ``` +impl fmt::Display for NSEC { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.next_domain_name)?; + + for ty in self.type_bit_maps.iter() { + write!(f, " {ty}")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::dbg_macro, clippy::print_stdout)] + + use std::println; + + use alloc::vec::Vec; + + use super::*; + + #[test] + fn test() { + use crate::rr::RecordType; + use core::str::FromStr; + + let rdata = NSEC::new( + Name::from_str("www.example.com.").unwrap(), + [ + RecordType::A, + RecordType::AAAA, + RecordType::DS, + RecordType::RRSIG, + ], + ); + + let mut bytes = Vec::new(); + let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes); + assert!(rdata.emit(&mut encoder).is_ok()); + let bytes = encoder.into_bytes(); + + println!("bytes: {bytes:?}"); + + let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes); + let restrict = Restrict::new(bytes.len() as u16); + let read_rdata = NSEC::read_data(&mut decoder, restrict).expect("Decoding error"); + assert_eq!(rdata, read_rdata); + } + + #[test] + fn rfc4034_example_rdata() { + // From section 4.3 of RFC 4034 + let bytes = b"\x04host\ + \x07example\ + \x03com\x00\ + \x00\x06\x40\x01\x00\x00\x00\x03\ + \x04\x1b\x00\x00\x00\x00\x00\x00\ + \x00\x00\x00\x00\x00\x00\x00\x00\ + \x00\x00\x00\x00\x00\x00\x00\x00\ + \x00\x00\x00\x00\x20"; + let rdata = NSEC::new( + Name::parse("host.example.com.", None).unwrap(), + [ + RecordType::A, + RecordType::MX, + RecordType::RRSIG, + RecordType::NSEC, + RecordType::Unknown(1234), + ], + ); + + let mut buffer = Vec::new(); + let mut encoder = BinEncoder::new(&mut buffer); + rdata.emit(&mut encoder).expect("Encoding error"); + assert_eq!(encoder.into_bytes(), bytes); + + let mut decoder = BinDecoder::new(bytes); + let decoded = NSEC::read_data(&mut decoder, Restrict::new(bytes.len() as u16)) + .expect("Decoding error"); + assert_eq!(decoded, rdata); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec3.rs b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec3.rs new file mode 100644 index 00000000..ef13337c --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/dnssec/rdata/nsec3.rs @@ -0,0 +1,611 @@ +// Copyright 2015-2023 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! NSEC record types + +use alloc::{fmt, string::ToString, vec::Vec}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::{ + dnssec::Nsec3HashAlgorithm, + error::ProtoResult, + rr::{RData, RecordData, RecordDataDecodable, RecordType, RecordTypeSet, domain::Label}, + serialize::binary::*, +}; + +use super::{DNSSECRData, NSEC3PARAM}; + +/// [RFC 5155](https://tools.ietf.org/html/rfc5155#section-3), NSEC3, March 2008 +/// +/// ```text +/// 3. The NSEC3 Resource Record +/// +/// The NSEC3 Resource Record (RR) provides authenticated denial of +/// existence for DNS Resource Record Sets. +/// +/// The NSEC3 RR lists RR types present at the original owner name of the +/// NSEC3 RR. It includes the next hashed owner name in the hash order +/// of the zone. The complete set of NSEC3 RRs in a zone indicates which +/// RRSets exist for the original owner name of the RR and form a chain +/// of hashed owner names in the zone. This information is used to +/// provide authenticated denial of existence for DNS data. To provide +/// protection against zone enumeration, the owner names used in the +/// NSEC3 RR are cryptographic hashes of the original owner name +/// prepended as a single label to the name of the zone. The NSEC3 RR +/// indicates which hash function is used to construct the hash, which +/// salt is used, and how many iterations of the hash function are +/// performed over the original owner name. The hashing technique is +/// described fully in Section 5. +/// +/// Hashed owner names of unsigned delegations may be excluded from the +/// chain. An NSEC3 RR whose span covers the hash of an owner name or +/// "next closer" name of an unsigned delegation is referred to as an +/// Opt-Out NSEC3 RR and is indicated by the presence of a flag. +/// +/// The owner name for the NSEC3 RR is the base32 encoding of the hashed +/// owner name prepended as a single label to the name of the zone. +/// +/// The type value for the NSEC3 RR is 50. +/// +/// The NSEC3 RR RDATA format is class independent and is described +/// below. +/// +/// The class MUST be the same as the class of the original owner name. +/// +/// The NSEC3 RR SHOULD have the same TTL value as the SOA minimum TTL +/// field. This is in the spirit of negative caching [RFC2308]. +/// +/// 3.2. NSEC3 RDATA Wire Format +/// +/// The RDATA of the NSEC3 RR is as shown below: +/// +/// 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Hash Alg. | Flags | Iterations | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Salt Length | Salt / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Hash Length | Next Hashed Owner Name / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// / Type Bit Maps / +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// Hash Algorithm is a single octet. +/// +/// Flags field is a single octet, the Opt-Out flag is the least +/// significant bit, as shown below: +/// +/// 0 1 2 3 4 5 6 7 +/// +-+-+-+-+-+-+-+-+ +/// | |O| +/// +-+-+-+-+-+-+-+-+ +/// +/// Iterations is represented as a 16-bit unsigned integer, with the most +/// significant bit first. +/// +/// Salt Length is represented as an unsigned octet. Salt Length +/// represents the length of the Salt field in octets. If the value is +/// zero, the following Salt field is omitted. +/// +/// Salt, if present, is encoded as a sequence of binary octets. The +/// length of this field is determined by the preceding Salt Length +/// field. +/// +/// Hash Length is represented as an unsigned octet. Hash Length +/// represents the length of the Next Hashed Owner Name field in octets. +/// +/// The next hashed owner name is not base32 encoded, unlike the owner +/// name of the NSEC3 RR. It is the unmodified binary hash value. It +/// does not include the name of the containing zone. The length of this +/// field is determined by the preceding Hash Length field. +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct NSEC3 { + hash_algorithm: Nsec3HashAlgorithm, + opt_out: bool, + iterations: u16, + salt: Vec, + next_hashed_owner_name: Vec, + /// The next hashed owner name, in base32-encoded form. If the next hashed owner name field is + /// too long, this may be `None` instead. + #[cfg_attr(feature = "serde", serde(skip_serializing))] + next_hashed_owner_name_base32: Option

+where + Q: EmitAndCount, + A: EmitAndCount, + N: EmitAndCount, + D: EmitAndCount, +{ + let place = encoder.place::
()?; + + let query_count = queries.emit(encoder)?; + + let answer_count; + let authority_count; + let mut additional_count; + + if let Some(mut edns) = edns.cloned() { + // need to commit the error code + edns.set_rcode_high(metadata.response_code.high()); + + // RFC 6891 section 7: "The minimal response MUST be the DNS header, question section, + // and an OPT record. This MUST also occur when a truncated response (using the DNS + // header's TC bit) is returned." + // + // The OPT belongs in the additional section, which must follow answers and authority on + // the wire (parsers assign records to sections by count, not by type), so it cannot be + // emitted first. Instead, reserve space: the OPT's owner name is always root (one byte), + // so its encoded size is stable and can be measured up front. + let opt_record = Record::from(&edns); + let opt_len = { + let mut buf = Vec::new(); + BinEncoder::new(&mut buf).emit_all(iter::once(&opt_record))?; + buf.len() as u16 + }; + + let max_size = encoder.max_size(); + encoder.set_max_size(max_size.saturating_sub(opt_len)); + + answer_count = count_was_truncated(answers.emit(encoder))?; + authority_count = count_was_truncated(authorities.emit(encoder))?; + additional_count = count_was_truncated(additionals.emit(encoder))?; + + encoder.set_max_size(max_size); + + let count = count_was_truncated(encoder.emit_all(iter::once(&opt_record)))?; + additional_count.0 += count.0; + additional_count.1 |= count.1; + } else { + answer_count = count_was_truncated(answers.emit(encoder))?; + authority_count = count_was_truncated(authorities.emit(encoder))?; + additional_count = count_was_truncated(additionals.emit(encoder))?; + + if metadata.response_code.high() > 0 { + warn!( + "response code: {} for request: {} requires EDNS but none available", + metadata.response_code, metadata.id + ); + } + } + + // this is a little hacky, but if we are Verifying a signature, i.e. the original Message + // then the TSIG record should not be encoded and the edns record (if it exists) is + // already part of the additionals section. + let count = match signature { + Some(rec) => count_was_truncated(encoder.emit_all(iter::once(rec)))?, + None => (0, false), + }; + additional_count.0 += count.0; + additional_count.1 |= count.1; + + let counts = HeaderCounts { + queries: match u16::try_from(query_count) { + Ok(count) => count, + Err(_) => { + return Err(ProtoError::Message( + "too many queries to fit in header count", + )); + } + }, + answers: answer_count.0, + authorities: authority_count.0, + additionals: additional_count.0, + }; + + let mut final_metadata = *metadata; + final_metadata.truncation = + metadata.truncation || answer_count.1 || authority_count.1 || additional_count.1; + + let header = Header { + metadata: final_metadata, + counts, + }; + + place.replace(encoder, header)?; + Ok(header) +} + +impl BinEncodable for Message { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + emit_message_parts( + &self.metadata, + &mut self.queries.iter(), + &mut self.answers.iter(), + &mut self.authorities.iter(), + &mut self.additionals.iter(), + self.edns.as_ref(), + self.signature.as_deref(), + encoder, + )?; + + Ok(()) + } +} + +impl<'r> BinDecodable<'r> for Message { + fn read(decoder: &mut BinDecoder<'r>) -> Result { + let Header { + mut metadata, + counts, + } = Header::read(decoder)?; + + // TODO: return just header, and in the case of the rest of message getting an error. + // this could improve error detection while decoding. + + // get the questions + let count = counts.queries as usize; + let mut queries = Vec::with_capacity(count); + for _ in 0..count { + queries.push(Query::read(decoder)?); + } + + let (answers, _, _) = + Self::read_records(decoder, counts.answers as usize, false, metadata.op_code)?; + let (authorities, _, _) = Self::read_records( + decoder, + counts.authorities as usize, + false, + metadata.op_code, + )?; + let (additionals, edns, signature) = + Self::read_records(decoder, counts.additionals as usize, true, metadata.op_code)?; + + // need to grab error code from EDNS (which might have a higher value) + if let Some(edns) = &edns { + let high_response_code = edns.rcode_high(); + metadata.merge_response_code(high_response_code); + } + + Ok(Self { + metadata, + queries, + answers, + authorities, + additionals, + signature, + edns, + }) + } +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let write_query = |slice, f: &mut fmt::Formatter<'_>| -> Result<(), fmt::Error> { + for d in slice { + writeln!(f, ";; {d}")?; + } + + Ok(()) + }; + + let write_slice = |slice, f: &mut fmt::Formatter<'_>| -> Result<(), fmt::Error> { + for d in slice { + writeln!(f, "{d}")?; + } + + Ok(()) + }; + + writeln!(f, "; header {header}", header = self.metadata)?; + + if let Some(edns) = &self.edns { + writeln!(f, "; edns {edns}")?; + } + + writeln!(f, "; query")?; + write_query(&self.queries, f)?; + + if self.metadata.message_type == MessageType::Response + || self.metadata.op_code == OpCode::Update + { + writeln!(f, "; answers {}", self.answers.len())?; + write_slice(&self.answers, f)?; + writeln!(f, "; authorities {}", self.authorities.len())?; + write_slice(&self.authorities, f)?; + writeln!(f, "; additionals {}", self.additionals.len())?; + write_slice(&self.additionals, f)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::rr::rdata::A; + #[cfg(feature = "std")] + use crate::rr::rdata::OPT; + #[cfg(feature = "std")] + use crate::rr::rdata::opt::{ClientSubnet, EdnsCode, EdnsOption}; + #[cfg(feature = "__dnssec")] + use crate::rr::rdata::{TSIG, tsig::TsigAlgorithm}; + use crate::rr::{Name, RData}; + #[cfg(feature = "std")] + use crate::std::net::IpAddr; + #[cfg(feature = "std")] + use crate::std::string::ToString; + + #[test] + fn test_emit_and_read_header() { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = false; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.response_code = ResponseCode::ServFail; + + test_emit_and_read(message); + } + + #[test] + fn test_emit_and_read_query() { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.response_code = ResponseCode::ServFail; + message.add_query(Query::new()); + + test_emit_and_read(message); + } + + #[test] + fn test_emit_and_read_records() { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.authentic_data = true; + message.metadata.checking_disabled = true; + message.metadata.response_code = ResponseCode::ServFail; + + message.add_answer(Record::stub()); + message.add_authority(Record::stub()); + message.add_additional(Record::stub()); + + test_emit_and_read(message); + } + + #[cfg(test)] + fn test_emit_and_read(message: Message) { + let mut byte_vec: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut byte_vec); + message.emit(&mut encoder).unwrap(); + } + + let mut decoder = BinDecoder::new(&byte_vec); + let got = Message::read(&mut decoder).unwrap(); + + assert_eq!(got, message); + } + + #[test] + fn test_header_counts_correction_after_emit_read() { + let mut message = Message::response(10, OpCode::Update); + message.metadata.authoritative = true; + message.metadata.truncation = true; + message.metadata.recursion_desired = true; + message.metadata.recursion_available = true; + message.metadata.authentic_data = true; + message.metadata.checking_disabled = true; + message.metadata.response_code = ResponseCode::ServFail; + + message.add_answer(Record::stub()); + message.add_authority(Record::stub()); + message.add_additional(Record::stub()); + + let got = get_message_after_emitting_and_reading(message); + assert_eq!(got.queries.len(), 0); + assert_eq!(got.answers.len(), 1); + assert_eq!(got.authorities.len(), 1); + assert_eq!(got.additionals.len(), 1); + } + + #[cfg(test)] + fn get_message_after_emitting_and_reading(message: Message) -> Message { + let mut byte_vec: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut byte_vec); + message.emit(&mut encoder).unwrap(); + } + + let mut decoder = BinDecoder::new(&byte_vec); + + Message::read(&mut decoder).unwrap() + } + + #[test] + fn test_legit_message() { + #[rustfmt::skip] + let buf: Vec = vec![ + 0x10, 0x00, 0x81, + 0x80, // id = 4096, response, op=query, recursion_desired, recursion_available, no_error + 0x00, 0x01, 0x00, 0x01, // 1 query, 1 answer, + 0x00, 0x00, 0x00, 0x00, // 0 nameservers, 0 additional record + 0x03, b'w', b'w', b'w', // query --- www.example.com + 0x07, b'e', b'x', b'a', // + b'm', b'p', b'l', b'e', // + 0x03, b'c', b'o', b'm', // + 0x00, // 0 = endname + 0x00, 0x01, 0x00, 0x01, // RecordType = A, Class = IN + 0xC0, 0x0C, // name pointer to www.example.com + 0x00, 0x01, 0x00, 0x01, // RecordType = A, Class = IN + 0x00, 0x00, 0x00, 0x02, // TTL = 2 seconds + 0x00, 0x04, // record length = 4 (ipv4 address) + 0x5D, 0xB8, 0xD7, 0x0E, // address = 93.184.215.14 + ]; + + let mut decoder = BinDecoder::new(&buf); + let message = Message::read(&mut decoder).unwrap(); + + assert_eq!(message.id, 4_096); + + let mut buf: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut buf); + message.emit(&mut encoder).unwrap(); + } + + let mut decoder = BinDecoder::new(&buf); + let message = Message::read(&mut decoder).unwrap(); + + assert_eq!(message.id, 4_096); + } + + #[test] + fn rdata_zero_roundtrip() { + let buf = &[ + 160, 160, 0, 13, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, + ]; + + assert!(Message::from_bytes(buf).is_err()); + } + + #[test] + fn nsec_deserialization() { + const CRASHING_MESSAGE: &[u8] = &[ + 0, 0, 132, 0, 0, 0, 0, 1, 0, 0, 0, 1, 36, 49, 101, 48, 101, 101, 51, 100, 51, 45, 100, + 52, 50, 52, 45, 52, 102, 55, 56, 45, 57, 101, 52, 99, 45, 99, 51, 56, 51, 51, 55, 55, + 56, 48, 102, 50, 98, 5, 108, 111, 99, 97, 108, 0, 0, 1, 128, 1, 0, 0, 0, 120, 0, 4, + 192, 168, 1, 17, 36, 49, 101, 48, 101, 101, 51, 100, 51, 45, 100, 52, 50, 52, 45, 52, + 102, 55, 56, 45, 57, 101, 52, 99, 45, 99, 51, 56, 51, 51, 55, 55, 56, 48, 102, 50, 98, + 5, 108, 111, 99, 97, 108, 0, 0, 47, 128, 1, 0, 0, 0, 120, 0, 5, 192, 70, 0, 1, 64, + ]; + + Message::from_vec(CRASHING_MESSAGE).expect("failed to parse message"); + } + + #[test] + fn test_read_records_unsigned() { + let records = vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + Record::from_rdata( + Name::from_labels(vec!["www", "example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + ]; + let result = encode_and_read_records(records.clone(), false); + let (output_records, edns, signature) = result.unwrap(); + assert_eq!(output_records.len(), records.len()); + assert!(edns.is_none()); + assert!(signature.is_none()); + } + + #[cfg(feature = "std")] + #[test] + fn test_read_records_edns() { + let records = vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + Record::from_rdata( + Name::new(), + 0, + RData::OPT(OPT::new(vec![( + EdnsCode::Subnet, + EdnsOption::Subnet(ClientSubnet::new(IpAddr::from([127, 0, 0, 1]), 0, 24)), + )])), + ), + ]; + let result = encode_and_read_records(records, true); + let (output_records, edns, signature) = result.unwrap(); + assert_eq!(output_records.len(), 1); // Only the A record, OPT becomes EDNS + assert!(edns.is_some()); + assert!(signature.is_none()); + } + + #[cfg(feature = "__dnssec")] + #[test] + fn test_read_records_tsig() { + let records = vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + ]; + let result = encode_and_read_records(records, true); + let (output_records, edns, signature) = result.unwrap(); + assert_eq!(output_records.len(), 1); // Only the A record, TSIG becomes signature + assert!(edns.is_none()); + assert!(signature.is_some()); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_edns_tsig() { + let records = vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + Record::from_rdata( + Name::new(), + 0, + RData::OPT(OPT::new(vec![( + EdnsCode::Subnet, + EdnsOption::Subnet(ClientSubnet::new(IpAddr::from([127, 0, 0, 1]), 0, 24)), + )])), + ), + Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + ]; + + let result = encode_and_read_records(records, true); + assert!(result.is_ok()); + let (output_records, edns, signature) = result.unwrap(); + assert_eq!(output_records.len(), 1); // Only the A record + assert!(edns.is_some()); + assert!(signature.is_some()); + } + + #[cfg(feature = "std")] + #[test] + fn test_read_records_unsigned_multiple_edns() { + let opt_record = Record::from_rdata( + Name::new(), + 0, + RData::OPT(OPT::new(vec![( + EdnsCode::Subnet, + EdnsOption::Subnet(ClientSubnet::new(IpAddr::from([127, 0, 0, 1]), 0, 24)), + )])), + ); + let error = encode_and_read_records( + vec![ + opt_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + opt_record.clone(), + ], + true, + ) + .unwrap_err(); + assert!(error.to_string().contains("more than one EDNS record")); + } + + #[cfg(feature = "std")] + #[test] + fn test_read_records_opt_not_additional() { + let opt_record = Record::from_rdata( + Name::new(), + 0, + RData::OPT(OPT::new(vec![( + EdnsCode::Subnet, + EdnsOption::Subnet(ClientSubnet::new(IpAddr::from([127, 0, 0, 1]), 0, 24)), + )])), + ); + let err = encode_and_read_records( + vec![ + opt_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + ], + false, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("record type OPT only allowed in additional") + ); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_signed_multiple_edns() { + let opt_record = Record::from_rdata( + Name::new(), + 0, + RData::OPT(OPT::new(vec![( + EdnsCode::Subnet, + EdnsOption::Subnet(ClientSubnet::new(IpAddr::from([127, 0, 0, 1]), 0, 24)), + )])), + ); + let error = encode_and_read_records( + vec![ + opt_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + opt_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + ], + true, + ) + .unwrap_err(); + assert!(error.to_string().contains("more than one EDNS record")); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_tsig_not_additional() { + let err = encode_and_read_records( + vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + ], + false, + ) + .unwrap_err(); + assert!( + err.to_string() + .contains("record type TSIG only allowed in additional") + ); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_tsig_not_last() { + let a_record = Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ); + let error = encode_and_read_records( + vec![ + a_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + a_record.clone(), + ], + true, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("record after TSIG or SIG(0)")); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_sig0_not_last() { + let a_record = Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ); + let error = encode_and_read_records( + vec![ + a_record.clone(), + Record::from_rdata( + Name::from_labels(vec!["sig0", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ), + a_record.clone(), + ], + true, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("record after TSIG or SIG(0)")); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_multiple_tsig() { + let tsig_record = Record::from_rdata( + Name::from_labels(vec!["tsig", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ); + let error = encode_and_read_records( + vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + tsig_record.clone(), + tsig_record.clone(), + ], + true, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("record after TSIG or SIG(0)")); + } + + #[cfg(all(feature = "std", feature = "__dnssec"))] + #[test] + fn test_read_records_multiple_sig0() { + let sig0_record = Record::from_rdata( + Name::from_labels(vec!["sig0", "example", "com"]).unwrap(), + 0, + fake_tsig(), + ); + let error = encode_and_read_records( + vec![ + Record::from_rdata( + Name::from_labels(vec!["example", "com"]).unwrap(), + 300, + RData::A(A::new(127, 0, 0, 1)), + ), + sig0_record.clone(), + sig0_record.clone(), + ], + true, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("record after TSIG or SIG(0)")); + } + + #[expect(clippy::type_complexity)] + fn encode_and_read_records( + records: Vec, + is_additional: bool, + ) -> ProtoResult<(Vec, Option, Option>>)> { + let mut bytes = Vec::new(); + let mut encoder = BinEncoder::new(&mut bytes); + encoder.emit_all(records.iter())?; + Ok(Message::read_records( + &mut BinDecoder::new(&bytes), + records.len(), + is_additional, + OpCode::Query, + )?) + } + + #[cfg(feature = "__dnssec")] + fn fake_tsig() -> RData { + RData::TSIG(TSIG::new( + TsigAlgorithm::HmacSha256, + 0, + 0, + vec![], + 0, + None, + vec![], + )) + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/op/mod.rs b/vendor/hickory-dns/hickory-proto/src/op/mod.rs new file mode 100644 index 00000000..19c25e18 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/mod.rs @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Operations to send with a `Client` or server, e.g. `Query`, `Message`, or `UpdateMessage` can +//! be used together to either query or update resource records sets. + +use core::time::Duration; + +mod dns_request; +pub use dns_request::{DnsRequest, DnsRequestOptions}; + +mod dns_response; +pub use dns_response::DnsResponse; + +mod edns; +pub use edns::{DEFAULT_MAX_PAYLOAD_LEN, Edns, EdnsFlags}; + +mod header; +pub use header::{Flags, Header, HeaderCounts, MessageType, Metadata}; + +mod lower_query; +pub use lower_query::LowerQuery; + +mod message; +pub use message::{EmitAndCount, Message, emit_message_parts}; + +mod op_code; +pub use op_code::OpCode; + +mod query; +pub use query::Query; + +mod response_code; +pub use response_code::ResponseCode; + +mod serial_message; +pub use serial_message::SerialMessage; + +pub mod update_message; +pub use update_message::UpdateMessage; + +/// Default retry interval floor. This value is somewhat arbitrary, but is based on +/// observed, real-world latencies and offers the chance to send three queries in a +/// second to maximize the chance of a successful response in periods of high packet +/// loss without overwhelming upstream servers. +pub const DEFAULT_RETRY_FLOOR: Duration = Duration::from_millis(333); diff --git a/vendor/hickory-dns/hickory-proto/src/op/op_code.rs b/vendor/hickory-dns/hickory-proto/src/op/op_code.rs new file mode 100644 index 00000000..80b53393 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/op_code.rs @@ -0,0 +1,109 @@ +// Copyright 2015-2021 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Operation code for queries, updates, and responses + +use core::{convert::From, fmt}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Operation code for queries, updates, and responses +/// +/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035) +/// +/// ```text +/// OPCODE A four bit field that specifies kind of query in this +/// message. This value is set by the originator of a query +/// and copied into the response. The values are: +/// +/// 0 a standard query (QUERY) +/// +/// 1 an inverse query (IQUERY) +/// +/// 2 a server status request (STATUS) +/// +/// 3-15 reserved for future use +/// ``` +#[derive(Debug, PartialEq, Eq, PartialOrd, Copy, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[allow(dead_code)] +pub enum OpCode { + /// Query request [RFC 1035](https://tools.ietf.org/html/rfc1035) + Query, + + /// Status message [RFC 1035](https://tools.ietf.org/html/rfc1035) + Status, + + /// Notify of change [RFC 1996](https://tools.ietf.org/html/rfc1996) + Notify, + + /// Update message [RFC 2136](https://tools.ietf.org/html/rfc2136) + Update, + + /// Any other opcode + Unknown(u8), +} + +impl fmt::Display for OpCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Self::Query => f.write_str("QUERY"), + Self::Status => f.write_str("STATUS"), + Self::Notify => f.write_str("NOTIFY"), + Self::Update => f.write_str("UPDATE"), + Self::Unknown(opcode) => write!(f, "Unknown opcode ({opcode})"), + } + } +} + +/// Convert from `OpCode` to `u8` +/// +/// ``` +/// use hickory_proto::op::OpCode; +/// +/// let var: u8 = From::from(OpCode::Query); +/// assert_eq!(0, var); +/// +/// let var: u8 = OpCode::Query.into(); +/// assert_eq!(0, var); +/// ``` +impl From for u8 { + fn from(rt: OpCode) -> Self { + match rt { + OpCode::Query => 0, + // 1 IQuery (Inverse Query, OBSOLETE) [RFC3425] + OpCode::Status => 2, + // 3 Unassigned + OpCode::Notify => 4, + OpCode::Update => 5, + // 6-15 Unassigned + OpCode::Unknown(opcode) => opcode, + } + } +} + +/// Convert from `u8` to `OpCode` +/// +/// ``` +/// use hickory_proto::op::OpCode; +/// +/// let var: OpCode = OpCode::from_u8(0); +/// assert_eq!(OpCode::Query, var); +/// ``` +impl OpCode { + /// Decodes the binary value of the OpCode + pub fn from_u8(value: u8) -> Self { + match value { + 0 => Self::Query, + 2 => Self::Status, + 4 => Self::Notify, + 5 => Self::Update, + _ => Self::Unknown(value), + } + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/op/query.rs b/vendor/hickory-dns/hickory-proto/src/op/query.rs new file mode 100644 index 00000000..f57202fd --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/query.rs @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2015 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Query struct for looking up resource records + +#[cfg(test)] +use alloc::vec::Vec; +use core::fmt::{self, Display, Formatter}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::error::*; +use crate::rr::dns_class::DNSClass; +use crate::rr::domain::Name; +use crate::rr::record_type::RecordType; +use crate::serialize::binary::*; + +#[cfg(feature = "mdns")] +/// From [RFC 6762](https://tools.ietf.org/html/rfc6762#section-5.4) +/// ```text +// To avoid large floods of potentially unnecessary responses in these +// cases, Multicast DNS defines the top bit in the class field of a DNS +// question as the unicast-response bit. +/// ``` +const MDNS_UNICAST_RESPONSE: u16 = 1 << 15; + +/// Query struct for looking up resource records, basically a resource record without RDATA. +/// +/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035) +/// +/// ```text +/// 4.1.2. Question section format +/// +/// The question section is used to carry the "question" in most queries, +/// i.e., the parameters that define what is being asked. The section +/// contains QDCOUNT (usually 1) entries, each of the following format: +/// +/// 1 1 1 1 1 1 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +/// | | +/// / QNAME / ZNAME / +/// / / +/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +/// | QTYPE / ZTYPE | +/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +/// | QCLASS / ZCLASS | +/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +/// +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub struct Query { + /// QNAME + pub name: Name, + + /// QTYPE + pub query_type: RecordType, + + /// QCLASS + pub query_class: DNSClass, + + /// mDNS unicast-response bit set or not + #[cfg(feature = "mdns")] + pub mdns_unicast_response: bool, +} + +impl Default for Query { + /// Return a default query with an empty name and A, IN for the query_type and query_class + fn default() -> Self { + Self { + name: Name::root(), + query_type: RecordType::A, + query_class: DNSClass::IN, + #[cfg(feature = "mdns")] + mdns_unicast_response: false, + } + } +} + +impl Query { + /// Return a default query with an empty name and A, IN for the query_type and query_class + pub fn new() -> Self { + Self::default() + } + + /// Create a new query from name and type, class defaults to IN + #[allow(clippy::self_named_constructors)] + pub fn query(name: Name, query_type: RecordType) -> Self { + Self { + name, + query_type, + query_class: DNSClass::IN, + #[cfg(feature = "mdns")] + mdns_unicast_response: false, + } + } + + /// replaces name with the new name + pub fn set_name(&mut self, name: Name) -> &mut Self { + self.name = name; + self + } + + /// Specify the RecordType being queried + pub fn set_query_type(&mut self, query_type: RecordType) -> &mut Self { + self.query_type = query_type; + self + } + + /// Specify÷ the DNS class of the Query, almost always IN + pub fn set_query_class(&mut self, query_class: DNSClass) -> &mut Self { + self.query_class = query_class; + self + } + + /// Changes mDNS unicast-response bit + /// See [RFC 6762](https://tools.ietf.org/html/rfc6762#section-5.4) + #[cfg(feature = "mdns")] + pub fn set_mdns_unicast_response(&mut self, flag: bool) -> &mut Self { + self.mdns_unicast_response = flag; + self + } + + /// ```text + /// QNAME a domain name represented as a sequence of labels, where + /// each label consists of a length octet followed by that + /// number of octets. The domain name terminates with the + /// zero length octet for the null label of the root. Note + /// that this field may be an odd number of octets; no + /// padding is used. + /// ``` + pub fn name(&self) -> &Name { + &self.name + } + + /// ```text + /// QTYPE a two octet code which specifies the type of the query. + /// The values for this field include all codes valid for a + /// TYPE field, together with some more general codes which + /// can match more than one type of RR. + /// ``` + pub fn query_type(&self) -> RecordType { + self.query_type + } + + /// ```text + /// QCLASS a two octet code that specifies the class of the query. + /// For example, the QCLASS field is IN for the Internet. + /// ``` + pub fn query_class(&self) -> DNSClass { + self.query_class + } + + /// Returns if the mDNS unicast-response bit is set or not + /// See [RFC 6762](https://tools.ietf.org/html/rfc6762#section-5.4) + #[cfg(feature = "mdns")] + pub fn mdns_unicast_response(&self) -> bool { + self.mdns_unicast_response + } +} + +impl BinEncodable for Query { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + self.name.emit(encoder)?; + self.query_type.emit(encoder)?; + + #[cfg(not(feature = "mdns"))] + self.query_class.emit(encoder)?; + + #[cfg(feature = "mdns")] + { + if self.mdns_unicast_response { + encoder.emit_u16(u16::from(self.query_class()) | MDNS_UNICAST_RESPONSE)?; + } else { + self.query_class.emit(encoder)?; + } + } + + Ok(()) + } +} + +impl<'r> BinDecodable<'r> for Query { + fn read(decoder: &mut BinDecoder<'r>) -> Result { + let name = Name::read(decoder)?; + let query_type = RecordType::read(decoder)?; + + #[cfg(feature = "mdns")] + let mut mdns_unicast_response = false; + + #[cfg(not(feature = "mdns"))] + let query_class = DNSClass::read(decoder)?; + + #[cfg(feature = "mdns")] + let query_class = { + let query_class_value = + decoder.read_u16()?.unverified(/*DNSClass::from_u16 will verify the value*/); + if query_class_value & MDNS_UNICAST_RESPONSE > 0 { + mdns_unicast_response = true; + DNSClass::from(query_class_value & !MDNS_UNICAST_RESPONSE) + } else { + DNSClass::from(query_class_value) + } + }; + + Ok(Self { + name, + query_type, + query_class, + #[cfg(feature = "mdns")] + mdns_unicast_response, + }) + } +} + +impl Display for Query { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + #[cfg(not(feature = "mdns"))] + { + write!( + f, + "{name} {class} {ty}", + name = self.name, + class = self.query_class, + ty = self.query_type, + ) + } + + #[cfg(feature = "mdns")] + { + write!( + f, + "{name} {class} {ty}; mdns_unicast_response: {mdns}", + name = self.name, + class = self.query_class, + ty = self.query_type, + mdns = self.mdns_unicast_response + ) + } + } +} + +#[test] +fn test_read_and_emit() { + let expect = Query { + name: Name::from_ascii("WWW.example.com.").unwrap(), + query_type: RecordType::AAAA, + query_class: DNSClass::IN, + ..Query::default() + }; + + let mut byte_vec: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut byte_vec); + expect.emit(&mut encoder).unwrap(); + } + + let mut decoder = BinDecoder::new(&byte_vec); + let got = Query::read(&mut decoder).unwrap(); + assert_eq!(got, expect); +} + +#[cfg(feature = "mdns")] +#[test] +fn test_mdns_unicast_response_bit_handling() { + const QCLASS_OFFSET: usize = 1 /* empty name */ + + size_of::() /* query_type */; + + let mut query = Query::new(); + query.set_mdns_unicast_response(true); + + let mut vec_bytes: Vec = Vec::with_capacity(512); + { + let mut encoder = BinEncoder::new(&mut vec_bytes); + query.emit(&mut encoder).unwrap(); + + let query_class_slice = encoder.slice_of(QCLASS_OFFSET, QCLASS_OFFSET + 2); + assert_eq!(query_class_slice, &[0x80, 0x01]); + } + + let mut decoder = BinDecoder::new(&vec_bytes); + + let got = Query::read(&mut decoder).unwrap(); + + assert_eq!(got.query_class(), DNSClass::IN); + assert!(got.mdns_unicast_response()); +} diff --git a/vendor/hickory-dns/hickory-proto/src/op/response_code.rs b/vendor/hickory-dns/hickory-proto/src/op/response_code.rs new file mode 100644 index 00000000..938f7ab0 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/response_code.rs @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2015 Benjamin Fry + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// there is not much to format in this file, and we don't want rustfmt to mess up the comments + +//! All defined response codes in DNS + +use core::fmt::{self, Display, Formatter}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// The status code of the response to a query. +/// +/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035) +/// +/// ```text +/// RCODE Response code - this 4 bit field is set as part of +/// responses. The values have the following +/// interpretation: +/// +/// 0 No error condition +/// +/// 1 Format error - The name server was +/// unable to interpret the query. +/// +/// 2 Server failure - The name server was +/// unable to process this query due to a +/// problem with the name server. +/// +/// 3 Name Error - Meaningful only for +/// responses from an authoritative name +/// server, this code signifies that the +/// domain name referenced in the query does +/// not exist. +/// +/// 4 Not Implemented - The name server does +/// not support the requested kind of query. +/// +/// 5 Refused - The name server refuses to +/// perform the specified operation for +/// policy reasons. For example, a name +/// server may not wish to provide the +/// information to the particular requester, +/// or a name server may not wish to perform +/// a particular operation (e.g., zone +/// transfer) for particular data. +/// +/// 6-15 Reserved for future use. +/// ``` +#[derive(Debug, Default, Eq, PartialEq, PartialOrd, Copy, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[allow(dead_code)] +pub enum ResponseCode { + /// No Error [RFC 1035](https://tools.ietf.org/html/rfc1035) + #[default] + NoError, + + /// Format Error [RFC 1035](https://tools.ietf.org/html/rfc1035) + FormErr, + + /// Server Failure [RFC 1035](https://tools.ietf.org/html/rfc1035) + ServFail, + + /// Non-Existent Domain [RFC 1035](https://tools.ietf.org/html/rfc1035) + NXDomain, + + /// Not Implemented [RFC 1035](https://tools.ietf.org/html/rfc1035) + NotImp, + + /// Query Refused [RFC 1035](https://tools.ietf.org/html/rfc1035) + Refused, + + /// Name Exists when it should not [RFC 2136](https://tools.ietf.org/html/rfc2136) + YXDomain, + + /// RR Set Exists when it should not [RFC 2136](https://tools.ietf.org/html/rfc2136) + YXRRSet, + + /// RR Set that should exist does not [RFC 2136](https://tools.ietf.org/html/rfc2136) + NXRRSet, + + /// Server Not Authoritative for zone [RFC 2136](https://tools.ietf.org/html/rfc2136) + /// or Not Authorized [RFC 8945](https://www.rfc-editor.org/rfc/rfc8945) + NotAuth, + + /// Name not contained in zone [RFC 2136](https://tools.ietf.org/html/rfc2136) + NotZone, + + /// Bad OPT Version [RFC 6891](https://tools.ietf.org/html/rfc6891#section-9) + BADVERS, + + /// TSIG Signature Failure [RFC 8945](https://www.rfc-editor.org/rfc/rfc8945) + BADSIG, + + /// Key not recognized [RFC 8945](https://www.rfc-editor.org/rfc/rfc8945) + BADKEY, + + /// Signature out of time window [RFC 8945](https://www.rfc-editor.org/rfc/rfc8945) + BADTIME, + + /// Bad TKEY Mode [RFC 2930](https://tools.ietf.org/html/rfc2930#section-2.6) + BADMODE, + + /// Duplicate key name [RFC 2930](https://tools.ietf.org/html/rfc2930#section-2.6) + BADNAME, + + /// Algorithm not supported [RFC 2930](https://tools.ietf.org/html/rfc2930#section-2.6) + BADALG, + + /// Bad Truncation [RFC 4635](https://tools.ietf.org/html/rfc4635#section-4) + BADTRUNC, + + /// Bad/missing Server Cookie [RFC 7873](https://datatracker.ietf.org/doc/html/rfc7873) + BADCOOKIE, + // 24-3840 Unassigned + // 3841-4095 Reserved for Private Use [RFC6895] + // 4096-65534 Unassigned + // 65535 Reserved, can be allocated by Standards Action [RFC6895] + /// An unknown or unregistered response code was received. + Unknown(u16), +} + +impl ResponseCode { + /// returns the lower 4 bits of the response code (for the original header portion of the code) + pub fn low(self) -> u8 { + (u16::from(self) & 0x000F) as u8 + } + + /// returns the high 8 bits for the EDNS portion of the response code + pub fn high(self) -> u8 { + ((u16::from(self) & 0x0FF0) >> 4) as u8 + } + + /// DNS can not store the entire space of ResponseCodes in 4 bit space of the Header, this function + /// allows for a initial value of the first 4 bits to be set. + /// + /// After the EDNS is read, the entire ResponseCode (12 bits) can be reconstructed for the full ResponseCode. + pub fn from_low(low: u8) -> Self { + ((u16::from(low)) & 0x000F).into() + } + + /// Combines the EDNS high and low from the Header to produce the Extended ResponseCode + pub fn from(high: u8, low: u8) -> Self { + ((u16::from(high) << 4) | ((u16::from(low)) & 0x000F)).into() + } + + /// Transforms the response code into the human message + pub fn to_str(self) -> &'static str { + match self { + Self::NoError => "No Error", + Self::FormErr => "Form Error", // 1 FormErr Format Error [RFC1035] + Self::ServFail => "Server Failure", // 2 ServFail Server Failure [RFC1035] + Self::NXDomain => "Non-Existent Domain", // 3 NXDomain Non-Existent Domain [RFC1035] + Self::NotImp => "Not Implemented", // 4 NotImp Not Implemented [RFC1035] + Self::Refused => "Query Refused", // 5 Refused Query Refused [RFC1035] + Self::YXDomain => "Name should not exist", // 6 YXDomain Name Exists when it should not [RFC2136][RFC6672] + Self::YXRRSet => "RR Set should not exist", // 7 YXRRSet RR Set Exists when it should not [RFC2136] + Self::NXRRSet => "RR Set does not exist", // 8 NXRRSet RR Set that should exist does not [RFC2136] + Self::NotAuth => "Not authorized", // 9 NotAuth Server Not Authoritative for zone [RFC2136] + Self::NotZone => "Name not in zone", // 10 NotZone Name not contained in zone [RFC2136] + Self::BADVERS => "Bad option versions", // 16 BADVERS Bad OPT Version [RFC6891] + Self::BADSIG => "TSIG Failure", // 16 BADSIG TSIG Signature Failure [RFC2845] + Self::BADKEY => "Key not recognized", // 17 BADKEY Key not recognized [RFC2845] + Self::BADTIME => "Signature out of time window", // 18 BADTIME Signature out of time window [RFC2845] + Self::BADMODE => "Bad TKEY mode", // 19 BADMODE Bad TKEY Mode [RFC2930] + Self::BADNAME => "Duplicate key name", // 20 BADNAME Duplicate key name [RFC2930] + Self::BADALG => "Algorithm not supported", // 21 BADALG Algorithm not supported [RFC2930] + Self::BADTRUNC => "Bad truncation", // 22 BADTRUNC Bad Truncation [RFC4635] + Self::BADCOOKIE => "Bad server cookie", // 23 BADCOOKIE Bad/missing Server Cookie [RFC7873] + Self::Unknown(_) => "Unknown response code", + } + } +} + +impl Display for ResponseCode { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str(self.to_str()) + } +} + +/// Convert from `ResponseCode` to `u16` +/// +/// ``` +/// use hickory_proto::op::ResponseCode; +/// +/// let var: ResponseCode = From::from(0); +/// assert_eq!(ResponseCode::NoError, var); +/// +/// let var: ResponseCode = 0.into(); +/// assert_eq!(ResponseCode::NoError, var); +/// ``` +impl From for u16 { + fn from(rt: ResponseCode) -> Self { + match rt { + ResponseCode::NoError => 0, // 0 NoError No Error [RFC1035] + ResponseCode::FormErr => 1, // 1 FormErr Format Error [RFC1035] + ResponseCode::ServFail => 2, // 2 ServFail Server Failure [RFC1035] + ResponseCode::NXDomain => 3, // 3 NXDomain Non-Existent Domain [RFC1035] + ResponseCode::NotImp => 4, // 4 NotImp Not Implemented [RFC1035] + ResponseCode::Refused => 5, // 5 Refused Query Refused [RFC1035] + ResponseCode::YXDomain => 6, // 6 YXDomain Name Exists when it should not [RFC2136][RFC6672] + ResponseCode::YXRRSet => 7, // 7 YXRRSet RR Set Exists when it should not [RFC2136] + ResponseCode::NXRRSet => 8, // 8 NXRRSet RR Set that should exist does not [RFC2136] + ResponseCode::NotAuth => 9, // 9 NotAuth Server Not Authoritative for zone [RFC2136] + ResponseCode::NotZone => 10, // 10 NotZone Name not contained in zone [RFC2136] + // + // 11-15 Unassigned + // + // 16 BADVERS Bad OPT Version [RFC6891] + // 16 BADSIG TSIG Signature Failure [RFC2845] + ResponseCode::BADVERS | ResponseCode::BADSIG => 16, + ResponseCode::BADKEY => 17, // 17 BADKEY Key not recognized [RFC2845] + ResponseCode::BADTIME => 18, // 18 BADTIME Signature out of time window [RFC2845] + ResponseCode::BADMODE => 19, // 19 BADMODE Bad TKEY Mode [RFC2930] + ResponseCode::BADNAME => 20, // 20 BADNAME Duplicate key name [RFC2930] + ResponseCode::BADALG => 21, // 21 BADALG Algorithm not supported [RFC2930] + ResponseCode::BADTRUNC => 22, // 22 BADTRUNC Bad Truncation [RFC4635] + ResponseCode::BADCOOKIE => 23, // 23 BADCOOKIE Bad/missing Server Cookie [RFC7873] + ResponseCode::Unknown(code) => code, + } + } +} + +/// Convert from `u16` to `ResponseCode` +/// +/// ``` +/// use hickory_proto::op::ResponseCode; +/// +/// let var: u16 = From::from(ResponseCode::NoError); +/// assert_eq!(0, var); +/// +/// let var: u16 = ResponseCode::NoError.into(); +/// assert_eq!(0, var); +/// ``` +impl From for ResponseCode { + fn from(value: u16) -> Self { + match value { + 0 => Self::NoError, // 0 NoError No Error [RFC1035] + 1 => Self::FormErr, // 1 FormErr Format Error [RFC1035] + 2 => Self::ServFail, // 2 ServFail Server Failure [RFC1035] + 3 => Self::NXDomain, // 3 NXDomain Non-Existent Domain [RFC1035] + 4 => Self::NotImp, // 4 NotImp Not Implemented [RFC1035] + 5 => Self::Refused, // 5 Refused Query Refused [RFC1035] + 6 => Self::YXDomain, // 6 YXDomain Name Exists when it should not [RFC2136][RFC6672] + 7 => Self::YXRRSet, // 7 YXRRSet RR Set Exists when it should not [RFC2136] + 8 => Self::NXRRSet, // 8 NXRRSet RR Set that should exist does not [RFC2136] + 9 => Self::NotAuth, // 9 NotAuth Server Not Authoritative for zone [RFC2136] + 10 => Self::NotZone, // 10 NotZone Name not contained in zone [RFC2136] + // this looks to be backwards compat for 4 bit ResponseCodes. + // 16 BADVERS Bad OPT Version [RFC6891] + // 16 => ResponseCode::BADVERS, + 16 => Self::BADSIG, // 16 BADSIG TSIG Signature Failure [RFC2845] + 17 => Self::BADKEY, // 17 BADKEY Key not recognized [RFC2845] + 18 => Self::BADTIME, // 18 BADTIME Signature out of time window [RFC2845] + 19 => Self::BADMODE, // 19 BADMODE Bad TKEY Mode [RFC2930] + 20 => Self::BADNAME, // 20 BADNAME Duplicate key name [RFC2930] + 21 => Self::BADALG, // 21 BADALG Algorithm not supported [RFC2930] + 22 => Self::BADTRUNC, // 22 BADTRUNC Bad Truncation [RFC4635] + 23 => Self::BADCOOKIE, // 23 BADCOOKIE Bad/missing Server Cookie [RFC7873] + code => Self::Unknown(code), + } + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/op/serial_message.rs b/vendor/hickory-dns/hickory-proto/src/op/serial_message.rs new file mode 100644 index 00000000..1efbfac6 --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/serial_message.rs @@ -0,0 +1,64 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use alloc::vec::Vec; +use core::net::SocketAddr; + +use crate::op::Message; +use crate::serialize::binary::DecodeError; + +/// A DNS message in serialized form, with either the target address or source address +pub struct SerialMessage { + // TODO: change to Bytes? this would be more compatible with some underlying libraries + message: Vec, + addr: SocketAddr, +} + +impl SerialMessage { + /// Construct a new SerialMessage and the source or destination address + pub fn new(message: Vec, addr: SocketAddr) -> Self { + Self { message, addr } + } + + /// Get a reference to the bytes + pub fn bytes(&self) -> &[u8] { + &self.message + } + + /// Get the source or destination address (context dependent) + pub fn addr(&self) -> SocketAddr { + self.addr + } + + /// Unwrap the Bytes and address + pub fn into_parts(self) -> (Vec, SocketAddr) { + self.into() + } + + /// Build a `SerialMessage` from some bytes and an address + pub fn from_parts(message: Vec, addr: SocketAddr) -> Self { + (message, addr).into() + } + + /// Deserializes the inner data into a Message + pub fn to_message(&self) -> Result { + Message::from_vec(&self.message) + } +} + +impl From<(Vec, SocketAddr)> for SerialMessage { + fn from((message, addr): (Vec, SocketAddr)) -> Self { + Self { message, addr } + } +} + +impl From for (Vec, SocketAddr) { + fn from(msg: SerialMessage) -> Self { + let SerialMessage { message, addr } = msg; + (message, addr) + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/op/update_message.rs b/vendor/hickory-dns/hickory-proto/src/op/update_message.rs new file mode 100644 index 00000000..c5467f5b --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/op/update_message.rs @@ -0,0 +1,593 @@ +// Copyright 2015-2017 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Update related operations for Messages + +use core::fmt::Debug; + +#[cfg(any(feature = "std", feature = "no-std-rand"))] +use crate::{ + op::{Edns, OpCode, edns::DEFAULT_MAX_PAYLOAD_LEN}, + rr::{DNSClass, Name, RData, RecordSet, RecordType, rdata::SOA}, +}; +use crate::{ + op::{Message, Query}, + rr::{Record, rdata::TSIG}, +}; + +/// To reduce errors in using the Message struct as an Update, this will do the call throughs +/// to properly do that. +/// +/// Generally rather than constructing this by hand, see the update methods on `Client` +pub trait UpdateMessage: Debug { + /// see `Header::id` + fn id(&self) -> u16; + + /// Adds the zone section, i.e. name.example.com would be example.com + fn add_zone(&mut self, query: Query); + + /// Add the pre-requisite records + /// + /// These must exist, or not, for the Update request to go through. + fn add_pre_requisite(&mut self, record: Record); + + /// Add all the Records from the Iterator to the pre-requisites section + fn add_pre_requisites(&mut self, records: R) + where + R: IntoIterator, + I: Iterator; + + /// Add the Record to be updated + fn add_update(&mut self, record: Record); + + /// Add the Records from the Iterator to the updates section + fn add_updates(&mut self, records: R) + where + R: IntoIterator, + I: Iterator; + + /// Add Records to the additional Section of the UpdateMessage + fn add_additional(&mut self, record: Record); + + /// Returns the Zones to be updated, generally should only be one. + fn zones(&self) -> &[Query]; + + /// Returns the pre-requisites + fn prerequisites(&self) -> &[Record]; + + /// Returns the records to be updated + fn updates(&self) -> &[Record]; + + /// Returns the additional records + fn additionals(&self) -> &[Record]; + + /// Return the message's signature (if any) + /// + /// This is used to authenticate update messages. + fn signature(&self) -> Option<&Record>; +} + +/// to reduce errors in using the Message struct as an Update, this will do the call throughs +/// to properly do that. +impl UpdateMessage for Message { + fn id(&self) -> u16 { + self.metadata.id + } + + fn add_zone(&mut self, query: Query) { + self.add_query(query); + } + + fn add_pre_requisite(&mut self, record: Record) { + self.add_answer(record); + } + + fn add_pre_requisites(&mut self, records: R) + where + R: IntoIterator, + I: Iterator, + { + self.add_answers(records); + } + + fn add_update(&mut self, record: Record) { + self.add_authority(record); + } + + fn add_updates(&mut self, records: R) + where + R: IntoIterator, + I: Iterator, + { + self.add_authorities(records); + } + + fn add_additional(&mut self, record: Record) { + self.add_additional(record); + } + + fn zones(&self) -> &[Query] { + &self.queries + } + + fn prerequisites(&self) -> &[Record] { + &self.answers + } + + fn updates(&self) -> &[Record] { + &self.authorities + } + + fn additionals(&self) -> &[Record] { + &self.additionals + } + + fn signature(&self) -> Option<&Record> { + self.signature() + } +} + +/// Sends a record to create on the server, this will fail if the record exists (atomicity +/// depends on the server) +/// +/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 +/// +/// ```text +/// 2.4.3 - RRset Does Not Exist +/// +/// No RRs with a specified NAME and TYPE (in the zone and class denoted +/// by the Zone Section) can exist. +/// +/// For this prerequisite, a requestor adds to the section a single RR +/// whose NAME and TYPE are equal to that of the RRset whose nonexistence +/// is required. The RDLENGTH of this record is zero (0), and RDATA +/// field is therefore empty. CLASS must be specified as NONE in order +/// to distinguish this condition from a valid RR whose RDLENGTH is +/// naturally zero (0) (for example, the NULL RR). TTL must be specified +/// as zero (0). +/// +/// 2.5.1 - Add To An RRset +/// +/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH +/// and RDATA are those being added, and CLASS is the same as the zone +/// class. Any duplicate RRs will be silently ignored by the Primary +/// Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `rrset` - the record(s) to create +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection) +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn create(rrset: RecordSet, zone_origin: Name, use_edns: bool) -> Message { + // TODO: assert non-empty rrset? + assert!(zone_origin.zone_of(rrset.name())); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(rrset.dns_class()) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + let mut prerequisite = Record::update0(rrset.name().clone(), 0, rrset.record_type()); + prerequisite.dns_class = DNSClass::NONE; + message.add_pre_requisite(prerequisite.into_record_of_rdata()); + message.add_updates(rrset); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +/// Appends a record to an existing rrset, optionally require the rrset to exist (atomicity +/// depends on the server) +/// +/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 +/// +/// ```text +/// 2.4.1 - RRset Exists (Value Independent) +/// +/// At least one RR with a specified NAME and TYPE (in the zone and class +/// specified in the Zone Section) must exist. +/// +/// For this prerequisite, a requestor adds to the section a single RR +/// whose NAME and TYPE are equal to that of the zone RRset whose +/// existence is required. RDLENGTH is zero and RDATA is therefore +/// empty. CLASS must be specified as ANY to differentiate this +/// condition from that of an actual RR whose RDLENGTH is naturally zero +/// (0) (e.g., NULL). TTL is specified as zero (0). +/// +/// 2.5.1 - Add To An RRset +/// +/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH +/// and RDATA are those being added, and CLASS is the same as the zone +/// class. Any duplicate RRs will be silently ignored by the Primary +/// Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `rrset` - the record(s) to append to an RRSet +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `must_exist` - if true, the request will fail if the record does not exist +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection). If +/// the rrset does not exist and must_exist is false, then the RRSet will be created. +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn append(rrset: RecordSet, zone_origin: Name, must_exist: bool, use_edns: bool) -> Message { + assert!(zone_origin.zone_of(rrset.name())); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(rrset.dns_class()) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + if must_exist { + let mut prerequisite = Record::update0(rrset.name().clone(), 0, rrset.record_type()); + prerequisite.dns_class = DNSClass::ANY; + message.add_pre_requisite(prerequisite.into_record_of_rdata()); + } + + message.add_updates(rrset); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +/// Compares and if it matches, swaps it for the new value (atomicity depends on the server) +/// +/// ```text +/// 2.4.2 - RRset Exists (Value Dependent) +/// +/// A set of RRs with a specified NAME and TYPE exists and has the same +/// members with the same RDATAs as the RRset specified here in this +/// section. While RRset ordering is undefined and therefore not +/// significant to this comparison, the sets be identical in their +/// extent. +/// +/// For this prerequisite, a requestor adds to the section an entire +/// RRset whose preexistence is required. NAME and TYPE are that of the +/// RRset being denoted. CLASS is that of the zone. TTL must be +/// specified as zero (0) and is ignored when comparing RRsets for +/// identity. +/// +/// 2.5.4 - Delete An RR From An RRset +/// +/// RRs to be deleted are added to the Update Section. The NAME, TYPE, +/// RDLENGTH and RDATA must match the RR being deleted. TTL must be +/// specified as zero (0) and will otherwise be ignored by the Primary +/// Zone Server. CLASS must be specified as NONE to distinguish this from an +/// RR addition. If no such RRs exist, then this Update RR will be +/// silently ignored by the Primary Zone Server. +/// +/// 2.5.1 - Add To An RRset +/// +/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH +/// and RDATA are those being added, and CLASS is the same as the zone +/// class. Any duplicate RRs will be silently ignored by the Primary +/// Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `current` - the current rrset which must exist for the swap to complete +/// * `new` - the new rrset with which to replace the current rrset +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection). +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn compare_and_swap( + current: RecordSet, + new: RecordSet, + zone_origin: Name, + use_edns: bool, +) -> Message { + assert!(zone_origin.zone_of(current.name())); + assert!(zone_origin.zone_of(new.name())); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(new.dns_class()) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + // make sure the record is what is expected + let mut prerequisite = current.clone(); + prerequisite.set_ttl(0); + message.add_pre_requisites(prerequisite); + + // add the delete for the old record + let mut delete = current; + // the class must be none for delete + delete.set_dns_class(DNSClass::NONE); + // the TTL should be 0 + delete.set_ttl(0); + message.add_updates(delete); + + // insert the new record... + message.add_updates(new); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +/// Deletes a record (by rdata) from an rrset +/// +/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 +/// +/// ```text +/// 2.5.4 - Delete An RR From An RRset +/// +/// RRs to be deleted are added to the Update Section. The NAME, TYPE, +/// RDLENGTH and RDATA must match the RR being deleted. TTL must be +/// specified as zero (0) and will otherwise be ignored by the Primary +/// Zone Server. CLASS must be specified as NONE to distinguish this from an +/// RR addition. If no such RRs exist, then this Update RR will be +/// silently ignored by the Primary Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `rrset` - the record(s) to delete from a RRSet, the name, type and rdata must match the +/// record to delete +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection). +/// If no such RRs exist, then this Update RR will be silently ignored by the Primary Zone Server. +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn delete_by_rdata(mut rrset: RecordSet, zone_origin: Name, use_edns: bool) -> Message { + assert!(zone_origin.zone_of(rrset.name())); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(rrset.dns_class()) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + // the class must be none to delete a record + rrset.set_dns_class(DNSClass::NONE); + // the TTL should be 0 + rrset.set_ttl(0); + message.add_updates(rrset); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +/// Deletes an entire rrset +/// +/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 +/// +/// ```text +/// 2.5.2 - Delete An RRset +/// +/// One RR is added to the Update Section whose NAME and TYPE are those +/// of the RRset to be deleted. TTL must be specified as zero (0) and is +/// otherwise not used by the Primary Zone Server. CLASS must be specified as +/// ANY. RDLENGTH must be zero (0) and RDATA must therefore be empty. +/// If no such RRset exists, then this Update RR will be silently ignored +/// by the Primary Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `record` - The name, class and record_type will be used to match and delete the RecordSet +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection). +/// If no such RRset exists, then this Update RR will be silently ignored by the Primary Zone Server. +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn delete_rrset(mut record: Record, zone_origin: Name, use_edns: bool) -> Message { + assert!(zone_origin.zone_of(&record.name)); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(record.dns_class) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + // the class must be any to delete an rrset + record.dns_class = DNSClass::ANY; + // the TTL should be 0 + record.ttl = 0; + // the rdata must be null to delete an rrset + record.data = RData::Update0(record.record_type()); + message.add_update(record); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +/// Deletes all rrsets at the specified name +/// +/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 +/// +/// ```text +/// 2.5.3 - Delete All RRsets From A Name +/// +/// One RR is added to the Update Section whose NAME is that of the name +/// to be cleansed of RRsets. TYPE must be specified as ANY. TTL must +/// be specified as zero (0) and is otherwise not used by the Primary +/// Zone Server. CLASS must be specified as ANY. RDLENGTH must be zero (0) +/// and RDATA must therefore be empty. If no such RRsets exist, then +/// this Update RR will be silently ignored by the Primary Zone Server. +/// ``` +/// +/// # Arguments +/// +/// * `name_of_records` - the name of all the record sets to delete +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `dns_class` - the class of the SOA +/// * `use_edns` - if true, EDNS options will be added to the request +/// +/// The update must go to a zone authority (i.e. the server used in the ClientConnection). This +/// operation attempts to delete all resource record sets at the specified name, regardless of +/// the record type. +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn delete_all( + name_of_records: Name, + zone_origin: Name, + dns_class: DNSClass, + use_edns: bool, +) -> Message { + assert!(zone_origin.zone_of(&name_of_records)); + + // for updates, the query section is used for the zone + let mut zone: Query = Query::new(); + zone.set_name(zone_origin) + .set_query_class(dns_class) + .set_query_type(RecordType::SOA); + + // build the message + let mut message = Message::query(); + message.metadata.op_code = OpCode::Update; + message.metadata.recursion_desired = false; + message.add_zone(zone); + + // the TTL should be 0 + // the rdata must be null to delete all rrsets + // the record type must be any + let mut record = Record::update0(name_of_records, 0, RecordType::ANY); + + // the class must be any to delete all rrsets + record.dns_class = DNSClass::ANY; + + message.add_update(record.into_record_of_rdata()); + + // Extended dns + if use_edns { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} + +// not an update per-se, but it fits nicely with other functions here +/// Download all records from a zone, or all records modified since given SOA was observed. +/// The request will either be a AXFR Query (ask for full zone transfer) if a SOA was not +/// provided, or a IXFR Query (incremental zone transfer) if a SOA was provided. +/// +/// # Arguments +/// * `zone_origin` - the zone name to update, i.e. SOA name +/// * `last_soa` - the last SOA known, if any. If provided, name must match `zone_origin` +#[cfg(any(feature = "std", feature = "no-std-rand"))] +pub fn zone_transfer(zone_origin: Name, last_soa: Option) -> Message { + if let Some(soa) = &last_soa { + assert_eq!(zone_origin, soa.mname); + } + + let mut zone: Query = Query::new(); + zone.set_name(zone_origin).set_query_class(DNSClass::IN); + if last_soa.is_some() { + zone.set_query_type(RecordType::IXFR); + } else { + zone.set_query_type(RecordType::AXFR); + } + + // build the message + let mut message = Message::query(); + message.metadata.recursion_desired = false; + message.add_zone(zone); + + if let Some(soa) = last_soa { + // for IXFR, old SOA is put as authority to indicate last known version + let record = Record::from_rdata(soa.mname.clone(), 0, RData::SOA(soa)); + message.add_authority(record); + } + + // Extended dns + { + message + .edns + .get_or_insert_with(Edns::new) + .set_max_payload(DEFAULT_MAX_PAYLOAD_LEN) + .set_version(0); + } + + message +} diff --git a/vendor/hickory-dns/hickory-proto/src/rr/dns_class.rs b/vendor/hickory-dns/hickory-proto/src/rr/dns_class.rs new file mode 100644 index 00000000..d312477e --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/rr/dns_class.rs @@ -0,0 +1,214 @@ +// Copyright 2015-2017 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! class of DNS operations, in general always IN for internet +#![allow(clippy::use_self)] + +use alloc::borrow::ToOwned; +use core::{ + cmp::Ordering, + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::error::*; +use crate::serialize::binary::*; + +/// The DNS Record class +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +#[allow(dead_code)] +pub enum DNSClass { + /// Internet + IN, + /// Chaos + CH, + /// Hesiod + HS, + /// QCLASS NONE + NONE, + /// QCLASS * (ANY) + ANY, + /// Special class for OPT Version, it was overloaded for EDNS - RFC 6891 + /// From the RFC: `Values lower than 512 MUST be treated as equal to 512` + OPT(u16), + /// Unknown DNSClass was parsed + Unknown(u16), +} + +impl FromStr for DNSClass { + type Err = DecodeError; + + /// Convert from `&str` to `DNSClass` + /// + /// ``` + /// use std::str::FromStr; + /// use hickory_proto::rr::DNSClass; + /// + /// let var: DNSClass = DNSClass::from_str("IN").unwrap(); + /// assert_eq!(DNSClass::IN, var); + /// ``` + fn from_str(str: &str) -> Result { + match str { + "IN" => Ok(Self::IN), + "CH" => Ok(Self::CH), + "HS" => Ok(Self::HS), + "NONE" => Ok(Self::NONE), + "ANY" | "*" => Ok(Self::ANY), + _ => Err(DecodeError::UnknownDnsClassStr(str.to_owned())), + } + } +} + +impl DNSClass { + /// Return the OPT version from value + pub fn for_opt(value: u16) -> Self { + // From RFC 6891: `Values lower than 512 MUST be treated as equal to 512` + let value = value.max(512); + Self::OPT(value) + } +} + +impl BinEncodable for DNSClass { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + encoder.emit_u16((*self).into()) + } +} + +impl BinDecodable<'_> for DNSClass { + fn read(decoder: &mut BinDecoder<'_>) -> Result { + let this = Self::from( + decoder.read_u16()?.unverified(/*DNSClass is verified as safe in processing this*/), + ); + + Ok(this) + } +} + +// TODO make these a macro or annotation + +/// Convert from `DNSClass` to `&str` +/// +/// ``` +/// use hickory_proto::rr::DNSClass; +/// +/// let var: &'static str = DNSClass::IN.into(); +/// assert_eq!("IN", var); +/// ``` +impl From for &'static str { + fn from(rt: DNSClass) -> &'static str { + match rt { + DNSClass::IN => "IN", + DNSClass::CH => "CH", + DNSClass::HS => "HS", + DNSClass::NONE => "NONE", + DNSClass::ANY => "ANY", + DNSClass::OPT(_) => "OPT", + DNSClass::Unknown(_) => "UNKNOWN", + } + } +} + +/// Convert from `u16` to `DNSClass` +/// +/// ``` +/// use hickory_proto::rr::DNSClass; +/// +/// let var: DNSClass = 1u16.into(); +/// assert_eq!(DNSClass::IN, var); +/// ``` +impl From for DNSClass { + fn from(value: u16) -> Self { + match value { + 1 => Self::IN, + 3 => Self::CH, + 4 => Self::HS, + 254 => Self::NONE, + 255 => Self::ANY, + _ => Self::Unknown(value), + } + } +} + +/// Convert from `DNSClass` to `u16` +/// +/// ``` +/// use hickory_proto::rr::DNSClass; +/// +/// let var: u16 = DNSClass::IN.into(); +/// assert_eq!(1, var); +/// ``` +impl From for u16 { + fn from(rt: DNSClass) -> Self { + match rt { + DNSClass::IN => 1, + DNSClass::CH => 3, + DNSClass::HS => 4, + DNSClass::NONE => 254, + DNSClass::ANY => 255, + // see https://tools.ietf.org/html/rfc6891#section-6.1.2 + DNSClass::OPT(max_payload_len) => max_payload_len.max(512), + DNSClass::Unknown(unknown) => unknown, + } + } +} + +impl PartialOrd for DNSClass { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for DNSClass { + fn cmp(&self, other: &Self) -> Ordering { + u16::from(*self).cmp(&u16::from(*other)) + } +} + +impl Display for DNSClass { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str(Into::<&str>::into(*self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_order() { + let ordered = vec![ + DNSClass::IN, + DNSClass::CH, + DNSClass::HS, + DNSClass::NONE, + DNSClass::ANY, + ]; + let mut unordered = vec![ + DNSClass::NONE, + DNSClass::HS, + DNSClass::CH, + DNSClass::IN, + DNSClass::ANY, + ]; + + unordered.sort(); + + assert_eq!(unordered, ordered); + } + + #[test] + fn check_dns_class_parse_wont_panic_with_symbols() { + let dns_class = "a-b-c".to_ascii_uppercase().parse::(); + assert!(matches!( + &dns_class, + Err(DecodeError::UnknownDnsClassStr(_)) + )); + } +} diff --git a/vendor/hickory-dns/hickory-proto/src/rr/domain/label.rs b/vendor/hickory-dns/hickory-proto/src/rr/domain/label.rs new file mode 100644 index 00000000..7fee915d --- /dev/null +++ b/vendor/hickory-dns/hickory-proto/src/rr/domain/label.rs @@ -0,0 +1,595 @@ +// Copyright 2015-2018 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! Labels are used as the internal components of a Name. +//! +//! A label is stored internally as ascii, where all unicode characters are converted to punycode internally. + +use alloc::{string::String, vec::Vec}; +use core::borrow::Borrow; +use core::cmp::{Ordering, PartialEq}; +use core::fmt::{self, Debug, Display, Formatter, Write}; +use core::hash::{Hash, Hasher}; + +use idna::uts46::{AsciiDenyList, DnsLength, Hyphens, Uts46}; +use tinyvec::TinyVec; +use tracing::debug; + +use crate::error::*; +use crate::serialize::binary::DecodeError; + +const WILDCARD: &[u8] = b"*"; +const IDNA_PREFIX: &[u8] = b"xn--"; + +/// Labels are always stored as ASCII, unicode characters must be encoded with punycode +#[derive(Clone, Eq)] +pub struct Label(TinyVec<[u8; 24]>); + +impl Label { + /// These must only be ASCII, with unicode encoded to PunyCode, or other such transformation. + /// + /// This uses the bytes as raw ascii values, with nothing escaped on the wire. + /// Generally users should use `from_str` or `from_ascii` + pub fn from_raw_bytes(bytes: &[u8]) -> ProtoResult { + // Check for label validity. + // RFC 2181, Section 11 "Name Syntax". + // > The length of any one label is limited to between 1 and 63 octets. + if bytes.is_empty() { + return Err("Label requires a minimum length of 1".into()); + } + if bytes.len() > 63 { + return Err(DecodeError::LabelBytesTooLong(bytes.len()).into()); + }; + Ok(Self(TinyVec::from(bytes))) + } + + /// Translates this string into IDNA safe name, encoding to punycode as necessary. + pub fn from_utf8(s: &str) -> ProtoResult { + if s.as_bytes() == WILDCARD { + return Ok(Self::wildcard()); + } + + // special case for SRV type records + if s.starts_with('_') { + return Self::from_ascii(s); + } + + // length don't exceeding 63 is done in `from_ascii` + // on puny encoded string + // idna error are opaque so early failure is not possible. + match Uts46::new().to_ascii( + s.as_bytes(), + AsciiDenyList::STD3, + Hyphens::Allow, + DnsLength::Ignore, + ) { + Ok(puny) => Self::from_ascii(&puny), + e => Err(format!("Label contains invalid characters: {e:?}").into()), + } + } + + /// Takes the ascii string and returns a new label. + /// + /// This will return an Error if the label is not an ascii string + pub fn from_ascii(s: &str) -> ProtoResult { + if s.len() > 63 { + return Err(DecodeError::LabelBytesTooLong(s.len()).into()); + } + + if s.as_bytes() == WILDCARD { + return Ok(Self::wildcard()); + } + + if !s.is_empty() + && s.is_ascii() + && s.chars().take(1).all(|c| is_safe_ascii(c, true, false)) + && s.chars().skip(1).all(|c| is_safe_ascii(c, false, false)) + { + Self::from_raw_bytes(s.as_bytes()) + } else { + Err(format!("Malformed label: {s}").into()) + } + } + + /// Returns a new Label of the Wildcard, i.e. "*" + pub fn wildcard() -> Self { + Self(TinyVec::from(WILDCARD)) + } + + /// Converts this label to lowercase + pub fn to_lowercase(&self) -> Self { + // TODO: replace case conversion when (ascii_ctype #39658) stabilizes + if let Some((idx, _)) = self + .0 + .iter() + .enumerate() + .find(|&(_, c)| *c != c.to_ascii_lowercase()) + { + let mut lower_label: Vec = self.0.to_vec(); + lower_label[idx..].make_ascii_lowercase(); + Self(TinyVec::from(lower_label.as_slice())) + } else { + self.clone() + } + } + + /// Returns true if this label is the wildcard, '*', label + pub fn is_wildcard(&self) -> bool { + self.as_bytes() == WILDCARD + } + + /// Returns the length in bytes of this label + pub fn len(&self) -> usize { + self.0.len() + } + + /// True if the label contains no characters + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the raw bytes of the label, this is good for writing to the wire. + /// + /// See [`Display`] for presentation version (unescaped from punycode, etc) + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Performs the equivalence operation disregarding case + pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool { + self.0.eq_ignore_ascii_case(&other.0) + } + + /// compares with the other label, ignoring case + pub fn cmp_with_f(&self, other: &Self) -> Ordering { + let s = self.0.iter(); + let o = other.0.iter(); + + for (s, o) in s.zip(o) { + match F::cmp_u8(*s, *o) { + Ordering::Equal => continue, + not_eq => return not_eq, + } + } + + self.0.len().cmp(&other.0.len()) + } + + /// Performs the conversion to utf8 from IDNA as necessary, see `fmt` for more details + pub fn to_utf8(&self) -> String { + format!("{self}") + } + + /// Converts this label to safe ascii, escaping characters as necessary + /// + /// If this is an IDNA, punycode, label, then the xn-- prefix will be maintained as ascii + pub fn to_ascii(&self) -> String { + let mut ascii = String::with_capacity(self.as_bytes().len()); + + self.write_ascii(&mut ascii) + .expect("should never fail to write a new string"); + ascii + } + + /// Writes this label to safe ascii, escaping characters as necessary + pub fn write_ascii(&self, f: &mut W) -> Result<(), fmt::Error> { + // We can't guarantee that the same input will always translate to the same output + fn escape_non_ascii( + byte: u8, + f: &mut W, + is_first: bool, + ) -> Result<(), fmt::Error> { + let to_triple_escape = |ch: u8| format!("\\{ch:03o}"); + let to_single_escape = |ch: char| format!("\\{ch}"); + + match char::from(byte) { + c if is_safe_ascii(c, is_first, true) => f.write_char(c)?, + // it's not a control and is printable as well as inside the standard ascii range + c if byte > b'\x20' && byte < b'\x7f' => f.write_str(&to_single_escape(c))?, + _ => f.write_str(&to_triple_escape(byte))?, + } + + Ok(()) + } + + // traditional ascii case... + let mut chars = self.as_bytes().iter(); + if let Some(ch) = chars.next() { + escape_non_ascii(*ch, f, true)?; + } + + for ch in chars { + escape_non_ascii(*ch, f, false)?; + } + + Ok(()) + } +} + +impl AsRef<[u8]> for Label { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl Borrow<[u8]> for Label { + fn borrow(&self) -> &[u8] { + &self.0 + } +} + +fn is_safe_ascii(c: char, is_first: bool, for_encoding: bool) -> bool { + match c { + c if !c.is_ascii() => false, + c if c.is_alphanumeric() => true, + '-' if !is_first => true, // dash is allowed + '_' => true, // SRV like labels + '*' if is_first => true, // wildcard + '.' if !for_encoding => true, // needed to allow dots, for things like email addresses + _ => false, + } +} + +impl Display for Label { + /// outputs characters in a safe string manner. + /// + /// if the string is punycode, i.e. starts with `xn--`, otherwise it translates to a safe ascii string + /// escaping characters as necessary. + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + if self.as_bytes().starts_with(IDNA_PREFIX) { + // this should never be outside the ascii codes... + let label = String::from_utf8_lossy(self.borrow()); + let (label, e) = + Uts46::new().to_unicode(label.as_bytes(), AsciiDenyList::EMPTY, Hyphens::Allow); + + if e.is_ok() { + return f.write_str(&label); + } else { + debug!( + "xn-- prefixed string did not translate via IDNA properly: {:?}", + e + ) + } + } + + // it wasn't known to be utf8 + self.write_ascii(f) + } +} + +impl Debug for Label { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + let label = String::from_utf8_lossy(self.borrow()); + f.write_str(&label) + } +} + +impl PartialEq for Label { + fn eq(&self, other: &Self) -> bool { + self.eq_ignore_ascii_case(other) + } +} + +impl PartialOrd for Label { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Label { + fn cmp(&self, other: &Self) -> Ordering { + self.cmp_with_f::(other) + } +} + +impl Hash for Label { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + // Hash the length of the label by hashing a slice of ZSTs. Doing this ensures we use + // `Hasher::write_length_prefix()`, which has different domain separation behavior. + // Constructing a slice of ZSTs lets us use this method via existing standard library + // implementations, without needing to rely on nightly features. This does not allocate, + // since `()` is a ZST. + let vec = vec![(); self.len()]; + vec.hash(state); + // Hash the label contents in canonical form. + for b in self.borrow() as &[u8] { + state.write_u8(b.to_ascii_lowercase()); + } + } +} + +/// Label comparison trait for case sensitive or insensitive comparisons +pub trait LabelCmp { + /// this should mimic the cmp method from [`PartialOrd`] + fn cmp_u8(l: u8, r: u8) -> Ordering; +} + +/// For case sensitive comparisons +pub(super) struct CaseSensitive; + +impl LabelCmp for CaseSensitive { + fn cmp_u8(l: u8, r: u8) -> Ordering { + l.cmp(&r) + } +} + +/// For case insensitive comparisons +pub(super) struct CaseInsensitive; + +impl LabelCmp for CaseInsensitive { + fn cmp_u8(l: u8, r: u8) -> Ordering { + l.to_ascii_lowercase().cmp(&r.to_ascii_lowercase()) + } +} + +/// Conversion into a Label +pub trait IntoLabel: Sized { + /// Convert this into Label + fn into_label(self) -> ProtoResult