hickory-server-0.25.2/.cargo_vcs_info.json0000644000000001530000000000100140750ustar { "git": { "sha1": "527c9f470a418cf6b92da902ea0aaa5749963d59" }, "path_in_vcs": "crates/server" }hickory-server-0.25.2/Cargo.lock0000644000002047560000000000100120670ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy 0.7.35", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-trait" version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", "zeroize", ] [[package]] name = "aws-lc-sys" version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", ] [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "bindgen" version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools", "lazy_static", "lazycell", "log", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn", "which", ] [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "cmake" version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "core-foundation" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fallible-iterator" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generator" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" dependencies = [ "cfg-if", "libc", "log", "rustversion", "windows", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "h3" version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfb059a4f28a66f186ed16ad912d142f490676acba59353831d7cb45a96b0d3" dependencies = [ "bytes", "fastrand", "futures-util", "http", "pin-project-lite", "tokio", ] [[package]] name = "h3-quinn" version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d482318ae94198fc8e3cbb0b7ba3099c865d744e6ec7c62039ca7b6b6c66fbf" dependencies = [ "bytes", "futures", "h3", "quinn", "tokio", "tokio-util", ] [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "foldhash", ] [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ "hashbrown", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hickory-proto" version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "aws-lc-rs", "backtrace", "bitflags", "bytes", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", "h2", "h3", "h3-quinn", "http", "idna", "ipnet", "once_cell", "pin-project-lite", "quinn", "rand", "ring", "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "thiserror 2.0.12", "time", "tinyvec", "tokio", "tokio-rustls", "tracing", "url", "webpki-roots", ] [[package]] name = "hickory-recursor" version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9ec7f3d578a10e8f67a30b3668a324b49c2583e47f04b69ebb32fb735fee41" dependencies = [ "async-recursion", "async-trait", "bytes", "cfg-if", "enum-as-inner", "futures-util", "hickory-proto", "hickory-resolver", "ipnet", "lru-cache", "parking_lot", "prefix-trie", "serde", "thiserror 2.0.12", "tokio", "tracing", ] [[package]] name = "hickory-resolver" version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", "moka", "once_cell", "parking_lot", "quinn", "rand", "resolv-conf", "rustls", "serde", "smallvec", "thiserror 2.0.12", "tokio", "tokio-rustls", "tracing", "webpki-roots", ] [[package]] name = "hickory-server" version = "0.25.2" dependencies = [ "async-trait", "bytes", "cfg-if", "data-encoding", "enum-as-inner", "futures-executor", "futures-util", "h2", "h3", "h3-quinn", "hickory-proto", "hickory-recursor", "hickory-resolver", "http", "ipnet", "metrics", "prefix-trie", "rusqlite", "rustls", "serde", "thiserror 2.0.12", "time", "tokio", "tokio-rustls", "tokio-util", "toml", "tracing", "tracing-subscriber", ] [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", "winreg", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" dependencies = [ "serde", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", "combine", "jni-sys", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ "getrandom 0.3.2", "libc", ] [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.48.5", ] [[package]] name = "libsqlite3-sys" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" dependencies = [ "cc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ "cfg-if", "generator", "scoped-tls", "tracing", "tracing-subscriber", ] [[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ "linked-hash-map", ] [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "metrics" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" dependencies = [ "ahash", "portable-atomic", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "moka" version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "loom", "parking_lot", "portable-atomic", "rustc_version", "smallvec", "tagptr", "thiserror 1.0.69", "uuid", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" dependencies = [ "critical-section", "portable-atomic", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy 0.8.25", ] [[package]] name = "prefix-trie" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20" dependencies = [ "ipnet", "num-traits", ] [[package]] name = "prettyplease" version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", "cfg_aliases", "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", "rustls", "socket2", "thiserror 2.0.12", "tokio", "tracing", "web-time", ] [[package]] name = "quinn-proto" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.2", "rand", "ring", "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", "thiserror 2.0.12", "tinyvec", "tracing", "web-time", ] [[package]] name = "quinn-udp" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", "windows-sys 0.52.0", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.2", ] [[package]] name = "redox_syscall" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resolv-conf" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "rusqlite" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", "time", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] [[package]] name = "rustls-platform-verifier" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4937d110d34408e9e5ad30ba0b0ca3b6a8a390f8db3636db60144ac4fa792750" dependencies = [ "core-foundation", "core-foundation-sys", "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.52.0", ] [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", "untrusted 0.9.0", ] [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tagptr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "toml_write", "winnow", ] [[package]] name = "toml_write" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom 0.3.2", ] [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-time" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-root-certs" version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c99403924bc5f23afefc319b8ac67ed0e50669f6e52a413314cccb1fdbc93ba0" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" dependencies = [ "rustls-pki-types", ] [[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", "home", "once_cell", "rustix", ] [[package]] name = "widestring" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ "windows-core", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", "windows-result", "windows-strings", "windows-targets 0.52.6", ] [[package]] name = "windows-implement" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-result" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive 0.8.25", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerocopy-derive" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn", ] hickory-server-0.25.2/Cargo.toml0000644000000137600000000000100121030ustar # 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.71.1" name = "hickory-server" version = "0.25.2" authors = ["The contributors to Hickory DNS"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ Hickory DNS is a safe and secure DNS server with DNSSEC support. Eventually this could be a replacement for BIND9. The DNSSEC support allows for live signing of all records, in it does not currently support records signed offline. The server supports dynamic DNS with SIG0 authenticated requests. Hickory DNS is based on the Tokio and Futures libraries, which means it should be easily integrated into other software that also use those libraries. """ homepage = "https://hickory-dns.org/" documentation = "https://docs.rs/hickory-server" readme = "README.md" keywords = [ "DNS", "BIND", "dig", "named", "dnssec", ] categories = ["network-programming"] license = "MIT OR Apache-2.0" repository = "https://github.com/hickory-dns/hickory-dns" [package.metadata.cargo-all-features] denylist = [ "__tls", "__https", "__quic", "__h3", "__dnssec", ] max_combination_size = 2 skip_optional_dependencies = true [package.metadata.docs.rs] all-features = true default-target = "x86_64-unknown-linux-gnu" rustdoc-args = [ "--cfg", "docsrs", ] targets = [ "x86_64-apple-darwin", "x86_64-pc-windows-msvc", ] [features] __dnssec = [] __h3 = [ "dep:h3", "dep:h3-quinn", "__quic", ] __https = [ "dep:h2", "dep:http", "__tls", ] __quic = ["__tls"] __tls = [ "dep:rustls", "dep:tokio-rustls", ] backtrace = ["hickory-proto/backtrace"] blocklist = ["resolver"] dnssec-aws-lc-rs = [ "hickory-proto/dnssec-aws-lc-rs", "hickory-recursor?/dnssec-aws-lc-rs", "hickory-resolver?/dnssec-aws-lc-rs", "serde/rc", "__dnssec", ] dnssec-ring = [ "hickory-proto/dnssec-ring", "hickory-recursor?/dnssec-ring", "hickory-resolver?/dnssec-ring", "serde/rc", "__dnssec", ] h3-aws-lc-rs = [ "hickory-proto/h3-aws-lc-rs", "hickory-resolver?/h3-aws-lc-rs", "quic-aws-lc-rs", "__h3", ] h3-ring = [ "hickory-proto/h3-ring", "hickory-resolver?/h3-ring", "quic-ring", "__h3", ] https-aws-lc-rs = [ "hickory-proto/https-aws-lc-rs", "hickory-resolver?/https-aws-lc-rs", "tls-aws-lc-rs", "__https", ] https-ring = [ "hickory-proto/https-ring", "hickory-resolver?/https-ring", "tls-ring", "__https", ] metrics = ["dep:metrics"] quic-aws-lc-rs = [ "hickory-proto/quic-aws-lc-rs", "hickory-resolver?/quic-aws-lc-rs", "tls-aws-lc-rs", "__quic", ] quic-ring = [ "hickory-proto/quic-ring", "hickory-resolver?/quic-ring", "tls-ring", "__quic", ] recursor = [ "dep:hickory-recursor", "dep:hickory-resolver", ] resolver = ["dep:hickory-resolver"] rustls-platform-verifier = ["hickory-resolver?/rustls-platform-verifier"] sqlite = ["rusqlite"] testing = [] tls-aws-lc-rs = [ "hickory-proto/tls-aws-lc-rs", "hickory-resolver?/tls-aws-lc-rs", "__tls", ] tls-ring = [ "hickory-proto/tls-ring", "hickory-resolver?/tls-ring", "__tls", ] toml = ["dep:toml"] webpki-roots = ["hickory-resolver?/webpki-roots"] [lib] name = "hickory_server" path = "src/lib.rs" [dependencies.async-trait] version = "0.1.43" [dependencies.bytes] version = "1" [dependencies.cfg-if] version = "1" [dependencies.data-encoding] version = "2.2.0" default-features = false [dependencies.enum-as-inner] version = "0.6" [dependencies.futures-util] version = "0.3.5" features = ["std"] default-features = false [dependencies.h2] version = "0.4.0" features = ["stream"] optional = true [dependencies.h3] version = "0.0.7" optional = true [dependencies.h3-quinn] version = "0.0.9" optional = true [dependencies.hickory-proto] version = "0.25" features = [ "std", "serde", "text-parsing", "tokio", ] default-features = false [dependencies.hickory-recursor] version = "0.25" features = ["serde"] optional = true default-features = false [dependencies.hickory-resolver] version = "0.25" features = [ "serde", "system-config", "tokio", ] optional = true default-features = false [dependencies.http] version = "1.1" optional = true [dependencies.ipnet] version = "2.3.0" features = [ "serde", "std", ] default-features = false [dependencies.metrics] version = "0.24.1" optional = true [dependencies.prefix-trie] version = "0.7" [dependencies.rusqlite] version = "0.35" features = [ "bundled", "time", ] optional = true [dependencies.rustls] version = "0.23.23" features = [ "logging", "std", "tls12", ] optional = true default-features = false [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.thiserror] version = "2" default-features = false [dependencies.time] version = "0.3" [dependencies.tokio] version = "1.21" features = [ "macros", "net", "sync", ] [dependencies.tokio-rustls] version = "0.26" optional = true default-features = false [dependencies.tokio-util] version = "0.7.9" [dependencies.toml] version = "0.8.14" optional = true [dependencies.tracing] version = "0.1.30" default-features = false [dev-dependencies.futures-executor] version = "0.3.5" features = ["std"] default-features = false [dev-dependencies.tokio] version = "1.21" features = [ "macros", "rt", ] [dev-dependencies.tracing-subscriber] version = "0.3" features = [ "env-filter", "fmt", "std", ] default-features = false [lints.rust.unexpected_cfgs] level = "warn" priority = 0 check-cfg = ["cfg(nightly)"] hickory-server-0.25.2/Cargo.toml.orig000064400000000000000000000115121046102023000155550ustar 00000000000000[package] name = "hickory-server" # A short blurb about the package. This is not rendered in any format when # uploaded to crates.io (aka this is not markdown) description = """ Hickory DNS is a safe and secure DNS server with DNSSEC support. Eventually this could be a replacement for BIND9. The DNSSEC support allows for live signing of all records, in it does not currently support records signed offline. The server supports dynamic DNS with SIG0 authenticated requests. Hickory DNS is based on the Tokio and Futures libraries, which means it should be easily integrated into other software that also use those libraries. """ # These URLs point to more information about the repository documentation = "https://docs.rs/hickory-server" # This points to a file in the repository (relative to this Cargo.toml). The # contents of this file are stored and indexed in the registry. readme = "README.md" version.workspace = true authors.workspace = true edition.workspace = true rust-version.workspace = true homepage.workspace = true repository.workspace = true keywords.workspace = true categories.workspace = true license.workspace = true [features] backtrace = ["hickory-proto/backtrace"] # Recursive Resolution is Experimental! recursor = ["dep:hickory-recursor", "dep:hickory-resolver"] resolver = ["dep:hickory-resolver"] sqlite = ["rusqlite"] blocklist = ["resolver"] toml = ["dep:toml"] metrics = ["dep:metrics"] tls-aws-lc-rs = [ "hickory-proto/tls-aws-lc-rs", "hickory-resolver?/tls-aws-lc-rs", "__tls", ] https-aws-lc-rs = [ "hickory-proto/https-aws-lc-rs", "hickory-resolver?/https-aws-lc-rs", "tls-aws-lc-rs", "__https", ] quic-aws-lc-rs = [ "hickory-proto/quic-aws-lc-rs", "hickory-resolver?/quic-aws-lc-rs", "tls-aws-lc-rs", "__quic", ] h3-aws-lc-rs = [ "hickory-proto/h3-aws-lc-rs", "hickory-resolver?/h3-aws-lc-rs", "quic-aws-lc-rs", "__h3", ] tls-ring = [ "hickory-proto/tls-ring", "hickory-resolver?/tls-ring", "__tls", ] https-ring = [ "hickory-proto/https-ring", "hickory-resolver?/https-ring", "tls-ring", "__https", ] quic-ring = [ "hickory-proto/quic-ring", "hickory-resolver?/quic-ring", "tls-ring", "__quic", ] h3-ring = [ "hickory-proto/h3-ring", "hickory-resolver?/h3-ring", "quic-ring", "__h3", ] __tls = ["dep:rustls", "dep:tokio-rustls"] __https = ["dep:h2", "dep:http", "__tls"] __quic = ["__tls"] __h3 = ["dep:h3", "dep:h3-quinn", "__quic"] dnssec-aws-lc-rs = ["hickory-proto/dnssec-aws-lc-rs", "hickory-recursor?/dnssec-aws-lc-rs", "hickory-resolver?/dnssec-aws-lc-rs", "serde/rc", "__dnssec"] dnssec-ring = ["hickory-proto/dnssec-ring", "hickory-recursor?/dnssec-ring", "hickory-resolver?/dnssec-ring", "serde/rc", "__dnssec"] __dnssec = [] webpki-roots = ["hickory-resolver?/webpki-roots"] rustls-platform-verifier = ["hickory-resolver?/rustls-platform-verifier"] testing = [] [lib] name = "hickory_server" path = "src/lib.rs" [dependencies] async-trait.workspace = true toml = { workspace = true, optional = true } bytes.workspace = true cfg-if.workspace = true data-encoding.workspace = true enum-as-inner.workspace = true futures-util = { workspace = true, default-features = false, features = ["std"] } h2 = { workspace = true, features = ["stream"], optional = true } h3 = { workspace = true, optional = true } h3-quinn = { workspace = true, optional = true } http = { workspace = true, optional = true } ipnet = { workspace = true, features = ["serde", "std"] } prefix-trie.workspace = true rusqlite = { workspace = true, features = ["bundled", "time"], optional = true } rustls = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } thiserror.workspace = true time.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["macros", "net", "sync"] } tokio-rustls = { workspace = true, optional = true } tokio-util.workspace = true hickory-proto = { workspace = true, features = ["serde", "text-parsing", "tokio"] } hickory-recursor = { workspace = true, features = ["serde"], optional = true } hickory-resolver = { workspace = true, features = ["serde", "system-config", "tokio"], optional = true } metrics = { workspace = true, optional = true } [dev-dependencies] futures-executor = { workspace = true, default-features = false, features = ["std"] } test-support.workspace = true tokio = { workspace = true, features = ["macros", "rt"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "std"] } [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"] [lints] workspace = true [package.metadata.cargo-all-features] skip_optional_dependencies = true max_combination_size = 2 denylist = ["__tls", "__https", "__quic", "__h3", "__dnssec"] hickory-server-0.25.2/LICENSE-APACHE000064400000000000000000000261401046102023000146150ustar 00000000000000 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. hickory-server-0.25.2/LICENSE-MIT000064400000000000000000000021151046102023000143210ustar 00000000000000Copyright (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. hickory-server-0.25.2/README.md000064400000000000000000000072121046102023000141470ustar 00000000000000# Overview Hickory DNS Server is a library which implements the zone authoritory functionality. This library contains basic implementations for DNS zone hosting. It is capable of performing signing all records in the zone for server DNSSEC RRSIG records associated with all records in a zone. There is also a `hickory-dns` binary that can be generated from the library with `cargo install hickory-dns`. Dynamic updates are supported via `SIG0` (an mTLS authentication method is under development). **NOTICE** This project was rebranded from Trust-DNS to Hickory DNS and has been moved to the https://github.com/hickory-dns/hickory-dns organization and repo, this crate/binary has been moved to [hickory-server](https://crates.io/crates/hickory-server), from `0.24` and onward, for prior versions see [trust-dns-server](https://crates.io/crates/trust-dns-server). ## Status The server code is complete, the daemon supports IPv4 and IPv6, UDP and TCP. There currently is no way to limit TCP and AXFR operations, so it is still not recommended to put into production as TCP can be used to DOS the service. Zone file parsing is complete and supported. There is currently no forking option, and the server is not yet threaded (although it is implemented with async IO, so threading may not be a huge benefit). There is still a lot of work to do before a server can be trusted with this externally. Running it behind a firewall on a private network would be safe. Zone signing support is complete, to insert a key store a pem encoded rsa file in the same directory as the initial zone file with the `.key` suffix. _Note_: this must be only readable by the current user. If one is not present one will be created and written to the correct location. This also acts as the initial key for dynamic update SIG(0) validation. To get the public key, the `DNSKEY` record for the zone can be queried. This is needed to provide to other upstream servers to create the `DS` key. Dynamic DNS is also complete, if enabled, a journal file will be stored next to the zone file with the `jrnl` suffix. _Note_: if the key is changed or updated, it is currently the operators responsibility to remove the only public key from the zone, this allows for the `DNSKEY` to exist for some unspecified period of time during key rotation. Rotating the key while online is not currently supported, so a restart of the server process is required. ## Features - Dynamic Update with sqlite journaling backend (SIG0) - DNSSEC online signing (NSEC and NSEC3) - DNS over TLS (DoT) - DNS over HTTPS (DoH) - Forwarding stub resolver - ANAME resolution, for zone mapping aliass to A and AAAA records - Additionals section generation for aliasing record types ## Future goals - Distributed dynamic DNS updates, with consensus - mTLS based authorization for Dynamic Updates - Online NSEC creation for queries - Full hint based resolving - Maybe NSEC5 support ## Minimum Rust Version The current minimum rustc version for this project is `1.70` ## Versioning Hickory DNS does it's best job to follow semver. Hickory DNS will be promoted to 1.0 upon stabilization of the publicly exposed APIs. This does not mean that Hickory DNS will necessarily break on upgrades between 0.x updates. Whenever possible, old APIs will be deprecated with notes on what replaced those deprecations. Hickory DNS will make a best effort to never break software which depends on it due to API changes, though this can not be guaranteed. Deprecated interfaces will be maintained for at minimum one major release after that in which they were deprecated (where possible), with the exception of the upgrade to 1.0 where all deprecated interfaces will be planned to be removed. hickory-server-0.25.2/src/access.rs000064400000000000000000000143431046102023000152710ustar 00000000000000use std::net::IpAddr; use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use prefix_trie::{Prefix, PrefixSet}; /// Type to evaluate access from a source address for accessing the server. /// /// Allowed networks will override denied networks, i.e. if a network is allowed, the deny rules will not be evaluated. /// Allowed networks are processed in the context of denied networks, that is, if there are no denied networks, then /// the allowed list will effectively deny access to anything that's not in the allowed list. On the other hand, if /// denied networks are specified, then allowed networks will only apply if the deny rule matched, but otherwise the /// address will be allowed. #[derive(Default)] pub(crate) struct AccessControl { ipv4: InnerAccessControl, ipv6: InnerAccessControl, } impl AccessControl { /// Insert a new network that is denied access to the server pub(crate) fn insert_deny(&mut self, networks: &[IpNet]) { for network in networks { match network { IpNet::V4(v4) => { self.ipv4.deny.insert(*v4); } IpNet::V6(v6) => { self.ipv6.deny.insert(*v6); } } } } /// Insert a new network that is allowed access to the server pub(crate) fn insert_allow(&mut self, networks: &[IpNet]) { for network in networks { match network { IpNet::V4(v4) => { self.ipv4.allow.insert(*v4); } IpNet::V6(v6) => { self.ipv6.allow.insert(*v6); } } } } /// Evaluate the IP address against the allowed networks /// /// # Arguments /// /// * `ip` - source ip address to evaluate /// /// # Return /// /// Ok if access is granted, Err otherwise #[must_use] pub(crate) fn allow(&self, ip: IpAddr) -> bool { match ip { IpAddr::V4(v4) => { let v4 = Ipv4Net::from(v4); self.ipv4.allow(&v4) } IpAddr::V6(v6) => { let v6 = Ipv6Net::from(v6); self.ipv6.allow(&v6) } } } } #[derive(Default)] struct InnerAccessControl { allow: PrefixSet, deny: PrefixSet, } impl InnerAccessControl { /// Evaluate the IP address against the allowed networks /// /// This allows for generic evaluation over IPv4 or IPv6 address spaces /// /// # Arguments /// /// * `ip` - source ip address to evaluate /// /// # Return /// /// Ok if access is granted, Err otherwise #[must_use] fn allow(&self, ip: &I) -> bool { // If there are no allows or denies specified, we will always default to allow. // Allows without denies always translate to deny all except those in the allow list. // Denies without allows only deny those in the specified deny list. // If there are both allow and deny lists, then the deny list takes precedent with the allow list // overriding the deny if it is more specific. match (self.deny.get_lpm(ip), self.allow.get_lpm(ip)) { (Some(denied), Some(allowed)) => allowed.prefix_len() > denied.prefix_len(), (Some(_denied), None) => false, (None, Some(_allowed)) => true, (None, None) => match ( self.deny.iter().next().is_some(), self.allow.iter().next().is_some(), ) { (true, _) => true, // there are deny entries, but this isn't one (false, true) => false, // there are only allow entries, but this isn't one (false, false) => true, // there are no entries }, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_none() { let access = AccessControl::default(); assert!(access.allow("192.168.1.1".parse().unwrap())); assert!(access.allow("fd00::1".parse().unwrap())); } #[test] fn test_v4() { let mut access = AccessControl::default(); access.insert_allow(&["192.168.1.0/24".parse().unwrap()]); assert!(access.allow("192.168.1.1".parse().unwrap())); assert!(access.allow("192.168.1.255".parse().unwrap())); assert!(!access.allow("192.168.2.1".parse().unwrap())); assert!(!access.allow("192.168.0.0".parse().unwrap())); } #[test] fn test_v6() { let mut access = AccessControl::default(); access.insert_allow(&["fd00::/120".parse().unwrap()]); assert!(access.allow("fd00::1".parse().unwrap())); assert!(access.allow("fd00::00ff".parse().unwrap())); assert!(!access.allow("fd00::ffff".parse().unwrap())); assert!(!access.allow("fd00::1:1".parse().unwrap())); } #[test] fn test_deny_v4() { let mut access = AccessControl::default(); access.insert_deny(&["192.168.1.0/24".parse().unwrap()]); assert!(!access.allow("192.168.1.1".parse().unwrap())); assert!(!access.allow("192.168.1.255".parse().unwrap())); assert!(access.allow("192.168.2.1".parse().unwrap())); assert!(access.allow("192.168.0.0".parse().unwrap())); } #[test] fn test_deny_v6() { let mut access = AccessControl::default(); access.insert_deny(&["fd00::/120".parse().unwrap()]); assert!(!access.allow("fd00::1".parse().unwrap())); assert!(!access.allow("fd00::00ff".parse().unwrap())); assert!(access.allow("fd00::ffff".parse().unwrap())); assert!(access.allow("fd00::1:1".parse().unwrap())); } #[test] fn test_deny_allow_v4() { let mut access = AccessControl::default(); access.insert_deny(&["192.168.0.0/16".parse().unwrap()]); access.insert_allow(&["192.168.1.0/24".parse().unwrap()]); assert!(access.allow("192.168.1.1".parse().unwrap())); assert!(access.allow("192.168.1.255".parse().unwrap())); assert!(!access.allow("192.168.2.1".parse().unwrap())); assert!(!access.allow("192.168.0.0".parse().unwrap())); // but all other networks should be allowed assert!(access.allow("10.0.0.1".parse().unwrap())); } } hickory-server-0.25.2/src/authority/auth_lookup.rs000064400000000000000000000300561046102023000204110ustar 00000000000000// 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. use std::iter::Chain; use std::slice::Iter; use std::sync::Arc; use cfg_if::cfg_if; use crate::authority::{LookupObject, LookupOptions}; use crate::proto::rr::{LowerName, Record, RecordSet, RecordType, RrsetRecords}; /// The result of a lookup on an Authority /// /// # Lifetimes /// /// * `'c` - the catalogue lifetime /// * `'r` - the recordset lifetime, subset of 'c /// * `'q` - the queries lifetime #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum AuthLookup { /// No records Empty, // TODO: change the result of a lookup to a set of chained iterators... /// Records Records { /// Authoritative answers answers: LookupRecords, /// Optional set of LookupRecords additionals: Option, }, /// Soa only differs from Records in that the lifetime on the name is from the authority, and not the query SOA(LookupRecords), /// An axfr starts with soa, chained to all the records, then another soa... AXFR { /// The first SOA record in an AXFR response start_soa: LookupRecords, /// The records to return records: LookupRecords, /// The last SOA record of an AXFR (matches the first) end_soa: LookupRecords, }, } impl AuthLookup { /// Construct an answer with additional section pub fn answers(answers: LookupRecords, additionals: Option) -> Self { Self::Records { answers, additionals, } } /// Returns true if either the associated Records are empty, or this is a NameExists or NxDomain pub fn is_empty(&self) -> bool { // TODO: this needs to be cheap self.was_empty() } /// This is an NxDomain or NameExists, and has no associated records /// /// this consumes the iterator, and verifies it is empty pub fn was_empty(&self) -> bool { self.iter().count() == 0 } /// Conversion to an iterator pub fn iter(&self) -> AuthLookupIter<'_> { self.into_iter() } /// Does not panic, but will return no records if it is not of that type pub fn unwrap_records(self) -> LookupRecords { match self { // TODO: this is ugly, what about the additionals? Self::Records { answers, .. } => answers, _ => LookupRecords::default(), } } /// Takes the additional records, leaving behind None pub fn take_additionals(&mut self) -> Option { match self { Self::Records { additionals, .. } => additionals.take(), _ => None, } } } impl LookupObject for AuthLookup { fn is_empty(&self) -> bool { Self::is_empty(self) } fn iter<'a>(&'a self) -> Box + Send + 'a> { let boxed_iter = Self::iter(self); Box::new(boxed_iter) } fn take_additionals(&mut self) -> Option> { let additionals = Self::take_additionals(self); additionals.map(|a| Box::new(a) as Box) } } impl Default for AuthLookup { fn default() -> Self { Self::Empty } } impl<'a> IntoIterator for &'a AuthLookup { type Item = &'a Record; type IntoIter = AuthLookupIter<'a>; fn into_iter(self) -> Self::IntoIter { match self { AuthLookup::Empty => AuthLookupIter::Empty, // TODO: what about the additionals? is IntoIterator a bad idea? AuthLookup::Records { answers: r, .. } | AuthLookup::SOA(r) => { AuthLookupIter::Records(r.into_iter()) } AuthLookup::AXFR { start_soa, records, end_soa, } => AuthLookupIter::AXFR(start_soa.into_iter().chain(records).chain(end_soa)), } } } /// An iterator over an Authority Lookup #[allow(clippy::large_enum_variant)] #[derive(Default)] pub enum AuthLookupIter<'r> { /// The empty set #[default] Empty, /// An iteration over a set of Records Records(LookupRecordsIter<'r>), /// An iteration over an AXFR AXFR(Chain, LookupRecordsIter<'r>>, LookupRecordsIter<'r>>), } impl<'r> Iterator for AuthLookupIter<'r> { type Item = &'r Record; fn next(&mut self) -> Option { match self { AuthLookupIter::Empty => None, AuthLookupIter::Records(i) => i.next(), AuthLookupIter::AXFR(i) => i.next(), } } } impl From for AuthLookup { fn from(lookup: LookupRecords) -> Self { Self::Records { answers: lookup, additionals: None, } } } /// An iterator over an ANY query for Records. /// /// The length of this result cannot be known without consuming the iterator. /// /// # Lifetimes /// /// * `'r` - the record_set's lifetime, from the catalog /// * `'q` - the lifetime of the query/request #[derive(Debug)] pub struct AnyRecords { lookup_options: LookupOptions, rrsets: Vec>, query_type: RecordType, query_name: LowerName, } impl AnyRecords { /// construct a new lookup of any set of records pub fn new( lookup_options: LookupOptions, // TODO: potentially very expensive rrsets: Vec>, query_type: RecordType, query_name: LowerName, ) -> Self { Self { lookup_options, rrsets, query_type, query_name, } } fn iter(&self) -> AnyRecordsIter<'_> { self.into_iter() } } impl<'r> IntoIterator for &'r AnyRecords { type Item = &'r Record; type IntoIter = AnyRecordsIter<'r>; fn into_iter(self) -> Self::IntoIter { AnyRecordsIter { lookup_options: self.lookup_options, // TODO: potentially very expensive rrsets: self.rrsets.iter(), rrset: None, records: None, query_type: self.query_type, query_name: &self.query_name, } } } /// An iteration over a lookup for any Records #[allow(unused)] pub struct AnyRecordsIter<'r> { lookup_options: LookupOptions, rrsets: Iter<'r, Arc>, rrset: Option<&'r RecordSet>, records: Option>, query_type: RecordType, query_name: &'r LowerName, } impl<'r> Iterator for AnyRecordsIter<'r> { type Item = &'r Record; fn next(&mut self) -> Option { use std::borrow::Borrow; let query_type = self.query_type; let query_name = self.query_name; loop { if let Some(records) = &mut self.records { let record = records .by_ref() .filter(|rr_set| { query_type == RecordType::ANY || rr_set.record_type() != RecordType::SOA }) .find(|rr_set| { query_type == RecordType::AXFR || &LowerName::from(rr_set.name()) == query_name }); if record.is_some() { return record; } } self.rrset = self.rrsets.next().map(Borrow::borrow); // if there are no more RecordSets, then return self.rrset?; // getting here, we must have exhausted our records from the rrset cfg_if! { if #[cfg(feature = "__dnssec")] { self.records = Some( self.rrset .expect("rrset should not be None at this point") .records(self.lookup_options.dnssec_ok()), ); } else { self.records = Some(self.rrset.expect("rrset should not be None at this point").records_without_rrsigs()); } } } } } /// The result of a lookup #[derive(Debug)] pub enum LookupRecords { /// The empty set of records Empty, /// The associate records Records { /// LookupOptions for the request, e.g. dnssec lookup_options: LookupOptions, /// the records found based on the query records: Arc, }, /// Vec of disjoint record sets ManyRecords(LookupOptions, Vec>), // TODO: need a better option for very large zone xfrs... /// A generic lookup response where anything is desired AnyRecords(AnyRecords), } impl LookupRecords { /// Construct a new LookupRecords pub fn new(lookup_options: LookupOptions, records: Arc) -> Self { Self::Records { lookup_options, records, } } /// Construct a new LookupRecords over a set of RecordSets pub fn many(lookup_options: LookupOptions, mut records: Vec>) -> Self { // we're reversing the records because they are output in reverse order, via pop() records.reverse(); Self::ManyRecords(lookup_options, records) } /// This is an NxDomain or NameExists, and has no associated records /// /// this consumes the iterator, and verifies it is empty pub fn was_empty(&self) -> bool { self.iter().count() == 0 } /// Conversion to an iterator pub fn iter(&self) -> LookupRecordsIter<'_> { self.into_iter() } } impl Default for LookupRecords { fn default() -> Self { Self::Empty } } impl<'a> IntoIterator for &'a LookupRecords { type Item = &'a Record; type IntoIter = LookupRecordsIter<'a>; #[allow(unused_variables)] fn into_iter(self) -> Self::IntoIter { match self { LookupRecords::Empty => LookupRecordsIter::Empty, LookupRecords::Records { lookup_options, records, } => LookupRecordsIter::RecordsIter(lookup_options.rrset_with_rrigs(records)), LookupRecords::ManyRecords(lookup_options, r) => LookupRecordsIter::ManyRecordsIter( r.iter() .map(|r| lookup_options.rrset_with_rrigs(r)) .collect(), None, ), LookupRecords::AnyRecords(r) => LookupRecordsIter::AnyRecordsIter(r.iter()), } } } /// Iterator over lookup records #[derive(Default)] pub enum LookupRecordsIter<'r> { /// An iteration over batch record type results AnyRecordsIter(AnyRecordsIter<'r>), /// An iteration over a single RecordSet RecordsIter(RrsetRecords<'r>), /// An iteration over many rrsets ManyRecordsIter(Vec>, Option>), /// An empty set #[default] Empty, } impl<'r> Iterator for LookupRecordsIter<'r> { type Item = &'r Record; fn next(&mut self) -> Option { match self { LookupRecordsIter::Empty => None, LookupRecordsIter::AnyRecordsIter(current) => current.next(), LookupRecordsIter::RecordsIter(current) => current.next(), LookupRecordsIter::ManyRecordsIter(set, current) => loop { if let Some(o) = current.as_mut().and_then(Iterator::next) { return Some(o); } *current = set.pop(); if current.is_none() { return None; } }, } } } impl From for LookupRecords { fn from(rrset_records: AnyRecords) -> Self { Self::AnyRecords(rrset_records) } } impl LookupObject for LookupRecords { fn is_empty(&self) -> bool { Self::was_empty(self) } fn iter<'a>(&'a self) -> Box + Send + 'a> { Box::new(self.iter()) } fn take_additionals(&mut self) -> Option> { None } } hickory-server-0.25.2/src/authority/authority.rs000064400000000000000000000415711046102023000201130ustar 00000000000000// 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. //! All authority related types use cfg_if::cfg_if; use std::fmt; use crate::{ authority::{LookupError, LookupObject, MessageRequest, UpdateResult, ZoneType}, proto::rr::{LowerName, RecordSet, RecordType, RrsetRecords}, server::RequestInfo, }; #[cfg(feature = "__dnssec")] use crate::{ dnssec::NxProofKind, proto::{ ProtoError, dnssec::{DnsSecResult, Nsec3HashAlgorithm, SigSigner, crypto::Digest, rdata::key::KEY}, rr::Name, }, }; /// LookupOptions that specify different options from the client to include or exclude various records in the response. /// /// For example, `dnssec_ok` (DO) will include `RRSIG` in the response. #[derive(Clone, Copy, Debug, Default)] pub struct LookupOptions { dnssec_ok: bool, } /// Lookup Options for the request to the authority impl LookupOptions { /// Return a new LookupOptions #[cfg(feature = "__dnssec")] pub fn for_dnssec(dnssec_ok: bool) -> Self { Self { dnssec_ok } } /// Specify that this lookup should return DNSSEC related records as well, e.g. RRSIG #[allow(clippy::needless_update)] pub fn set_dnssec_ok(self, val: bool) -> Self { Self { dnssec_ok: val, ..self } } /// If true this lookup should return DNSSEC related records as well, e.g. RRSIG pub fn dnssec_ok(&self) -> bool { self.dnssec_ok } /// Returns the rrset's records with or without RRSIGs, depending on the DO flag. pub fn rrset_with_rrigs<'r>(&self, record_set: &'r RecordSet) -> RrsetRecords<'r> { cfg_if! { if #[cfg(feature = "__dnssec")] { record_set.records(self.dnssec_ok()) } else { record_set.records_without_rrsigs() } } } } /// Authority implementations can be used with a `Catalog` #[async_trait::async_trait] pub trait Authority: Send + Sync { /// Result of a lookup type Lookup: Send + Sync + Sized + 'static; /// What type is this zone fn zone_type(&self) -> ZoneType; /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool; /// Whether the authority can perform DNSSEC validation fn can_validate_dnssec(&self) -> bool { false } /// Perform a dynamic update of a zone async fn update(&self, update: &MessageRequest) -> UpdateResult; /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName; /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow; /// Consulting lookup for all Resource Records matching the given `Name` and `RecordType`. /// This will be called in a chained authority configuration after an authority in the chain /// has returned a lookup with a LookupControlFlow::Continue action. Every other authority in /// the chain will be called via this consult method, until one either returns a /// LookupControlFlow::Break action, or all authorities have been consulted. The authority that /// generated the primary lookup (the one returned via 'lookup') will not be consulted. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// * `last_result` - The lookup returned by a previous authority in a chained configuration. /// If a subsequent authority does not modify this lookup, it will be returned /// to the client after consulting all authorities in the chain. /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. This can /// be the same last_result that was passed in, or a new lookup, depending on the logic of the /// authority in question. async fn consult( &self, _name: &LowerName, _rtype: RecordType, _lookup_options: LookupOptions, last_result: LookupControlFlow>, ) -> LookupControlFlow> { last_result } /// Using the specified query, perform a lookup against this zone. /// /// # Arguments /// /// * `request` - the query to perform the lookup with. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn search( &self, request: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow; /// Get the NS, NameServer, record for the zone async fn ns(&self, lookup_options: LookupOptions) -> LookupControlFlow { self.lookup(self.origin(), RecordType::NS, lookup_options) .await } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow; /// Return the NSEC3 records based on the information available for a query. #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow; /// Returns the SOA of the authority. /// /// *Note*: This will only return the SOA, if this is fulfilling a request, a standard lookup /// should be used, see `soa_secure()`, which will optionally return RRSIGs. async fn soa(&self) -> LookupControlFlow { // SOA should be origin|SOA self.lookup(self.origin(), RecordType::SOA, LookupOptions::default()) .await } /// Returns the SOA record for the zone async fn soa_secure(&self, lookup_options: LookupOptions) -> LookupControlFlow { self.lookup(self.origin(), RecordType::SOA, lookup_options) .await } /// Returns the kind of non-existence proof used for this zone. #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind>; } /// Extension to Authority to allow for DNSSEC features #[cfg(feature = "__dnssec")] #[async_trait::async_trait] pub trait DnssecAuthority: Authority { /// Add a (Sig0) key that is authorized to perform updates against this authority async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()>; /// Add Signer async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()>; /// Sign the zone for DNSSEC async fn secure_zone(&self) -> DnsSecResult<()>; } /// Result of a Lookup in the Catalog and Authority /// /// * **All authorities should default to using LookupControlFlow::Continue to wrap their responses.** /// These responses may be passed to other authorities for analysis or requery purposes. /// * Authorities may use LookupControlFlow::Break to indicate the response must be returned /// immediately to the client, without consulting any other authorities. For example, if the /// the user configures a blocklist authority, it would not be appropriate to pass the query to /// any additional authorities to try to resolve, as that might be used to leak information to a /// hostile party, and so a blocklist (or similar) authority should wrap responses for any /// blocklist hits in LookupControlFlow::Break. /// * Authorities may use LookupControlFlow::Skip to indicate the authority did not attempt to /// process a particular query. This might be used, for example, in a block list authority for /// any queries that **did not** match the blocklist, to allow the recursor or forwarder to /// resolve the query. Skip must not be used to represent an empty lookup; (use /// Continue(EmptyLookup) or Break(EmptyLookup) for that.) pub enum LookupControlFlow { /// A lookup response that may be passed to one or more additional authorities before /// being returned to the client. Continue(Result), /// A lookup response that must be immediately returned to the client without consulting /// any other authorities. Break(Result), /// The authority did not answer the query and the next authority in the chain should /// be consulted. Skip, } impl fmt::Display for LookupControlFlow { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Continue(cont) => match cont { Ok(_) => write!(f, "LookupControlFlow::Continue(Ok)"), Err(_) => write!(f, "LookupControlFlow::Continue(Err)"), }, Self::Break(b) => match b { Ok(_) => write!(f, "LookupControlFlow::Break(Ok)"), Err(_) => write!(f, "LookupControlFlow::Break(Err)"), }, Self::Skip => write!(f, "LookupControlFlow::Skip"), } } } /// The following are a minimal set of methods typically used with Result or Option, and that /// were used in the server code or test suite prior to when the LookupControlFlow type was created /// (authority lookup functions previously returned a Result over a Lookup or LookupError type.) impl LookupControlFlow { /// Return true if self is LookupControlFlow::Continue pub fn is_continue(&self) -> bool { matches!(self, Self::Continue(_)) } /// Return true if self is LookupControlFlow::Break pub fn is_break(&self) -> bool { matches!(self, Self::Break(_)) } /// Maps inner Ok(T) and Err(E) to Some(Result) and Skip to None pub fn map_result(self) -> Option> { match self { Self::Continue(Ok(lookup)) | Self::Break(Ok(lookup)) => Some(Ok(lookup)), Self::Continue(Err(e)) | Self::Break(Err(e)) => Some(Err(e)), Self::Skip => None, } } } impl LookupControlFlow { /// Return inner Ok variant or panic with a custom error message. pub fn expect(self, msg: &str) -> T { match self { Self::Continue(Ok(ok)) | Self::Break(Ok(ok)) => ok, _ => { panic!("lookupcontrolflow::expect() called on unexpected variant {self}: {msg}"); } } } /// Return inner Err variant or panic with a custom error message. pub fn expect_err(self, msg: &str) -> E { match self { Self::Continue(Err(e)) | Self::Break(Err(e)) => e, _ => { panic!( "lookupcontrolflow::expect_err() called on unexpected variant {self}: {msg}" ); } } } /// Return inner Ok variant or panic pub fn unwrap(self) -> T { match self { Self::Continue(Ok(ok)) | Self::Break(Ok(ok)) => ok, Self::Continue(Err(e)) | Self::Break(Err(e)) => { panic!("lookupcontrolflow::unwrap() called on unexpected variant _(Err(_)): {e}"); } _ => { panic!("lookupcontrolflow::unwrap() called on unexpected variant: {self}"); } } } /// Return inner Err variant or panic pub fn unwrap_err(self) -> E { match self { Self::Continue(Err(e)) | Self::Break(Err(e)) => e, _ => { panic!("lookupcontrolflow::unwrap_err() called on unexpected variant: {self}"); } } } /// Return inner Ok Variant or default value pub fn unwrap_or_default(self) -> T where T: Default, { match self { Self::Continue(Ok(ok)) | Self::Break(Ok(ok)) => ok, _ => T::default(), } } /// Maps inner Ok(T) to Ok(U), passing inner Err and Skip values unchanged. pub fn map U>(self, op: F) -> LookupControlFlow { match self { Self::Continue(cont) => match cont { Ok(t) => LookupControlFlow::Continue(Ok(op(t))), Err(e) => LookupControlFlow::Continue(Err(e)), }, Self::Break(b) => match b { Ok(t) => LookupControlFlow::Break(Ok(op(t))), Err(e) => LookupControlFlow::Break(Err(e)), }, Self::Skip => LookupControlFlow::::Skip, } } /// Maps inner Ok(T) to Ok(Box<dyn LookupObject>), passing inner Err and Skip values unchanged. pub fn map_dyn(self) -> LookupControlFlow, E> { match self { Self::Continue(cont) => match cont { Ok(lookup) => { LookupControlFlow::Continue(Ok(Box::new(lookup) as Box)) } Err(e) => LookupControlFlow::Continue(Err(e)), }, Self::Break(b) => match b { Ok(lookup) => { LookupControlFlow::Break(Ok(Box::new(lookup) as Box)) } Err(e) => LookupControlFlow::Break(Err(e)), }, Self::Skip => LookupControlFlow::, E>::Skip, } } /// Maps inner Err(T) to Err(U), passing Ok and Skip values unchanged. pub fn map_err U>(self, op: F) -> LookupControlFlow { match self { Self::Continue(cont) => match cont { Ok(lookup) => LookupControlFlow::Continue(Ok(lookup)), Err(e) => LookupControlFlow::Continue(Err(op(e))), }, Self::Break(b) => match b { Ok(lookup) => LookupControlFlow::Break(Ok(lookup)), Err(e) => LookupControlFlow::Break(Err(op(e))), }, Self::Skip => LookupControlFlow::Skip, } } } /// Information required to compute the NSEC3 records that should be sent for a query. #[cfg(feature = "__dnssec")] pub struct Nsec3QueryInfo<'q> { /// The queried name. pub qname: &'q LowerName, /// The queried record type. pub qtype: RecordType, /// Whether there was a wildcard match for `qname` regardless of `qtype`. pub has_wildcard_match: bool, /// The algorithm used to hash the names. pub algorithm: Nsec3HashAlgorithm, /// The salt used for hashing. pub salt: &'q [u8], /// The number of hashing iterations. pub iterations: u16, } #[cfg(feature = "__dnssec")] impl Nsec3QueryInfo<'_> { /// Computes the hash of a given name. pub(crate) fn hash_name(&self, name: &Name) -> Result { self.algorithm.hash(self.salt, name, self.iterations) } /// Computes the hashed owner name from a given name. That is, the hash of the given name, /// followed by the zone name. pub(crate) fn get_hashed_owner_name( &self, name: &LowerName, zone: &Name, ) -> Result { let hash = self.hash_name(name)?; let label = data_encoding::BASE32_DNSSEC.encode(hash.as_ref()); Ok(LowerName::new(&zone.prepend_label(label)?)) } } hickory-server-0.25.2/src/authority/authority_object.rs000064400000000000000000000347531046102023000214450ustar 00000000000000// 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. //! Object-safe authority and lookup traits use tracing::debug; #[cfg(feature = "__dnssec")] use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind, proto::dnssec::Proof}; use crate::{ authority::{ Authority, LookupControlFlow, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, proto::rr::{LowerName, Record, RecordType}, server::RequestInfo, }; /// An Object safe Authority #[async_trait::async_trait] pub trait AuthorityObject: Send + Sync { /// What type is this zone fn zone_type(&self) -> ZoneType; /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool; /// Whether the authority can perform DNSSEC validation fn can_validate_dnssec(&self) -> bool; /// Perform a dynamic update of a zone async fn update(&self, update: &MessageRequest) -> UpdateResult; /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName; /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow>; /// Consulting lookup for all Resource Records matching the given `Name` and `RecordType`. /// This will be called in a chained authority configuration after an authority in the chain /// has returned a lookup with a LookupControlFlow::Continue action. Every other authority in /// the chain will be called via this consult method, until one either returns a /// LookupControlFlow::Break action, or all authorities have been consulted. The authority that /// generated the primary lookup (the one returned via 'lookup') will not be consulted. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// * `last_result` - The lookup returned by a previous authority in a chained configuration. /// If a subsequent authority does not modify this lookup, it will be returned /// to the client after consulting all authorities in the chain. /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. This can /// be the same last_result that was passed in, or a new lookup, depending on the logic of the /// authority in question. async fn consult( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, last_result: LookupControlFlow>, ) -> LookupControlFlow>; /// Using the specified query, perform a lookup against this zone. /// /// # Arguments /// /// * `request_info` - the query to perform the lookup with. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow>; /// Get the NS, NameServer, record for the zone async fn ns(&self, lookup_options: LookupOptions) -> LookupControlFlow> { self.lookup(self.origin(), RecordType::NS, lookup_options) .await } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow>; /// Return the NSEC3 records based on the given query information. #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow>; /// Returns the SOA of the authority. /// /// *Note*: This will only return the SOA, if this is fulfilling a request, a standard lookup /// should be used, see `soa_secure()`, which will optionally return RRSIGs. async fn soa(&self) -> LookupControlFlow> { // SOA should be origin|SOA self.lookup(self.origin(), RecordType::SOA, LookupOptions::default()) .await } /// Returns the SOA record for the zone async fn soa_secure( &self, lookup_options: LookupOptions, ) -> LookupControlFlow> { self.lookup(self.origin(), RecordType::SOA, lookup_options) .await } /// Returns the kind of non-existence proof used for this zone. #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind>; } #[async_trait::async_trait] impl AuthorityObject for A where A: Authority + Send + Sync + 'static, L: LookupObject + Send + Sync + 'static, { /// What type is this zone fn zone_type(&self) -> ZoneType { Authority::zone_type(self) } /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool { Authority::is_axfr_allowed(self) } /// Whether the authority can perform DNSSEC validation fn can_validate_dnssec(&self) -> bool { Authority::can_validate_dnssec(self) } /// Perform a dynamic update of a zone async fn update(&self, update: &MessageRequest) -> UpdateResult { Authority::update(self, update).await } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName { Authority::origin(self) } /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow> { Authority::lookup(self, name, rtype, lookup_options) .await .map_dyn() } /// Consulting lookup for all Resource Records matching the given `Name` and `RecordType`. /// This will be called in a chained authority configuration after an authority in the chain /// has returned a lookup with a LookupControlFlow::Continue action. Every other authority in /// the chain will be called via this consult method, until one either returns a /// LookupControlFlow::Break action, or all authorities have been consulted. The authority that /// generated the primary lookup (the one returned via 'lookup') will not be consulted. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// * `last_result` - The lookup returned by a previous authority in a chained configuration. /// If a subsequent authority does not modify this lookup, it will be returned /// to the client after consulting all authorities in the chain. /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. This can /// be the same last_result that was passed in, or a new lookup, depending on the logic of the /// authority in question. async fn consult( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, last_result: LookupControlFlow>, ) -> LookupControlFlow> { Authority::consult(self, name, rtype, lookup_options, last_result).await } /// Using the specified query, perform a lookup against this zone. /// /// # Arguments /// /// * `request_info` - the query to perform the lookup with. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow> { debug!("performing {} on {}", request_info.query, self.origin()); Authority::search(self, request_info, lookup_options) .await .map_dyn() } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow> { Authority::get_nsec_records(self, name, lookup_options) .await .map_dyn() } /// Return the NSEC3 records based on the given query information. #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow> { Authority::get_nsec3_records(self, info, lookup_options) .await .map_dyn() } /// Returns the kind of non-existence proof used for this zone. #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { Authority::nx_proof_kind(self) } } /// DNSSEC status of an answer #[derive(Clone, Copy, Debug)] 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, } /// An Object Safe Lookup for Authority pub trait LookupObject: Send { /// Returns true if either the associated Records are empty, or this is a NameExists or NxDomain fn is_empty(&self) -> bool; /// Conversion to an iterator fn iter<'a>(&'a self) -> Box + Send + 'a>; /// For CNAME and similar records, this is an additional set of lookup records /// /// it is acceptable for this to return None after the first call. fn take_additionals(&mut self) -> Option>; /// Whether the records have been DNSSEC validated or not #[cfg(feature = "__dnssec")] fn dnssec_summary(&self) -> DnssecSummary { let mut all_secure = None; for record in self.iter() { match record.proof() { Proof::Secure => { all_secure.get_or_insert(true); } Proof::Bogus => return DnssecSummary::Bogus, _ => all_secure = Some(false), } } if all_secure.unwrap_or(false) { DnssecSummary::Secure } else { DnssecSummary::Insecure } } /// Whether the records have been DNSSEC validated or not #[cfg(not(feature = "__dnssec"))] fn dnssec_summary(&self) -> DnssecSummary { DnssecSummary::Insecure } } /// A lookup that returns no records #[derive(Clone, Copy, Debug)] pub struct EmptyLookup; impl LookupObject for EmptyLookup { fn is_empty(&self) -> bool { true } fn iter<'a>(&'a self) -> Box + Send + 'a> { Box::new([].iter()) } fn take_additionals(&mut self) -> Option> { None } } hickory-server-0.25.2/src/authority/catalog.rs000064400000000000000000001054531046102023000174750ustar 00000000000000// 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. // TODO, I've implemented this as a separate entity from the cache, but I wonder if the cache // should be the only "front-end" for lookups, where if that misses, then we go to the catalog // then, if requested, do a recursive lookup... i.e. the catalog would only point to files. use std::{borrow::Borrow, collections::HashMap, io, sync::Arc}; use cfg_if::cfg_if; use tracing::{debug, error, info, trace, warn}; #[cfg(feature = "__dnssec")] use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind}; use crate::{ authority::{ AuthLookup, AuthorityObject, EmptyLookup, LookupControlFlow, LookupError, LookupObject, LookupOptions, LookupRecords, MessageResponse, MessageResponseBuilder, ZoneType, authority_object::DnssecSummary, }, proto::{ op::{Edns, Header, LowerQuery, MessageType, OpCode, ResponseCode}, rr::{LowerName, Record, RecordSet, RecordType}, }, server::{Request, RequestHandler, RequestInfo, ResponseHandler, ResponseInfo}, }; /// Set of authorities, zones, available to this server. #[derive(Default)] pub struct Catalog { authorities: HashMap>>, } #[allow(unused_mut, unused_variables)] async fn send_response<'a, R: ResponseHandler>( response_edns: Option, mut response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, mut response_handle: R, ) -> io::Result { if let Some(mut resp_edns) = response_edns { response.set_edns(resp_edns); } response_handle.send_response(response).await } #[async_trait::async_trait] impl RequestHandler for Catalog { /// Determines what needs to happen given the type of request, i.e. Query or Update. /// /// # Arguments /// /// * `request` - the requested action to perform. /// * `response_handle` - sink for the response message to be sent async fn handle_request( &self, request: &Request, mut response_handle: R, ) -> ResponseInfo { trace!("request: {:?}", request); let response_edns: Option; // check if it's edns if let Some(req_edns) = request.edns() { let mut response = MessageResponseBuilder::new(request.raw_queries()); let mut response_header = Header::response_from_request(request.header()); let mut resp_edns: Edns = Edns::new(); // check our version against the request // TODO: what version are we? let our_version = 0; resp_edns.set_dnssec_ok(true); resp_edns.set_max_payload(req_edns.max_payload().max(512)); resp_edns.set_version(our_version); if req_edns.version() > our_version { warn!( "request edns version greater than {}: {}", our_version, req_edns.version() ); response_header.set_response_code(ResponseCode::BADVERS); resp_edns.set_rcode_high(ResponseCode::BADVERS.high()); response.edns(resp_edns); // TODO: should ResponseHandle consume self? let result = response_handle .send_response(response.build_no_records(response_header)) .await; // couldn't handle the request return match result { Err(e) => { error!("request error: {}", e); ResponseInfo::serve_failed() } Ok(info) => info, }; } response_edns = Some(resp_edns); } else { response_edns = None; } let result = match request.message_type() { // TODO think about threading query lookups for multiple lookups, this could be a huge improvement // especially for recursive lookups MessageType::Query => match request.op_code() { OpCode::Query => { debug!("query received: {}", request.id()); let info = self.lookup(request, response_edns, response_handle).await; Ok(info) } OpCode::Update => { debug!("update received: {}", request.id()); self.update(request, response_edns, response_handle).await } c => { warn!("unimplemented op_code: {:?}", c); let response = MessageResponseBuilder::new(request.raw_queries()); response_handle .send_response(response.error_msg(request.header(), ResponseCode::NotImp)) .await } }, MessageType::Response => { warn!("got a response as a request from id: {}", request.id()); let response = MessageResponseBuilder::new(request.raw_queries()); response_handle .send_response(response.error_msg(request.header(), ResponseCode::FormErr)) .await } }; match result { Err(e) => { error!("request failed: {}", e); ResponseInfo::serve_failed() } Ok(info) => info, } } } impl Catalog { /// Constructs a new Catalog pub fn new() -> Self { Self { authorities: HashMap::new(), } } /// Insert or update a zone authority /// /// # Arguments /// /// * `name` - zone name, e.g. example.com. /// * `authority` - the zone data pub fn upsert(&mut self, name: LowerName, authorities: Vec>) { self.authorities.insert(name, authorities); } /// Remove a zone from the catalog pub fn remove(&mut self, name: &LowerName) -> Option>> { self.authorities.remove(name) } /// Update the zone given the Update request. /// /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// 3.1 - Process Zone Section /// /// 3.1.1. The Zone Section is checked to see that there is exactly one /// RR therein and that the RR's ZTYPE is SOA, else signal FORMERR to the /// requestor. Next, the ZNAME and ZCLASS are checked to see if the zone /// so named is one of this server's authority zones, else signal NOTAUTH /// to the requestor. If the server is a zone Secondary, the request will be /// forwarded toward the Primary Zone Server. /// /// 3.1.2 - Pseudocode For Zone Section Processing /// /// if (zcount != 1 || ztype != SOA) /// return (FORMERR) /// if (zone_type(zname, zclass) == SECONDARY) /// return forward() /// if (zone_type(zname, zclass) == PRIMARY) /// return update() /// return (NOTAUTH) /// /// Sections 3.2 through 3.8 describe the primary's behaviour, /// whereas Section 6 describes a forwarder's behaviour. /// /// 3.8 - Response /// /// At the end of UPDATE processing, a response code will be known. A /// response message is generated by copying the ID and Opcode fields /// from the request, and either copying the ZOCOUNT, PRCOUNT, UPCOUNT, /// and ADCOUNT fields and associated sections, or placing zeros (0) in /// the these "count" fields and not including any part of the original /// update. The QR bit is set to one (1), and the response is sent back /// to the requestor. If the requestor used UDP, then the response will /// be sent to the requestor's source UDP port. If the requestor used /// TCP, then the response will be sent back on the requestor's open TCP /// connection. /// ``` /// /// The "request" should be an update formatted message. /// The response will be in the alternate, all 0's format described in RFC 2136 section 3.8 /// as this is more efficient. /// /// # Arguments /// /// * `request` - an update message /// * `response_handle` - sink for the response message to be sent pub async fn update( &self, update: &Request, response_edns: Option, response_handle: R, ) -> io::Result { let request_info = update.request_info()?; let verify_request = move || -> Result, ResponseCode> { // 2.3 - Zone Section // // All records to be updated must be in the same zone, and // therefore the Zone Section is allowed to contain exactly one record. // The ZNAME is the zone name, the ZTYPE must be SOA, and the ZCLASS is // the zone's class. let ztype = request_info.query.query_type(); if ztype != RecordType::SOA { warn!( "invalid update request zone type must be SOA, ztype: {}", ztype ); return Err(ResponseCode::FormErr); } Ok(request_info) }; let Ok(verify_request) = verify_request() else { return Ok(ResponseInfo::serve_failed()); }; // verify the zone type and number of zones in request, then find the zone to update if let Some(authorities) = self.find(verify_request.query.name()) { #[allow(clippy::never_loop)] for authority in authorities { #[allow(deprecated)] let response_code = match authority.zone_type() { ZoneType::Secondary | ZoneType::Slave => { error!("secondary forwarding for update not yet implemented"); ResponseCode::NotImp } ZoneType::Primary | ZoneType::Master => { let update_result = authority.update(update).await; match update_result { // successful update Ok(..) => ResponseCode::NoError, Err(response_code) => response_code, } } _ => ResponseCode::NotAuth, }; let response = MessageResponseBuilder::new(update.raw_queries()); let mut response_header = Header::default(); response_header.set_id(update.id()); response_header.set_op_code(OpCode::Update); response_header.set_message_type(MessageType::Response); response_header.set_response_code(response_code); return send_response( response_edns, response.build_no_records(response_header), response_handle, ) .await; } }; Ok(ResponseInfo::serve_failed()) } /// Checks whether the `Catalog` contains DNS records for `name` /// /// Use this when you know the exact `LowerName` that was used when /// adding an authority and you don't care about the authority it /// contains. For public domain names, `LowerName` is usually the /// top level domain name like `example.com.`. /// /// If you do not know the exact domain name to use or you actually /// want to use the authority it contains, use `find` instead. pub fn contains(&self, name: &LowerName) -> bool { self.authorities.contains_key(name) } /// Given the requested query, lookup and return any matching results. /// /// # Arguments /// /// * `request` - the query message. /// * `response_handle` - sink for the response message to be sent pub async fn lookup( &self, request: &Request, response_edns: Option, response_handle: R, ) -> ResponseInfo { let Ok(request_info) = request.request_info() else { // Wrong number of queries let response = MessageResponseBuilder::new(request.raw_queries()); let result = send_response( response_edns, response.error_msg(request.header(), ResponseCode::FormErr), response_handle, ) .await; match result { Err(e) => { error!("failed to send response: {e}"); return ResponseInfo::serve_failed(); } Ok(r) => return r, } }; let authorities = self.find(request_info.query.name()); let Some(authorities) = authorities else { // There are no authorities registered that can handle the request let response = MessageResponseBuilder::new(request.raw_queries()); let result = send_response( response_edns, response.error_msg(request.header(), ResponseCode::Refused), response_handle, ) .await; match result { Err(e) => { error!("failed to send response: {e}"); return ResponseInfo::serve_failed(); } Ok(r) => return r, } }; let result = lookup( request_info.clone(), authorities, request, response_edns .as_ref() .map(|arc| Borrow::::borrow(arc).clone()), response_handle.clone(), ) .await; match result { Ok(lookup) => lookup, Err(_e) => ResponseInfo::serve_failed(), } } /// Recursively searches the catalog for a matching authority pub fn find(&self, name: &LowerName) -> Option<&Vec>> { debug!("searching authorities for: {name}"); self.authorities.get(name).or_else(|| { if !name.is_root() { let name = name.base_name(); self.find(&name) } else { None } }) } } async fn lookup( request_info: RequestInfo<'_>, authorities: &[Arc], request: &Request, response_edns: Option, response_handle: R, ) -> Result { let edns = request.edns(); let lookup_options = lookup_options_for_edns(edns); let request_id = request.id(); // log algorithms being requested if lookup_options.dnssec_ok() { info!("request: {request_id} lookup_options: {lookup_options:?}"); } let query = request_info.query; for (authority_index, authority) in authorities.iter().enumerate() { debug!( "performing {query} on authority {origin} with request id {request_id}", origin = authority.origin(), ); // Wait so we can determine if we need to fire a request to the next authority in a chained // configuration if the current authority declines to answer. let mut result = authority.search(request_info.clone(), lookup_options).await; if let LookupControlFlow::Skip = result { trace!("catalog::lookup::authority did not handle request"); continue; } else if result.is_continue() { trace!("catalog::lookup::authority did handle request with continue"); // For LookupControlFlow::Continue results, we'll call consult on every // authority, except the authority that returned the Continue result. for (continue_index, consult_authority) in authorities.iter().enumerate() { if continue_index == authority_index { trace!("skipping current authority consult (index {continue_index})"); continue; } else { trace!("calling authority consult (index {continue_index})"); } result = consult_authority .consult( request_info.query.name(), request_info.query.query_type(), lookup_options_for_edns(response_edns.as_ref()), result, ) .await; } } else { trace!("catalog::lookup::authority did handle request with break"); } // We no longer need the context from LookupControlFlow, so decompose into a standard Result // to clean up the rest of the match conditions let Some(result) = result.map_result() else { error!("impossible skip detected after final lookup result"); return Err(LookupError::ResponseCode(ResponseCode::ServFail)); }; let (response_header, sections) = build_response( result, &**authority, request_id, request.header(), query, edns, ) .await; let message_response = MessageResponseBuilder::new(request.raw_queries()).build( response_header, sections.answers.iter(), sections.ns.iter(), sections.soa.iter(), sections.additionals.iter(), ); let result = send_response(response_edns, message_response, response_handle).await; match result { Err(e) => { error!("error sending response: {e}"); return Err(LookupError::Io(e)); } Ok(l) => return Ok(l), } } error!("end of chained authority loop reached with all authorities not answering"); Err(LookupError::ResponseCode(ResponseCode::ServFail)) } #[allow(unused_variables)] fn lookup_options_for_edns(edns: Option<&Edns>) -> LookupOptions { let edns = match edns { Some(edns) => edns, None => return LookupOptions::default(), }; cfg_if! { if #[cfg(feature = "__dnssec")] { LookupOptions::for_dnssec(edns.flags().dnssec_ok) } else { LookupOptions::default() } } } /// Build Header and LookupSections (answers) given a query response from an authority async fn build_response( result: Result, LookupError>, authority: &dyn AuthorityObject, request_id: u16, request_header: &Header, query: &LowerQuery, edns: Option<&Edns>, ) -> (Header, LookupSections) { let lookup_options = lookup_options_for_edns(edns); let mut response_header = Header::response_from_request(request_header); response_header.set_authoritative(authority.zone_type().is_authoritative()); #[allow(deprecated)] let sections = match authority.zone_type() { ZoneType::Primary | ZoneType::Secondary | ZoneType::Master | ZoneType::Slave => { build_authoritative_response( result, authority, &mut response_header, lookup_options, request_id, query, ) .await } ZoneType::External => { build_forwarded_response( result, request_header, &mut response_header, authority.can_validate_dnssec(), query, lookup_options, ) .await } }; (response_header, sections) } /// Prepare a response for an authoritative zone async fn build_authoritative_response( response: Result, LookupError>, authority: &dyn AuthorityObject, response_header: &mut Header, lookup_options: LookupOptions, _request_id: u16, query: &LowerQuery, ) -> LookupSections { // In this state we await the records, on success we transition to getting // NS records, which indicate an authoritative response. // // On Errors, the transition depends on the type of error. let answers = match response { Ok(records) => { response_header.set_response_code(ResponseCode::NoError); response_header.set_authoritative(true); Some(records) } // This request was refused // TODO: there are probably other error cases that should just drop through (FormErr, ServFail) Err(LookupError::ResponseCode(ResponseCode::Refused)) => { response_header.set_response_code(ResponseCode::Refused); return LookupSections { answers: Box::::default(), ns: Box::::default(), soa: Box::::default(), additionals: Box::::default(), }; } Err(e) => { if e.is_nx_domain() { response_header.set_response_code(ResponseCode::NXDomain); } else if e.is_name_exists() { response_header.set_response_code(ResponseCode::NoError); }; None } }; let (ns, soa) = if answers.is_some() { // SOA queries should return the NS records as well. if query.query_type().is_soa() { // This was a successful authoritative lookup for SOA: // get the NS records as well. match authority.ns(lookup_options).await.map_result() { Some(Ok(ns)) => (Some(ns), None), Some(Err(e)) => { warn!("ns_lookup errored: {e}"); (None, None) } None => { warn!("ns_lookup unexpected skip"); (None, None) } } } else { #[cfg(feature = "__dnssec")] { if let Some(NxProofKind::Nsec3 { algorithm, salt, iterations, opt_out: _, }) = authority.nx_proof_kind() { // This unwrap will not panic as we know that `answers` is `Some`. let has_wildcard_match = answers.as_ref().unwrap().iter().any(|rr| { rr.record_type() == RecordType::RRSIG && rr.name().is_wildcard() }); match authority .get_nsec3_records( Nsec3QueryInfo { qname: query.name(), qtype: query.query_type(), has_wildcard_match, algorithm: *algorithm, salt, iterations: *iterations, }, lookup_options, ) .await .map_result() { // run the soa lookup Some(Ok(nsecs)) => (Some(nsecs), None), Some(Err(e)) => { warn!("failed to lookup nsecs for request {_request_id}: {e}"); (None, None) } None => { warn!("unexpected lookup skip for request {_request_id}"); (None, None) } } } else { (None, None) } } #[cfg(not(feature = "__dnssec"))] (None, None) } } else { let nsecs = if lookup_options.dnssec_ok() { #[cfg(feature = "__dnssec")] { // in the dnssec case, nsec records should exist, we return NoError + NoData + NSec... debug!("request: {_request_id} non-existent adding nsecs"); match authority.nx_proof_kind() { Some(nx_proof_kind) => { // run the nsec lookup future, and then transition to get soa let future = match nx_proof_kind { NxProofKind::Nsec => { authority.get_nsec_records(query.name(), lookup_options) } NxProofKind::Nsec3 { algorithm, salt, iterations, opt_out: _, } => authority.get_nsec3_records( Nsec3QueryInfo { qname: query.name(), qtype: query.query_type(), has_wildcard_match: false, algorithm: *algorithm, salt, iterations: *iterations, }, lookup_options, ), }; match future.await.map_result() { // run the soa lookup Some(Ok(nsecs)) => Some(nsecs), Some(Err(e)) => { warn!("failed to lookup nsecs for request {_request_id}: {e}"); None } None => { warn!("unexpected lookup skip for request {_request_id}"); None } } } None => None, } } #[cfg(not(feature = "__dnssec"))] None } else { None }; match authority.soa_secure(lookup_options).await.map_result() { Some(Ok(soa)) => (nsecs, Some(soa)), Some(Err(e)) => { warn!("failed to lookup soa: {e}"); (nsecs, None) } None => { warn!("unexpected lookup skip"); (None, None) } } }; // everything is done, return results. let (answers, additionals) = match answers { Some(mut answers) => match answers.take_additionals() { Some(additionals) => (answers, additionals), None => ( answers, Box::::default() as Box, ), }, None => ( Box::::default() as Box, Box::::default() as Box, ), }; LookupSections { answers, ns: ns.unwrap_or_else(|| Box::::default()), soa: soa.unwrap_or_else(|| Box::::default()), additionals, } } /// Prepare a response for a forwarded zone. async fn build_forwarded_response( response: Result, LookupError>, request_header: &Header, response_header: &mut Header, can_validate_dnssec: bool, query: &LowerQuery, lookup_options: LookupOptions, ) -> LookupSections { response_header.set_recursion_available(true); response_header.set_authoritative(false); enum Answer { Normal(Box), NoRecords(Box), } let (mut answers, authorities) = match response { Ok(_) | Err(_) if !request_header.recursion_desired() => { info!( id = request_header.id(), "request disabled recursion, returning REFUSED" ); response_header.set_response_code(ResponseCode::Refused); return LookupSections { answers: Box::new(EmptyLookup), ns: Box::new(EmptyLookup), soa: Box::new(EmptyLookup), additionals: Box::new(EmptyLookup), }; } Ok(l) => (Answer::Normal(l), Box::::default()), Err(e) if e.is_no_records_found() || e.is_nx_domain() => { debug!(error = ?e, "error resolving"); if e.is_nx_domain() { response_header.set_response_code(ResponseCode::NXDomain); } // Collect all of the authority records, except the SOA let authorities = if let Some(authorities) = e.authorities() { let authorities = authorities .iter() .filter_map(|x| { // if we have another record (probably a dnssec record) that // matches the query name, but wasn't included in the answers // section, change the NXDomain response to NoError if *x.name() == **query.name() { debug!( query_name = %query.name(), record = ?x, "changing response code from NXDomain to NoError due to other record", ); response_header.set_response_code(ResponseCode::NoError); } match x.record_type() { RecordType::SOA => None, _ => Some(Arc::new(RecordSet::from(x.clone()))), } }) .collect(); Box::new(AuthLookup::answers( LookupRecords::many(LookupOptions::default(), authorities), None, )) } else { Box::::default() }; if let Some(soa) = e.into_soa() { let soa = soa.into_record_of_rdata(); let record_set = Arc::new(RecordSet::from(soa)); let records = LookupRecords::new(LookupOptions::default(), record_set); ( Answer::NoRecords(Box::new(AuthLookup::SOA(records))), authorities, ) } else { (Answer::Normal(Box::new(EmptyLookup)), authorities) } } Err(e) => { response_header.set_response_code(ResponseCode::ServFail); debug!(error = ?e, "error resolving"); ( Answer::Normal(Box::new(EmptyLookup)), Box::::default(), ) } }; if can_validate_dnssec { // section 3.2.2 ("the CD bit") of RFC4035 is a bit underspecified because it does not use // RFC2119 vocabulary ("MUST", "MAY", etc.) in some sentences that describe the resolver's // behavior. // // A. it is clear that if CD=1 in the query then data that fails DNSSEC validation SHOULD // be returned // // B. it also clear that if CD=0 and DNSSEC validation fails then the status MUST be // SERVFAIL // // C. it's less clear if DNSSEC validation can be skipped altogether when CD=1 // // the logic here follows `unbound`'s interpretation of that section // // 0. the requirements A and B are implemented // 1. DNSSEC validation happens regardless of the state of the CD bit // 2. the AD bit gets set if DNSSEC validation succeeded regardless of the state of the // CD bit // // this last point can result in responses that have both AD=1 and CD=1. RFC4035 is unclear // whether that's a valid state but that's what `unbound` does // // we may want to interpret (B) as allowed ("MAY be skipped") as a form of optimization in // the future to reduce the number of network transactions that a CD=1 query needs. match &mut answers { Answer::Normal(answers) => match answers.dnssec_summary() { DnssecSummary::Secure => { trace!("setting ad header"); response_header.set_authentic_data(true); } DnssecSummary::Bogus if !request_header.checking_disabled() => { response_header.set_response_code(ResponseCode::ServFail); // do not return Bogus records when CD=0 *answers = Box::new(EmptyLookup); } _ => {} }, Answer::NoRecords(soa) => match authorities.dnssec_summary() { DnssecSummary::Secure => { trace!("setting ad header"); response_header.set_authentic_data(true); } DnssecSummary::Bogus if !request_header.checking_disabled() => { response_header.set_response_code(ResponseCode::ServFail); // do not return Bogus records when CD=0 *soa = Box::::default(); trace!("clearing SOA record from response"); } _ => {} }, } } // Strip out DNSSEC records unless the DO bit is set. let authorities = if !lookup_options.dnssec_ok() { let auth = authorities .into_iter() .filter_map(|rrset| { let record_type = rrset.record_type(); if record_type == query.query_type() || !record_type.is_dnssec() { Some(Arc::new(RecordSet::from(rrset.clone()))) } else { None } }) .collect(); Box::new(AuthLookup::answers( LookupRecords::many(LookupOptions::default(), auth), None, )) } else { authorities }; match answers { Answer::Normal(answers) => LookupSections { answers, ns: authorities, soa: Box::::default(), additionals: Box::::default(), }, Answer::NoRecords(soa) => LookupSections { answers: Box::new(EmptyLookup), ns: authorities, soa, additionals: Box::::default(), }, } } struct LookupSections { answers: Box, ns: Box, soa: Box, additionals: Box, } hickory-server-0.25.2/src/authority/message_request.rs000064400000000000000000000276221046102023000212600ustar 00000000000000// 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. use crate::proto::{ ProtoError, ProtoErrorKind, op::{ Edns, Header, LowerQuery, Message, MessageType, OpCode, ResponseCode, message::{self, EmitAndCount}, }, rr::Record, serialize::binary::{BinDecodable, BinDecoder, BinEncodable, BinEncoder}, }; /// A Message which captures the data from an inbound request #[derive(Debug, PartialEq)] pub struct MessageRequest { header: Header, queries: Queries, answers: Vec, name_servers: Vec, additionals: Vec, sig0: Vec, edns: Option, } impl MessageRequest { /// Return the request header pub fn header(&self) -> &Header { &self.header } /// see `Header::id()` pub fn id(&self) -> u16 { self.header.id() } /// see `Header::message_type()` pub fn message_type(&self) -> MessageType { self.header.message_type() } /// see `Header::op_code()` pub fn op_code(&self) -> OpCode { self.header.op_code() } /// see `Header::authoritative()` pub fn authoritative(&self) -> bool { self.header.authoritative() } /// see `Header::truncated()` pub fn truncated(&self) -> bool { self.header.truncated() } /// see `Header::recursion_desired()` pub fn recursion_desired(&self) -> bool { self.header.recursion_desired() } /// see `Header::recursion_available()` pub fn recursion_available(&self) -> bool { self.header.recursion_available() } /// see `Header::authentic_data()` pub fn authentic_data(&self) -> bool { self.header.authentic_data() } /// see `Header::checking_disabled()` pub fn checking_disabled(&self) -> bool { self.header.checking_disabled() } /// # Return value /// /// The `ResponseCode`, if this is an EDNS message then this will join the section from the OPT /// record to create the EDNS `ResponseCode` pub fn response_code(&self) -> ResponseCode { self.header.response_code() } /// ```text /// Question Carries the query name and other query parameters. /// ``` pub fn queries(&self) -> &[LowerQuery] { &self.queries.queries } /// ```text /// Answer Carries RRs which directly answer the query. /// ``` pub fn answers(&self) -> &[Record] { &self.answers } /// ```text /// Authority Carries RRs which describe other authoritative servers. /// May optionally carry the SOA RR for the authoritative /// data in the answer section. /// ``` pub fn name_servers(&self) -> &[Record] { &self.name_servers } /// ```text /// Additional Carries RRs which may be helpful in using the RRs in the /// other sections. /// ``` pub fn additionals(&self) -> &[Record] { &self.additionals } /// [RFC 6891, EDNS(0) Extensions, April 2013](https://tools.ietf.org/html/rfc6891#section-6.1.1) /// /// ```text /// 6.1.1. Basic Elements /// /// An OPT pseudo-RR (sometimes called a meta-RR) MAY be added to the /// additional data section of a request. /// /// The OPT RR has RR type 41. /// /// If an OPT record is present in a received request, compliant /// responders MUST include an OPT record in their respective responses. /// /// An OPT record does not carry any DNS data. It is used only to /// contain control information pertaining to the question-and-answer /// sequence of a specific transaction. OPT RRs MUST NOT be cached, /// forwarded, or stored in or loaded from zone files. /// /// The OPT RR MAY be placed anywhere within the additional data section. /// When an OPT RR is included within any DNS message, it MUST be the /// only OPT RR in that message. If a query message with more than one /// OPT RR is received, a FORMERR (RCODE=1) MUST be returned. The /// placement flexibility for the OPT RR does not override the need for /// the TSIG or SIG(0) RRs to be the last in the additional section /// whenever they are present. /// ``` /// # Return value /// /// Returns the EDNS record if it was found in the additional section. pub fn edns(&self) -> Option<&Edns> { self.edns.as_ref() } /// Any SIG0 records for signed messages pub fn sig0(&self) -> &[Record] { &self.sig0 } /// # Return value /// /// the max payload value as it's defined in the EDNS section. pub fn max_payload(&self) -> u16 { let max_size = self.edns.as_ref().map_or(512, Edns::max_payload); if max_size < 512 { 512 } else { max_size } } /// # Return value /// /// the version as defined in the EDNS record pub fn version(&self) -> u8 { self.edns.as_ref().map_or(0, Edns::version) } /// Returns the original queries received from the client pub(crate) fn raw_queries(&self) -> &Queries { &self.queries } } impl<'q> BinDecodable<'q> for MessageRequest { // TODO: generify this with Message? /// Reads a MessageRequest from the decoder fn read(decoder: &mut BinDecoder<'q>) -> Result { let mut header = Header::read(decoder)?; let mut try_parse_rest = move || { // get all counts before header moves let query_count = header.query_count() as usize; let answer_count = header.answer_count() as usize; let name_server_count = header.name_server_count() as usize; let additional_count = header.additional_count() as usize; let queries = Queries::read(decoder, query_count)?; let (answers, _, _) = Message::read_records(decoder, answer_count, false)?; let (name_servers, _, _) = Message::read_records(decoder, name_server_count, false)?; let (additionals, edns, sig0) = Message::read_records(decoder, additional_count, true)?; // 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(); header.merge_response_code(high_response_code); } Ok(Self { header, queries, answers, name_servers, additionals, sig0, edns, }) }; match try_parse_rest() { Ok(message) => Ok(message), Err(e) => Err(ProtoErrorKind::FormError { header, error: Box::new(e), } .into()), } } } /// A set of Queries with the associated serialized data #[derive(Debug, PartialEq, Eq)] pub struct Queries { queries: Vec, original: Box<[u8]>, } impl Queries { fn read_queries( decoder: &mut BinDecoder<'_>, count: usize, ) -> Result, ProtoError> { let mut queries = Vec::with_capacity(count); for _ in 0..count { queries.push(LowerQuery::read(decoder)?); } Ok(queries) } /// Read queries from a decoder pub fn read(decoder: &mut BinDecoder<'_>, num_queries: usize) -> Result { let queries_start = decoder.index(); let queries = Self::read_queries(decoder, num_queries)?; let original = decoder .slice_from(queries_start)? .to_vec() .into_boxed_slice(); Ok(Self { queries, original }) } /// return the number of queries in the request pub fn len(&self) -> usize { self.queries.len() } /// Returns true if there are no queries pub fn is_empty(&self) -> bool { self.queries.is_empty() } /// Returns the queries from the request pub fn queries(&self) -> &[LowerQuery] { &self.queries } /// returns the bytes as they were seen from the Client pub fn as_bytes(&self) -> &[u8] { self.original.as_ref() } pub(crate) fn as_emit_and_count(&self) -> QueriesEmitAndCount<'_> { QueriesEmitAndCount { length: self.queries.len(), // We don't generally support more than one query, but this will at least give us one // cache entry. first_query: self.queries.first(), cached_serialized: self.original.as_ref(), } } /// Validate that this set of Queries contains exactly one Query, and return a reference to the /// `LowerQuery` if so. pub(crate) fn try_as_query(&self) -> Result<&LowerQuery, ProtoError> { let count = self.queries.len(); if count != 1 { return Err(ProtoErrorKind::BadQueryCount(count).into()); } Ok(&self.queries[0]) } /// Construct an empty set of queries pub(crate) fn empty() -> Self { Self { queries: Vec::new(), original: (*b"").into(), } } } pub(crate) struct QueriesEmitAndCount<'q> { /// Number of queries in this segment length: usize, /// Use the first query, if it exists, to pre-populate the string compression cache first_query: Option<&'q LowerQuery>, /// The cached rendering of the original (wire-format) queries cached_serialized: &'q [u8], } impl EmitAndCount for QueriesEmitAndCount<'_> { fn emit(&mut self, encoder: &mut BinEncoder<'_>) -> Result { let original_offset = encoder.offset(); encoder.emit_vec(self.cached_serialized)?; if !encoder.is_canonical_names() && self.first_query.is_some() { encoder.store_label_pointer( original_offset, original_offset + self.cached_serialized.len(), ) } Ok(self.length) } } impl BinEncodable for MessageRequest { fn emit(&self, encoder: &mut BinEncoder<'_>) -> Result<(), ProtoError> { message::emit_message_parts( &self.header, // we emit the queries, not the raw bytes, in order to guarantee canonical form // in cases where that's necessary, like SIG0 validation &mut self.queries.queries.iter(), &mut self.answers.iter(), &mut self.name_servers.iter(), &mut self.additionals.iter(), self.edns.as_ref(), &self.sig0, encoder, )?; Ok(()) } } /// A type which represents an MessageRequest for dynamic Update. pub trait UpdateRequest { /// Id of the Message fn id(&self) -> u16; /// Zone being updated, this should be the query of a Message fn zone(&self) -> Result<&LowerQuery, ProtoError>; /// Prerequisites map to the answers of a Message fn prerequisites(&self) -> &[Record]; /// Records to update map to the name_servers of a Message fn updates(&self) -> &[Record]; /// Additional records fn additionals(&self) -> &[Record]; /// SIG0 records for verifying the Message fn sig0(&self) -> &[Record]; } impl UpdateRequest for MessageRequest { fn id(&self) -> u16 { Self::id(self) } fn zone(&self) -> Result<&LowerQuery, ProtoError> { // RFC 2136 says "the Zone Section is allowed to contain exactly one record." self.raw_queries().try_as_query() } fn prerequisites(&self) -> &[Record] { self.answers() } fn updates(&self) -> &[Record] { self.name_servers() } fn additionals(&self) -> &[Record] { self.additionals() } fn sig0(&self) -> &[Record] { self.sig0() } } hickory-server-0.25.2/src/authority/message_response.rs000064400000000000000000000263031046102023000214210ustar 00000000000000// 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. use crate::{ authority::{Queries, message_request::MessageRequest}, proto::{ ProtoError, op::{Edns, Header, ResponseCode, message}, rr::Record, serialize::binary::BinEncoder, }, server::ResponseInfo, }; /// A EncodableMessage with borrowed data for Responses in the Server #[derive(Debug)] pub struct MessageResponse<'q, 'a, Answers, NameServers, Soa, Additionals> where Answers: Iterator + Send + 'a, NameServers: Iterator + Send + 'a, Soa: Iterator + Send + 'a, Additionals: Iterator + Send + 'a, { header: Header, queries: &'q Queries, answers: Answers, name_servers: NameServers, soa: Soa, additionals: Additionals, sig0: Vec, edns: Option, } impl<'a, A, N, S, D> MessageResponse<'_, 'a, A, N, S, D> where A: Iterator + Send + 'a, N: Iterator + Send + 'a, S: Iterator + Send + 'a, D: Iterator + Send + 'a, { /// Returns the header of the message pub fn header(&self) -> &Header { &self.header } /// Get a mutable reference to the header pub fn header_mut(&mut self) -> &mut Header { &mut self.header } /// Set the EDNS options for the Response pub fn set_edns(&mut self, edns: Edns) -> &mut Self { self.edns = Some(edns); self } /// Gets a reference to the EDNS options for the Response. pub fn get_edns(&self) -> &Option { &self.edns } /// Consumes self, and emits to the encoder. pub fn destructive_emit( mut self, encoder: &mut BinEncoder<'_>, ) -> Result { // soa records are part of the nameserver section let mut name_servers = self.name_servers.chain(self.soa); message::emit_message_parts( &self.header, &mut self.queries.as_emit_and_count(), &mut self.answers, &mut name_servers, &mut self.additionals, self.edns.as_ref(), &self.sig0, encoder, ) .map(Into::into) } } /// A builder for MessageResponses pub struct MessageResponseBuilder<'q> { queries: &'q Queries, sig0: Option>, edns: Option, } impl<'q> MessageResponseBuilder<'q> { /// Constructs a new response builder /// /// # Arguments /// /// * `queries` - queries (from the Request) to associate with the Response pub(crate) fn new(queries: &'q Queries) -> Self { MessageResponseBuilder { queries, sig0: None, edns: None, } } /// Constructs a new response builder /// /// # Arguments /// /// * `message` - original request message to associate with the response pub fn from_message_request(message: &'q MessageRequest) -> Self { Self::new(message.raw_queries()) } /// Associate EDNS with the Response pub fn edns(&mut self, edns: Edns) -> &mut Self { self.edns = Some(edns); self } /// Constructs the new MessageResponse with associated Header /// /// # Arguments /// /// * `header` - set of [Header]s for the Message pub fn build<'a, A, N, S, D>( self, header: Header, answers: A, name_servers: N, soa: S, additionals: D, ) -> MessageResponse<'q, 'a, A::IntoIter, N::IntoIter, S::IntoIter, D::IntoIter> where A: IntoIterator + Send + 'a, A::IntoIter: Send, N: IntoIterator + Send + 'a, N::IntoIter: Send, S: IntoIterator + Send + 'a, S::IntoIter: Send, D: IntoIterator + Send + 'a, D::IntoIter: Send, { MessageResponse { header, queries: self.queries, answers: answers.into_iter(), name_servers: name_servers.into_iter(), soa: soa.into_iter(), additionals: additionals.into_iter(), sig0: self.sig0.unwrap_or_default(), edns: self.edns, } } /// Construct a Response with no associated records pub fn build_no_records<'a>( self, header: Header, ) -> MessageResponse< 'q, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, > { MessageResponse { header, queries: self.queries, answers: Box::new(None.into_iter()), name_servers: Box::new(None.into_iter()), soa: Box::new(None.into_iter()), additionals: Box::new(None.into_iter()), sig0: self.sig0.unwrap_or_default(), edns: self.edns, } } /// Constructs a new error MessageResponse with associated settings /// /// # Arguments /// /// * `id` - request id to which this is a response /// * `op_code` - operation for which this is a response /// * `response_code` - the type of error pub fn error_msg<'a>( self, request_header: &Header, response_code: ResponseCode, ) -> MessageResponse< 'q, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, > { let mut header = Header::response_from_request(request_header); header.set_response_code(response_code); MessageResponse { header, queries: self.queries, answers: Box::new(None.into_iter()), name_servers: Box::new(None.into_iter()), soa: Box::new(None.into_iter()), additionals: Box::new(None.into_iter()), sig0: self.sig0.unwrap_or_default(), edns: self.edns, } } } #[cfg(test)] mod tests { use std::iter; use std::net::Ipv4Addr; use std::str::FromStr; use crate::proto::op::{Header, Message}; use crate::proto::rr::{DNSClass, Name, RData, Record}; use crate::proto::serialize::binary::BinEncoder; use super::*; #[test] fn test_truncation_ridiculous_number_answers() { let mut buf = Vec::with_capacity(512); { let mut encoder = BinEncoder::new(&mut buf); encoder.set_max_size(512); let answer = Record::from_rdata( Name::from_str("www.example.com.").unwrap(), 0, RData::A(Ipv4Addr::new(93, 184, 215, 14).into()), ) .set_dns_class(DNSClass::NONE) .clone(); let message = MessageResponse { header: Header::new(), queries: &Queries::empty(), answers: iter::repeat(&answer), name_servers: iter::once(&answer), soa: iter::once(&answer), additionals: iter::once(&answer), sig0: vec![], edns: None, }; message .destructive_emit(&mut encoder) .expect("failed to encode"); } let response = Message::from_vec(&buf).expect("failed to decode"); assert!(response.header().truncated()); assert!(response.answer_count() > 1); // should never have written the name server field... assert_eq!(response.name_server_count(), 0); } #[test] fn test_truncation_ridiculous_number_nameservers() { let mut buf = Vec::with_capacity(512); { let mut encoder = BinEncoder::new(&mut buf); encoder.set_max_size(512); let answer = Record::from_rdata( Name::from_str("www.example.com.").unwrap(), 0, RData::A(Ipv4Addr::new(93, 184, 215, 14).into()), ) .set_dns_class(DNSClass::NONE) .clone(); let message = MessageResponse { header: Header::new(), queries: &Queries::empty(), answers: iter::empty(), name_servers: iter::repeat(&answer), soa: iter::repeat(&answer), additionals: iter::repeat(&answer), sig0: vec![], edns: None, }; message .destructive_emit(&mut encoder) .expect("failed to encode"); } let response = Message::from_vec(&buf).expect("failed to decode"); assert!(response.header().truncated()); assert_eq!(response.answer_count(), 0); assert!(response.name_server_count() > 1); } // https://github.com/hickory-dns/hickory-dns/issues/2210 // If a client sends this DNS request to the hickory 0.24.0 DNS server: // // 08 00 00 00 00 01 00 00 00 00 00 00 c0 00 00 00 00 00 00 00 00 00 00 // 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 00 00 // // i.e.: // 08 00 ID // 00 00 flags // 00 01 QDCOUNT // 00 00 ANCOUNT // 00 00 NSCOUNT // 00 00 ARCOUNT // c0 00 QNAME // 00 00 QTYPE // 00 00 QCLASS // // hickory-dns fails the 2nd assert here while building the reply message // (really while remembering names for pointers): // // pub fn slice_of(&self, start: usize, end: usize) -> &[u8] { // assert!(start < self.offset); // assert!(end <= self.buffer.len()); // &self.buffer.buffer()[start..end] // } // The name is eight bytes long, but the current message size (after the // current offset of 12) is only six, because QueriesEmitAndCount::emit() // stored just the six bytes of the original encoded query: // // encoder.emit_vec(self.cached_serialized)?; #[test] fn bad_length_of_named_pointers() { use hickory_proto::serialize::binary::BinDecodable; let mut buf = Vec::with_capacity(512); let mut encoder = BinEncoder::new(&mut buf); let data: &[u8] = &[ 0x08u8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; let msg = MessageRequest::from_bytes(data).unwrap(); eprintln!("queries: {:?}", msg.queries()); MessageResponseBuilder::new(msg.raw_queries()) .build_no_records(Header::response_from_request(msg.header())) .destructive_emit(&mut encoder) .unwrap(); } } hickory-server-0.25.2/src/authority/mod.rs000064400000000000000000000143251046102023000166370ustar 00000000000000// 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. //! Module for `Catalog` of `Authority` zones which are responsible for storing `RRSet` records. use std::{io, sync::Arc}; use enum_as_inner::EnumAsInner; use thiserror::Error; use crate::proto::op::ResponseCode; use crate::proto::rr::{Record, rdata::SOA}; use crate::proto::{ProtoError, ProtoErrorKind}; #[cfg(feature = "recursor")] use crate::recursor::ErrorKind; #[cfg(feature = "resolver")] use crate::resolver::ResolveError; mod auth_lookup; #[allow(clippy::module_inception)] mod authority; pub(crate) mod authority_object; mod catalog; pub(crate) mod message_request; mod message_response; pub use self::auth_lookup::{ AnyRecords, AuthLookup, AuthLookupIter, LookupRecords, LookupRecordsIter, }; pub use self::authority::{Authority, LookupControlFlow, LookupOptions}; #[cfg(feature = "__dnssec")] pub use self::authority::{DnssecAuthority, Nsec3QueryInfo}; pub use self::authority_object::{AuthorityObject, DnssecSummary, EmptyLookup, LookupObject}; pub use self::catalog::Catalog; pub use self::message_request::{MessageRequest, Queries, UpdateRequest}; pub use self::message_response::{MessageResponse, MessageResponseBuilder}; /// Result of an Update operation pub type UpdateResult = Result; // TODO: should this implement Failure? #[allow(clippy::large_enum_variant)] /// A query could not be fulfilled #[derive(Debug, EnumAsInner, Error)] #[non_exhaustive] pub enum LookupError { /// A record at the same Name as the query exists, but not of the queried RecordType #[error("The name exists, but not for the record requested")] NameExists, /// There was an error performing the lookup #[error("Error performing lookup: {0}")] ResponseCode(ResponseCode), /// Proto error #[error("Proto error: {0}")] ProtoError(#[from] ProtoError), /// Resolve Error #[cfg(feature = "resolver")] #[error("Forward resolution error: {0}")] ResolveError(#[from] ResolveError), /// Recursive Resolver Error #[cfg(feature = "recursor")] #[error("Recursive resolution error: {0}")] RecursiveError(#[from] hickory_recursor::Error), /// An underlying IO error occurred #[error("io error: {0}")] Io(io::Error), } impl LookupError { /// Create a lookup error, specifying that a name exists at the location, but no matching RecordType pub fn for_name_exists() -> Self { Self::NameExists } /// This is a non-existent domain name pub fn is_nx_domain(&self) -> bool { match self { Self::ResponseCode(ResponseCode::NXDomain) => true, #[cfg(feature = "resolver")] Self::ResolveError(e) if e.is_nx_domain() => true, #[cfg(feature = "recursor")] Self::RecursiveError(e) if e.is_nx_domain() => true, _ => false, } } /// Returns true if no records were returned pub fn is_no_records_found(&self) -> bool { match self { #[cfg(feature = "resolver")] Self::ResolveError(e) if e.is_no_records_found() => true, #[cfg(feature = "recursor")] Self::RecursiveError(e) if e.is_no_records_found() => true, _ => false, } } /// Returns the SOA record, if the error contains one pub fn into_soa(self) -> Option>> { match self { #[cfg(feature = "resolver")] Self::ResolveError(e) => e.into_soa(), #[cfg(feature = "recursor")] Self::RecursiveError(e) => e.into_soa(), _ => None, } } /// Return authority records pub fn authorities(&self) -> Option> { match self { Self::ProtoError(e) => match e.kind() { ProtoErrorKind::NoRecordsFound { authorities, .. } => authorities.clone(), _ => None, }, #[cfg(feature = "recursor")] Self::RecursiveError(e) => match e.kind() { ErrorKind::Forward(fwd) => fwd.authorities.clone(), ErrorKind::Proto(proto) => match proto.kind() { ProtoErrorKind::NoRecordsFound { authorities, .. } => authorities.clone(), _ => None, }, _ => None, }, _ => None, } } /// This is a non-existent domain name pub fn is_refused(&self) -> bool { matches!(*self, Self::ResponseCode(ResponseCode::Refused)) } } impl From for LookupError { fn from(code: ResponseCode) -> Self { // this should never be a NoError debug_assert!(code != ResponseCode::NoError); Self::ResponseCode(code) } } impl From for LookupError { fn from(e: io::Error) -> Self { Self::Io(e) } } impl From for io::Error { fn from(e: LookupError) -> Self { Self::new(io::ErrorKind::Other, Box::new(e)) } } #[allow(deprecated)] mod zone_type { use serde::{Deserialize, Serialize}; /// The type of zone stored in a Catalog #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum ZoneType { /// This authority for a zone Primary, /// This authority for a zone, i.e. the Primary #[deprecated = "please read about Juneteenth"] Master, /// A secondary, i.e. replicated from the Primary Secondary, /// A secondary, i.e. replicated from the Primary #[deprecated = "please read about Juneteenth"] Slave, /// A cached zone that queries other nameservers External, } impl ZoneType { /// Is this an authoritative Authority, i.e. it owns the records of the zone. #[allow(deprecated)] pub fn is_authoritative(self) -> bool { matches!( self, Self::Primary | Self::Secondary | Self::Master | Self::Slave ) } } } pub use zone_type::ZoneType; hickory-server-0.25.2/src/error.rs000064400000000000000000000117531046102023000151630ustar 00000000000000/* * 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. */ //! All defined errors for Hickory DNS use std::{fmt, io}; use thiserror::Error; use crate::proto::serialize::txt::ParseError; #[cfg(feature = "backtrace")] use crate::proto::{ExtBacktrace, trace}; use crate::proto::{ProtoError, ProtoErrorKind}; /// The error kind for errors that get returned in the crate #[derive(Debug, Error)] #[non_exhaustive] pub enum PersistenceErrorKind { /// An error that occurred when recovering from journal #[error("error recovering from journal: {}", _0)] Recovery(&'static str), /// The number of inserted records didn't match the expected amount #[error("wrong insert count: {} expect: {}", got, expect)] WrongInsertCount { /// The number of inserted records got: usize, /// The number of records expected to be inserted expect: usize, }, // foreign /// An error got returned by the hickory-proto crate #[error("proto error: {0}")] Proto(#[from] ProtoError), /// An error got returned from the sqlite crate #[cfg(feature = "sqlite")] #[error("sqlite error: {0}")] Sqlite(#[from] rusqlite::Error), /// A request timed out #[error("request timed out")] Timeout, } /// The error type for errors that get returned in the crate #[derive(Debug, Error)] pub struct PersistenceError { kind: PersistenceErrorKind, #[cfg(feature = "backtrace")] backtrack: Option, } impl PersistenceError { /// Get the kind of the error pub fn kind(&self) -> &PersistenceErrorKind { &self.kind } } impl fmt::Display for PersistenceError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { cfg_if::cfg_if! { if #[cfg(feature = "backtrace")] { if let Some(backtrace) = &self.backtrack { fmt::Display::fmt(&self.kind, f)?; fmt::Debug::fmt(backtrace, f) } else { fmt::Display::fmt(&self.kind, f) } } else { fmt::Display::fmt(&self.kind, f) } } } } impl From for PersistenceError { fn from(kind: PersistenceErrorKind) -> Self { Self { kind, #[cfg(feature = "backtrace")] backtrack: trace!(), } } } impl From for PersistenceError { fn from(e: ProtoError) -> Self { match e.kind() { ProtoErrorKind::Timeout => PersistenceErrorKind::Timeout.into(), _ => PersistenceErrorKind::from(e).into(), } } } #[cfg(feature = "sqlite")] impl From for PersistenceError { fn from(e: rusqlite::Error) -> Self { PersistenceErrorKind::from(e).into() } } /// The error kind for errors that get returned in the crate #[derive(Debug, Error)] #[non_exhaustive] pub enum ConfigErrorKind { // foreign /// An error got returned from IO #[error("io error: {0}")] Io(#[from] io::Error), /// An error occurred while decoding toml data #[cfg(feature = "toml")] #[error("toml decode error: {0}")] TomlDecode(#[from] toml::de::Error), /// An error occurred while parsing a zone file #[error("failed to parse the zone file: {0}")] ZoneParse(#[from] ParseError), } /// The error type for errors that get returned in the crate #[derive(Debug)] pub struct ConfigError { kind: Box, #[cfg(feature = "backtrace")] backtrack: Option, } impl ConfigError { /// Get the kind of the error pub fn kind(&self) -> &ConfigErrorKind { &self.kind } } impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { cfg_if::cfg_if! { if #[cfg(feature = "backtrace")] { if let Some(backtrace) = &self.backtrack { fmt::Display::fmt(&self.kind, f)?; fmt::Debug::fmt(backtrace, f) } else { fmt::Display::fmt(&self.kind, f) } } else { fmt::Display::fmt(&self.kind, f) } } } } impl From for ConfigError where E: Into, { fn from(error: E) -> Self { let kind: ConfigErrorKind = error.into(); Self { kind: Box::new(kind), #[cfg(feature = "backtrace")] backtrack: trace!(), } } } hickory-server-0.25.2/src/lib.rs000064400000000000000000000057531046102023000146030ustar 00000000000000/* * 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. */ // LIBRARY WARNINGS #![warn( clippy::default_trait_access, clippy::dbg_macro, clippy::print_stdout, clippy::unimplemented, clippy::use_self, missing_copy_implementations, missing_docs, non_snake_case, non_upper_case_globals, rust_2018_idioms, unreachable_pub )] #![allow( clippy::single_component_path_imports, clippy::upper_case_acronyms, // can be removed on a major release boundary )] #![recursion_limit = "2048"] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] //! Hickory DNS is intended to be a fully compliant domain name server and client library. //! //! # Goals //! //! * Only safe Rust //! * All errors handled //! * Simple to manage servers //! * High level abstraction for clients //! * Secure dynamic update //! * New features for securing public information #[cfg(feature = "blocklist")] pub use crate::store::blocklist; pub use hickory_proto as proto; #[cfg(feature = "recursor")] pub use hickory_recursor as recursor; #[cfg(any(feature = "resolver", feature = "recursor"))] pub use hickory_resolver as resolver; mod access; pub mod authority; mod error; pub use error::{ConfigError, ConfigErrorKind, PersistenceError, PersistenceErrorKind}; pub mod server; pub mod store; pub use self::server::ServerFuture; /// Low-level types for DNSSEC operations #[cfg(feature = "__dnssec")] pub mod dnssec { use crate::proto::dnssec::Nsec3HashAlgorithm; use serde::Deserialize; use std::sync::Arc; /// The kind of non-existence proof provided by the nameserver #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum NxProofKind { /// Use NSEC Nsec, /// Use NSEC3 Nsec3 { /// The algorithm used to hash the names. #[serde(default)] algorithm: Nsec3HashAlgorithm, /// The salt used for hashing. #[serde(default = "default_salt")] salt: Arc<[u8]>, /// The number of hashing iterations. #[serde(default)] iterations: u16, /// The Opt-Out flag. #[serde(default)] opt_out: bool, }, } // MSRV: works in 1.80, fails in 1.78 fn default_salt() -> Arc<[u8]> { Arc::new([]) } } /// Returns the current version of Hickory DNS pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") } hickory-server-0.25.2/src/server/h2_handler.rs000064400000000000000000000107401046102023000173410ustar 00000000000000// 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. use std::{io, net::SocketAddr, sync::Arc}; use bytes::Bytes; use futures_util::lock::Mutex; use h2::server; use hickory_proto::{http::Version, rr::Record}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, warn}; use crate::{ access::AccessControl, authority::MessageResponse, proto::{h2::h2_server, xfer::Protocol}, server::{ ResponseInfo, request_handler::RequestHandler, response_handler::{ResponseHandler, encode_fallback_servfail_response}, }, }; pub(crate) async fn h2_handler( access: Arc, handler: Arc, io: I, src_addr: SocketAddr, dns_hostname: Option>, http_endpoint: Arc, shutdown: CancellationToken, ) where T: RequestHandler, I: AsyncRead + AsyncWrite + Unpin, { let dns_hostname = dns_hostname.clone(); let http_endpoint = http_endpoint.clone(); // Start the HTTP/2.0 connection handshake let mut h2 = match server::handshake(io).await { Ok(h2) => h2, Err(err) => { warn!("handshake error from {}: {}", src_addr, err); return; } }; // Accept all inbound HTTP/2.0 streams sent over the // connection. loop { let (request, respond) = tokio::select! { result = h2.accept() => match result { Some(Ok(next_request)) => next_request, Some(Err(err)) => { warn!("error accepting request {}: {}", src_addr, err); return; } None => { return; } }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. return }, }; debug!("Received request: {:#?}", request); let dns_hostname = dns_hostname.clone(); let http_endpoint = http_endpoint.clone(); let handler = handler.clone(); let access = access.clone(); let responder = HttpsResponseHandle(Arc::new(Mutex::new(respond))); tokio::spawn(async move { let body = match h2_server::message_from(dns_hostname, http_endpoint, request).await { Ok(bytes) => bytes, Err(err) => { warn!("error while handling request from {}: {}", src_addr, err); return; } }; super::handle_request(&body, src_addr, Protocol::Https, access, handler, responder) .await }); // we'll continue handling requests from here. } } #[derive(Clone)] struct HttpsResponseHandle(Arc>>); #[async_trait::async_trait] impl ResponseHandler for HttpsResponseHandle { async fn send_response<'a>( &mut self, response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result { use crate::proto::h2::HttpsError; use crate::proto::http::response; use crate::proto::serialize::binary::BinEncoder; let id = response.header().id(); let mut bytes = Vec::with_capacity(512); // mut block let info = { let mut encoder = BinEncoder::new(&mut bytes); response.destructive_emit(&mut encoder).or_else(|error| { error!(%error, "error encoding message"); encode_fallback_servfail_response(id, &mut bytes) })? }; let bytes = Bytes::from(bytes); let response = response::new(Version::Http2, bytes.len())?; debug!("sending response: {:#?}", response); let mut stream = self .0 .lock() .await .send_response(response, false) .map_err(HttpsError::from)?; stream.send_data(bytes, true).map_err(HttpsError::from)?; Ok(info) } } hickory-server-0.25.2/src/server/h3_handler.rs000064400000000000000000000107141046102023000173430ustar 00000000000000// 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. use std::{io, net::SocketAddr, sync::Arc}; use bytes::{Buf, Bytes}; use futures_util::lock::Mutex; use h3::server::RequestStream; use h3_quinn::BidiStream; use tokio_util::sync::CancellationToken; use tracing::{debug, error, warn}; use crate::{ access::AccessControl, authority::MessageResponse, server::{ ResponseInfo, request_handler::RequestHandler, response_handler::{ResponseHandler, encode_fallback_servfail_response}, }, }; use hickory_proto::{ ProtoError, h3::{H3Error, h3_server::H3Connection}, http::Version, rr::Record, xfer::Protocol, }; pub(crate) async fn h3_handler( access: Arc, handler: Arc, mut connection: H3Connection, src_addr: SocketAddr, _dns_hostname: Option>, shutdown: CancellationToken, ) -> Result<(), ProtoError> where T: RequestHandler, { // TODO: we should make this configurable let mut max_requests = 100u32; // Accept all inbound requests sent over the connection. loop { let (_, mut stream) = tokio::select! { result = connection.accept() => match result { Some(Ok(next_request)) => next_request, Some(Err(err)) => { warn!("error accepting request {}: {}", src_addr, err); return Err(err); } None => { break; } }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. break; }, }; let request = match stream .recv_data() .await .map_err(|e| ProtoError::from(format!("h3 stream receive data failed: {e}")))? { Some(mut request) => request.copy_to_bytes(request.remaining()), None => continue, }; debug!( "Received bytes {} from {src_addr} {request:?}", request.remaining() ); let handler = handler.clone(); let access = access.clone(); let stream = Arc::new(Mutex::new(stream)); let responder = H3ResponseHandle(stream.clone()); tokio::spawn(async move { super::handle_request(&request, src_addr, Protocol::H3, access, handler, responder) .await }); max_requests -= 1; if max_requests == 0 { warn!("exceeded request count, shutting down h3 conn: {src_addr}"); connection.shutdown().await?; break; } // we'll continue handling requests from here. } Ok(()) } #[derive(Clone)] struct H3ResponseHandle(Arc, Bytes>>>); #[async_trait::async_trait] impl ResponseHandler for H3ResponseHandle { async fn send_response<'a>( &mut self, response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result { use crate::proto::http::response; use crate::proto::serialize::binary::BinEncoder; let id = response.header().id(); let mut bytes = Vec::with_capacity(512); // mut block let info = { let mut encoder = BinEncoder::new(&mut bytes); response.destructive_emit(&mut encoder).or_else(|error| { error!(%error, "error encoding message"); encode_fallback_servfail_response(id, &mut bytes) })? }; let bytes = Bytes::from(bytes); let response = response::new(Version::Http3, bytes.len())?; debug!("sending response: {:#?}", response); let mut stream = self.0.lock().await; stream .send_response(response) .await .map_err(H3Error::from)?; stream.send_data(bytes).await.map_err(H3Error::from)?; stream.finish().await.map_err(H3Error::from)?; Ok(info) } } hickory-server-0.25.2/src/server/metrics.rs000064400000000000000000000231361046102023000170040ustar 00000000000000use hickory_proto::op::{Header, OpCode, ResponseCode}; use hickory_proto::xfer::Protocol; use metrics::{Counter, Unit, counter, describe_counter}; #[derive(Clone)] pub(super) struct ResponseHandlerMetrics { pub(super) proto: ProtocolMetrics, pub(super) operation: OpCodeMetrics, pub(super) request_flags: FlagMetrics, pub(super) response_code: ResponseCodeMetrics, pub(super) response_flags: FlagMetrics, } impl Default for ResponseHandlerMetrics { fn default() -> Self { Self { proto: ProtocolMetrics::default(), operation: OpCodeMetrics::default(), request_flags: FlagMetrics::new("request"), response_code: ResponseCodeMetrics::default(), response_flags: FlagMetrics::new("response"), } } } #[derive(Clone)] pub(super) struct FlagMetrics { authoritative: Counter, authentic_data: Counter, checking_disabled: Counter, recursion_available: Counter, recursion_desired: Counter, truncation: Counter, } impl FlagMetrics { fn new(direction: &'static str) -> Self { let flags_name = format!("hickory_{}_flags_total", direction); let key = "flag"; Self { authoritative: { let new = counter!(flags_name.clone(), key => "aa"); describe_counter!( flags_name.clone(), Unit::Count, "number of dns request flags" ); new }, authentic_data: counter!(flags_name.clone(), key => "ad"), checking_disabled: counter!(flags_name.clone(), key => "cd"), recursion_available: counter!(flags_name.clone(), key => "ra"), recursion_desired: counter!(flags_name.clone(), key => "rd"), truncation: counter!(flags_name, key => "tc"), } } } impl FlagMetrics { pub(super) fn increment(&self, header: &Header) { if header.authoritative() { self.authoritative.increment(1); } if header.authentic_data() { self.authentic_data.increment(1); } if header.checking_disabled() { self.checking_disabled.increment(1); } if header.recursion_available() { self.recursion_available.increment(1); } if header.recursion_desired() { self.recursion_desired.increment(1); } if header.truncated() { self.truncation.increment(1); } } } #[derive(Clone)] pub(super) struct ProtocolMetrics { udp: Counter, tcp: Counter, #[cfg(feature = "__tls")] tls: Counter, #[cfg(feature = "__https")] https: Counter, #[cfg(feature = "__quic")] quic: Counter, #[cfg(feature = "__h3")] h3: Counter, } impl Default for ProtocolMetrics { fn default() -> Self { let request_protocols_name = "hickory_request_protocols_total"; let key = "protocol"; Self { udp: { let new = counter!(request_protocols_name, key => "udp"); describe_counter!( request_protocols_name, Unit::Count, "number of dns requests operations" ); new }, tcp: counter!(request_protocols_name, key => "tcp"), #[cfg(feature = "__tls")] tls: counter!(request_protocols_name, key => "tls"), #[cfg(feature = "__https")] https: counter!(request_protocols_name, key => "https"), #[cfg(feature = "__quic")] quic: counter!(request_protocols_name, key => "quic"), #[cfg(feature = "__h3")] h3: counter!(request_protocols_name, key => "http3"), } } } impl ProtocolMetrics { pub(super) fn increment(&self, proto: &Protocol) { match proto { Protocol::Udp => self.udp.increment(1), Protocol::Tcp => self.tcp.increment(1), #[cfg(feature = "__tls")] Protocol::Tls => self.tls.increment(1), #[cfg(feature = "__https")] Protocol::Https => self.https.increment(1), #[cfg(feature = "__quic")] Protocol::Quic => self.quic.increment(1), #[cfg(feature = "__h3")] Protocol::H3 => self.h3.increment(1), _ => {} } } } #[derive(Clone)] pub(super) struct OpCodeMetrics { query: Counter, status: Counter, notify: Counter, update: Counter, unknown: Counter, } impl Default for OpCodeMetrics { fn default() -> Self { let request_operations_name = "hickory_request_operations_total"; let key = "operation"; Self { query: { let new = counter!(request_operations_name, key => "query"); describe_counter!( request_operations_name, Unit::Count, "number of dns request operations" ); new }, status: counter!(request_operations_name, key => "status"), notify: counter!(request_operations_name, key => "notify"), update: counter!(request_operations_name, key => "update"), unknown: counter!(request_operations_name, key => "unknown"), } } } impl OpCodeMetrics { pub(super) fn increment(&self, op_code: &OpCode) { match op_code { OpCode::Query => self.query.increment(1), OpCode::Status => self.status.increment(1), OpCode::Notify => self.notify.increment(1), OpCode::Update => self.update.increment(1), OpCode::Unknown(_) => self.unknown.increment(1), } } } #[derive(Clone)] pub(super) struct ResponseCodeMetrics { no_error: Counter, form_error: Counter, serv_fail: Counter, nx_domain: Counter, not_imp: Counter, refused: Counter, yx_domain: Counter, yx_rrset: Counter, nx_rrset: Counter, not_auth: Counter, not_zone: Counter, bad_vers: Counter, bad_sig: Counter, bad_key: Counter, bad_time: Counter, bad_mode: Counter, bad_name: Counter, bad_alg: Counter, bad_trunc: Counter, bad_cookie: Counter, unknown: Counter, } impl Default for ResponseCodeMetrics { fn default() -> Self { let response_codes_name = "hickory_response_codes_total"; let key = "code"; Self { no_error: { let new = counter!(response_codes_name, "code" => "no_error"); describe_counter!( response_codes_name, Unit::Count, "number of dns response codes" ); new }, form_error: counter!(response_codes_name, key => "form_error"), serv_fail: counter!(response_codes_name, key => "serv_fail"), nx_domain: counter!(response_codes_name, key => "nx_domain"), not_imp: counter!(response_codes_name, key => "not_imp"), refused: counter!(response_codes_name, key => "refused"), yx_domain: counter!(response_codes_name, key => "yx_domain"), yx_rrset: counter!(response_codes_name, key => "yx_rrset"), nx_rrset: counter!(response_codes_name, key => "nx_rrset"), not_auth: counter!(response_codes_name, key => "not_auth"), not_zone: counter!(response_codes_name, key => "not_zone"), bad_vers: counter!(response_codes_name, key => "bad_vers"), bad_sig: counter!(response_codes_name, key => "bad_sig"), bad_key: counter!(response_codes_name, key => "bad_key"), bad_time: counter!(response_codes_name, key => "bad_time"), bad_mode: counter!(response_codes_name, key => "bad_mode"), bad_name: counter!(response_codes_name, key => "bad_name"), bad_alg: counter!(response_codes_name, key => "bad_alg"), bad_trunc: counter!(response_codes_name, key => "bad_trunc"), bad_cookie: counter!(response_codes_name, key => "bad_cookie"), unknown: counter!(response_codes_name, key => "unknown"), } } } impl ResponseCodeMetrics { pub(super) fn increment(&self, response_code: &ResponseCode) { match response_code { ResponseCode::NoError => self.no_error.increment(1), ResponseCode::FormErr => self.form_error.increment(1), ResponseCode::ServFail => self.serv_fail.increment(1), ResponseCode::NXDomain => self.nx_domain.increment(1), ResponseCode::NotImp => self.not_imp.increment(1), ResponseCode::Refused => self.refused.increment(1), ResponseCode::YXDomain => self.yx_domain.increment(1), ResponseCode::YXRRSet => self.yx_rrset.increment(1), ResponseCode::NXRRSet => self.nx_rrset.increment(1), ResponseCode::NotAuth => self.not_auth.increment(1), ResponseCode::NotZone => self.not_zone.increment(1), ResponseCode::BADVERS => self.bad_vers.increment(1), ResponseCode::BADSIG => self.bad_sig.increment(1), ResponseCode::BADKEY => self.bad_key.increment(1), ResponseCode::BADTIME => self.bad_time.increment(1), ResponseCode::BADMODE => self.bad_mode.increment(1), ResponseCode::BADNAME => self.bad_name.increment(1), ResponseCode::BADALG => self.bad_alg.increment(1), ResponseCode::BADTRUNC => self.bad_trunc.increment(1), ResponseCode::BADCOOKIE => self.bad_cookie.increment(1), ResponseCode::Unknown(_) => self.unknown.increment(1), } } } hickory-server-0.25.2/src/server/mod.rs000064400000000000000000001475041046102023000161230ustar 00000000000000// 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. //! `Server` component for hosting a domain name servers operations. use std::{ io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, time::Duration, }; use futures_util::{FutureExt, StreamExt}; use hickory_proto::ProtoErrorKind; use ipnet::IpNet; #[cfg(feature = "__tls")] use rustls::{ServerConfig, server::ResolvesServerCert}; #[cfg(feature = "__tls")] use tokio::time::timeout; use tokio::{net, task::JoinSet}; use tokio_util::sync::CancellationToken; use tracing::{debug, info, warn}; #[cfg(feature = "__tls")] use crate::proto::rustls::default_provider; use crate::{ access::AccessControl, authority::{MessageRequest, MessageResponseBuilder, Queries}, proto::{ BufDnsStreamHandle, ProtoError, op::{Header, LowerQuery, MessageType, ResponseCode}, rr::Record, runtime::{TokioRuntimeProvider, iocompat::AsyncIoTokioAsStd}, serialize::binary::{BinDecodable, BinDecoder}, tcp::TcpStream, udp::UdpStream, xfer::{Protocol, SerialMessage}, }, }; #[cfg(feature = "__https")] mod h2_handler; #[cfg(feature = "__h3")] mod h3_handler; #[cfg(feature = "__quic")] mod quic_handler; mod request_handler; pub use request_handler::{Request, RequestHandler, RequestInfo, ResponseInfo}; mod response_handler; pub use response_handler::{ResponseHandle, ResponseHandler}; #[cfg(feature = "metrics")] mod metrics; #[cfg(feature = "metrics")] use metrics::ResponseHandlerMetrics; mod timeout_stream; pub use timeout_stream::TimeoutStream; // TODO, would be nice to have a Slab for buffers here... /// A Futures based implementation of a DNS server pub struct ServerFuture { handler: Arc, join_set: JoinSet>, shutdown_token: CancellationToken, access: Arc, } impl ServerFuture { /// Creates a new ServerFuture with the specified Handler. pub fn new(handler: T) -> Self { Self::with_access(handler, &[], &[]) } /// Creates a new ServerFuture with the specified Handler and Access pub fn with_access(handler: T, denied_networks: &[IpNet], allowed_networks: &[IpNet]) -> Self { let mut access = AccessControl::default(); access.insert_deny(denied_networks); access.insert_allow(allowed_networks); Self { handler: Arc::new(handler), join_set: JoinSet::new(), shutdown_token: CancellationToken::new(), access: Arc::new(access), } } /// Register a UDP socket. Should be bound before calling this function. pub fn register_socket(&mut self, socket: net::UdpSocket) { debug!("registering udp: {:?}", socket); // create the new UdpStream, the IP address isn't relevant, and ideally goes essentially no where. // the address used is acquired from the inbound queries let (mut stream, stream_handle) = UdpStream::::with_bound(socket, ([127, 255, 255, 254], 0).into()); let shutdown = self.shutdown_token.clone(); let handler = self.handler.clone(); let access = self.access.clone(); // this spawns a ForEach future which handles all the requests into a Handler. self.join_set.spawn({ async move { let mut inner_join_set = JoinSet::new(); loop { let message = tokio::select! { message = stream.next() => match message { None => break, Some(message) => message, }, _ = shutdown.cancelled() => break, }; let message = match message { Err(e) => { warn!("error receiving message on udp_socket: {}", e); if is_unrecoverable_socket_error(&e) { break; } continue; } Ok(message) => message, }; let src_addr = message.addr(); debug!("received udp request from: {}", src_addr); // verify that the src address is safe for responses if let Err(e) = sanitize_src_address(src_addr) { warn!( "address can not be responded to {src_addr}: {e}", src_addr = src_addr, e = e ); continue; } let handler = handler.clone(); let access = access.clone(); let stream_handle = stream_handle.with_remote_addr(src_addr); inner_join_set.spawn(async move { handle_raw_request(message, Protocol::Udp, access, handler, stream_handle) .await; }); reap_tasks(&mut inner_join_set); } if shutdown.is_cancelled() { Ok(()) } else { // TODO: let's consider capturing all the initial configuration details so that the socket could be recreated... Err(ProtoError::from("unexpected close of UDP socket")) } } }); } /// Register a UDP socket. Should be bound before calling this function. pub fn register_socket_std(&mut self, socket: std::net::UdpSocket) -> io::Result<()> { socket.set_nonblocking(true)?; self.register_socket(net::UdpSocket::from_std(socket)?); Ok(()) } /// Register a TcpListener to the Server. This should already be bound to either an IPv6 or an /// IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. pub fn register_listener(&mut self, listener: net::TcpListener, timeout: Duration) { debug!("register tcp: {:?}", listener); let handler = self.handler.clone(); let access = self.access.clone(); // for each incoming request... let shutdown = self.shutdown_token.clone(); self.join_set.spawn(async move { let mut inner_join_set = JoinSet::new(); loop { let (tcp_stream, src_addr) = tokio::select! { tcp_stream = listener.accept() => match tcp_stream { Ok((t, s)) => (t, s), Err(e) => { debug!("error receiving TCP tcp_stream error: {}", e); if is_unrecoverable_socket_error(&e) { break; } continue; }, }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. Break out of the loop. break; }, }; // verify that the src address is safe for responses if let Err(e) = sanitize_src_address(src_addr) { warn!( "address can not be responded to {src_addr}: {e}", src_addr = src_addr, e = e ); continue; } let handler = handler.clone(); let access = access.clone(); // and spawn to the io_loop inner_join_set.spawn(async move { debug!("accepted request from: {}", src_addr); // take the created stream... let (buf_stream, stream_handle) = TcpStream::from_stream(AsyncIoTokioAsStd(tcp_stream), src_addr); let mut timeout_stream = TimeoutStream::new(buf_stream, timeout); while let Some(message) = timeout_stream.next().await { let message = match message { Ok(message) => message, Err(e) => { debug!( "error in TCP request_stream src: {} error: {}", src_addr, e ); // we're going to bail on this connection... return; } }; // we don't spawn here to limit clients from getting too many resources handle_raw_request( message, Protocol::Tcp, access.clone(), handler.clone(), stream_handle.clone(), ) .await; } }); reap_tasks(&mut inner_join_set); } if shutdown.is_cancelled() { Ok(()) } else { Err(ProtoError::from("unexpected close of socket")) } }); } /// Register a TcpListener to the Server. This should already be bound to either an IPv6 or an /// IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. pub fn register_listener_std( &mut self, listener: std::net::TcpListener, timeout: Duration, ) -> io::Result<()> { listener.set_nonblocking(true)?; self.register_listener(net::TcpListener::from_std(listener)?, timeout); Ok(()) } /// Register a TlsListener to the Server. The TlsListener should already be bound to either an /// IPv6 or an IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP (needs to be on a different port from standard TCP connections) socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. /// * `tls_config` - rustls server config #[cfg(feature = "__tls")] pub fn register_tls_listener_with_tls_config( &mut self, listener: net::TcpListener, handshake_timeout: Duration, tls_config: Arc, ) -> io::Result<()> { use crate::proto::rustls::tls_from_stream; use tokio_rustls::TlsAcceptor; let handler = self.handler.clone(); let access = self.access.clone(); debug!("registered tcp: {:?}", listener); let tls_acceptor = TlsAcceptor::from(tls_config); // for each incoming request... let shutdown = self.shutdown_token.clone(); self.join_set.spawn(async move { let mut inner_join_set = JoinSet::new(); loop { let (tcp_stream, src_addr) = tokio::select! { tcp_stream = listener.accept() => match tcp_stream { Ok((t, s)) => (t, s), Err(e) => { debug!("error receiving TLS tcp_stream error: {}", e); if is_unrecoverable_socket_error(&e) { break; } continue; }, }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. Break out of the loop. break; }, }; // verify that the src address is safe for responses if let Err(e) = sanitize_src_address(src_addr) { warn!( "address can not be responded to {src_addr}: {e}", src_addr = src_addr, e = e ); continue; } let handler = handler.clone(); let access = access.clone(); let tls_acceptor = tls_acceptor.clone(); // kick out to a different task immediately, let them do the TLS handshake inner_join_set.spawn(async move { debug!("starting TLS request from: {}", src_addr); // perform the TLS let Ok(tls_stream) = timeout(handshake_timeout, tls_acceptor.accept(tcp_stream)).await else { warn!("tls timeout expired during handshake"); return; }; let tls_stream = match tls_stream { Ok(tls_stream) => AsyncIoTokioAsStd(tls_stream), Err(e) => { debug!("tls handshake src: {} error: {}", src_addr, e); return; } }; debug!("accepted TLS request from: {}", src_addr); let (buf_stream, stream_handle) = tls_from_stream(tls_stream, src_addr); let mut timeout_stream = TimeoutStream::new(buf_stream, handshake_timeout); while let Some(message) = timeout_stream.next().await { let message = match message { Ok(message) => message, Err(e) => { debug!( "error in TLS request_stream src: {:?} error: {}", src_addr, e ); // kill this connection return; } }; handle_raw_request( message, Protocol::Tls, access.clone(), handler.clone(), stream_handle.clone(), ) .await; } }); reap_tasks(&mut inner_join_set); } if shutdown.is_cancelled() { Ok(()) } else { Err(ProtoError::from("unexpected close of socket")) } }); Ok(()) } /// Register a TlsListener to the Server by providing a pkcs12 certificate and key. The TlsListener /// should already be bound to either an IPv6 or an IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP (needs to be on a different port from standard TCP connections) socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. /// * `server_cert_resolver` - resolver for the certificate and key used to announce to clients #[cfg(feature = "__tls")] pub fn register_tls_listener( &mut self, listener: net::TcpListener, timeout: Duration, server_cert_resolver: Arc, ) -> io::Result<()> { let config = tls_server_config(b"dot", server_cert_resolver)?; Self::register_tls_listener_with_tls_config(self, listener, timeout, Arc::new(config)) } /// Register a TcpListener for HTTPS (h2) to the Server for supporting DoH (DNS-over-HTTPS). The TcpListener should already be bound to either an /// IPv6 or an IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP (needs to be on a different port from standard TCP connections) socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. /// * `server_cert_resolver` - resolver for the certificate and key used to announce to clients #[cfg(feature = "__https")] pub fn register_https_listener( &mut self, listener: net::TcpListener, // TODO: need to set a timeout between requests. handshake_timeout: Duration, server_cert_resolver: Arc, dns_hostname: Option, http_endpoint: String, ) -> io::Result<()> { use crate::server::h2_handler::h2_handler; use tokio_rustls::TlsAcceptor; let dns_hostname: Option> = dns_hostname.map(|n| n.into()); let http_endpoint: Arc = Arc::from(http_endpoint); let handler = self.handler.clone(); let access = self.access.clone(); debug!("registered https: {listener:?}"); let tls_acceptor = TlsAcceptor::from(Arc::new(tls_server_config(b"h2", server_cert_resolver)?)); // for each incoming request... let shutdown = self.shutdown_token.clone(); self.join_set.spawn(async move { let mut inner_join_set = JoinSet::new(); loop { let shutdown = shutdown.clone(); let (tcp_stream, src_addr) = tokio::select! { tcp_stream = listener.accept() => match tcp_stream { Ok((t, s)) => (t, s), Err(e) => { debug!("error receiving HTTPS tcp_stream error: {}", e); if is_unrecoverable_socket_error(&e) { break; } continue; }, }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. Break out of the loop. break; }, }; // verify that the src address is safe for responses if let Err(e) = sanitize_src_address(src_addr) { warn!("address can not be responded to {src_addr}: {e}"); continue; } let handler = handler.clone(); let access = access.clone(); let tls_acceptor = tls_acceptor.clone(); let dns_hostname = dns_hostname.clone(); let http_endpoint = http_endpoint.clone(); inner_join_set.spawn(async move { debug!("starting HTTPS request from: {src_addr}"); // TODO: need to consider timeout of total connect... // take the created stream... let Ok(tls_stream) = timeout(handshake_timeout, tls_acceptor.accept(tcp_stream)).await else { warn!("https timeout expired during handshake"); return; }; let tls_stream = match tls_stream { Ok(tls_stream) => tls_stream, Err(e) => { debug!("https handshake src: {src_addr} error: {e}"); return; } }; debug!("accepted HTTPS request from: {src_addr}"); h2_handler( access, handler, tls_stream, src_addr, dns_hostname, http_endpoint, shutdown.clone(), ) .await; }); reap_tasks(&mut inner_join_set); } if shutdown.is_cancelled() { Ok(()) } else { Err(ProtoError::from("unexpected close of socket")) } }); Ok(()) } /// Register a UdpSocket to the Server for supporting DoQ (DNS-over-QUIC). The UdpSocket should already be bound to either an /// IPv6 or an IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP (needs to be on a different port from standard TCP connections) socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. /// * `server_cert_resolver` - resolver for certificate and key used to announce to clients #[cfg(feature = "__quic")] pub fn register_quic_listener( &mut self, socket: net::UdpSocket, // TODO: need to set a timeout between requests. _timeout: Duration, server_cert_resolver: Arc, dns_hostname: Option, ) -> io::Result<()> { use crate::proto::quic::QuicServer; use crate::server::quic_handler::quic_handler; let dns_hostname: Option> = dns_hostname.map(|n| n.into()); let handler = self.handler.clone(); let access = self.access.clone(); debug!("registered quic: {:?}", socket); let mut server = QuicServer::with_socket(socket, server_cert_resolver)?; // for each incoming request... let shutdown = self.shutdown_token.clone(); self.join_set.spawn(async move { let mut inner_join_set = JoinSet::new(); loop { let shutdown = shutdown.clone(); let (streams, src_addr) = tokio::select! { result = server.next() => match result { Ok(Some(c)) => c, Ok(None) => continue, Err(e) => { debug!("error receiving quic connection: {e}"); continue; } }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. Break out of the loop. break; }, }; // verify that the src address is safe for responses // TODO: we're relying the quinn library to actually validate responses before we get here, but this check is still worth doing if let Err(e) = sanitize_src_address(src_addr) { warn!( "address can not be responded to {src_addr}: {e}", src_addr = src_addr, e = e ); continue; } let handler = handler.clone(); let access = access.clone(); let dns_hostname = dns_hostname.clone(); inner_join_set.spawn(async move { debug!("starting quic stream request from: {src_addr}"); // TODO: need to consider timeout of total connect... let result = quic_handler( access, handler, streams, src_addr, dns_hostname, shutdown.clone(), ) .await; if let Err(e) = result { warn!("quic stream processing failed from {src_addr}: {e}") } }); reap_tasks(&mut inner_join_set); } Ok(()) }); Ok(()) } /// Register a UdpSocket to the Server for supporting DoH3 (DNS-over-HTTP/3). The UdpSocket should already be bound to either an /// IPv6 or an IPv4 address. /// /// To make the server more resilient to DOS issues, there is a timeout. Care should be taken /// to not make this too low depending on use cases. /// /// # Arguments /// * `listener` - a bound TCP (needs to be on a different port from standard TCP connections) socket /// * `timeout` - timeout duration of incoming requests, any connection that does not send /// requests within this time period will be closed. In the future it should be /// possible to create long-lived queries, but these should be from trusted sources /// only, this would require some type of whitelisting. /// * `server_cert_resolver` - resolver for certificate and key used to announce to clients #[cfg(feature = "__h3")] pub fn register_h3_listener( &mut self, socket: net::UdpSocket, // TODO: need to set a timeout between requests. _timeout: Duration, server_cert_resolver: Arc, dns_hostname: Option, ) -> io::Result<()> { use crate::proto::h3::h3_server::H3Server; use crate::server::h3_handler::h3_handler; let dns_hostname: Option> = dns_hostname.map(|n| n.into()); let handler = self.handler.clone(); let access = self.access.clone(); debug!("registered h3: {:?}", socket); let mut server = H3Server::with_socket(socket, server_cert_resolver)?; // for each incoming request... let shutdown = self.shutdown_token.clone(); self.join_set.spawn(async move { let mut inner_join_set = JoinSet::new(); loop { let shutdown = shutdown.clone(); let (streams, src_addr) = tokio::select! { result = server.accept() => match result { Ok(Some(c)) => c, Ok(None) => continue, Err(e) => { debug!("error receiving h3 connection: {e}"); continue; } }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. Break out of the loop. break; }, }; // verify that the src address is safe for responses // TODO: we're relying the quinn library to actually validate responses before we get here, but this check is still worth doing if let Err(e) = sanitize_src_address(src_addr) { warn!( "address can not be responded to {src_addr}: {e}", src_addr = src_addr, e = e ); continue; } let handler = handler.clone(); let access = access.clone(); let dns_hostname = dns_hostname.clone(); inner_join_set.spawn(async move { debug!("starting h3 stream request from: {src_addr}"); // TODO: need to consider timeout of total connect... let result = h3_handler( access, handler, streams, src_addr, dns_hostname, shutdown.clone(), ) .await; if let Err(e) = result { warn!("h3 stream processing failed from {src_addr}: {e}") } }); reap_tasks(&mut inner_join_set); } Ok(()) }); Ok(()) } /// Triggers a graceful shutdown the server. All background tasks will stop accepting /// new connections and the returned future will complete once all tasks have terminated. pub async fn shutdown_gracefully(&mut self) -> Result<(), ProtoError> { self.shutdown_token.cancel(); // Wait for the server to complete. block_until_done(&mut self.join_set).await } /// Returns a reference to the [`CancellationToken`] used to gracefully shut down the server. /// /// Once cancellation is requested, all background tasks will stop accepting new connections, /// and `block_until_done()` will complete once all tasks have terminated. pub fn shutdown_token(&self) -> &CancellationToken { &self.shutdown_token } /// This will run until all background tasks complete. If one or more tasks return an error, /// one will be chosen as the returned error for this future. pub async fn block_until_done(&mut self) -> Result<(), ProtoError> { block_until_done(&mut self.join_set).await } } async fn block_until_done( join_set: &mut JoinSet>, ) -> Result<(), ProtoError> { if join_set.is_empty() { warn!("block_until_done called with no pending tasks"); return Ok(()); } // Now wait for all of the tasks to complete. let mut out = Ok(()); while let Some(join_result) = join_set.join_next().await { match join_result { Ok(result) => { match result { Ok(_) => (), Err(e) => { // Save the last error. out = Err(e); } } } Err(e) => return Err(ProtoError::from(format!("Internal error in spawn: {e}"))), } } out } /// Reap finished tasks from a `JoinSet`, without awaiting or blocking. fn reap_tasks(join_set: &mut JoinSet<()>) { while FutureExt::now_or_never(join_set.join_next()) .flatten() .is_some() {} } pub(crate) async fn handle_raw_request( message: SerialMessage, protocol: Protocol, access: Arc, request_handler: Arc, response_handler: BufDnsStreamHandle, ) { let src_addr = message.addr(); let response_handler = ResponseHandle::new(message.addr(), response_handler, protocol); handle_request( message.bytes(), src_addr, protocol, access, request_handler, response_handler, ) .await; } #[cfg(feature = "__tls")] fn tls_server_config( protocol: &[u8], server_cert_resolver: Arc, ) -> io::Result { let mut config = ServerConfig::builder_with_provider(Arc::new(default_provider())) .with_safe_default_protocol_versions() .map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("error creating TLS acceptor: {e}"), ) })? .with_no_client_auth() .with_cert_resolver(server_cert_resolver); config.alpn_protocols = vec![protocol.to_vec()]; Ok(config) } #[derive(Clone)] struct ReportingResponseHandler { request_header: Header, queries: Vec, protocol: Protocol, src_addr: SocketAddr, handler: R, #[cfg(feature = "metrics")] metrics: ResponseHandlerMetrics, } #[async_trait::async_trait] #[allow(clippy::uninlined_format_args)] impl ResponseHandler for ReportingResponseHandler { async fn send_response<'a>( &mut self, response: crate::authority::MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result { let response_info = self.handler.send_response(response).await?; let id = self.request_header.id(); let rid = response_info.id(); if id != rid { warn!("request id:{id} does not match response id:{rid}"); debug_assert_eq!(id, rid, "request id and response id should match"); } let rflags = response_info.flags(); let answer_count = response_info.answer_count(); let authority_count = response_info.name_server_count(); let additional_count = response_info.additional_count(); let response_code = response_info.response_code(); info!( "request:{id} src:{proto}://{addr}#{port} {op} qflags:{qflags} response:{code:?} rr:{answers}/{authorities}/{additionals} rflags:{rflags}", id = rid, proto = self.protocol, addr = self.src_addr.ip(), port = self.src_addr.port(), op = self.request_header.op_code(), qflags = self.request_header.flags(), code = response_code, answers = answer_count, authorities = authority_count, additionals = additional_count, rflags = rflags ); for query in self.queries.iter() { info!( "query:{query}:{qtype}:{class}", query = query.name(), qtype = query.query_type(), class = query.query_class() ); } #[cfg(feature = "metrics")] self.metrics.update(self, &response_info); Ok(response_info) } } #[cfg(feature = "metrics")] impl ResponseHandlerMetrics { fn update( &self, response_handler: &ReportingResponseHandler, response_info: &ResponseInfo, ) { self.proto.increment(&response_handler.protocol); self.operation .increment(&response_handler.request_header.op_code()); self.request_flags .increment(&response_handler.request_header); self.response_code.increment(&response_info.response_code()); self.response_flags.increment(response_info); } } pub(crate) async fn handle_request( message_bytes: &[u8], src_addr: SocketAddr, protocol: Protocol, access: Arc, request_handler: Arc, response_handler: R, ) { let mut decoder = BinDecoder::new(message_bytes); // method to handle the request let inner_handle_request = |message: MessageRequest, response_handler: R| async move { if message.message_type() == MessageType::Response { // Don't process response messages to avoid DoS attacks from reflection. return; } let id = message.id(); let qflags = message.header().flags(); let qop_code = message.op_code(); let message_type = message.message_type(); let is_dnssec = message.edns().is_some_and(|edns| edns.flags().dnssec_ok); let request = Request::new(message, src_addr, protocol); debug!( "request:{id} src:{proto}://{addr}#{port} type:{message_type} dnssec:{is_dnssec} {op} qflags:{qflags}", id = id, proto = protocol, addr = src_addr.ip(), port = src_addr.port(), message_type = message_type, is_dnssec = is_dnssec, op = qop_code, qflags = qflags ); for query in request.queries().iter() { debug!( "query:{query}:{qtype}:{class}", query = query.name(), qtype = query.query_type(), class = query.query_class() ); } // The reporter will handle making sure to log the result of the request let queries = request.queries().to_vec(); let reporter = ReportingResponseHandler { request_header: *request.header(), queries, protocol, src_addr, handler: response_handler, #[cfg(feature = "metrics")] metrics: ResponseHandlerMetrics::default(), }; request_handler.handle_request(&request, reporter).await; }; // method to return an error to the client let error_response_handler = |protocol: Protocol, src_addr: SocketAddr, header: Header, queries: Queries, response_code: ResponseCode, error: Box, response_handler: R| async move { // debug for more info on why the message parsing failed debug!( "request:{id} src:{proto}://{addr}#{port} type:{message_type} {op}:{response_code}:{error}", id = header.id(), proto = protocol, addr = src_addr.ip(), port = src_addr.port(), message_type = header.message_type(), op = header.op_code(), response_code = response_code, error = error, ); // The reporter will handle making sure to log the result of the request let mut reporter = ReportingResponseHandler { request_header: header, queries: queries.queries().to_vec(), protocol, src_addr, handler: response_handler, #[cfg(feature = "metrics")] metrics: ResponseHandlerMetrics::default(), }; let response = MessageResponseBuilder::new(&queries); let result = reporter .send_response(response.error_msg(&header, response_code)) .await; if let Err(e) = result { warn!("failed to return FormError to client: {}", e); } }; if !access.allow(src_addr.ip()) { info!( "request:Refused src:{proto}://{addr}#{port}", proto = protocol, addr = src_addr.ip(), port = src_addr.port(), ); let Ok(header) = Header::read(&mut decoder) else { // This will only fail if the message is less than twelve bytes long. Such messages are // definitely not valid DNS queries, so it should be fine to return without sending a // response. return; }; let queries = match Queries::read(&mut decoder, header.query_count() as usize) { Ok(queries) => queries, Err(_) => Queries::empty(), }; error_response_handler( protocol, src_addr, header, queries, ResponseCode::Refused, Box::new(ProtoErrorKind::RequestRefused.into()), response_handler, ) .await; return; } // Attempt to decode the message match MessageRequest::read(&mut decoder) { Ok(message) => { inner_handle_request(message, response_handler).await; } Err(ProtoError { kind, .. }) if kind.as_form_error().is_some() => { // We failed to parse the request due to some issue in the message, but the header is available, so we can respond let (header, error) = kind .into_form_error() .expect("as form_error already confirmed this is a FormError"); let queries = Queries::empty(); error_response_handler( protocol, src_addr, header, queries, ResponseCode::FormErr, error, response_handler, ) .await; } Err(error) => info!( "request:Failed src:{proto}://{addr}#{port} error:{error}", proto = protocol, addr = src_addr.ip(), port = src_addr.port(), ), } } /// Checks if the IP address is safe for returning messages /// /// Examples of unsafe addresses are any with a port of `0` /// /// # Returns /// /// Error if the address should not be used for returned requests fn sanitize_src_address(src: SocketAddr) -> Result<(), String> { // currently checks that the src address aren't either the undefined IPv4 or IPv6 address, and not port 0. if src.port() == 0 { return Err(format!("cannot respond to src on port 0: {src}")); } fn verify_v4(src: Ipv4Addr) -> Result<(), String> { if src.is_unspecified() { return Err(format!("cannot respond to unspecified v4 addr: {src}")); } if src.is_broadcast() { return Err(format!("cannot respond to broadcast v4 addr: {src}")); } // TODO: add check for is_reserved when that stabilizes Ok(()) } fn verify_v6(src: Ipv6Addr) -> Result<(), String> { if src.is_unspecified() { return Err(format!("cannot respond to unspecified v6 addr: {src}")); } Ok(()) } // currently checks that the src address aren't either the undefined IPv4 or IPv6 address, and not port 0. match src.ip() { IpAddr::V4(v4) => verify_v4(v4), IpAddr::V6(v6) => verify_v6(v6), } } fn is_unrecoverable_socket_error(err: &io::Error) -> bool { matches!( err.kind(), io::ErrorKind::NotConnected | io::ErrorKind::ConnectionAborted ) } #[cfg(test)] mod tests { use super::*; use crate::authority::Catalog; use futures_util::future; #[cfg(feature = "__tls")] use rustls::{ pki_types::{CertificateDer, PrivateKeyDer}, sign::{CertifiedKey, SingleCertAndKey}, }; use std::net::SocketAddr; use test_support::subscribe; use tokio::net::{TcpListener, UdpSocket}; use tokio::time::timeout; #[tokio::test] async fn abort() { subscribe(); let endpoints = Endpoints::new().await; let endpoints2 = endpoints.clone(); let (abortable, abort_handle) = future::abortable(async move { let mut server_future = ServerFuture::new(Catalog::new()); endpoints2.register(&mut server_future).await; server_future.block_until_done().await }); abort_handle.abort(); abortable.await.expect_err("expected abort"); endpoints.rebind_all().await; } #[tokio::test] async fn graceful_shutdown() { subscribe(); let mut server_future = ServerFuture::new(Catalog::new()); let endpoints = Endpoints::new().await; endpoints.register(&mut server_future).await; timeout(Duration::from_secs(2), server_future.shutdown_gracefully()) .await .expect("timed out waiting for the server to complete") .expect("error while awaiting tasks"); endpoints.rebind_all().await; } #[test] fn test_sanitize_src_addr() { // ipv4 tests assert!(sanitize_src_address(SocketAddr::from(([192, 168, 1, 1], 4_096))).is_ok()); assert!(sanitize_src_address(SocketAddr::from(([127, 0, 0, 1], 53))).is_ok()); assert!(sanitize_src_address(SocketAddr::from(([0, 0, 0, 0], 0))).is_err()); assert!(sanitize_src_address(SocketAddr::from(([192, 168, 1, 1], 0))).is_err()); assert!(sanitize_src_address(SocketAddr::from(([0, 0, 0, 0], 4_096))).is_err()); assert!(sanitize_src_address(SocketAddr::from(([255, 255, 255, 255], 4_096))).is_err()); // ipv6 tests assert!( sanitize_src_address(SocketAddr::from(([0x20, 0, 0, 0, 0, 0, 0, 0x1], 4_096))).is_ok() ); assert!(sanitize_src_address(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 4_096))).is_ok()); assert!(sanitize_src_address(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 4_096))).is_err()); assert!(sanitize_src_address(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0))).is_err()); assert!( sanitize_src_address(SocketAddr::from(([0x20, 0, 0, 0, 0, 0, 0, 0x1], 0))).is_err() ); } #[derive(Clone)] struct Endpoints { udp_addr: SocketAddr, udp_std_addr: SocketAddr, tcp_addr: SocketAddr, tcp_std_addr: SocketAddr, #[cfg(feature = "__tls")] rustls_addr: SocketAddr, #[cfg(feature = "__https")] https_rustls_addr: SocketAddr, #[cfg(feature = "__quic")] quic_addr: SocketAddr, #[cfg(feature = "__h3")] h3_addr: SocketAddr, } impl Endpoints { async fn new() -> Self { let udp = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let udp_std = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let tcp = TcpListener::bind("127.0.0.1:0").await.unwrap(); let tcp_std = TcpListener::bind("127.0.0.1:0").await.unwrap(); #[cfg(feature = "__tls")] let rustls = TcpListener::bind("127.0.0.1:0").await.unwrap(); #[cfg(feature = "__https")] let https_rustls = TcpListener::bind("127.0.0.1:0").await.unwrap(); #[cfg(feature = "__quic")] let quic = UdpSocket::bind("127.0.0.1:0").await.unwrap(); #[cfg(feature = "__h3")] let h3 = UdpSocket::bind("127.0.0.1:0").await.unwrap(); Self { udp_addr: udp.local_addr().unwrap(), udp_std_addr: udp_std.local_addr().unwrap(), tcp_addr: tcp.local_addr().unwrap(), tcp_std_addr: tcp_std.local_addr().unwrap(), #[cfg(feature = "__tls")] rustls_addr: rustls.local_addr().unwrap(), #[cfg(feature = "__https")] https_rustls_addr: https_rustls.local_addr().unwrap(), #[cfg(feature = "__quic")] quic_addr: quic.local_addr().unwrap(), #[cfg(feature = "__h3")] h3_addr: h3.local_addr().unwrap(), } } async fn register(&self, server: &mut ServerFuture) { server.register_socket(UdpSocket::bind(self.udp_addr).await.unwrap()); server .register_socket_std(std::net::UdpSocket::bind(self.udp_std_addr).unwrap()) .unwrap(); server.register_listener( TcpListener::bind(self.tcp_addr).await.unwrap(), Duration::from_secs(1), ); server .register_listener_std( std::net::TcpListener::bind(self.tcp_std_addr).unwrap(), Duration::from_secs(1), ) .unwrap(); #[cfg(feature = "__tls")] { let cert_key = rustls_cert_key(); server .register_tls_listener( TcpListener::bind(self.rustls_addr).await.unwrap(), Duration::from_secs(30), cert_key, ) .unwrap(); } #[cfg(feature = "__https")] { let cert_key = rustls_cert_key(); server .register_https_listener( TcpListener::bind(self.https_rustls_addr).await.unwrap(), Duration::from_secs(1), cert_key, None, "/dns-query".into(), ) .unwrap(); } #[cfg(feature = "__quic")] { let cert_key = rustls_cert_key(); server .register_quic_listener( UdpSocket::bind(self.quic_addr).await.unwrap(), Duration::from_secs(1), cert_key, None, ) .unwrap(); } #[cfg(feature = "__h3")] { let cert_key = rustls_cert_key(); server .register_h3_listener( UdpSocket::bind(self.h3_addr).await.unwrap(), Duration::from_secs(1), cert_key, None, ) .unwrap(); } } async fn rebind_all(&self) { UdpSocket::bind(self.udp_addr).await.unwrap(); UdpSocket::bind(self.udp_std_addr).await.unwrap(); TcpListener::bind(self.tcp_addr).await.unwrap(); TcpListener::bind(self.tcp_std_addr).await.unwrap(); #[cfg(feature = "__tls")] TcpListener::bind(self.rustls_addr).await.unwrap(); #[cfg(feature = "__https")] TcpListener::bind(self.https_rustls_addr).await.unwrap(); #[cfg(feature = "__quic")] UdpSocket::bind(self.quic_addr).await.unwrap(); #[cfg(feature = "__h3")] UdpSocket::bind(self.h3_addr).await.unwrap(); } } #[cfg(feature = "__tls")] fn rustls_cert_key() -> Arc { use rustls::pki_types::pem::PemObject; use std::env; let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned()); let cert_chain = CertificateDer::pem_file_iter(format!("{}/tests/test-data/cert.pem", server_path)) .unwrap() .collect::, _>>() .unwrap(); let key = PrivateKeyDer::from_pem_file(format!("{server_path}/tests/test-data/cert.key")) .unwrap(); let certified_key = CertifiedKey::from_der(cert_chain, key, &default_provider()).unwrap(); Arc::new(SingleCertAndKey::from(certified_key)) } #[test] fn task_reap_on_empty_joinset() { let mut joinset = JoinSet::new(); // this should return immediately reap_tasks(&mut joinset); } #[tokio::test] async fn task_reap_on_nonempty_joinset() { let mut joinset = JoinSet::new(); let t = joinset.spawn(tokio::time::sleep(Duration::from_secs(2))); // this should return immediately since no task is ready reap_tasks(&mut joinset); t.abort(); // this should also return immediately since the task has been aborted reap_tasks(&mut joinset); } } hickory-server-0.25.2/src/server/quic_handler.rs000064400000000000000000000102721046102023000177710ustar 00000000000000// 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 std::{io, net::SocketAddr, sync::Arc}; use bytes::Bytes; use futures_util::lock::Mutex; use tokio_util::sync::CancellationToken; use tracing::{debug, error, warn}; use crate::{ access::AccessControl, authority::MessageResponse, proto::{ ProtoError, quic::{DoqErrorCode, QuicStream, QuicStreams}, rr::Record, xfer::Protocol, }, server::{ ResponseInfo, request_handler::RequestHandler, response_handler::{ResponseHandler, encode_fallback_servfail_response}, }, }; pub(crate) async fn quic_handler( access: Arc, handler: Arc, mut quic_streams: QuicStreams, src_addr: SocketAddr, _dns_hostname: Option>, shutdown: CancellationToken, ) -> Result<(), ProtoError> where T: RequestHandler, { // TODO: we should make this configurable let mut max_requests = 100u32; // Accept all inbound quic streams sent over the connection. loop { let mut request_stream = tokio::select! { result = quic_streams.next() => match result { Some(Ok(next_request)) => next_request, Some(Err(err)) => { warn!("error accepting request {}: {}", src_addr, err); return Err(err); } None => { break; } }, _ = shutdown.cancelled() => { // A graceful shutdown was initiated. break; }, }; let request = request_stream.receive_bytes().await?; debug!( "Received bytes {} from {src_addr} {request:?}", request.len() ); let handler = handler.clone(); let access = access.clone(); let stream = Arc::new(Mutex::new(request_stream)); let responder = QuicResponseHandle(stream.clone()); super::handle_request( &request, src_addr, Protocol::Quic, access, handler, responder, ) .await; max_requests -= 1; if max_requests == 0 { warn!("exceeded request count, shutting down quic conn: {src_addr}"); // 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. stream.lock().await.stop(DoqErrorCode::NoError)?; break; } // we'll continue handling requests from here. } Ok(()) } #[derive(Clone)] struct QuicResponseHandle(Arc>); #[async_trait::async_trait] impl ResponseHandler for QuicResponseHandle { // TODO: rethink this entire interface async fn send_response<'a>( &mut self, mut response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result { use crate::proto::serialize::binary::BinEncoder; // The id should always be 0 in DoQ response.header_mut().set_id(0); let id = response.header().id(); let mut bytes = Vec::with_capacity(512); let info = { let mut encoder = BinEncoder::new(&mut bytes); response.destructive_emit(&mut encoder).or_else(|error| { error!(%error, "error encoding message"); encode_fallback_servfail_response(id, &mut bytes) })? }; let bytes = Bytes::from(bytes); debug!("sending quic response: {}", bytes.len()); let mut lock = self.0.lock().await; lock.send_bytes(bytes).await?; lock.finish().await?; Ok(info) } } hickory-server-0.25.2/src/server/request_handler.rs000064400000000000000000000114361046102023000205230ustar 00000000000000// 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. //! Request Handler for incoming requests use std::net::SocketAddr; use hickory_proto::ProtoError; use crate::{ authority::MessageRequest, proto::{ op::{Header, LowerQuery, ResponseCode}, xfer::Protocol, }, server::ResponseHandler, }; /// An incoming request to the DNS catalog #[derive(Debug)] pub struct Request { /// Message with the associated query or update data message: MessageRequest, /// Source address of the Client src: SocketAddr, /// Protocol of the request protocol: Protocol, } impl Request { /// Build a new requests with the inbound message, source address, and protocol. /// /// This will return an error on bad verification. pub fn new(message: MessageRequest, src: SocketAddr, protocol: Protocol) -> Self { Self { message, src, protocol, } } /// Return just the header and request information from the Request Message /// /// Returns an error if there is not exactly one query pub fn request_info(&self) -> Result, ProtoError> { Ok(RequestInfo { src: self.src, protocol: self.protocol, header: self.message.header(), query: self.message.raw_queries().try_as_query()?, }) } /// The IP address from which the request originated. pub fn src(&self) -> SocketAddr { self.src } /// The protocol that was used for the request pub fn protocol(&self) -> Protocol { self.protocol } } impl std::ops::Deref for Request { type Target = MessageRequest; fn deref(&self) -> &Self::Target { &self.message } } // TODO: add ProtocolInfo that would have TLS details or other additional things... /// A narrow view of the Request, specifically a verified single query for the request #[non_exhaustive] #[derive(Clone)] pub struct RequestInfo<'a> { /// The source address from which the request came pub src: SocketAddr, /// The protocol used for the request pub protocol: Protocol, /// The header from the original request pub header: &'a Header, /// The query from the request pub query: &'a LowerQuery, } impl<'a> RequestInfo<'a> { /// Construct a new RequestInfo /// /// # Arguments /// /// * `src` - The source address from which the request came /// * `protocol` - The protocol used for the request /// * `header` - The header from the original request /// * `query` - The query from the request, LowerQuery is intended to reduce complexity for lookups in authorities pub fn new( src: SocketAddr, protocol: Protocol, header: &'a Header, query: &'a LowerQuery, ) -> Self { Self { src, protocol, header, query, } } } /// Information about the response sent for a request #[derive(Clone, Copy, Debug)] #[repr(transparent)] pub struct ResponseInfo(Header); impl ResponseInfo { pub(crate) fn serve_failed() -> Self { let mut header = Header::new(); header.set_response_code(ResponseCode::ServFail); header.into() } } impl From
for ResponseInfo { fn from(header: Header) -> Self { Self(header) } } impl std::ops::Deref for ResponseInfo { type Target = Header; fn deref(&self) -> &Self::Target { &self.0 } } /// Trait for handling incoming requests, and providing a message response. #[async_trait::async_trait] pub trait RequestHandler: Send + Sync + Unpin + 'static { /// Determines what needs to happen given the type of request, i.e. Query or Update. /// /// # Arguments /// /// * `request` - the requested action to perform. /// * `response_handle` - handle to which a return message should be sent async fn handle_request( &self, request: &Request, response_handle: R, ) -> ResponseInfo; } #[cfg(test)] mod tests { use super::*; use crate::proto::op::{Header, Query}; #[test] fn request_info_clone() { let query: Query = Query::new(); let header = Header::new(); let lower_query = query.into(); let origin = RequestInfo::new( "127.0.0.1:3000".parse().unwrap(), Protocol::Udp, &header, &lower_query, ); let cloned = origin.clone(); assert_eq!(origin.header, cloned.header); } } hickory-server-0.25.2/src/server/response_handler.rs000064400000000000000000000120721046102023000206660ustar 00000000000000// 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. use std::{io, net::SocketAddr}; use hickory_proto::{ ProtoError, op::{Header, ResponseCode}, rr::Record, serialize::binary::BinEncodable, }; use tracing::{debug, error, trace}; use crate::{ authority::MessageResponse, proto::{ BufDnsStreamHandle, DnsStreamHandle, serialize::binary::BinEncoder, xfer::{Protocol, SerialMessage}, }, server::ResponseInfo, }; /// A handler for send a response to a client #[async_trait::async_trait] pub trait ResponseHandler: Clone + Send + Sync + Unpin + 'static { // TODO: add associated error type //type Error; /// Serializes and sends a message to the wrapped handle /// /// self is consumed as only one message should ever be sent in response to a Request async fn send_response<'a>( &mut self, response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result; } /// A handler for wrapping a BufStreamHandle, which will properly serialize the message and add the /// associated destination. #[derive(Clone)] pub struct ResponseHandle { dst: SocketAddr, stream_handle: BufDnsStreamHandle, protocol: Protocol, } impl ResponseHandle { /// Returns a new `ResponseHandle` for sending a response message pub fn new(dst: SocketAddr, stream_handle: BufDnsStreamHandle, protocol: Protocol) -> Self { Self { dst, stream_handle, protocol, } } /// Selects an appropriate maximum serialized size for the given response. fn max_size_for_response<'a>( &self, response: &MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> u16 { match self.protocol { Protocol::Udp => { // Use EDNS, if available. if let Some(edns) = response.get_edns() { edns.max_payload() } else { // No EDNS, use the recommended max from RFC6891. hickory_proto::udp::MAX_RECEIVE_BUFFER_SIZE as u16 } } _ => u16::MAX, } } } #[async_trait::async_trait] impl ResponseHandler for ResponseHandle { /// Serializes and sends a message to to the wrapped handle /// /// self is consumed as only one message should ever be sent in response to a Request async fn send_response<'a>( &mut self, response: MessageResponse< '_, 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, impl Iterator + Send + 'a, >, ) -> io::Result { let id = response.header().id(); debug!( id, response_code = %response.header().response_code(), "sending response", ); let mut buffer = Vec::with_capacity(512); let encode_result = { let mut encoder = BinEncoder::new(&mut buffer); // Set an appropriate maximum on the encoder. let max_size = self.max_size_for_response(&response); trace!( "setting response max size: {max_size} for protocol: {:?}", self.protocol ); encoder.set_max_size(max_size); response.destructive_emit(&mut encoder) }; let info = encode_result.or_else(|error| { error!(%error, "error encoding message"); encode_fallback_servfail_response(id, &mut buffer) })?; self.stream_handle .send(SerialMessage::new(buffer, self.dst)) .map_err(|_| io::Error::new(io::ErrorKind::Other, "unknown"))?; Ok(info) } } /// Clears the buffer, encodes a SERVFAIL response in it, and returns a matching /// ResponseInfo. pub(crate) fn encode_fallback_servfail_response( id: u16, buffer: &mut Vec, ) -> Result { buffer.clear(); let mut encoder = BinEncoder::new(buffer); encoder.set_max_size(512); let mut header = Header::new(); header.set_id(id); header.set_response_code(ResponseCode::ServFail); header.emit(&mut encoder)?; Ok(ResponseInfo::serve_failed()) } hickory-server-0.25.2/src/server/timeout_stream.rs000064400000000000000000000121041046102023000203700ustar 00000000000000use std::io; use std::mem; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Duration; use futures_util::FutureExt; use futures_util::stream::{Stream, StreamExt}; use tokio::time::Sleep; use tracing::{debug, warn}; /// This wraps the underlying Stream in a timeout. /// /// Any `Ok(Poll::Ready(_))` from the underlying Stream will reset the timeout. pub struct TimeoutStream { stream: S, timeout_duration: Duration, timeout: Option>>, } impl TimeoutStream { /// Returns a new TimeoutStream /// /// # Arguments /// /// * `stream` - stream to wrap /// * `timeout_duration` - timeout between each request, once exceed the connection is killed /// * `reactor_handle` - reactor used for registering new timeouts pub fn new(stream: S, timeout_duration: Duration) -> Self { Self { stream, timeout_duration, timeout: None, } } fn timeout(timeout_duration: Duration) -> Option>> { if timeout_duration > Duration::from_millis(0) { Some(Box::pin(tokio::time::sleep(timeout_duration))) } else { None } } } impl Stream for TimeoutStream where S: Stream> + Unpin, { type Item = Result; // somehow insert a timeout here... fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // if the timer isn't set, set one now if self.timeout.is_none() { let timeout = Self::timeout(self.timeout_duration); self.as_mut().timeout = timeout; } match self.stream.poll_next_unpin(cx) { r @ Poll::Ready(_) => { // reset the timeout to wait for the next request... let timeout = if let Some(mut timeout) = Self::timeout(self.timeout_duration) { // ensure that interest in the Timeout is registered match timeout.poll_unpin(cx) { Poll::Ready(_) => { warn!("timeout fired immediately!"); return Poll::Ready(Some(Err(io::Error::new( io::ErrorKind::TimedOut, "timeout fired immediately!", )))); } Poll::Pending => (), // this is the expected state... } Some(timeout) } else { None }; drop(mem::replace(&mut self.timeout, timeout)); r } Poll::Pending => { if let Some(timeout) = &mut self.timeout { match timeout.poll_unpin(cx) { Poll::Pending => Poll::Pending, Poll::Ready(()) => { debug!("timeout on stream"); Poll::Ready(Some(Err(io::Error::new( io::ErrorKind::TimedOut, format!("nothing ready in {:?}", self.timeout_duration), )))) } } } else { Poll::Pending } } } } } #[cfg(test)] mod tests { use futures_util::stream::{TryStreamExt, iter}; use test_support::subscribe; use tokio::runtime::Runtime; use super::*; #[test] fn test_no_timeout() { subscribe(); #[allow(deprecated)] let sequence = iter(vec![Ok(1), Err("error"), Ok(2)]) .map_err(|e| io::Error::new(io::ErrorKind::Other, e)); let core = Runtime::new().expect("could not get core"); let timeout_stream = TimeoutStream::new(sequence, Duration::from_secs(360)); let (val, timeout_stream) = core.block_on(timeout_stream.into_future()); assert_eq!(val.expect("nothing in stream").ok(), Some(1)); let (error, timeout_stream) = core.block_on(timeout_stream.into_future()); assert!(error.expect("nothing in stream").is_err()); let (val, timeout_stream) = core.block_on(timeout_stream.into_future()); assert_eq!(val.expect("nothing in stream").ok(), Some(2)); let (val, _) = core.block_on(timeout_stream.into_future()); assert!(val.is_none()) } struct NeverStream {} impl Stream for NeverStream { type Item = Result<(), io::Error>; // somehow insert a timeout here... fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { Poll::Pending } } #[test] fn test_timeout() { subscribe(); let core = Runtime::new().expect("could not get core"); let timeout_stream = TimeoutStream::new(NeverStream {}, Duration::from_millis(1)); assert!( core.block_on(timeout_stream.into_future()) .0 .expect("nothing in stream") .is_err() ); } } hickory-server-0.25.2/src/store/blocklist.rs000064400000000000000000000661101046102023000171510ustar 00000000000000// 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. //! Blocklist resolver related types #![cfg(feature = "blocklist")] use std::{ collections::HashMap, fs::File, io, io::{Error, Read}, net::{Ipv4Addr, Ipv6Addr}, path::Path, str::FromStr, time::{Duration, Instant}, }; use serde::Deserialize; use tracing::{error, info, trace}; #[cfg(feature = "__dnssec")] use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind}; use crate::{ authority::{ Authority, LookupControlFlow, LookupError, LookupObject, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, proto::{ op::{Query, ResponseCode}, rr::{ LowerName, Name, RData, Record, RecordType, rdata::{A, AAAA, TXT}, }, }, resolver::lookup::Lookup, server::RequestInfo, }; // TODO: // * Add (optional) support for logging the client IP address. This will require some Authority // trait changes to accomplish // * Add query-type specific results for non-address queries // * Add support for per-blocklist sinkhole IPs, block messages, actions // * Add support for an exclusion list: allow the user to configure a list of patterns that // will never be insert into the in-memory blocklist (such as their own domain) // * Add support for regex matching /// A conditional authority that will resolve queries against one or more block lists and return a /// forged response. The typical use case will be to use this in a chained configuration before a /// forwarding or recursive resolver in order to pre-emptively block queries for hosts that are on /// a block list. Refer to tests/test-data/test_configs/chained_blocklist.toml for an example /// of this configuration. /// /// The blocklist authority also supports the consult interface, which allows an authority to review /// a query/response that has been processed by another authority, and, optionally, overwrite that /// response before returning it to the requestor. There is an example of this configuration in /// tests/test-data/test_configs/example_consulting_blocklist.toml. The main intended use of this /// feature is to allow log-only configurations, to allow administrators to see if blocklist domains /// are being queried. While this can be configured to overwrite responses, it is not recommended /// to do so - it is both more efficient, and more secure, to allow the blocklist to drop queries /// pre-emptively, as in the first example. pub struct BlocklistAuthority { origin: LowerName, blocklist: HashMap, wildcard_match: bool, min_wildcard_depth: u8, sinkhole_ipv4: Ipv4Addr, sinkhole_ipv6: Ipv6Addr, ttl: u32, block_message: Option, consult_action: BlocklistConsultAction, } impl BlocklistAuthority { /// Read the Authority for the origin from the specified configuration pub async fn try_from_config( origin: Name, _zone_type: ZoneType, config: &BlocklistConfig, base_dir: Option<&Path>, ) -> Result { info!("loading blocklist config: {origin}"); let mut authority = Self { origin: origin.into(), blocklist: HashMap::new(), wildcard_match: config.wildcard_match, min_wildcard_depth: config.min_wildcard_depth, sinkhole_ipv4: match config.sinkhole_ipv4 { Some(ip) => ip, None => Ipv4Addr::UNSPECIFIED, }, sinkhole_ipv6: match config.sinkhole_ipv6 { Some(ip) => ip, None => Ipv6Addr::UNSPECIFIED, }, ttl: config.ttl, block_message: config.block_message.clone(), consult_action: config.consult_action, }; let base_dir = match base_dir { Some(dir) => dir.display(), None => { return Err(format!( "invalid blocklist (zone directory) base path specified: '{base_dir:?}'" )); } }; // Load block lists into the block table cache for this authority. for bl in &config.lists { info!("adding blocklist {bl}"); match File::open(format!("{base_dir}/{bl}")) { Ok(handle) => { if let Err(e) = authority.add(handle) { return Err(format!( "unable to add data from blocklist {base_dir}/{bl}: {e:?}" )); } } Err(e) => { return Err(format!( "unable to open blocklist file {base_dir}/{bl}: {e:?}" )); } } } Ok(authority) } /// Add the contents of a block list to the in-memory cache. This function is normally called /// from try_from_config, but it can be invoked after the blocklist authority is created. /// /// # Arguments /// /// * `handle` - A source implementing `std::io::Read` that contains the blocklist entries /// to insert into the in-memory cache. /// /// # Return value /// /// `Result<(), std::io::Error>` /// /// # Expected format of blocklist entries /// /// * One entry per line /// * Any character after a '\#' will be treated as a comment and stripped out. /// * Leading wildcard entries are supported when the user has wildcard_match set to true. /// E.g., '\*.foo.com' will match any host in the foo.com domain. Intermediate wildcard /// matches, such as 'www.\*.com' are not supported. **Note: when wildcard matching is enabled, /// min_wildcard_depth (default: 2) controls how many static name labels must be present for a /// wildcard entry to be valid. With the default value of 2, an entry for '\*.foo.com' would /// be accepted, but an entry for '\*.com' would not.** /// * All entries are treated as being fully-qualified. If an entry does not contain a trailing /// '.', one will be added before insertion into the cache. /// /// # Example /// ``` /// use std::{fs::File, net::{Ipv4Addr, Ipv6Addr}, path::Path, str::FromStr, sync::Arc}; /// use hickory_proto::rr::{LowerName, RecordType}; /// use hickory_resolver::Name; /// use hickory_server::{authority::{AuthorityObject, LookupControlFlow, LookupOptions, ZoneType}, store::blocklist::*}; /// /// #[tokio::main] /// async fn main() { /// let config = BlocklistConfig { /// wildcard_match: true, /// min_wildcard_depth: 2, /// lists: vec!["default/blocklist.txt".to_string()], /// sinkhole_ipv4: None, /// sinkhole_ipv6: None, /// block_message: None, /// ttl: 86_400, /// consult_action: BlocklistConsultAction::Disabled, /// }; /// /// let mut blocklist = BlocklistAuthority::try_from_config( /// Name::root(), /// ZoneType::External, /// &config, /// Some(Path::new("../../tests/test-data/test_configs")), /// ).await.unwrap(); /// /// let handle = File::open("../../tests/test-data/test_configs/default/blocklist2.txt").unwrap(); /// if let Err(e) = blocklist.add(handle) { /// panic!("error adding blocklist: {e:?}"); /// } /// /// let origin = blocklist.origin().clone(); /// let authority = Arc::new(blocklist) as Arc; /// /// // In this example, malc0de.com only exists in the blocklist2.txt file we added to the /// // authority after instantiating it. The following simulates a lookup against the blocklist /// // authority, and checks for the expected response for a blocklist match. /// use LookupControlFlow::*; /// let Break(Ok(_res)) = authority.lookup( /// &LowerName::from(Name::from_ascii("malc0de.com.").unwrap()), /// RecordType::A, /// LookupOptions::default(), /// ).await else { /// panic!("blocklist authority did not return expected match"); /// }; /// } /// ``` pub fn add(&mut self, mut handle: impl Read) -> Result<(), Error> { let mut contents = String::new(); if let Err(e) = handle.read_to_string(&mut contents) { error!("unable to read blocklist data: {e:?}"); return Err(e); } for mut entry in contents.lines() { // Strip comments if let Some((item, _)) = entry.split_once('#') { entry = item.trim(); } if entry.is_empty() { continue; } let mut str_entry = entry.to_string(); if !entry.ends_with('.') { str_entry += "."; } let Ok(name) = LowerName::from_str(&str_entry[..]) else { error!( "unable to derive LowerName for blocklist entry '{str_entry}'; skipping entry" ); continue; }; trace!("inserting blocklist entry {str_entry}"); // The boolean value is not significant; only the key is used. self.blocklist.insert(name, true); } Ok(()) } /// Build a wildcard match list for a given host fn wildcards(&self, host: &Name) -> Vec { host.iter() .enumerate() .filter_map(|(i, _x)| { if i > ((self.min_wildcard_depth - 1) as usize) { Some(host.trim_to(i + 1).into_wildcard().into()) } else { None } }) .collect() } /// Perform a blocklist lookup. Returns true on match, false on no match. This is also where /// wildcard expansion is done, if wildcard support is enabled for the blocklist authority. fn is_blocked(&self, name: &LowerName) -> bool { let mut match_list = vec![name.to_owned()]; if self.wildcard_match { match_list.append(&mut self.wildcards(name)); } trace!("blocklist match list: {match_list:?}"); if match_list .iter() .any(|entry| self.blocklist.contains_key(entry)) { info!("block list matched query {name}"); return true; } false } /// Generate a BlocklistLookup to return on a blocklist match. This will return a lookup with /// either an A or AAAA record and, if the user has configured a block message, a TXT record /// with the contents of that message. fn blocklist_response(&self, name: Name, rtype: RecordType) -> BlocklistLookup { let mut records = vec![]; match rtype { RecordType::AAAA => records.push(Record::from_rdata( name.clone(), self.ttl, RData::AAAA(AAAA(self.sinkhole_ipv6)), )), _ => records.push(Record::from_rdata( name.clone(), self.ttl, RData::A(A(self.sinkhole_ipv4)), )), } if let Some(block_message) = &self.block_message { records.push(Record::from_rdata( name.clone(), self.ttl, RData::TXT(TXT::new(vec![block_message.clone()])), )); } BlocklistLookup(Lookup::new_with_deadline( Query::query(name.clone(), rtype), records.into(), Instant::now() + Duration::from_secs(u64::from(self.ttl)), )) } } #[async_trait::async_trait] impl Authority for BlocklistAuthority { type Lookup = BlocklistLookup; fn zone_type(&self) -> ZoneType { ZoneType::External } fn is_axfr_allowed(&self) -> bool { false } async fn update(&self, _update: &MessageRequest) -> UpdateResult { Err(ResponseCode::NotImp) } fn origin(&self) -> &LowerName { &self.origin } /// Perform a blocklist lookup. This will return LookupControlFlow::Break(Ok) on a match, or /// LookupControlFlow::Skip on no match. async fn lookup( &self, name: &LowerName, rtype: RecordType, _lookup_options: LookupOptions, ) -> LookupControlFlow { use LookupControlFlow::*; trace!("blocklist lookup: {name} {rtype}"); if self.is_blocked(name) { return Break(Ok(self.blocklist_response(Name::from(name), rtype))); } trace!("query '{name}' is not in blocklist; returning Skip..."); Skip } /// Optionally, perform a blocklist lookup after another authority has done a lookup for this /// query. async fn consult( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, last_result: LookupControlFlow>, ) -> LookupControlFlow> { match self.consult_action { BlocklistConsultAction::Disabled => last_result, BlocklistConsultAction::Log => { self.is_blocked(name); last_result } BlocklistConsultAction::Enforce => { let lookup = self.lookup(name, rtype, lookup_options).await; if lookup.is_break() { lookup.map_dyn() } else { last_result } } } } async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { self.lookup( request_info.query.name(), request_info.query.query_type(), lookup_options, ) .await } async fn get_nsec_records( &self, _name: &LowerName, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "Getting NSEC records is unimplemented for the blocklist", )))) } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, _info: Nsec3QueryInfo<'_>, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "getting NSEC3 records is unimplemented for the forwarder", )))) } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { None } } /// Consult action enum. Controls how consult lookups are handled. #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] pub enum BlocklistConsultAction { /// Do not log or block any request when the blocklist is called via consult #[default] Disabled, /// Log and block matching requests when the blocklist is called via consult Enforce, /// Log but do not block matching requests when the blocklist is called via consult Log, } /// Configuration for file based zones #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(default, deny_unknown_fields)] pub struct BlocklistConfig { /// Support wildcards? Defaults to true. If set to true, block list entries containing /// asterisks will be expanded to match queries. pub wildcard_match: bool, /// Minimum wildcard depth. Defaults to 2. Any wildcard entries without at least this many /// static elements will not be expanded (e.g., *.com has a depth of 1; *.example.com has a /// depth of two.) This is meant as a safeguard against an errant block list entry, such as * /// or *.com that might block many more hosts than intended. pub min_wildcard_depth: u8, /// Block lists to load. These should be specified as relative (to the server zone directory) /// paths in the config file. pub lists: Vec, /// IPv4 sinkhole IP. This is the IP that is returned when a blocklist entry is matched for an /// A query. If unspecified, an implementation-provided default will be used. pub sinkhole_ipv4: Option, /// IPv6 sinkhole IP. This is the IP that is returned when a blocklist entry is matched for a /// AAAA query. If unspecified, an implementation-provided default will be used. pub sinkhole_ipv6: Option, /// Block TTL. This is the length of time a block response should be stored in the requesting /// resolvers cache, in seconds. Defaults to 86,400 seconds. pub ttl: u32, /// Block message to return to the user. This is an optional message that, if configured, will /// be returned as a TXT record in the additionals section when a blocklist entry is matched for /// a query. pub block_message: Option, /// The consult action controls how the blocklist handles queries where another authority has /// already provided an answer. By default, it ignores any such queries ("Disabled",) however /// it can be configured to log blocklist matches for those queries ("Log",) or can be /// configured to overwrite the previous responses ("Enforce".) pub consult_action: BlocklistConsultAction, } impl Default for BlocklistConfig { fn default() -> Self { Self { wildcard_match: true, min_wildcard_depth: 2, lists: vec![], sinkhole_ipv4: None, sinkhole_ipv6: None, ttl: 86_400, block_message: None, consult_action: BlocklistConsultAction::default(), } } } /// A lookup object that is returned when a blocklist entry is matched. pub struct BlocklistLookup(Lookup); impl LookupObject for BlocklistLookup { fn is_empty(&self) -> bool { self.0.is_empty() } fn iter<'a>(&'a self) -> Box + Send + 'a> { Box::new(self.0.record_iter()) } fn take_additionals(&mut self) -> Option> { None } } #[cfg(test)] mod test { use std::{ net::{Ipv4Addr, Ipv6Addr}, path::Path, str::FromStr, sync::Arc, }; use tracing::error; use crate::{ authority::{AuthorityObject, LookupOptions, ZoneType}, proto::rr::domain::Name, proto::rr::{ LowerName, RData, RecordType, rdata::{A, AAAA}, }, store::blocklist::BlocklistConsultAction, }; use test_support::subscribe; enum TestResult { Break, Skip, } #[tokio::test] async fn test_blocklist_basic() { subscribe(); let config = super::BlocklistConfig { wildcard_match: true, min_wildcard_depth: 2, lists: vec!["default/blocklist.txt".to_string()], sinkhole_ipv4: None, sinkhole_ipv6: None, block_message: None, ttl: 86_400, consult_action: BlocklistConsultAction::Disabled, }; let blocklist = super::BlocklistAuthority::try_from_config( Name::root(), ZoneType::External, &config, Some(Path::new("../../tests/test-data/test_configs/")), ); let authority = blocklist.await; // Test: verify the blocklist authority was successfully created. match authority { Ok(ref _authority) => {} Err(e) => { panic!("Unable to create blocklist authority: {e}"); } } let ao = Arc::new(authority.unwrap()) as Arc; let v4 = A::new(0, 0, 0, 0); let v6 = AAAA::new(0, 0, 0, 0, 0, 0, 0, 0); use RecordType::{A as Rec_A, AAAA as Rec_AAAA}; use TestResult::*; // Test: lookup a record that is in the blocklist and that should match without a wildcard. basic_test(&ao, "foo.com.", Rec_A, Break, Some(v4), None, None).await; // test: lookup a record that is not in the blocklist. This test should fail. basic_test(&ao, "test.com.", Rec_A, Skip, None, None, None).await; // Test: lookup a record that will match a wildcard that is in the blocklist. basic_test(&ao, "www.foo.com.", Rec_A, Break, Some(v4), None, None).await; // Test: lookup a record that will match a wildcard that is in the blocklist. basic_test(&ao, "www.com.foo.com.", Rec_A, Break, Some(v4), None, None).await; // Test: lookup a record that is in the blocklist and that should match without a wildcard. basic_test(&ao, "foo.com.", Rec_AAAA, Break, None, Some(v6), None).await; // test: lookup a record that is not in the blocklist. This test should fail. basic_test(&ao, "test.com.", Rec_AAAA, Skip, None, None, None).await; // Test: lookup a record that will match a wildcard that is in the blocklist. basic_test(&ao, "www.foo.com.", Rec_AAAA, Break, None, Some(v6), None).await; // Test: lookup a record that will match a wildcard that is in the blocklist. basic_test(&ao, "ab.cd.foo.com.", Rec_AAAA, Break, None, Some(v6), None).await; } #[tokio::test] async fn test_blocklist_wildcard_disabled() { subscribe(); let config = super::BlocklistConfig { min_wildcard_depth: 2, wildcard_match: false, lists: vec!["default/blocklist.txt".to_string()], sinkhole_ipv4: Some(Ipv4Addr::new(192, 0, 2, 1)), sinkhole_ipv6: Some(Ipv6Addr::new(0, 0, 0, 0, 0xc0, 0, 2, 1)), block_message: Some(String::from("blocked")), ttl: 86_400, consult_action: BlocklistConsultAction::Disabled, }; let blocklist = super::BlocklistAuthority::try_from_config( Name::root(), ZoneType::External, &config, Some(Path::new("../../tests/test-data/test_configs/")), ); let authority = blocklist.await; // Test: verify the blocklist authority was successfully created. match authority { Ok(ref _authority) => {} Err(e) => { panic!("Unable to create blocklist authority: {e}"); } } let ao = Arc::new(authority.unwrap()) as Arc; let v4 = A::new(192, 0, 2, 1); let v6 = AAAA::new(0, 0, 0, 0, 0xc0, 0, 2, 1); let msg = config.block_message; use RecordType::{A as Rec_A, AAAA as Rec_AAAA}; use TestResult::*; // Test: lookup a record that is in the blocklist and that should match without a wildcard. basic_test(&ao, "foo.com.", Rec_A, Break, Some(v4), None, msg.clone()).await; // Test: lookup a record that is not in the blocklist, but would match a wildcard; this // should fail. basic_test(&ao, "www.foo.com.", Rec_A, Skip, None, None, msg.clone()).await; // Test: lookup a record that is in the blocklist and that should match without a wildcard. basic_test(&ao, "foo.com.", Rec_AAAA, Break, None, Some(v6), msg).await; } #[tokio::test] #[should_panic] async fn test_blocklist_wrong_block_message() { subscribe(); let config = super::BlocklistConfig { min_wildcard_depth: 2, wildcard_match: false, lists: vec!["default/blocklist.txt".to_string()], sinkhole_ipv4: Some(Ipv4Addr::new(192, 0, 2, 1)), sinkhole_ipv6: Some(Ipv6Addr::new(0, 0, 0, 0, 0xc0, 0, 2, 1)), block_message: Some(String::from("blocked")), ttl: 86_400, consult_action: BlocklistConsultAction::Disabled, }; let blocklist = super::BlocklistAuthority::try_from_config( Name::root(), ZoneType::External, &config, Some(Path::new("../../tests/test-data/test_configs/")), ); let authority = blocklist.await; // Test: verify the blocklist authority was successfully created. match authority { Ok(ref _authority) => {} Err(e) => { error!("Unable to create blocklist authority: {e}"); return; } } let ao = Arc::new(authority.unwrap()) as Arc; let sinkhole_v4 = A::new(192, 0, 2, 1); // Test: lookup a record that is in the blocklist, but specify an incorrect block message to // match. basic_test( &ao, "foo.com.", RecordType::A, TestResult::Break, Some(sinkhole_v4), None, Some(String::from("wrong message")), ) .await; } #[allow(clippy::borrowed_box)] async fn basic_test( ao: &Arc, query: &'static str, q_type: RecordType, r_type: TestResult, ipv4: Option, ipv6: Option, msg: Option, ) { let res = ao .lookup( &LowerName::from_str(query).unwrap(), q_type, LookupOptions::default(), ) .await; use super::LookupControlFlow::*; match r_type { TestResult::Break => match res { Break(Ok(l)) => { if !l.iter().all(|x| match x.record_type() { RecordType::TXT => { if let Some(msg) = &msg { x.data().to_string() == *msg } else { false } } RecordType::AAAA => { let Some(rec_ip) = ipv6 else { panic!("expected to validate record IPv6, but None was passed"); }; x.name() == &Name::from_str(query).unwrap() && x.data() == &RData::AAAA(rec_ip) } _ => { let Some(rec_ip) = ipv4 else { panic!("expected to validate record IPv4, but None was passed"); }; x.name() == &Name::from_str(query).unwrap() && x.data() == &RData::A(rec_ip) } }) { panic!("{query} lookup data is incorrect."); } } _ => panic!("Unexpected result for {query}: {res}"), }, TestResult::Skip => match res { Skip => {} _ => { panic!("unexpected result for {query}; expected Skip, found {res}"); } }, } } } hickory-server-0.25.2/src/store/file.rs000064400000000000000000000315271046102023000161060ustar 00000000000000// 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. //! Zone file based serving with Dynamic DNS and journaling support use std::{ collections::BTreeMap, fs, ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; use serde::Deserialize; use tracing::{debug, info}; #[cfg(feature = "metrics")] use crate::store::metrics::StoreMetrics; use crate::{ authority::{ Authority, LookupControlFlow, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, proto::rr::{LowerName, Name, RecordSet, RecordType, RrKey}, proto::serialize::txt::Parser, server::RequestInfo, store::in_memory::InMemoryAuthority, }; #[cfg(feature = "__dnssec")] use crate::{ authority::{DnssecAuthority, Nsec3QueryInfo}, dnssec::NxProofKind, proto::dnssec::{DnsSecResult, SigSigner, rdata::key::KEY}, }; /// FileAuthority is responsible for storing the resource records for a particular zone. /// /// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the /// start of authority for the zone, is a Secondary, or a cached zone. pub struct FileAuthority { in_memory: InMemoryAuthority, #[cfg(feature = "metrics")] metrics: StoreMetrics, } impl FileAuthority { /// Creates a new Authority. /// /// # Arguments /// /// * `origin` - The zone `Name` being created, this should match that of the `RecordType::SOA` /// record. /// * `records` - The map of the initial set of records in the zone. /// * `zone_type` - The type of zone, i.e. is this authoritative? /// * `allow_axfr` - Whether AXFR is allowed. /// * `nx_proof_kind` - The kind of non-existence proof to be used by the server. /// /// # Return value /// /// The new `Authority`. pub fn new( origin: Name, records: BTreeMap, zone_type: ZoneType, allow_axfr: bool, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, ) -> Result { Ok(Self { #[cfg(feature = "metrics")] metrics: { let new = StoreMetrics::new("file"); new.persistent.zone_records.increment(records.len() as f64); new }, in_memory: InMemoryAuthority::new( origin, records, zone_type, allow_axfr, #[cfg(feature = "__dnssec")] nx_proof_kind, )?, }) } /// Read the Authority for the origin from the specified configuration pub fn try_from_config( origin: Name, zone_type: ZoneType, allow_axfr: bool, root_dir: Option<&Path>, config: &FileConfig, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, ) -> Result { Self::try_from_config_internal( origin, zone_type, allow_axfr, root_dir, config, #[cfg(feature = "__dnssec")] nx_proof_kind, #[cfg(feature = "metrics")] false, ) } // internal load for e.g. sqlite db creation pub(crate) fn try_from_config_internal( origin: Name, zone_type: ZoneType, allow_axfr: bool, root_dir: Option<&Path>, config: &FileConfig, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, #[cfg(feature = "metrics")] is_internal_load: bool, ) -> Result { let root_dir_path = root_dir.map(PathBuf::from).unwrap_or_default(); let zone_path = root_dir_path.join(&config.zone_file_path); info!("loading zone file: {:?}", zone_path); // TODO: this should really use something to read line by line or some other method to // keep the usage down. and be a custom lexer... let buf = fs::read_to_string(&zone_path).map_err(|e| { format!( "failed to read {}: {:?}", config.zone_file_path.display(), e ) })?; let (origin, records) = Parser::new(buf, Some(zone_path), Some(origin)) .parse() .map_err(|e| { format!( "failed to parse {}: {:?}", config.zone_file_path.display(), e ) })?; info!( "zone file loaded: {} with {} records", origin, records.len() ); debug!("zone: {:#?}", records); Ok(Self { #[cfg(feature = "metrics")] metrics: { let new = StoreMetrics::new("file"); if !is_internal_load { new.persistent.zone_records.increment(records.len() as f64); } new }, in_memory: InMemoryAuthority::new( origin, records, zone_type, allow_axfr, #[cfg(feature = "__dnssec")] nx_proof_kind, )?, }) } /// Unwrap the InMemoryAuthority pub fn unwrap(self) -> InMemoryAuthority { self.in_memory } } impl Deref for FileAuthority { type Target = InMemoryAuthority; fn deref(&self) -> &Self::Target { &self.in_memory } } impl DerefMut for FileAuthority { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.in_memory } } #[async_trait::async_trait] impl Authority for FileAuthority { type Lookup = ::Lookup; /// What type is this zone fn zone_type(&self) -> ZoneType { self.in_memory.zone_type() } /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool { self.in_memory.is_axfr_allowed() } /// Perform a dynamic update of a zone async fn update(&self, _update: &MessageRequest) -> UpdateResult { use crate::proto::op::ResponseCode; Err(ResponseCode::NotImp) } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName { self.in_memory.origin() } /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow { let lookup = self.in_memory.lookup(name, rtype, lookup_options).await; #[cfg(feature = "metrics")] self.metrics.query.increment_lookup(&lookup); lookup } /// Using the specified query, perform a lookup against this zone. /// /// # Arguments /// /// * `request_info` - the query to perform the lookup with. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { let search = self.in_memory.search(request_info, lookup_options).await; #[cfg(feature = "metrics")] self.metrics.query.increment_lookup(&search); search } /// Get the NS, NameServer, record for the zone async fn ns(&self, lookup_options: LookupOptions) -> LookupControlFlow { self.in_memory.ns(lookup_options).await } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow { self.in_memory.get_nsec_records(name, lookup_options).await } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { self.in_memory.get_nsec3_records(info, lookup_options).await } /// Returns the SOA of the authority. /// /// *Note*: This will only return the SOA, if this is fulfilling a request, a standard lookup /// should be used, see `soa_secure()`, which will optionally return RRSIGs. async fn soa(&self) -> LookupControlFlow { self.in_memory.soa().await } /// Returns the SOA record for the zone async fn soa_secure(&self, lookup_options: LookupOptions) -> LookupControlFlow { self.in_memory.soa_secure(lookup_options).await } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { self.in_memory.nx_proof_kind() } } #[cfg(feature = "__dnssec")] #[async_trait::async_trait] impl DnssecAuthority for FileAuthority { /// Add a (Sig0) key that is authorized to perform updates against this authority async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()> { self.in_memory.add_update_auth_key(name, key).await } /// Add Signer async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()> { self.in_memory.add_zone_signing_key(signer).await } /// Sign the zone for DNSSEC async fn secure_zone(&self) -> DnsSecResult<()> { DnssecAuthority::secure_zone(&self.in_memory).await } } /// Configuration for file based zones #[derive(Deserialize, PartialEq, Eq, Debug)] #[serde(deny_unknown_fields)] pub struct FileConfig { /// path to the zone file pub zone_file_path: PathBuf, } #[cfg(test)] mod tests { use std::str::FromStr; use crate::proto::rr::{RData, rdata::A}; use futures_executor::block_on; use test_support::subscribe; use super::*; use crate::authority::ZoneType; #[test] fn test_load_zone() { subscribe(); #[cfg(feature = "__dnssec")] let config = FileConfig { zone_file_path: PathBuf::from( "../../tests/test-data/test_configs/dnssec/example.com.zone", ), }; #[cfg(not(feature = "__dnssec"))] let config = FileConfig { zone_file_path: PathBuf::from("../../tests/test-data/test_configs/example.com.zone"), }; let authority = FileAuthority::try_from_config( Name::from_str("example.com.").unwrap(), ZoneType::Primary, false, None, &config, #[cfg(feature = "__dnssec")] Some(NxProofKind::Nsec), ) .expect("failed to load file"); let lookup = block_on(Authority::lookup( &authority, &LowerName::from_str("www.example.com.").unwrap(), RecordType::A, LookupOptions::default(), )) .expect("lookup failed"); match lookup .into_iter() .next() .expect("A record not found in authority") .data() { RData::A(ip) => assert_eq!(A::new(127, 0, 0, 1), *ip), _ => panic!("wrong rdata type returned"), } let include_lookup = block_on(Authority::lookup( &authority, &LowerName::from_str("include.alias.example.com.").unwrap(), RecordType::A, LookupOptions::default(), )) .expect("INCLUDE lookup failed"); match include_lookup .into_iter() .next() .expect("A record not found in authority") .data() { RData::A(ip) => assert_eq!(A::new(127, 0, 0, 5), *ip), _ => panic!("wrong rdata type returned"), } } } hickory-server-0.25.2/src/store/forwarder.rs000064400000000000000000000262601046102023000171600ustar 00000000000000// 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. #![cfg(feature = "resolver")] //! Forwarding resolver related types use std::io; #[cfg(feature = "__dnssec")] use std::sync::Arc; use serde::Deserialize; use tracing::{debug, info}; #[cfg(feature = "metrics")] use crate::store::metrics::QueryStoreMetrics; #[cfg(feature = "__dnssec")] use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind, proto::dnssec::TrustAnchors}; use crate::{ authority::{ Authority, LookupControlFlow, LookupError, LookupObject, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, proto::{ op::ResponseCode, rr::{LowerName, Name, Record, RecordType}, }, resolver::{ Resolver, config::{NameServerConfigGroup, ResolveHosts, ResolverConfig, ResolverOpts}, lookup::Lookup as ResolverLookup, name_server::{ConnectionProvider, TokioConnectionProvider}, }, server::RequestInfo, }; /// A builder to construct a [`ForwardAuthority`]. /// /// Created by [`ForwardAuthority::builder`]. pub struct ForwardAuthorityBuilder { origin: Name, config: ForwardConfig, domain: Option, search: Vec, runtime: P, #[cfg(feature = "__dnssec")] trust_anchor: Option>, } impl ForwardAuthorityBuilder

{ /// Set the origin of the authority. pub fn with_origin(mut self, origin: Name) -> Self { self.origin = origin; self } /// Enables DNSSEC validation, and sets the DNSSEC trust anchors to be used by the forward /// authority. /// /// This overrides the trust anchor path in the `ResolverOpts`. #[cfg(feature = "__dnssec")] pub fn with_trust_anchor(mut self, trust_anchor: Arc) -> Self { self.trust_anchor = Some(trust_anchor); self } /// Returns a mutable reference to the [`ResolverOpts`]. pub fn options_mut(&mut self) -> &mut ResolverOpts { self.config .options .get_or_insert_with(ResolverOpts::default) } /// Set the system domain name. pub fn with_domain(mut self, domain: Name) -> Self { self.domain = Some(domain); self } /// Set the search domains. pub fn with_search(mut self, search: Vec) -> Self { self.search = search; self } /// Construct the authority. pub fn build(self) -> Result, String> { let Self { origin, config, domain, search, runtime, #[cfg(feature = "__dnssec")] trust_anchor, } = self; info!(%origin, "loading forwarder config"); let name_servers = config.name_servers; let mut options = config.options.unwrap_or_default(); // See RFC 1034, Section 4.3.2: // "If the data at the node is a CNAME, and QTYPE doesn't match // CNAME, copy the CNAME RR into the answer section of the response, // change QNAME to the canonical name in the CNAME RR, and go // back to step 1." // // Essentially, it's saying that servers (including forwarders) // should emit any found CNAMEs in a response ("copy the CNAME // RR into the answer section"). This is the behavior that // preserve_intermediates enables when set to true, and disables // when set to false. So we set it to true. if !options.preserve_intermediates { tracing::warn!( "preserve_intermediates set to false, which is invalid \ for a forwarder; switching to true" ); options.preserve_intermediates = true; } // Require people to explicitly request for /etc/hosts usage in forwarder // configs if options.use_hosts_file == ResolveHosts::Auto { options.use_hosts_file = ResolveHosts::Never; } let config = ResolverConfig::from_parts(domain, search, name_servers); let mut resolver_builder = Resolver::builder_with_config(config, runtime); #[cfg(feature = "__dnssec")] match (trust_anchor, &options.trust_anchor) { (Some(trust_anchor), _) => { resolver_builder = resolver_builder.with_trust_anchor(trust_anchor); options.validate = true; } (None, Some(path)) => { let trust_anchor = TrustAnchors::from_file(path).map_err(|err| err.to_string())?; resolver_builder = resolver_builder.with_trust_anchor(Arc::new(trust_anchor)); options.validate = true; } (None, None) => {} } *resolver_builder.options_mut() = options; let resolver = resolver_builder.build(); info!(%origin, "forward resolver configured"); Ok(ForwardAuthority { origin: origin.into(), resolver, #[cfg(feature = "metrics")] metrics: QueryStoreMetrics::new("forwarder"), }) } } /// An authority that will forward resolutions to upstream resolvers. /// /// This uses the hickory-resolver crate for resolving requests. pub struct ForwardAuthority { origin: LowerName, resolver: Resolver

, #[cfg(feature = "metrics")] metrics: QueryStoreMetrics, } impl ForwardAuthority

{ /// Construct a new [`ForwardAuthority`] via [`ForwardAuthorityBuilder`], using the operating /// system's resolver configuration. pub fn builder(runtime: P) -> Result, String> { let (resolver_config, options) = hickory_resolver::system_conf::read_system_conf() .map_err(|e| format!("error reading system configuration: {e}"))?; let forward_config = ForwardConfig { name_servers: resolver_config.name_servers().to_vec().into(), options: Some(options), }; let mut builder = Self::builder_with_config(forward_config, runtime); if let Some(domain) = resolver_config.domain() { builder = builder.with_domain(domain.clone()); } builder = builder.with_search(resolver_config.search().to_vec()); Ok(builder) } /// Construct a new [`ForwardAuthority`] via [`ForwardAuthorityBuilder`] with the provided configuration. pub fn builder_with_config(config: ForwardConfig, runtime: P) -> ForwardAuthorityBuilder

{ ForwardAuthorityBuilder { origin: Name::root(), config, domain: None, search: vec![], runtime, #[cfg(feature = "__dnssec")] trust_anchor: None, } } } impl ForwardAuthority { /// Construct a new [`ForwardAuthority`] via [`ForwardAuthorityBuilder`] with the provided configuration. pub fn builder_tokio( config: ForwardConfig, ) -> ForwardAuthorityBuilder { Self::builder_with_config(config, TokioConnectionProvider::default()) } } #[async_trait::async_trait] impl Authority for ForwardAuthority

{ type Lookup = ForwardLookup; /// Always External fn zone_type(&self) -> ZoneType { ZoneType::External } /// Always false for Forward zones fn is_axfr_allowed(&self) -> bool { false } /// Whether the authority can perform DNSSEC validation fn can_validate_dnssec(&self) -> bool { self.resolver.options().validate } async fn update(&self, _update: &MessageRequest) -> UpdateResult { Err(ResponseCode::NotImp) } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com /// /// In the context of a forwarder, this is either a zone which this forwarder is associated, /// or `.`, the root zone for all zones. If this is not the root zone, then it will only forward /// for lookups which match the given zone name. fn origin(&self) -> &LowerName { &self.origin } /// Forwards a lookup given the resolver configuration for this Forwarded zone async fn lookup( &self, name: &LowerName, rtype: RecordType, _lookup_options: LookupOptions, ) -> LookupControlFlow { // TODO: make this an error? debug_assert!(self.origin.zone_of(name)); debug!("forwarding lookup: {} {}", name, rtype); // Ignore FQDN when we forward DNS queries. Without this we can't look // up addresses from system hosts file. let mut name: Name = name.clone().into(); name.set_fqdn(false); use LookupControlFlow::*; let lookup = match self.resolver.lookup(name, rtype).await { Ok(lookup) => Continue(Ok(ForwardLookup(lookup))), Err(e) => Continue(Err(LookupError::from(e))), }; #[cfg(feature = "metrics")] self.metrics.increment_lookup(&lookup); lookup } async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { self.lookup( request_info.query.name(), request_info.query.query_type(), lookup_options, ) .await } async fn get_nsec_records( &self, _name: &LowerName, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "Getting NSEC records is unimplemented for the forwarder", )))) } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, _info: Nsec3QueryInfo<'_>, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "getting NSEC3 records is unimplemented for the forwarder", )))) } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { None } } /// A structure that holds the results of a forwarding lookup. /// /// This exposes an iterator interface for consumption downstream. pub struct ForwardLookup(pub ResolverLookup); impl LookupObject for ForwardLookup { fn is_empty(&self) -> bool { self.0.is_empty() } fn iter<'a>(&'a self) -> Box + Send + 'a> { Box::new(self.0.record_iter()) } fn take_additionals(&mut self) -> Option> { None } } /// Configuration for file based zones #[derive(Clone, Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct ForwardConfig { /// upstream name_server configurations pub name_servers: NameServerConfigGroup, /// Resolver options pub options: Option, } hickory-server-0.25.2/src/store/in_memory/inner.rs000064400000000000000000000760331046102023000203010ustar 00000000000000#[cfg(feature = "__dnssec")] use std::collections::{BTreeSet, HashMap, hash_map::Entry}; use std::{ collections::{BTreeMap, HashSet}, sync::Arc, }; use cfg_if::cfg_if; #[cfg(feature = "__dnssec")] use time::OffsetDateTime; #[cfg(feature = "__dnssec")] use tracing::debug; use tracing::{error, warn}; #[cfg(feature = "__dnssec")] use crate::{ authority::{LookupError, Nsec3QueryInfo}, dnssec::NxProofKind, proto::{ ProtoError, dnssec::{ DnsSecResult, Nsec3HashAlgorithm, SigSigner, TBS, rdata::{DNSSECRData, NSEC, NSEC3, NSEC3PARAM, RRSIG}, }, }, }; use super::maybe_next_name; use crate::{ authority::LookupOptions, proto::rr::{ DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType, RrKey, rdata::SOA, }, }; #[derive(Default)] pub(super) struct InnerInMemory { pub(super) records: BTreeMap>, // Private key mapped to the Record of the DNSKey // TODO: these private_keys should be stored securely. Ideally, we have keys only stored per // server instance, but that requires requesting updates from the parent zone, which may or // may not support dynamic updates to register the new key... Hickory DNS will provide support // for this, in some form, perhaps alternate root zones... #[cfg(feature = "__dnssec")] pub(super) secure_keys: Vec, } impl InnerInMemory { #[cfg(feature = "__dnssec")] pub(super) fn proof( &self, info: Nsec3QueryInfo<'_>, zone: &LowerName, ) -> Result>, LookupError> { let Nsec3QueryInfo { qname, qtype, has_wildcard_match, .. } = info; let rr_key = RrKey::new(info.get_hashed_owner_name(qname, zone)?, RecordType::NSEC3); let qname_match = self.records.get(&rr_key); if has_wildcard_match { // - Wildcard answer response. let closest_encloser_name = self.closest_encloser_proof(qname, zone, &info)?; let Some((closest_encloser_name, _)) = closest_encloser_name else { return Ok(vec![]); }; let cover = self.find_cover(&closest_encloser_name, zone, &info)?; return Ok(cover.map_or_else(Vec::new, |rr_set| vec![rr_set])); } match qname_match { // - No data response if the QTYPE is not DS. // - No data response if the QTYPE is DS and there is an NSEC3 record matching QNAME. Some(rr_set) => return Ok(vec![rr_set.clone()]), None => {} } // - Name error response. // - No data response if QTYPE is DS and there is not an NSEC3 record matching QNAME. // - Wildcard no data response. let mut records = Vec::new(); let (next_closer_name, closest_encloser_match) = self.closest_encloser_proof(qname, zone, &info)?.unzip(); if let Some(cover) = closest_encloser_match { records.push(cover); } let Some(next_closer_name) = next_closer_name else { return Ok(records); }; if let Some(cover) = self.find_cover(&next_closer_name, zone, &info)? { records.push(cover); } let wildcard_match = { let wildcard = qname.clone().into_wildcard(); self.records.keys().any(|rr_key| rr_key.name == wildcard) }; if wildcard_match { let wildcard_at_closest_encloser = next_closer_name.into_wildcard(); let rr_key = RrKey::new( info.get_hashed_owner_name(&wildcard_at_closest_encloser, zone)?, RecordType::NSEC3, ); if let Some(record) = self.records.get(&rr_key) { records.push(record.clone()); } } else if qtype != RecordType::DS { let wildcard_at_closest_encloser = next_closer_name.into_wildcard(); if let Some(cover) = self.find_cover(&wildcard_at_closest_encloser, zone, &info)? { records.push(cover); } } records.sort_by(|a, b| a.name().cmp(b.name())); records.dedup_by(|a, b| a.name() == b.name()); Ok(records) } #[cfg(feature = "__dnssec")] pub(super) fn closest_nsec(&self, name: &LowerName) -> Option> { for rr_set in self.records.values().rev() { if rr_set.record_type() != RecordType::NSEC { continue; } if *name < rr_set.name().into() { continue; } // there should only be one record let Some(record) = rr_set.records(false).next() else { continue; }; let RData::DNSSEC(DNSSECRData::NSEC(nsec)) = record.data() else { continue; }; let next_domain_name = nsec.next_domain_name(); // the search name is less than the next NSEC record if *name < next_domain_name.into() || // this is the last record, and wraps to the beginning of the zone next_domain_name < rr_set.name() { return Some(rr_set.clone()); } } None } fn inner_soa(&self, origin: &LowerName) -> Option<&SOA> { // TODO: can't there be an RrKeyRef? let rr_key = RrKey::new(origin.clone(), RecordType::SOA); self.records .get(&rr_key) .and_then(|rrset| rrset.records_without_rrsigs().next()) .map(Record::data) .and_then(RData::as_soa) } /// Returns the minimum ttl (as used in the SOA record) pub(super) fn minimum_ttl(&self, origin: &LowerName) -> u32 { match self.inner_soa(origin) { Some(soa) => soa.minimum(), None => { error!("could not lookup SOA for authority: {origin}"); 0 } } } /// get the current serial number for the zone. pub(super) fn serial(&self, origin: &LowerName) -> u32 { match self.inner_soa(origin) { Some(soa) => soa.serial(), None => { error!("could not lookup SOA for authority: {origin}"); 0 } } } pub(super) fn inner_lookup( &self, name: &LowerName, record_type: RecordType, lookup_options: LookupOptions, ) -> Option> { // this range covers all the records for any of the RecordTypes at a given label. let start_range_key = RrKey::new(name.clone(), RecordType::Unknown(u16::MIN)); let end_range_key = RrKey::new(name.clone(), RecordType::Unknown(u16::MAX)); fn aname_covers_type(key_type: RecordType, query_type: RecordType) -> bool { (query_type == RecordType::A || query_type == RecordType::AAAA) && key_type == RecordType::ANAME } let lookup = self .records .range(&start_range_key..&end_range_key) // remember CNAME can be the only record at a particular label .find(|(key, _)| { key.record_type == record_type || key.record_type == RecordType::CNAME || aname_covers_type(key.record_type, record_type) }) .map(|(_key, rr_set)| rr_set); // TODO: maybe unwrap this recursion. match lookup { None => self.inner_lookup_wildcard(name, record_type, lookup_options), l => l.cloned(), } } fn inner_lookup_wildcard( &self, name: &LowerName, record_type: RecordType, lookup_options: LookupOptions, ) -> Option> { // if this is a wildcard or a root, both should break continued lookups if name.is_wildcard() || name.is_root() { return None; } let mut wildcard = name.clone().into_wildcard(); loop { let Some(rrset) = self.inner_lookup(&wildcard, record_type, lookup_options) else { let parent = wildcard.base_name(); if parent.is_root() { return None; } wildcard = parent.into_wildcard(); continue; }; // we need to change the name to the query name in the result set since this was a wildcard let mut new_answer = RecordSet::with_ttl(Name::from(name), rrset.record_type(), rrset.ttl()); #[allow(clippy::needless_late_init)] let records; #[allow(clippy::needless_late_init)] let _rrsigs: Vec<&Record>; cfg_if! { if #[cfg(feature = "__dnssec")] { let (records_tmp, rrsigs_tmp) = rrset .records(lookup_options.dnssec_ok()) .partition(|r| r.record_type() != RecordType::RRSIG); records = records_tmp; _rrsigs = rrsigs_tmp; } else { let (records_tmp, rrsigs_tmp) = (rrset.records_without_rrsigs(), Vec::with_capacity(0)); records = records_tmp; _rrsigs = rrsigs_tmp; } }; for record in records { new_answer.add_rdata(record.data().clone()); } #[cfg(feature = "__dnssec")] for rrsig in _rrsigs { new_answer.insert_rrsig(rrsig.clone()) } return Some(Arc::new(new_answer)); } } /// Search for additional records to include in the response /// /// # Arguments /// /// * original_name - the original name that was being looked up /// * original_query_type - original type in the request query /// * next_name - the name from the CNAME, ANAME, MX, etc. record that is being searched /// * search_type - the root search type, ANAME, CNAME, MX, i.e. the beginning of the chain /// * lookup_options - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) pub(super) fn additional_search( &self, original_name: &LowerName, original_query_type: RecordType, next_name: LowerName, _search_type: RecordType, lookup_options: LookupOptions, ) -> Option>> { let mut additionals: Vec> = vec![]; // if it's a CNAME or other forwarding record, we'll be adding additional records based on the query_type let mut query_types_arr = [original_query_type; 2]; let query_types: &[RecordType] = match original_query_type { RecordType::ANAME | RecordType::NS | RecordType::MX | RecordType::SRV => { query_types_arr = [RecordType::A, RecordType::AAAA]; &query_types_arr[..] } _ => &query_types_arr[..1], }; for query_type in query_types { // loop and collect any additional records to send // Track the names we've looked up for this query type. let mut names = HashSet::new(); // If we're just going to repeat the same query then bail out. if query_type == &original_query_type { names.insert(original_name.clone()); } let mut next_name = Some(next_name.clone()); while let Some(search) = next_name.take() { // If we've already looked up this name then bail out. if names.contains(&search) { break; } let additional = self.inner_lookup(&search, *query_type, lookup_options); names.insert(search); if let Some(additional) = additional { // assuming no crazy long chains... if !additionals.contains(&additional) { additionals.push(additional.clone()); } next_name = maybe_next_name(&additional, *query_type).map(|(name, _search_type)| name); } } } if !additionals.is_empty() { Some(additionals) } else { None } } #[cfg(any(feature = "__dnssec", feature = "sqlite"))] pub(super) fn increment_soa_serial(&mut self, origin: &LowerName, dns_class: DNSClass) -> u32 { // we'll remove the SOA and then replace it let rr_key = RrKey::new(origin.clone(), RecordType::SOA); let record = self .records .remove(&rr_key) // TODO: there should be an unwrap on rrset, but it's behind Arc .and_then(|rrset| rrset.records_without_rrsigs().next().cloned()); let mut record = if let Some(record) = record { record } else { error!("could not lookup SOA for authority: {}", origin); return 0; }; let serial = if let RData::SOA(soa_rdata) = record.data_mut() { soa_rdata.increment_serial(); soa_rdata.serial() } else { panic!("This was not an SOA record"); // valid panic, never should happen }; self.upsert(record, serial, dns_class); serial } /// Inserts or updates a `Record` depending on it's existence in the authority. /// /// Guarantees that SOA, CNAME only has one record, will implicitly update if they already exist. /// /// # Arguments /// /// * `record` - The `Record` to be inserted or updated. /// * `serial` - Current serial number to be recorded against updates. /// /// # Return value /// /// true if the value was inserted, false otherwise pub(super) fn upsert(&mut self, record: Record, serial: u32, dns_class: DNSClass) -> bool { if dns_class != record.dns_class() { warn!( "mismatched dns_class on record insert, zone: {} record: {}", dns_class, record.dns_class() ); return false; } #[cfg(feature = "__dnssec")] fn is_nsec(upsert_type: RecordType, occupied_type: RecordType) -> bool { // NSEC is always allowed upsert_type == RecordType::NSEC || upsert_type == RecordType::NSEC3 || occupied_type == RecordType::NSEC || occupied_type == RecordType::NSEC3 } #[cfg(not(feature = "__dnssec"))] fn is_nsec(_upsert_type: RecordType, _occupied_type: RecordType) -> bool { // TODO: we should make the DNSSEC RecordTypes always visible false } /// returns true if an only if the label can not co-occupy space with the checked type #[allow(clippy::nonminimal_bool)] fn label_does_not_allow_multiple( upsert_type: RecordType, occupied_type: RecordType, check_type: RecordType, ) -> bool { // it's a CNAME/ANAME but there's a record that's not a CNAME/ANAME at this location (upsert_type == check_type && occupied_type != check_type) || // it's a different record, but there is already a CNAME/ANAME here (upsert_type != check_type && occupied_type == check_type) } // check that CNAME and ANAME is either not already present, or no other records are if it's a CNAME let start_range_key = RrKey::new(record.name().into(), RecordType::Unknown(u16::MIN)); let end_range_key = RrKey::new(record.name().into(), RecordType::Unknown(u16::MAX)); let multiple_records_at_label_disallowed = self .records .range(&start_range_key..&end_range_key) // remember CNAME can be the only record at a particular label .any(|(key, _)| { !is_nsec(record.record_type(), key.record_type) && label_does_not_allow_multiple( record.record_type(), key.record_type, RecordType::CNAME, ) }); if multiple_records_at_label_disallowed { // consider making this an error? return false; } let rr_key = RrKey::new(record.name().into(), record.record_type()); let records: &mut Arc = self.records.entry(rr_key).or_insert_with(|| { Arc::new(RecordSet::new( record.name().clone(), record.record_type(), serial, )) }); // because this is and Arc, we need to clone and then replace the entry let mut records_clone = RecordSet::clone(&*records); if records_clone.insert(record, serial) { *records = Arc::new(records_clone); true } else { false } } /// (Re)generates the nsec records, increments the serial number and signs the zone #[cfg(feature = "__dnssec")] pub(super) fn secure_zone_mut( &mut self, origin: &LowerName, dns_class: DNSClass, nx_proof_kind: Option<&NxProofKind>, ) -> DnsSecResult<()> { // TODO: only call nsec_zone after adds/deletes // needs to be called before incrementing the soa serial, to make sure IXFR works properly match nx_proof_kind { Some(NxProofKind::Nsec) => self.nsec_zone(origin, dns_class), Some(NxProofKind::Nsec3 { algorithm, salt, iterations, opt_out, }) => self.nsec3_zone(origin, dns_class, *algorithm, salt, *iterations, *opt_out)?, None => (), } // need to resign any records at the current serial number and bump the number. // first bump the serial number on the SOA, so that it is resigned with the new serial. self.increment_soa_serial(origin, dns_class); // TODO: should we auto sign here? or maybe up a level... self.sign_zone(origin, dns_class) } #[cfg(feature = "__dnssec")] fn nsec_zone(&mut self, origin: &LowerName, dns_class: DNSClass) { // only create nsec records for secure zones use std::mem; if self.secure_keys.is_empty() { return; } debug!("generating nsec records: {}", origin); // first remove all existing nsec records let delete_keys: Vec = self .records .keys() .filter(|k| k.record_type == RecordType::NSEC) .cloned() .collect(); for key in delete_keys { self.records.remove(&key); } // now go through and generate the nsec records let ttl = self.minimum_ttl(origin); let serial = self.serial(origin); let mut records: Vec = vec![]; { let mut nsec_info: Option<(&Name, BTreeSet)> = None; for key in self.records.keys() { match &mut nsec_info { None => nsec_info = Some((&key.name, BTreeSet::from([key.record_type]))), Some((name, vec)) if LowerName::new(name) == key.name => { vec.insert(key.record_type); } Some((name, vec)) => { // names aren't equal, create the NSEC record let rdata = NSEC::new_cover_self(key.name.clone().into(), mem::take(vec)); let record = Record::from_rdata(name.clone(), ttl, rdata); records.push(record.into_record_of_rdata()); // new record... nsec_info = Some((&key.name, BTreeSet::from([key.record_type]))) } } } // the last record if let Some((name, vec)) = nsec_info { // names aren't equal, create the NSEC record let rdata = NSEC::new_cover_self(origin.clone().into(), vec); let record = Record::from_rdata(name.clone(), ttl, rdata); records.push(record.into_record_of_rdata()); } } // insert all the nsec records for record in records { let upserted = self.upsert(record, serial, dns_class); debug_assert!(upserted); } } #[cfg(feature = "__dnssec")] fn nsec3_zone( &mut self, origin: &LowerName, dns_class: DNSClass, hash_alg: Nsec3HashAlgorithm, salt: &[u8], iterations: u16, opt_out: bool, ) -> DnsSecResult<()> { // only create nsec records for secure zones if self.secure_keys.is_empty() { return Ok(()); } debug!("generating nsec3 records: {origin}"); // first remove all existing nsec records let delete_keys = self .records .keys() .filter(|k| k.record_type == RecordType::NSEC3) .cloned() .collect::>(); for key in delete_keys { self.records.remove(&key); } // now go through and generate the nsec3 records let ttl = self.minimum_ttl(origin); let serial = self.serial(origin); // Store the record types of each domain name so we can generate NSEC3 records for each // domain name. let mut record_types = HashMap::new(); record_types.insert(origin.clone(), ([RecordType::NSEC3PARAM].into(), true)); let mut delegation_points = HashSet::::new(); for key in self.records.keys() { if !origin.zone_of(&key.name) { // Non-authoritative record outside of zone continue; } if delegation_points .iter() .any(|name| name.zone_of(&key.name) && name != &key.name) { // Non-authoritative record below zone cut continue; } if key.record_type == RecordType::NS && &key.name != origin { delegation_points.insert(key.name.clone()); } // Store the type of the current record under its domain name match record_types.entry(key.name.clone()) { Entry::Occupied(mut entry) => { let (rtypes, exists): &mut (HashSet, bool) = entry.get_mut(); rtypes.insert(key.record_type); *exists = true; } Entry::Vacant(entry) => { entry.insert((HashSet::from([key.record_type]), true)); } } } if opt_out { // Delete owner names that have unsigned delegations. let ns_only = HashSet::from([RecordType::NS]); record_types.retain(|_name, (types, _exists)| types != &ns_only); } // For every domain name between the current name and the origin, add it to `record_types` // without any record types. This covers all the empty non-terminals that must have an NSEC3 // record as well. for name in record_types.keys().cloned().collect::>() { let mut parent = name.base_name(); while parent.num_labels() > origin.num_labels() { record_types .entry(parent.clone()) .or_insert_with(|| (HashSet::new(), false)); parent = parent.base_name(); } } // Compute the hash of all the names. let mut record_types = record_types .into_iter() .map(|(name, (type_bit_maps, exists))| { let hashed_name = hash_alg.hash(salt, &name, iterations)?; Ok((hashed_name, (type_bit_maps, exists))) }) .collect::, ProtoError>>()?; // Sort by hash. record_types.sort_by(|(a, _), (b, _)| a.as_ref().cmp(b.as_ref())); let mut records = vec![]; // Generate an NSEC3 record for every name for (i, (hashed_name, (type_bit_maps, exists))) in record_types.iter().enumerate() { // Get the next hashed name following the hash order. let next_index = (i + 1) % record_types.len(); let next_hashed_name = record_types[next_index].0.as_ref().to_vec(); let rdata = NSEC3::new( hash_alg, opt_out, iterations, salt.to_vec(), next_hashed_name, type_bit_maps .iter() .copied() .chain(exists.then_some(RecordType::RRSIG)), ); let name = origin.prepend_label(data_encoding::BASE32_DNSSEC.encode(hashed_name.as_ref()))?; let record = Record::from_rdata(name, ttl, rdata); records.push(record.into_record_of_rdata()); } // Include the NSEC3PARAM record. let rdata = NSEC3PARAM::new(hash_alg, opt_out, iterations, salt.to_vec()); let record = Record::from_rdata(origin.into(), ttl, rdata); records.push(record.into_record_of_rdata()); // insert all the NSEC3 records. for record in records { let upserted = self.upsert(record, serial, dns_class); debug_assert!(upserted); } Ok(()) } /// Signs an RecordSet, and stores the RRSIGs in the RecordSet /// /// This will sign the RecordSet with all the registered keys in the zone /// /// # Arguments /// /// * `rr_set` - RecordSet to sign /// * `secure_keys` - Set of keys to use to sign the RecordSet, see `self.signers()` /// * `zone_ttl` - the zone TTL, see `self.minimum_ttl()` /// * `zone_class` - DNSClass of the zone, see `self.zone_class()` #[cfg(feature = "__dnssec")] pub(super) fn sign_rrset( rr_set: &mut RecordSet, secure_keys: &[SigSigner], zone_ttl: u32, zone_class: DNSClass, ) -> DnsSecResult<()> { let inception = OffsetDateTime::now_utc(); rr_set.clear_rrsigs(); let rrsig_temp = Record::update0(rr_set.name().clone(), zone_ttl, RecordType::RRSIG); for signer in secure_keys { debug!( "signing rr_set: {}, {} with: {}", rr_set.name(), rr_set.record_type(), signer.key().algorithm(), ); let expiration = inception + signer.sig_duration(); let tbs = TBS::from_rrset(rr_set, zone_class, inception, expiration, signer); // TODO, maybe chain these with some ETL operations instead? let tbs = match tbs { Ok(tbs) => tbs, Err(err) => { error!("could not serialize rrset to sign: {}", err); continue; } }; let signature = signer.sign(&tbs); let signature = match signature { Ok(signature) => signature, Err(err) => { error!("could not sign rrset: {}", err); continue; } }; let mut rrsig = rrsig_temp.clone(); rrsig.set_data(RData::DNSSEC(DNSSECRData::RRSIG(RRSIG::new( // type_covered: RecordType, rr_set.record_type(), // algorithm: Algorithm, signer.key().algorithm(), // num_labels: u8, rr_set.name().num_labels(), // original_ttl: u32, rr_set.ttl(), // sig_expiration: u32, expiration.unix_timestamp() as u32, // sig_inception: u32, inception.unix_timestamp() as u32, // key_tag: u16, signer.calculate_key_tag()?, // signer_name: Name, signer.signer_name().clone(), // sig: Vec signature, )))); rr_set.insert_rrsig(rrsig); } Ok(()) } /// Signs all records in the zone. #[cfg(feature = "__dnssec")] fn sign_zone(&mut self, origin: &LowerName, dns_class: DNSClass) -> DnsSecResult<()> { debug!("signing zone: {}", origin); let minimum_ttl = self.minimum_ttl(origin); let secure_keys = &self.secure_keys; let records = &mut self.records; // TODO: should this be an error? if secure_keys.is_empty() { warn!( "attempt to sign_zone {} for dnssec, but no keys available!", origin ) } // sign all record_sets, as of 0.12.1 this includes DNSKEY for rr_set_orig in records.values_mut() { // because the rrset is an Arc, it must be cloned before mutated let rr_set = Arc::make_mut(rr_set_orig); Self::sign_rrset(rr_set, secure_keys, minimum_ttl, dns_class)?; } Ok(()) } /// Find a record that covers the given name. That is, an NSEC3 record such that the hashed owner /// name of the given name falls between the record's owner name and its next hashed owner /// name. #[cfg(feature = "__dnssec")] fn find_cover( &self, name: &LowerName, zone: &Name, info: &Nsec3QueryInfo<'_>, ) -> Result>, ProtoError> { let owner_name = info.get_hashed_owner_name(name, zone)?; let records = self .records .values() .filter(|rr_set| rr_set.record_type() == RecordType::NSEC3); // Find the record with the largest owner name such that its owner name is before the // hashed QNAME. If this record exist, it already covers QNAME. Otherwise, the QNAME // preceeds all the existing NSEC3 records' owner names, meaning that it is covered by // the NSEC3 record with the largest owner name. Ok(records .clone() .filter(|rr_set| rr_set.record_type() == RecordType::NSEC3) .filter(|rr_set| rr_set.name() < &*owner_name) .max_by_key(|rr_set| rr_set.name()) .or_else(|| records.max_by_key(|rr_set| rr_set.name())) .cloned()) } /// Return the next closer name and the record that matches the closest encloser of a given name. #[cfg(feature = "__dnssec")] fn closest_encloser_proof( &self, name: &LowerName, zone: &Name, info: &Nsec3QueryInfo<'_>, ) -> Result)>, ProtoError> { let mut next_closer_name = name.clone(); let mut closest_encloser = next_closer_name.base_name(); while !closest_encloser.is_root() { let rr_key = RrKey::new( info.get_hashed_owner_name(&closest_encloser, zone)?, RecordType::NSEC3, ); if let Some(rrs) = self.records.get(&rr_key) { return Ok(Some((next_closer_name, rrs.clone()))); } next_closer_name = next_closer_name.base_name(); closest_encloser = closest_encloser.base_name(); } Ok(None) } } hickory-server-0.25.2/src/store/in_memory/mod.rs000064400000000000000000000752731046102023000177520ustar 00000000000000// 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. //! Zone file based serving with Dynamic DNS and journaling support #[cfg(all(feature = "__dnssec", feature = "testing"))] use std::ops::Deref; use std::{collections::BTreeMap, ops::DerefMut, sync::Arc}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tracing::debug; #[cfg(feature = "__dnssec")] use tracing::warn; use crate::{ authority::{ AnyRecords, AuthLookup, Authority, LookupControlFlow, LookupError, LookupOptions, LookupRecords, MessageRequest, UpdateResult, ZoneType, }, proto::{ op::ResponseCode, rr::{DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType, RrKey, rdata::SOA}, }, server::RequestInfo, }; #[cfg(feature = "__dnssec")] use crate::{ authority::{DnssecAuthority, Nsec3QueryInfo}, dnssec::NxProofKind, proto::dnssec::{ DnsSecResult, SigSigner, rdata::{DNSKEY, DNSSECRData, key::KEY}, }, }; mod inner; use inner::InnerInMemory; /// InMemoryAuthority is responsible for storing the resource records for a particular zone. /// /// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the /// start of authority for the zone, is a Secondary, or a cached zone. pub struct InMemoryAuthority { origin: LowerName, class: DNSClass, zone_type: ZoneType, allow_axfr: bool, inner: RwLock, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, } impl InMemoryAuthority { /// Creates a new Authority. /// /// # Arguments /// /// * `origin` - The zone `Name` being created, this should match that of the `RecordType::SOA` /// record. /// * `records` - The map of the initial set of records in the zone. /// * `zone_type` - The type of zone, i.e. is this authoritative? /// * `allow_axfr` - Whether AXFR is allowed. /// * `nx_proof_kind` - The kind of non-existence proof to be used by the server. /// /// # Return value /// /// The new `Authority`. pub fn new( origin: Name, records: BTreeMap, zone_type: ZoneType, allow_axfr: bool, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, ) -> Result { let mut this = Self::empty( origin.clone(), zone_type, allow_axfr, #[cfg(feature = "__dnssec")] nx_proof_kind, ); let inner = this.inner.get_mut(); // SOA must be present let serial = records .iter() .find(|(key, _)| key.record_type == RecordType::SOA) .and_then(|(_, rrset)| rrset.records_without_rrsigs().next()) .map(Record::data) .and_then(RData::as_soa) .map(SOA::serial) .ok_or_else(|| format!("SOA record must be present: {origin}"))?; let iter = records.into_values(); // add soa to the records for rrset in iter { let name = rrset.name().clone(); let rr_type = rrset.record_type(); for record in rrset.records_without_rrsigs() { if !inner.upsert(record.clone(), serial, this.class) { return Err(format!( "Failed to insert {name} {rr_type} to zone: {origin}" )); }; } } Ok(this) } /// Creates an empty Authority /// /// # Warning /// /// This is an invalid zone, SOA must be added pub fn empty( origin: Name, zone_type: ZoneType, allow_axfr: bool, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, ) -> Self { Self { origin: LowerName::new(&origin), class: DNSClass::IN, zone_type, allow_axfr, inner: RwLock::new(InnerInMemory::default()), #[cfg(feature = "__dnssec")] nx_proof_kind, } } /// The DNSClass of this zone pub fn class(&self) -> DNSClass { self.class } /// Allow AXFR's (zone transfers) #[cfg(any(test, feature = "testing"))] pub fn set_allow_axfr(&mut self, allow_axfr: bool) { self.allow_axfr = allow_axfr; } /// Clears all records (including SOA, etc) pub fn clear(&mut self) { self.inner.get_mut().records.clear() } /// Retrieve the Signer, which contains the private keys, for this zone #[cfg(all(feature = "__dnssec", feature = "testing"))] pub async fn secure_keys(&self) -> impl Deref + '_ { RwLockWriteGuard::map(self.inner.write().await, |i| i.secure_keys.as_mut_slice()) } /// Get all the records pub async fn records(&self) -> BTreeMap> { let records = RwLockReadGuard::map(self.inner.read().await, |i| &i.records); records.clone() } /// Get a mutable reference to the records pub async fn records_mut( &self, ) -> impl DerefMut>> + '_ { RwLockWriteGuard::map(self.inner.write().await, |i| &mut i.records) } /// Get a mutable reference to the records pub fn records_get_mut(&mut self) -> &mut BTreeMap> { &mut self.inner.get_mut().records } /// Returns the minimum ttl (as used in the SOA record) pub async fn minimum_ttl(&self) -> u32 { self.inner.read().await.minimum_ttl(self.origin()) } /// get the current serial number for the zone. pub async fn serial(&self) -> u32 { self.inner.read().await.serial(self.origin()) } #[cfg(any(feature = "__dnssec", feature = "sqlite"))] #[allow(unused)] pub(crate) async fn increment_soa_serial(&self) -> u32 { self.inner .write() .await .increment_soa_serial(self.origin(), self.class) } /// Inserts or updates a `Record` depending on it's existence in the authority. /// /// Guarantees that SOA, CNAME only has one record, will implicitly update if they already exist. /// /// # Arguments /// /// * `record` - The `Record` to be inserted or updated. /// * `serial` - Current serial number to be recorded against updates. /// /// # Return value /// /// true if the value was inserted, false otherwise pub async fn upsert(&self, record: Record, serial: u32) -> bool { self.inner.write().await.upsert(record, serial, self.class) } /// Non-async version of upsert when behind a mutable reference. pub fn upsert_mut(&mut self, record: Record, serial: u32) -> bool { self.inner.get_mut().upsert(record, serial, self.class) } /// Add a (Sig0) key that is authorized to perform updates against this authority #[cfg(feature = "__dnssec")] fn inner_add_update_auth_key( inner: &mut InnerInMemory, name: Name, key: KEY, origin: &LowerName, dns_class: DNSClass, ) -> DnsSecResult<()> { let rdata = RData::DNSSEC(DNSSECRData::KEY(key)); // TODO: what TTL? let record = Record::from_rdata(name, 86400, rdata); let serial = inner.serial(origin); if inner.upsert(record, serial, dns_class) { Ok(()) } else { Err("failed to add auth key".into()) } } /// Non-async method of add_update_auth_key when behind a mutable reference #[cfg(feature = "__dnssec")] pub fn add_update_auth_key_mut(&mut self, name: Name, key: KEY) -> DnsSecResult<()> { let Self { origin, inner, class, .. } = self; Self::inner_add_update_auth_key(inner.get_mut(), name, key, origin, *class) } /// By adding a secure key, this will implicitly enable dnssec for the zone. /// /// # Arguments /// /// * `signer` - Signer with associated private key #[cfg(feature = "__dnssec")] fn inner_add_zone_signing_key( inner: &mut InnerInMemory, signer: SigSigner, origin: &LowerName, dns_class: DNSClass, ) -> DnsSecResult<()> { // also add the key to the zone let zone_ttl = inner.minimum_ttl(origin); let dnskey = DNSKEY::from_key(&signer.key().to_public_key()?); let dnskey = Record::from_rdata( origin.clone().into(), zone_ttl, RData::DNSSEC(DNSSECRData::DNSKEY(dnskey)), ); // TODO: also generate the CDS and CDNSKEY let serial = inner.serial(origin); inner.upsert(dnskey, serial, dns_class); inner.secure_keys.push(signer); Ok(()) } /// Non-async method of add_zone_signing_key when behind a mutable reference #[cfg(feature = "__dnssec")] pub fn add_zone_signing_key_mut(&mut self, signer: SigSigner) -> DnsSecResult<()> { let Self { origin, inner, class, .. } = self; Self::inner_add_zone_signing_key(inner.get_mut(), signer, origin, *class) } /// (Re)generates the nsec records, increments the serial number and signs the zone #[cfg(feature = "__dnssec")] pub fn secure_zone_mut(&mut self) -> DnsSecResult<()> { let Self { origin, inner, .. } = self; inner .get_mut() .secure_zone_mut(origin, self.class, self.nx_proof_kind.as_ref()) } /// (Re)generates the nsec records, increments the serial number and signs the zone #[cfg(not(feature = "__dnssec"))] pub fn secure_zone_mut(&mut self) -> Result<(), &str> { Err("DNSSEC was not enabled during compilation.") } } #[async_trait::async_trait] impl Authority for InMemoryAuthority { type Lookup = AuthLookup; /// What type is this zone fn zone_type(&self) -> ZoneType { self.zone_type } /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool { self.allow_axfr } /// Takes the UpdateMessage, extracts the Records, and applies the changes to the record set. /// /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.4 - Process Update Section /// /// Next, the Update Section is processed as follows. /// /// 3.4.2 - Update /// /// The Update Section is parsed into RRs and these RRs are processed in /// order. /// /// 3.4.2.1. If any system failure (such as an out of memory condition, /// or a hardware error in persistent storage) occurs during the /// processing of this section, signal SERVFAIL to the requestor and undo /// all updates applied to the zone during this transaction. /// /// 3.4.2.2. Any Update RR whose CLASS is the same as ZCLASS is added to /// the zone. In case of duplicate RDATAs (which for SOA RRs is always /// the case, and for WKS RRs is the case if the ADDRESS and PROTOCOL /// fields both match), the Zone RR is replaced by Update RR. If the /// TYPE is SOA and there is no Zone SOA RR, or the new SOA.SERIAL is /// lower (according to [RFC1982]) than or equal to the current Zone SOA /// RR's SOA.SERIAL, the Update RR is ignored. In the case of a CNAME /// Update RR and a non-CNAME Zone RRset or vice versa, ignore the CNAME /// Update RR, otherwise replace the CNAME Zone RR with the CNAME Update /// RR. /// /// 3.4.2.3. For any Update RR whose CLASS is ANY and whose TYPE is ANY, /// all Zone RRs with the same NAME are deleted, unless the NAME is the /// same as ZNAME in which case only those RRs whose TYPE is other than /// SOA or NS are deleted. For any Update RR whose CLASS is ANY and /// whose TYPE is not ANY all Zone RRs with the same NAME and TYPE are /// deleted, unless the NAME is the same as ZNAME in which case neither /// SOA or NS RRs will be deleted. /// /// 3.4.2.4. For any Update RR whose class is NONE, any Zone RR whose /// NAME, TYPE, RDATA and RDLENGTH are equal to the Update RR is deleted, /// unless the NAME is the same as ZNAME and either the TYPE is SOA or /// the TYPE is NS and the matching Zone RR is the only NS remaining in /// the RRset, in which case this Update RR is ignored. /// /// 3.4.2.5. Signal NOERROR to the requestor. /// ``` /// /// # Arguments /// /// * `update` - The `UpdateMessage` records will be extracted and used to perform the update /// actions as specified in the above RFC. /// /// # Return value /// /// true if any of additions, updates or deletes were made to the zone, false otherwise. Err is /// returned in the case of bad data, etc. async fn update(&self, _update: &MessageRequest) -> UpdateResult { Err(ResponseCode::NotImp) } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName { &self.origin } /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `query_type` - The `RecordType` to look up. `RecordType::ANY` will return all records /// matching `name`. `RecordType::AXFR` will return all record types except /// `RecordType::SOA` due to the requirements that on zone transfers the /// `RecordType::SOA` must both precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, query_type: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow { let inner = self.inner.read().await; // Collect the records from each rr_set let (result, additionals): (LookupControlFlow, Option) = match query_type { RecordType::AXFR | RecordType::ANY => { let result = AnyRecords::new( lookup_options, inner.records.values().cloned().collect(), query_type, name.clone(), ); ( LookupControlFlow::Continue(Ok(LookupRecords::AnyRecords(result))), None, ) } _ => { // perform the lookup let answer = inner.inner_lookup(name, query_type, lookup_options); // evaluate any cnames for additional inclusion let additionals_root_chain_type: Option<(_, _)> = answer .as_ref() .and_then(|a| maybe_next_name(a, query_type)) .and_then(|(search_name, search_type)| { inner .additional_search( name, query_type, search_name, search_type, lookup_options, ) .map(|adds| (adds, search_type)) }); // if the chain started with an ANAME, take the A or AAAA record from the list let (additionals, answer) = match (additionals_root_chain_type, answer, query_type) { ( Some((additionals, RecordType::ANAME)), Some(answer), RecordType::A, ) | ( Some((additionals, RecordType::ANAME)), Some(answer), RecordType::AAAA, ) => { // This should always be true... debug_assert_eq!(answer.record_type(), RecordType::ANAME); // in the case of ANAME the final record should be the A or AAAA record let (rdatas, a_aaaa_ttl) = { let last_record = additionals.last(); let a_aaaa_ttl = last_record.map_or(u32::MAX, |r| r.ttl()); // grap the rdatas let rdatas: Option> = last_record .and_then(|record| match record.record_type() { RecordType::A | RecordType::AAAA => { // the RRSIGS will be useless since we're changing the record type Some(record.records_without_rrsigs()) } _ => None, }) .map(|records| { records.map(Record::data).cloned().collect::>() }); (rdatas, a_aaaa_ttl) }; // now build up a new RecordSet // the name comes from the ANAME record // according to the rfc the ttl is from the ANAME // TODO: technically we should take the min of the potential CNAME chain let ttl = answer.ttl().min(a_aaaa_ttl); let mut new_answer = RecordSet::new(answer.name().clone(), query_type, ttl); for rdata in rdatas.into_iter().flatten() { new_answer.add_rdata(rdata); } // if DNSSEC is enabled, and the request had the DO set, sign the recordset #[cfg(feature = "__dnssec")] // ANAME's are constructed on demand, so need to be signed before return if lookup_options.dnssec_ok() { InnerInMemory::sign_rrset( &mut new_answer, &inner.secure_keys, inner.minimum_ttl(self.origin()), self.class(), ) // rather than failing the request, we'll just warn .map_err(|e| warn!("failed to sign ANAME record: {}", e)) .ok(); } // prepend answer to additionals here (answer is the ANAME record) let additionals = std::iter::once(answer).chain(additionals).collect(); // return the new answer // because the searched set was an Arc, we need to arc too (Some(additionals), Some(Arc::new(new_answer))) } (Some((additionals, _)), answer, _) => (Some(additionals), answer), (None, answer, _) => (None, answer), }; // map the answer to a result let answer = answer.map_or( LookupControlFlow::Continue(Err(LookupError::from(ResponseCode::NXDomain))), |rr_set| { LookupControlFlow::Continue(Ok(LookupRecords::new( lookup_options, rr_set, ))) }, ); let additionals = additionals.map(|a| LookupRecords::many(lookup_options, a)); (answer, additionals) } }; // This is annoying. The 1035 spec literally specifies that most DNS authorities would want to store // records in a list except when there are a lot of records. But this makes indexed lookups by name+type // always return empty sets. This is only important in the negative case, where other DNS authorities // generally return NoError and no results when other types exist at the same name. bah. // TODO: can we get rid of this? use LookupControlFlow::*; let result = match result { Continue(Err(LookupError::ResponseCode(ResponseCode::NXDomain))) => { if inner .records .keys() .any(|key| key.name() == name || name.zone_of(key.name())) { return Continue(Err(LookupError::NameExists)); } else { let code = if self.origin().zone_of(name) { ResponseCode::NXDomain } else { ResponseCode::Refused }; return Continue(Err(LookupError::from(code))); } } Continue(Err(e)) => return Continue(Err(e)), o => o, }; result.map(|answers| AuthLookup::answers(answers, additionals)) } async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { debug!("searching InMemoryAuthority for: {}", request_info.query); let lookup_name = request_info.query.name(); let record_type: RecordType = request_info.query.query_type(); // if this is an AXFR zone transfer, verify that this is either the Secondary or Primary // for AXFR the first and last record must be the SOA if RecordType::AXFR == record_type { // TODO: support more advanced AXFR options if !self.is_axfr_allowed() { return LookupControlFlow::Continue(Err(LookupError::from(ResponseCode::Refused))); } #[allow(deprecated)] match self.zone_type() { ZoneType::Primary | ZoneType::Secondary | ZoneType::Master | ZoneType::Slave => (), // TODO: Forward? _ => { return LookupControlFlow::Continue(Err(LookupError::from( ResponseCode::NXDomain, ))); } } } // perform the actual lookup match record_type { RecordType::SOA => { self.lookup(self.origin(), record_type, lookup_options) .await } RecordType::AXFR => { // TODO: shouldn't these SOA's be secure? at least the first, perhaps not the last? use LookupControlFlow::Continue; let start_soa = if let Continue(Ok(res)) = self.soa_secure(lookup_options).await { res.unwrap_records() } else { LookupRecords::Empty }; let end_soa = if let Continue(Ok(res)) = self.soa().await { res.unwrap_records() } else { LookupRecords::Empty }; let records = if let Continue(Ok(res)) = self.lookup(lookup_name, record_type, lookup_options).await { res.unwrap_records() } else { LookupRecords::Empty }; LookupControlFlow::Continue(Ok(AuthLookup::AXFR { start_soa, end_soa, records, })) } // A standard Lookup path _ => self.lookup(lookup_name, record_type, lookup_options).await, } } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) #[cfg(feature = "__dnssec")] async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow { let inner = self.inner.read().await; // TODO: need a BorrowdRrKey let rr_key = RrKey::new(name.clone(), RecordType::NSEC); let no_data = inner .records .get(&rr_key) .map(|rr_set| LookupRecords::new(lookup_options, rr_set.clone())); if let Some(no_data) = no_data { return LookupControlFlow::Continue(Ok(no_data.into())); } let closest_proof = inner.closest_nsec(name); // we need the wildcard proof, but make sure that it's still part of the zone. let wildcard = name.base_name(); let origin = self.origin(); let wildcard = if origin.zone_of(&wildcard) { wildcard } else { origin.clone() }; // don't duplicate the record... let wildcard_proof = if wildcard != *name { inner.closest_nsec(&wildcard) } else { None }; let proofs = match (closest_proof, wildcard_proof) { (Some(closest_proof), Some(wildcard_proof)) => { // dedup with the wildcard proof if wildcard_proof != closest_proof { vec![wildcard_proof, closest_proof] } else { vec![closest_proof] } } (None, Some(proof)) | (Some(proof), None) => vec![proof], (None, None) => vec![], }; LookupControlFlow::Continue(Ok(LookupRecords::many(lookup_options, proofs).into())) } #[cfg(not(feature = "__dnssec"))] async fn get_nsec_records( &self, _name: &LowerName, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Ok(AuthLookup::default())) } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { let inner = self.inner.read().await; LookupControlFlow::Continue( inner .proof(info, self.origin()) .map(|proof| LookupRecords::many(lookup_options, proof).into()), ) } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { self.nx_proof_kind.as_ref() } } #[cfg(feature = "__dnssec")] #[async_trait::async_trait] impl DnssecAuthority for InMemoryAuthority { /// Add a (Sig0) key that is authorized to perform updates against this authority async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()> { let mut inner = self.inner.write().await; Self::inner_add_update_auth_key(&mut inner, name, key, self.origin(), self.class) } /// By adding a secure key, this will implicitly enable dnssec for the zone. /// /// # Arguments /// /// * `signer` - Signer with associated private key async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()> { let mut inner = self.inner.write().await; Self::inner_add_zone_signing_key(&mut inner, signer, self.origin(), self.class) } /// Sign the zone for DNSSEC async fn secure_zone(&self) -> DnsSecResult<()> { let mut inner = self.inner.write().await; inner.secure_zone_mut(self.origin(), self.class, self.nx_proof_kind.as_ref()) } } /// Gets the next search name, and returns the RecordType that it originated from fn maybe_next_name( record_set: &RecordSet, query_type: RecordType, ) -> Option<(LowerName, RecordType)> { match (record_set.record_type(), query_type) { // ANAME is similar to CNAME, // unlike CNAME, it is only something that continue to additional processing if the // the query was for address (A, AAAA, or ANAME itself) record types. (t @ RecordType::ANAME, RecordType::A) | (t @ RecordType::ANAME, RecordType::AAAA) | (t @ RecordType::ANAME, RecordType::ANAME) => record_set .records_without_rrsigs() .next() .map(Record::data) .and_then(RData::as_aname) .map(|aname| LowerName::from(&aname.0)) .map(|name| (name, t)), (t @ RecordType::NS, RecordType::NS) => record_set .records_without_rrsigs() .next() .map(Record::data) .and_then(RData::as_ns) .map(|ns| LowerName::from(&ns.0)) .map(|name| (name, t)), // CNAME will continue to additional processing for any query type (t @ RecordType::CNAME, _) => record_set .records_without_rrsigs() .next() .map(Record::data) .and_then(RData::as_cname) .map(|cname| LowerName::from(&cname.0)) .map(|name| (name, t)), (t @ RecordType::MX, RecordType::MX) => record_set .records_without_rrsigs() .next() .map(Record::data) .and_then(RData::as_mx) .map(|mx| mx.exchange().clone()) .map(LowerName::from) .map(|name| (name, t)), (t @ RecordType::SRV, RecordType::SRV) => record_set .records_without_rrsigs() .next() .map(Record::data) .and_then(RData::as_srv) .map(|srv| srv.target().clone()) .map(LowerName::from) .map(|name| (name, t)), // other additional collectors can be added here can be added here _ => None, } } hickory-server-0.25.2/src/store/metrics.rs000064400000000000000000000102501046102023000166230ustar 00000000000000use crate::authority::{LookupControlFlow, LookupObject}; use metrics::{Counter, Gauge, Unit, counter, describe_counter, describe_gauge, gauge}; pub(super) struct StoreMetrics { pub(crate) query: QueryStoreMetrics, pub(crate) persistent: PersistentStoreMetrics, } impl StoreMetrics { pub(super) fn new(store: &'static str) -> Self { Self { query: QueryStoreMetrics::new(store), persistent: PersistentStoreMetrics::new(store), } } } pub(super) struct PersistentStoreMetrics { pub(super) zone_records: Gauge, #[cfg(feature = "__dnssec")] pub(super) zone_records_added: Counter, #[cfg(feature = "__dnssec")] pub(super) zone_records_deleted: Counter, #[cfg(feature = "__dnssec")] pub(super) zone_records_updated: Counter, } impl PersistentStoreMetrics { pub(super) fn new(store: &'static str) -> Self { let store_key = "store"; let zone_records_name = "hickory_zone_records_total"; let zone_records = gauge!(zone_records_name, store_key => store); describe_gauge!( zone_records_name, Unit::Count, "number of dns zone records in persisted storages" ); #[cfg(feature = "__dnssec")] let (zone_records_added, zone_records_deleted, zone_records_updated) = { let zone_records_modified_name = "hickory_zone_records_modified_total"; let operation_key = "operation"; let records_added = counter!(zone_records_modified_name, store_key => store, operation_key => "added"); let records_deleted = counter!(zone_records_modified_name, store_key => store, operation_key => "deleted"); let records_updated = counter!(zone_records_modified_name, store_key => store, operation_key => "updated"); describe_counter!( zone_records_modified_name, Unit::Count, "number of dns zone records that had been modified" ); (records_added, records_deleted, records_updated) }; Self { zone_records, #[cfg(feature = "__dnssec")] zone_records_added, #[cfg(feature = "__dnssec")] zone_records_deleted, #[cfg(feature = "__dnssec")] zone_records_updated, } } #[cfg(feature = "__dnssec")] pub(super) fn added(&self) { self.zone_records_added.increment(1); self.zone_records.increment(1); } #[cfg(feature = "__dnssec")] pub(super) fn deleted(&self) { self.zone_records_deleted.increment(1); self.zone_records.decrement(1) } #[cfg(feature = "__dnssec")] pub(super) fn updated(&self) { self.zone_records_updated.increment(1); } } pub(super) struct QueryStoreMetrics { pub(super) zone_record_lookups_success: Counter, pub(super) zone_record_lookups_error: Counter, } impl QueryStoreMetrics { pub(crate) fn new(store: &'static str) -> Self { let zone_record_lookups_name = "hickory_zone_record_lookups_total"; let store_key = "store"; let success_key = "success"; let zone_record_lookups_success = counter!(zone_record_lookups_name, store_key => store, success_key => "true"); let zone_record_lookups_error = counter!(zone_record_lookups_name, store_key => store, success_key => "false"); describe_counter!( zone_record_lookups_name, Unit::Count, "number of occurred dns zone record lookups" ); Self { zone_record_lookups_success, zone_record_lookups_error, } } pub(super) fn increment_lookup( &self, lookup_control_flow: &LookupControlFlow, ) { let is_success = match lookup_control_flow { LookupControlFlow::Continue(res) => res.is_ok(), LookupControlFlow::Break(res) => res.is_ok(), LookupControlFlow::Skip => false, }; if is_success { self.zone_record_lookups_success.increment(1) } else { self.zone_record_lookups_error.increment(1) } } } hickory-server-0.25.2/src/store/mod.rs000064400000000000000000000011001046102023000157260ustar 00000000000000// 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. //! All persistent store implementations pub mod blocklist; pub mod file; pub mod forwarder; pub mod in_memory; #[cfg(feature = "metrics")] mod metrics; pub mod recursor; #[cfg(feature = "sqlite")] pub mod sqlite; hickory-server-0.25.2/src/store/recursor.rs000064400000000000000000000312701046102023000170260ustar 00000000000000// 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. #![cfg(feature = "recursor")] //! Recursive resolver related types #[cfg(feature = "__dnssec")] use std::sync::Arc; use std::{ borrow::Cow, collections::HashSet, fs::File, io::{self, Read}, net::SocketAddr, path::{Path, PathBuf}, time::Instant, }; use ipnet::IpNet; use serde::Deserialize; use tracing::{debug, info}; #[cfg(feature = "__dnssec")] use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind, proto::dnssec::TrustAnchors}; use crate::{ authority::{ Authority, LookupControlFlow, LookupError, LookupObject, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, error::ConfigError, proto::{ op::{Query, ResponseCode}, rr::{LowerName, Name, RData, Record, RecordSet, RecordType}, serialize::txt::{ParseError, Parser}, xfer::Protocol, }, recursor::{DnssecPolicy, Recursor}, resolver::{ config::{NameServerConfig, NameServerConfigGroup}, dns_lru::TtlConfig, lookup::Lookup, }, server::RequestInfo, }; /// An authority that performs recursive resolutions. /// /// This uses the hickory-recursor crate for resolving requests. pub struct RecursiveAuthority { origin: LowerName, recursor: Recursor, } impl RecursiveAuthority { /// Read the Authority for the origin from the specified configuration pub async fn try_from_config( origin: Name, _zone_type: ZoneType, config: &RecursiveConfig, root_dir: Option<&Path>, ) -> Result { info!("loading recursor config: {}", origin); // read the roots let root_addrs = config .read_roots(root_dir) .map_err(|e| format!("failed to read roots {}: {}", config.roots.display(), e))?; // Configure all the name servers let mut roots = NameServerConfigGroup::new(); for socket_addr in root_addrs { roots.push(NameServerConfig { socket_addr, protocol: Protocol::Tcp, tls_dns_name: None, http_endpoint: None, trust_negative_responses: false, bind_addr: None, // TODO: need to support bind addresses }); roots.push(NameServerConfig { socket_addr, protocol: Protocol::Udp, tls_dns_name: None, http_endpoint: None, trust_negative_responses: false, bind_addr: None, }); } let mut builder = Recursor::builder(); if let Some(ns_cache_size) = config.ns_cache_size { builder = builder.ns_cache_size(ns_cache_size); } if let Some(record_cache_size) = config.record_cache_size { builder = builder.record_cache_size(record_cache_size); } let recursor = builder .dnssec_policy(config.dnssec_policy.load().map_err(|e| e.to_string())?) .nameserver_filter(config.allow_server.iter(), config.deny_server.iter()) .recursion_limit(match config.recursion_limit { 0 => None, limit => Some(limit), }) .ns_recursion_limit(match config.ns_recursion_limit { 0 => None, limit => Some(limit), }) .avoid_local_udp_ports(config.avoid_local_udp_ports.clone()) .ttl_config(config.cache_policy.clone()) .case_randomization(config.case_randomization) .build(roots) .map_err(|e| format!("failed to initialize recursor: {e}"))?; Ok(Self { origin: origin.into(), recursor, }) } } #[async_trait::async_trait] impl Authority for RecursiveAuthority { type Lookup = RecursiveLookup; /// Always External fn zone_type(&self) -> ZoneType { ZoneType::External } /// Always false for Forward zones fn is_axfr_allowed(&self) -> bool { false } fn can_validate_dnssec(&self) -> bool { self.recursor.is_validating() } async fn update(&self, _update: &MessageRequest) -> UpdateResult { Err(ResponseCode::NotImp) } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com /// /// In the context of a forwarder, this is either a zone which this forwarder is associated, /// or `.`, the root zone for all zones. If this is not the root zone, then it will only forward /// for lookups which match the given zone name. fn origin(&self) -> &LowerName { &self.origin } /// Forwards a lookup given the resolver configuration for this Forwarded zone async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow { debug!("recursive lookup: {} {}", name, rtype); let query = Query::query(name.into(), rtype); let now = Instant::now(); let result = self .recursor .resolve(query, now, lookup_options.dnssec_ok()) .await; use LookupControlFlow::*; match result { Ok(lookup) => Continue(Ok(RecursiveLookup(lookup))), Err(error) => Continue(Err(LookupError::from(error))), } } async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { self.lookup( request_info.query.name(), request_info.query.query_type(), lookup_options, ) .await } async fn get_nsec_records( &self, _name: &LowerName, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "Getting NSEC records is unimplemented for the recursor", )))) } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, _info: Nsec3QueryInfo<'_>, _lookup_options: LookupOptions, ) -> LookupControlFlow { LookupControlFlow::Continue(Err(LookupError::from(io::Error::new( io::ErrorKind::Other, "getting NSEC3 records is unimplemented for the recursor", )))) } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { None } } /// A Lookup object for the recursive resolver pub struct RecursiveLookup(Lookup); impl LookupObject for RecursiveLookup { fn is_empty(&self) -> bool { self.0.is_empty() } fn iter<'a>(&'a self) -> Box + Send + 'a> { Box::new(self.0.record_iter()) } fn take_additionals(&mut self) -> Option> { None } } /// Configuration for file based zones #[derive(Clone, Deserialize, Eq, PartialEq, Debug)] #[serde(deny_unknown_fields)] pub struct RecursiveConfig { /// File with roots, aka hints pub roots: PathBuf, /// Maximum nameserver cache size pub ns_cache_size: Option, /// Maximum DNS record cache size pub record_cache_size: Option, /// Maximum recursion depth for queries. Set to 0 for unlimited recursion depth. #[serde(default = "recursion_limit_default")] pub recursion_limit: u8, /// Maximum recursion depth for building NS pools. Set to 0 for unlimited recursion depth. #[serde(default = "ns_recursion_limit_default")] pub ns_recursion_limit: u8, /// DNSSEC policy #[serde(default)] pub dnssec_policy: DnssecPolicyConfig, /// Networks that will be queried during resolution #[serde(default)] pub allow_server: Vec, /// Networks that will not be queried during resolution #[serde(default)] pub deny_server: Vec, /// Local UDP ports to avoid when making outgoing queries #[serde(default)] pub avoid_local_udp_ports: HashSet, /// Caching policy, setting minimum and maximum TTLs #[serde(default)] pub cache_policy: TtlConfig, /// Enable case randomization. /// /// Randomize the case of letters in query names, and require that responses preserve the case /// of the query name, in order to mitigate spoofing attacks. This is only applied over UDP. /// /// This implements the mechanism described in /// [draft-vixie-dnsext-dns0x20-00](https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00). #[serde(default)] pub case_randomization: bool, } impl RecursiveConfig { pub(crate) fn read_roots( &self, root_dir: Option<&Path>, ) -> Result, ConfigError> { let path = if let Some(root_dir) = root_dir { Cow::Owned(root_dir.join(&self.roots)) } else { Cow::Borrowed(&self.roots) }; let mut roots = File::open(path.as_ref())?; let mut roots_str = String::new(); roots.read_to_string(&mut roots_str)?; let (_zone, roots_zone) = Parser::new(roots_str, Some(path.into_owned()), Some(Name::root())).parse()?; // TODO: we may want to deny some of the root nameservers, for reasons... Ok(roots_zone .values() .flat_map(RecordSet::records_without_rrsigs) .map(Record::data) .filter_map(RData::ip_addr) // we only want IPs .map(|ip| SocketAddr::from((ip, 53))) // all the roots only have tradition DNS ports .collect()) } } fn recursion_limit_default() -> u8 { 12 } fn ns_recursion_limit_default() -> u8 { 16 } /// DNSSEC policy configuration #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] #[allow(missing_copy_implementations)] pub enum DnssecPolicyConfig { /// security unaware; DNSSEC records will not be requested nor processed #[default] SecurityUnaware, /// DNSSEC validation is disabled; DNSSEC records will be requested and processed #[cfg(feature = "__dnssec")] ValidationDisabled, /// DNSSEC validation is enabled and will use the chosen `trust_anchor` set of keys #[cfg(feature = "__dnssec")] ValidateWithStaticKey { /// set to `None` to use built-in trust anchor path: Option, }, } impl DnssecPolicyConfig { pub(crate) fn load(&self) -> Result { Ok(match self { Self::SecurityUnaware => DnssecPolicy::SecurityUnaware, #[cfg(feature = "__dnssec")] Self::ValidationDisabled => DnssecPolicy::ValidationDisabled, #[cfg(feature = "__dnssec")] Self::ValidateWithStaticKey { path } => DnssecPolicy::ValidateWithStaticKey { trust_anchor: path .as_ref() .map(|path| TrustAnchors::from_file(path)) .transpose()? .map(Arc::new), }, }) } } #[cfg(test)] mod tests { #[cfg(all(feature = "__dnssec", feature = "toml"))] use super::*; #[cfg(all(feature = "__dnssec", feature = "toml"))] #[test] fn can_parse_recursive_config() { let input = r#"roots = "/etc/root.hints" dnssec_policy.ValidateWithStaticKey.path = "/etc/trusted-key.key""#; let config: RecursiveConfig = toml::from_str(input).unwrap(); if let DnssecPolicyConfig::ValidateWithStaticKey { path } = config.dnssec_policy { assert_eq!(Some(Path::new("/etc/trusted-key.key")), path.as_deref()); } else { unreachable!() } } #[cfg(all(feature = "recursor", feature = "toml"))] #[test] fn can_parse_recursor_cache_policy() { use std::time::Duration; use hickory_proto::rr::RecordType; let input = r#"roots = "/etc/root.hints" [cache_policy.default] positive_max_ttl = 14400 [cache_policy.A] positive_max_ttl = 3600"#; let config: RecursiveConfig = toml::from_str(input).unwrap(); assert_eq!( *config .cache_policy .positive_response_ttl_bounds(RecordType::MX) .end(), Duration::from_secs(14400) ); assert_eq!( *config .cache_policy .positive_response_ttl_bounds(RecordType::A) .end(), Duration::from_secs(3600) ) } } hickory-server-0.25.2/src/store/sqlite/mod.rs000064400000000000000000001326371046102023000172530ustar 00000000000000// 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. //! SQLite serving with Dynamic DNS and journaling support use std::{ ops::{Deref, DerefMut}, path::{Path, PathBuf}, sync::Arc, }; use futures_util::lock::Mutex; use serde::Deserialize; use tracing::{error, info, warn}; #[cfg(feature = "metrics")] use crate::store::metrics::StoreMetrics; use crate::{ authority::{ Authority, LookupControlFlow, LookupOptions, MessageRequest, UpdateResult, ZoneType, }, error::{PersistenceError, PersistenceErrorKind}, proto::{ op::ResponseCode, rr::{DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType, RrKey}, }, server::RequestInfo, store::in_memory::InMemoryAuthority, }; #[cfg(feature = "__dnssec")] use crate::{ authority::{DnssecAuthority, Nsec3QueryInfo, UpdateRequest}, dnssec::NxProofKind, proto::dnssec::{ DnsSecResult, SigSigner, Verifier, rdata::{DNSSECRData, key::KEY}, }, }; #[cfg(feature = "__dnssec")] use LookupControlFlow::Continue; pub mod persistence; pub use persistence::Journal; /// SqliteAuthority is responsible for storing the resource records for a particular zone. /// /// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the /// start of authority for the zone, is a Secondary, or a cached zone. #[allow(dead_code)] pub struct SqliteAuthority { in_memory: InMemoryAuthority, journal: Mutex>, allow_update: bool, is_dnssec_enabled: bool, #[cfg(feature = "metrics")] metrics: StoreMetrics, } impl SqliteAuthority { /// Creates a new Authority. /// /// # Arguments /// /// * `in_memory` - InMemoryAuthority for all records. /// * `allow_update` - If true, then this zone accepts dynamic updates. /// * `is_dnssec_enabled` - If true, then the zone will sign the zone with all registered keys, /// (see `add_zone_signing_key()`) /// /// # Return value /// /// The new `Authority`. pub fn new(in_memory: InMemoryAuthority, allow_update: bool, is_dnssec_enabled: bool) -> Self { Self { in_memory, journal: Mutex::new(None), allow_update, is_dnssec_enabled, #[cfg(feature = "metrics")] metrics: StoreMetrics::new("sqlite"), } } /// load the authority from the configuration pub async fn try_from_config( origin: Name, zone_type: ZoneType, allow_axfr: bool, enable_dnssec: bool, root_dir: Option<&Path>, config: &SqliteConfig, #[cfg(feature = "__dnssec")] nx_proof_kind: Option, ) -> Result { use crate::store::file::{FileAuthority, FileConfig}; let zone_name: Name = origin; let root_zone_dir = root_dir.map(PathBuf::from).unwrap_or_default(); // to be compatible with previous versions, the extension might be zone, not jrnl let journal_path: PathBuf = root_zone_dir.join(&config.journal_file_path); let zone_path: PathBuf = root_zone_dir.join(&config.zone_file_path); // load the zone if journal_path.exists() { info!("recovering zone from journal: {:?}", journal_path); let journal = Journal::from_file(&journal_path) .map_err(|e| format!("error opening journal: {journal_path:?}: {e}"))?; let in_memory = InMemoryAuthority::empty( zone_name.clone(), zone_type, allow_axfr, #[cfg(feature = "__dnssec")] nx_proof_kind, ); let mut authority = Self::new(in_memory, config.allow_update, enable_dnssec); authority .recover_with_journal(&journal) .await .map_err(|e| format!("error recovering from journal: {e}"))?; authority.set_journal(journal).await; info!("recovered zone: {}", zone_name); Ok(authority) } else if zone_path.exists() { // TODO: deprecate this portion of loading, instantiate the journal through a separate tool info!("loading zone file: {:?}", zone_path); let file_config = FileConfig { zone_file_path: config.zone_file_path.clone(), }; let in_memory = FileAuthority::try_from_config_internal( zone_name.clone(), zone_type, allow_axfr, root_dir, &file_config, #[cfg(feature = "__dnssec")] nx_proof_kind, #[cfg(feature = "metrics")] true, )? .unwrap(); let mut authority = Self::new(in_memory, config.allow_update, enable_dnssec); // if dynamic update is enabled, enable the journal info!("creating new journal: {:?}", journal_path); let journal = Journal::from_file(&journal_path) .map_err(|e| format!("error creating journal {journal_path:?}: {e}"))?; authority.set_journal(journal).await; // preserve to the new journal, i.e. we just loaded the zone from disk, start the journal authority .persist_to_journal() .await .map_err(|e| format!("error persisting to journal {journal_path:?}: {e}"))?; info!("zone file loaded: {}", zone_name); Ok(authority) } else { Err(format!("no zone file or journal defined at: {zone_path:?}")) } } /// Recovers the zone from a Journal, returns an error on failure to recover the zone. /// /// # Arguments /// /// * `journal` - the journal from which to load the persisted zone. pub async fn recover_with_journal( &mut self, journal: &Journal, ) -> Result<(), PersistenceError> { assert!( self.in_memory.records_get_mut().is_empty(), "records should be empty during a recovery" ); info!("recovering from journal"); for record in journal.iter() { // AXFR is special, it is used to mark the dump of a full zone. // when recovering, if an AXFR is encountered, we should remove all the records in the // authority. if record.record_type() == RecordType::AXFR { self.in_memory.clear(); } else { match self.update_records(&[record], false).await { Ok(_) => { #[cfg(feature = "metrics")] self.metrics.persistent.zone_records.increment(1); } Err(error) => return Err(PersistenceErrorKind::Recovery(error.to_str()).into()), } } } Ok(()) } /// Persist the state of the current zone to the journal, does nothing if there is no associated /// Journal. /// /// Returns an error if there was an issue writing to the persistence layer. pub async fn persist_to_journal(&self) -> Result<(), PersistenceError> { if let Some(journal) = self.journal.lock().await.as_ref() { let serial = self.in_memory.serial().await; info!("persisting zone to journal at SOA.serial: {}", serial); // TODO: THIS NEEDS TO BE IN A TRANSACTION!!! journal.insert_record( serial, &Record::update0(Name::new(), 0, RecordType::AXFR).into_record_of_rdata(), )?; for rr_set in self.in_memory.records().await.values() { // TODO: should we preserve rr_sets or not? for record in rr_set.records_without_rrsigs() { journal.insert_record(serial, record)?; #[cfg(feature = "metrics")] self.metrics.persistent.zone_records.increment(1); } } // TODO: COMMIT THE TRANSACTION!!! } Ok(()) } /// Associate a backing Journal with this Authority for Updatable zones pub async fn set_journal(&mut self, journal: Journal) { *self.journal.lock().await = Some(journal); } /// Returns the associated Journal #[cfg(any(test, feature = "testing"))] pub async fn journal(&self) -> impl Deref> + '_ { self.journal.lock().await } /// Enables the zone for dynamic DNS updates pub fn set_allow_update(&mut self, allow_update: bool) { self.allow_update = allow_update; } /// Get serial #[cfg(any(test, feature = "testing"))] pub async fn serial(&self) -> u32 { self.in_memory.serial().await } /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.2 - Process Prerequisite Section /// /// Next, the Prerequisite Section is checked to see that all /// prerequisites are satisfied by the current state of the zone. Using /// the definitions expressed in Section 1.2, if any RR's NAME is not /// within the zone specified in the Zone Section, signal NOTZONE to the /// requestor. /// /// 3.2.1. For RRs in this section whose CLASS is ANY, test to see that /// TTL and RDLENGTH are both zero (0), else signal FORMERR to the /// requestor. If TYPE is ANY, test to see that there is at least one RR /// in the zone whose NAME is the same as that of the Prerequisite RR, /// else signal NXDOMAIN to the requestor. If TYPE is not ANY, test to /// see that there is at least one RR in the zone whose NAME and TYPE are /// the same as that of the Prerequisite RR, else signal NXRRSET to the /// requestor. /// /// 3.2.2. For RRs in this section whose CLASS is NONE, test to see that /// the TTL and RDLENGTH are both zero (0), else signal FORMERR to the /// requestor. If the TYPE is ANY, test to see that there are no RRs in /// the zone whose NAME is the same as that of the Prerequisite RR, else /// signal YXDOMAIN to the requestor. If the TYPE is not ANY, test to /// see that there are no RRs in the zone whose NAME and TYPE are the /// same as that of the Prerequisite RR, else signal YXRRSET to the /// requestor. /// /// 3.2.3. For RRs in this section whose CLASS is the same as the ZCLASS, /// test to see that the TTL is zero (0), else signal FORMERR to the /// requestor. Then, build an RRset for each unique and /// compare each resulting RRset for set equality (same members, no more, /// no less) with RRsets in the zone. If any Prerequisite RRset is not /// entirely and exactly matched by a zone RRset, signal NXRRSET to the /// requestor. If any RR in this section has a CLASS other than ZCLASS /// or NONE or ANY, signal FORMERR to the requestor. /// /// 3.2.4 - Table Of Metavalues Used In Prerequisite Section /// /// CLASS TYPE RDATA Meaning /// ------------------------------------------------------------ /// ANY ANY empty Name is in use /// ANY rrset empty RRset exists (value independent) /// NONE ANY empty Name is not in use /// NONE rrset empty RRset does not exist /// zone rrset rr RRset exists (value dependent) /// ``` pub async fn verify_prerequisites(&self, pre_requisites: &[Record]) -> UpdateResult<()> { // 3.2.5 - Pseudocode for Prerequisite Section Processing // // for rr in prerequisites // if (rr.ttl != 0) // return (FORMERR) // if (zone_of(rr.name) != ZNAME) // return (NOTZONE); // if (rr.class == ANY) // if (rr.rdlength != 0) // return (FORMERR) // if (rr.type == ANY) // if (!zone_name) // return (NXDOMAIN) // else // if (!zone_rrset) // return (NXRRSET) // if (rr.class == NONE) // if (rr.rdlength != 0) // return (FORMERR) // if (rr.type == ANY) // if (zone_name) // return (YXDOMAIN) // else // if (zone_rrset) // return (YXRRSET) // if (rr.class == zclass) // temp += rr // else // return (FORMERR) // // for rrset in temp // if (zone_rrset != rrset) // return (NXRRSET) for require in pre_requisites { let required_name = LowerName::from(require.name()); if require.ttl() != 0 { warn!("ttl must be 0 for: {:?}", require); return Err(ResponseCode::FormErr); } let origin = self.origin(); if !origin.zone_of(&require.name().into()) { warn!("{} is not a zone_of {}", require.name(), origin); return Err(ResponseCode::NotZone); } match require.dns_class() { DNSClass::ANY => { if let RData::Update0(_) | RData::NULL(..) = require.data() { match require.record_type() { // ANY ANY empty Name is in use RecordType::ANY => { if self .lookup( &required_name, RecordType::ANY, LookupOptions::default(), ) .await .unwrap_or_default() .was_empty() { return Err(ResponseCode::NXDomain); } else { continue; } } // ANY rrset empty RRset exists (value independent) rrset => { if self .lookup(&required_name, rrset, LookupOptions::default()) .await .unwrap_or_default() .was_empty() { return Err(ResponseCode::NXRRSet); } else { continue; } } } } else { return Err(ResponseCode::FormErr); } } DNSClass::NONE => { if let RData::Update0(_) | RData::NULL(..) = require.data() { match require.record_type() { // NONE ANY empty Name is not in use RecordType::ANY => { if !self .lookup( &required_name, RecordType::ANY, LookupOptions::default(), ) .await .unwrap_or_default() .was_empty() { return Err(ResponseCode::YXDomain); } else { continue; } } // NONE rrset empty RRset does not exist rrset => { if !self .lookup(&required_name, rrset, LookupOptions::default()) .await .unwrap_or_default() .was_empty() { return Err(ResponseCode::YXRRSet); } else { continue; } } } } else { return Err(ResponseCode::FormErr); } } class if class == self.in_memory.class() => // zone rrset rr RRset exists (value dependent) { if !self .lookup( &required_name, require.record_type(), LookupOptions::default(), ) .await .unwrap_or_default() .iter() .any(|rr| rr == require) { return Err(ResponseCode::NXRRSet); } else { continue; } } _ => return Err(ResponseCode::FormErr), } } // if we didn't bail everything checked out... Ok(()) } /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.3 - Check Requestor's Permissions /// /// 3.3.1. Next, the requestor's permission to update the RRs named in /// the Update Section may be tested in an implementation dependent /// fashion or using mechanisms specified in a subsequent Secure DNS /// Update protocol. If the requestor does not have permission to /// perform these updates, the server may write a warning message in its /// operations log, and may either signal REFUSED to the requestor, or /// ignore the permission problem and proceed with the update. /// /// 3.3.2. While the exact processing is implementation defined, if these /// verification activities are to be performed, this is the point in the /// server's processing where such performance should take place, since /// if a REFUSED condition is encountered after an update has been /// partially applied, it will be necessary to undo the partial update /// and restore the zone to its original state before answering the /// requestor. /// ``` /// #[cfg(feature = "__dnssec")] #[allow(clippy::blocks_in_conditions)] pub async fn authorize(&self, update_message: &MessageRequest) -> UpdateResult<()> { use tracing::debug; // 3.3.3 - Pseudocode for Permission Checking // // if (security policy exists) // if (this update is not permitted) // if (local option) // log a message about permission problem // if (local option) // return (REFUSED) // does this authority allow_updates? if !self.allow_update { warn!( "update attempted on non-updatable Authority: {}", self.origin() ); return Err(ResponseCode::Refused); } // verify sig0, currently the only authorization that is accepted. let sig0s: &[Record] = update_message.sig0(); debug!("authorizing with: {:?}", sig0s); if !sig0s.is_empty() { let mut found_key = false; for sig in sig0s .iter() .filter_map(|sig0| sig0.data().as_dnssec().and_then(DNSSECRData::as_sig)) { let name = LowerName::from(sig.signer_name()); let keys = self .lookup(&name, RecordType::KEY, LookupOptions::default()) .await; let keys = match keys { Continue(Ok(keys)) => keys, _ => continue, // error trying to lookup a key by that name, try the next one. }; debug!("found keys {:?}", keys); // TODO: check key usage flags and restrictions found_key = keys .iter() .filter_map(|rr_set| rr_set.data().as_dnssec().and_then(DNSSECRData::as_key)) .any(|key| { key.verify_message(update_message, sig.sig(), sig) .map(|_| { info!("verified sig: {:?} with key: {:?}", sig, key); true }) .unwrap_or_else(|_| { debug!("did not verify sig: {:?} with key: {:?}", sig, key); false }) }); if found_key { break; // stop searching for matching keys, we found one } } if found_key { return Ok(()); } } else { warn!( "no sig0 matched registered records: id {}", update_message.id() ); } // getting here, we will always default to rejecting the request // the code will only ever explicitly return authorized actions. Err(ResponseCode::Refused) } /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.4 - Process Update Section /// /// Next, the Update Section is processed as follows. /// /// 3.4.1 - Prescan /// /// The Update Section is parsed into RRs and each RR's CLASS is checked /// to see if it is ANY, NONE, or the same as the Zone Class, else signal /// a FORMERR to the requestor. Using the definitions in Section 1.2, /// each RR's NAME must be in the zone specified by the Zone Section, /// else signal NOTZONE to the requestor. /// /// 3.4.1.2. For RRs whose CLASS is not ANY, check the TYPE and if it is /// ANY, AXFR, MAILA, MAILB, or any other QUERY metatype, or any /// unrecognized type, then signal FORMERR to the requestor. For RRs /// whose CLASS is ANY or NONE, check the TTL to see that it is zero (0), /// else signal a FORMERR to the requestor. For any RR whose CLASS is /// ANY, check the RDLENGTH to make sure that it is zero (0) (that is, /// the RDATA field is empty), and that the TYPE is not AXFR, MAILA, /// MAILB, or any other QUERY metatype besides ANY, or any unrecognized /// type, else signal FORMERR to the requestor. /// ``` #[allow(clippy::unused_unit)] pub async fn pre_scan(&self, records: &[Record]) -> UpdateResult<()> { // 3.4.1.3 - Pseudocode For Update Section Prescan // // [rr] for rr in updates // if (zone_of(rr.name) != ZNAME) // return (NOTZONE); // if (rr.class == zclass) // if (rr.type & ANY|AXFR|MAILA|MAILB) // return (FORMERR) // elsif (rr.class == ANY) // if (rr.ttl != 0 || rr.rdlength != 0 // || rr.type & AXFR|MAILA|MAILB) // return (FORMERR) // elsif (rr.class == NONE) // if (rr.ttl != 0 || rr.type & ANY|AXFR|MAILA|MAILB) // return (FORMERR) // else // return (FORMERR) for rr in records { if !self.origin().zone_of(&rr.name().into()) { return Err(ResponseCode::NotZone); } let class: DNSClass = rr.dns_class(); if class == self.in_memory.class() { match rr.record_type() { RecordType::ANY | RecordType::AXFR | RecordType::IXFR => { return Err(ResponseCode::FormErr); } _ => (), } } else { match class { DNSClass::ANY => { if rr.ttl() != 0 { return Err(ResponseCode::FormErr); } if let RData::Update0(_) | RData::NULL(..) = rr.data() { () } else { return Err(ResponseCode::FormErr); } match rr.record_type() { RecordType::AXFR | RecordType::IXFR => { return Err(ResponseCode::FormErr); } _ => (), } } DNSClass::NONE => { if rr.ttl() != 0 { return Err(ResponseCode::FormErr); } match rr.record_type() { RecordType::ANY | RecordType::AXFR | RecordType::IXFR => { return Err(ResponseCode::FormErr); } _ => (), } } _ => return Err(ResponseCode::FormErr), } } } Ok(()) } /// Updates the specified records according to the update section. /// /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.4.2.6 - Table Of Metavalues Used In Update Section /// /// CLASS TYPE RDATA Meaning /// --------------------------------------------------------- /// ANY ANY empty Delete all RRsets from a name /// ANY rrset empty Delete an RRset /// NONE rrset rr Delete an RR from an RRset /// zone rrset rr Add to an RRset /// ``` /// /// # Arguments /// /// * `records` - set of record instructions for update following above rules /// * `auto_signing_and_increment` - if true, the zone will sign and increment the SOA, this /// should be disabled during recovery. pub async fn update_records( &self, records: &[Record], auto_signing_and_increment: bool, ) -> UpdateResult { let mut updated = false; let serial: u32 = self.in_memory.serial().await; // the persistence act as a write-ahead log. The WAL will also be used for recovery of a zone // subsequent to a failure of the server. if let Some(journal) = &*self.journal.lock().await { if let Err(error) = journal.insert_records(serial, records) { error!("could not persist update records: {}", error); return Err(ResponseCode::ServFail); } } // 3.4.2.7 - Pseudocode For Update Section Processing // // [rr] for rr in updates // if (rr.class == zclass) // if (rr.type == CNAME) // if (zone_rrset) // next [rr] // elsif (zone_rrset) // next [rr] // if (rr.type == SOA) // if (!zone_rrset || // zone_rr.serial > rr.soa.serial) // next [rr] // for zrr in zone_rrset // if (rr.type == CNAME || rr.type == SOA || // (rr.type == WKS && rr.proto == zrr.proto && // rr.address == zrr.address) || // rr.rdata == zrr.rdata) // zrr = rr // next [rr] // zone_rrset += rr // elsif (rr.class == ANY) // if (rr.type == ANY) // if (rr.name == zname) // zone_rrset = Nil // else // zone_rrset = Nil // elsif (rr.name == zname && // (rr.type == SOA || rr.type == NS)) // next [rr] // else // zone_rrset = Nil // elsif (rr.class == NONE) // if (rr.type == SOA) // next [rr] // if (rr.type == NS && zone_rrset == rr) // next [rr] // zone_rr = Nil // return (NOERROR) for rr in records { let rr_name = LowerName::from(rr.name()); let rr_key = RrKey::new(rr_name.clone(), rr.record_type()); match rr.dns_class() { class if class == self.in_memory.class() => { // RFC 2136 - 3.4.2.2. Any Update RR whose CLASS is the same as ZCLASS is added to // the zone. In case of duplicate RDATAs (which for SOA RRs is always // the case, and for WKS RRs is the case if the ADDRESS and PROTOCOL // fields both match), the Zone RR is replaced by Update RR. If the // TYPE is SOA and there is no Zone SOA RR, or the new SOA.SERIAL is // lower (according to [RFC1982]) than or equal to the current Zone SOA // RR's SOA.SERIAL, the Update RR is ignored. In the case of a CNAME // Update RR and a non-CNAME Zone RRset or vice versa, ignore the CNAME // Update RR, otherwise replace the CNAME Zone RR with the CNAME Update // RR. // zone rrset rr Add to an RRset info!("upserting record: {:?}", rr); let upserted = self.in_memory.upsert(rr.clone(), serial).await; #[cfg(all(feature = "metrics", feature = "__dnssec"))] if auto_signing_and_increment { if upserted { self.metrics.persistent.added(); } else { self.metrics.persistent.updated(); } } updated = upserted || updated } DNSClass::ANY => { // This is a delete of entire RRSETs, either many or one. In either case, the spec is clear: match rr.record_type() { t @ RecordType::SOA | t @ RecordType::NS if rr_name == *self.origin() => { // SOA and NS records are not to be deleted if they are the origin records info!("skipping delete of {:?} see RFC 2136 - 3.4.2.3", t); continue; } RecordType::ANY => { // RFC 2136 - 3.4.2.3. For any Update RR whose CLASS is ANY and whose TYPE is ANY, // all Zone RRs with the same NAME are deleted, unless the NAME is the // same as ZNAME in which case only those RRs whose TYPE is other than // SOA or NS are deleted. // ANY ANY empty Delete all RRsets from a name info!( "deleting all records at name (not SOA or NS at origin): {:?}", rr_name ); let origin = self.origin(); let to_delete = self .in_memory .records() .await .keys() .filter(|k| { !((k.record_type == RecordType::SOA || k.record_type == RecordType::NS) && k.name != *origin) }) .filter(|k| k.name == rr_name) .cloned() .collect::>(); for delete in to_delete { self.in_memory.records_mut().await.remove(&delete); updated = true; #[cfg(all(feature = "metrics", feature = "__dnssec"))] if auto_signing_and_increment { self.metrics.persistent.deleted() } } } _ => { // RFC 2136 - 3.4.2.3. For any Update RR whose CLASS is ANY and // whose TYPE is not ANY all Zone RRs with the same NAME and TYPE are // deleted, unless the NAME is the same as ZNAME in which case neither // SOA or NS RRs will be deleted. // ANY rrset empty Delete an RRset if let RData::Update0(_) | RData::NULL(..) = rr.data() { let deleted = self.in_memory.records_mut().await.remove(&rr_key); info!("deleted rrset: {:?}", deleted); updated = updated || deleted.is_some(); #[cfg(all(feature = "metrics", feature = "__dnssec"))] if auto_signing_and_increment { self.metrics.persistent.deleted() } } else { info!("expected empty rdata: {:?}", rr); return Err(ResponseCode::FormErr); } } } } DNSClass::NONE => { info!("deleting specific record: {:?}", rr); // NONE rrset rr Delete an RR from an RRset if let Some(rrset) = self.in_memory.records_mut().await.get_mut(&rr_key) { // b/c this is an Arc, we need to clone, then remove, and replace the node. let mut rrset_clone: RecordSet = RecordSet::clone(&*rrset); let deleted = rrset_clone.remove(rr, serial); info!("deleted ({}) specific record: {:?}", deleted, rr); updated = updated || deleted; if deleted { *rrset = Arc::new(rrset_clone); } #[cfg(all(feature = "metrics", feature = "__dnssec"))] if auto_signing_and_increment { self.metrics.persistent.deleted() } } } class => { info!("unexpected DNS Class: {:?}", class); return Err(ResponseCode::FormErr); } } } // update the serial... if updated && auto_signing_and_increment { if self.is_dnssec_enabled { cfg_if::cfg_if! { if #[cfg(feature = "__dnssec")] { self.secure_zone().await.map_err(|e| { error!("failure securing zone: {}", e); ResponseCode::ServFail })? } else { error!("failure securing zone, dnssec feature not enabled"); return Err(ResponseCode::ServFail) } } } else { // the secure_zone() function increments the SOA during it's operation, if we're not // dnssec, then we need to do it here... self.in_memory.increment_soa_serial().await; } } Ok(updated) } } impl Deref for SqliteAuthority { type Target = InMemoryAuthority; fn deref(&self) -> &Self::Target { &self.in_memory } } impl DerefMut for SqliteAuthority { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.in_memory } } #[async_trait::async_trait] impl Authority for SqliteAuthority { type Lookup = ::Lookup; /// What type is this zone fn zone_type(&self) -> ZoneType { self.in_memory.zone_type() } /// Return true if AXFR is allowed fn is_axfr_allowed(&self) -> bool { self.in_memory.is_axfr_allowed() } /// Takes the UpdateMessage, extracts the Records, and applies the changes to the record set. /// /// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997 /// /// ```text /// /// 3.4 - Process Update Section /// /// Next, the Update Section is processed as follows. /// /// 3.4.2 - Update /// /// The Update Section is parsed into RRs and these RRs are processed in /// order. /// /// 3.4.2.1. If any system failure (such as an out of memory condition, /// or a hardware error in persistent storage) occurs during the /// processing of this section, signal SERVFAIL to the requestor and undo /// all updates applied to the zone during this transaction. /// /// 3.4.2.2. Any Update RR whose CLASS is the same as ZCLASS is added to /// the zone. In case of duplicate RDATAs (which for SOA RRs is always /// the case, and for WKS RRs is the case if the ADDRESS and PROTOCOL /// fields both match), the Zone RR is replaced by Update RR. If the /// TYPE is SOA and there is no Zone SOA RR, or the new SOA.SERIAL is /// lower (according to [RFC1982]) than or equal to the current Zone SOA /// RR's SOA.SERIAL, the Update RR is ignored. In the case of a CNAME /// Update RR and a non-CNAME Zone RRset or vice versa, ignore the CNAME /// Update RR, otherwise replace the CNAME Zone RR with the CNAME Update /// RR. /// /// 3.4.2.3. For any Update RR whose CLASS is ANY and whose TYPE is ANY, /// all Zone RRs with the same NAME are deleted, unless the NAME is the /// same as ZNAME in which case only those RRs whose TYPE is other than /// SOA or NS are deleted. For any Update RR whose CLASS is ANY and /// whose TYPE is not ANY all Zone RRs with the same NAME and TYPE are /// deleted, unless the NAME is the same as ZNAME in which case neither /// SOA or NS RRs will be deleted. /// /// 3.4.2.4. For any Update RR whose class is NONE, any Zone RR whose /// NAME, TYPE, RDATA and RDLENGTH are equal to the Update RR is deleted, /// unless the NAME is the same as ZNAME and either the TYPE is SOA or /// the TYPE is NS and the matching Zone RR is the only NS remaining in /// the RRset, in which case this Update RR is ignored. /// /// 3.4.2.5. Signal NOERROR to the requestor. /// ``` /// /// # Arguments /// /// * `update` - The `UpdateMessage` records will be extracted and used to perform the update /// actions as specified in the above RFC. /// /// # Return value /// /// true if any of additions, updates or deletes were made to the zone, false otherwise. Err is /// returned in the case of bad data, etc. #[cfg(feature = "__dnssec")] async fn update(&self, update: &MessageRequest) -> UpdateResult { //let this = &mut self.in_memory.lock().await; // the spec says to authorize after prereqs, seems better to auth first. self.authorize(update).await?; self.verify_prerequisites(update.prerequisites()).await?; self.pre_scan(update.updates()).await?; self.update_records(update.updates(), true).await } /// Always fail when DNSSEC is disabled. #[cfg(not(feature = "__dnssec"))] async fn update(&self, _update: &MessageRequest) -> UpdateResult { Err(ResponseCode::NotImp) } /// Get the origin of this zone, i.e. example.com is the origin for www.example.com fn origin(&self) -> &LowerName { self.in_memory.origin() } /// Looks up all Resource Records matching the given `Name` and `RecordType`. /// /// # Arguments /// /// * `name` - The name to look up. /// * `rtype` - The `RecordType` to look up. `RecordType::ANY` will return all records matching /// `name`. `RecordType::AXFR` will return all record types except `RecordType::SOA` /// due to the requirements that on zone transfers the `RecordType::SOA` must both /// precede and follow all other records. /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) /// /// # Return value /// /// A LookupControlFlow containing the lookup that should be returned to the client. async fn lookup( &self, name: &LowerName, rtype: RecordType, lookup_options: LookupOptions, ) -> LookupControlFlow { let lookup = self.in_memory.lookup(name, rtype, lookup_options).await; #[cfg(feature = "metrics")] self.metrics.query.increment_lookup(&lookup); lookup } async fn search( &self, request_info: RequestInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { let search = self.in_memory.search(request_info, lookup_options).await; #[cfg(feature = "metrics")] self.metrics.query.increment_lookup(&search); search } /// Return the NSEC records based on the given name /// /// # Arguments /// /// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than /// this /// * `lookup_options` - Query-related lookup options (e.g., DNSSEC DO bit, supported hash /// algorithms, etc.) async fn get_nsec_records( &self, name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow { self.in_memory.get_nsec_records(name, lookup_options).await } #[cfg(feature = "__dnssec")] async fn get_nsec3_records( &self, info: Nsec3QueryInfo<'_>, lookup_options: LookupOptions, ) -> LookupControlFlow { self.in_memory.get_nsec3_records(info, lookup_options).await } #[cfg(feature = "__dnssec")] fn nx_proof_kind(&self) -> Option<&NxProofKind> { self.in_memory.nx_proof_kind() } } #[cfg(feature = "__dnssec")] #[async_trait::async_trait] impl DnssecAuthority for SqliteAuthority { async fn add_update_auth_key(&self, name: Name, key: KEY) -> DnsSecResult<()> { self.in_memory.add_update_auth_key(name, key).await } /// By adding a secure key, this will implicitly enable dnssec for the zone. /// /// # Arguments /// /// * `signer` - Signer with associated private key async fn add_zone_signing_key(&self, signer: SigSigner) -> DnsSecResult<()> { self.in_memory.add_zone_signing_key(signer).await } /// (Re)generates the nsec records, increments the serial number and signs the zone async fn secure_zone(&self) -> DnsSecResult<()> { self.in_memory.secure_zone().await } } /// Configuration for zone file for sqlite based zones #[derive(Deserialize, PartialEq, Eq, Debug)] #[serde(deny_unknown_fields)] pub struct SqliteConfig { /// path to initial zone file pub zone_file_path: PathBuf, /// path to the sqlite journal file pub journal_file_path: String, /// Are updates allowed to this zone #[serde(default)] pub allow_update: bool, } #[cfg(test)] #[allow(clippy::extra_unused_type_parameters)] mod tests { use crate::store::sqlite::SqliteAuthority; #[test] fn test_is_send_sync() { fn send_sync() -> bool { true } assert!(send_sync::()); } } hickory-server-0.25.2/src/store/sqlite/persistence.rs000064400000000000000000000262241046102023000210120ustar 00000000000000// 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. //! All zone persistence related types use std::iter::Iterator; use std::path::Path; use std::sync::{Mutex, MutexGuard}; use rusqlite::types::ToSql; use rusqlite::{self, Connection}; use time; use tracing::error; use crate::error::{PersistenceError, PersistenceErrorKind}; use crate::proto::rr::Record; use crate::proto::serialize::binary::{BinDecodable, BinDecoder, BinEncodable, BinEncoder}; /// The current Journal version of the application pub const CURRENT_VERSION: i64 = 1; /// The Journal is the audit log of all changes to a zone after initial creation. pub struct Journal { conn: Mutex, version: i64, } impl Journal { /// Constructs a new Journal, attaching to the specified Sqlite Connection pub fn new(conn: Connection) -> Result { let version = Self::select_schema_version(&conn)?; Ok(Self { conn: Mutex::new(conn), version, }) } /// Constructs a new Journal opening a Sqlite connection to the file at the specified path pub fn from_file(journal_file: &Path) -> Result { let result = Self::new(Connection::open(journal_file)?); let mut journal = result?; journal.schema_up()?; Ok(journal) } /// Returns a reference to the Sqlite Connection pub fn conn(&self) -> MutexGuard<'_, Connection> { self.conn.lock().expect("conn poisoned") } /// Returns the current schema version of the journal pub fn schema_version(&self) -> i64 { self.version } /// this returns an iterator from the beginning of time, to be used to recreate an authority pub fn iter(&self) -> JournalIter<'_> { JournalIter::new(self) } /// Inserts a record, this is an append only operation. /// /// Records should never be posthumously modified. The message will be serialized into the. /// the first message serialized to the journal, should be a single AXFR of the entire zone, /// this will be used as a starting point to reconstruct the zone. /// /// # Argument /// /// * `record` - will be serialized into the journal pub fn insert_record(&self, soa_serial: u32, record: &Record) -> Result<(), PersistenceError> { assert!( self.version == CURRENT_VERSION, "schema version mismatch, schema_up() resolves this" ); let mut serial_record: Vec = Vec::with_capacity(512); { let mut encoder = BinEncoder::new(&mut serial_record); record.emit(&mut encoder)?; } let timestamp = time::OffsetDateTime::now_utc(); let client_id: i64 = 0; // TODO: we need better id information about the client, like pub_key let soa_serial: i64 = i64::from(soa_serial); let count = self.conn.lock().expect("conn poisoned").execute( "INSERT \ INTO records (client_id, soa_serial, timestamp, \ record) \ VALUES ($1, $2, $3, $4)", [ &client_id as &dyn ToSql, &soa_serial, ×tamp, &serial_record, ], )?; // if count != 1 { return Err(PersistenceErrorKind::WrongInsertCount { got: count, expect: 1, } .into()); }; Ok(()) } /// Inserts a set of records into the Journal, a convenience method for insert_record pub fn insert_records( &self, soa_serial: u32, records: &[Record], ) -> Result<(), PersistenceError> { // TODO: NEED TRANSACTION HERE for record in records { self.insert_record(soa_serial, record)?; } Ok(()) } /// Selects a record from the given row_id. /// /// This allows for the entire set of records to be iterated through, by starting at 0, and /// incrementing each subsequent row. /// /// # Arguments /// /// * `row_id` - the row_id can either be exact, or start at 0 to get the earliest row in the /// list. pub fn select_record(&self, row_id: i64) -> Result, PersistenceError> { assert!( self.version == CURRENT_VERSION, "schema version mismatch, schema_up() resolves this" ); let conn = self.conn.lock().expect("conn poisoned"); let mut stmt = conn.prepare( "SELECT _rowid_, record \ FROM records \ WHERE _rowid_ >= $1 \ LIMIT 1", )?; let record_opt: Option> = stmt .query_and_then([&row_id], |row| -> Result<(i64, Record), rusqlite::Error> { let row_id: i64 = row.get(0)?; let record_bytes: Vec = row.get(1)?; let mut decoder = BinDecoder::new(&record_bytes); // todo add location to this... match Record::read(&mut decoder) { Ok(record) => Ok((row_id, record)), Err(decode_error) => Err(rusqlite::Error::InvalidParameterName(format!( "could not decode: {decode_error}" ))), } })? .next(); // match record_opt { Some(Ok((row_id, record))) => Ok(Some((row_id, record))), Some(Err(err)) => Err(err.into()), None => Ok(None), } } /// selects the current schema version of the journal DB, returns -1 if there is no schema /// /// /// # Arguments /// /// * `conn` - db connection to use pub fn select_schema_version(conn: &Connection) -> Result { // first see if our schema is there let mut stmt = conn.prepare( "SELECT name \ FROM sqlite_master \ WHERE type='table' \ AND name='tdns_schema'", )?; let tdns_schema_opt: Option> = stmt.query_map([], |row| row.get(0))?.next(); let tdns_schema = match tdns_schema_opt { Some(Ok(string)) => string, Some(Err(err)) => return Err(err.into()), None => return Ok(-1), }; assert_eq!(&tdns_schema, "tdns_schema"); let version: i64 = conn.query_row( "SELECT version \ FROM tdns_schema", [], |row| row.get(0), )?; Ok(version) } /// update the schema version fn update_schema_version(&self, new_version: i64) -> Result<(), PersistenceError> { // validate the versions of all the schemas... assert!(new_version <= CURRENT_VERSION); let count = self .conn .lock() .expect("conn poisoned") .execute("UPDATE tdns_schema SET version = $1", [&new_version])?; // assert_eq!(count, 1); Ok(()) } /// initializes the schema for the Journal pub fn schema_up(&mut self) -> Result { while self.version < CURRENT_VERSION { match self.version + 1 { 0 => self.version = self.init_up()?, 1 => self.version = self.records_up()?, _ => panic!("incorrect version somewhere"), // valid panic, non-recoverable state } self.update_schema_version(self.version)?; } Ok(self.version) } /// initial schema, include the tdns_schema table for tracking the Journal version fn init_up(&self) -> Result { let count = self.conn.lock().expect("conn poisoned").execute( "CREATE TABLE tdns_schema ( \ version INTEGER NOT NULL \ )", [], )?; // assert_eq!(count, 0); let count = self .conn .lock() .expect("conn poisoned") .execute("INSERT INTO tdns_schema (version) VALUES (0)", [])?; // assert_eq!(count, 1); Ok(0) } /// adds the records table, this is the main and single table for the history of changes to an /// authority. Each record is expected to be in the format of an update record fn records_up(&self) -> Result { // we'll be using rowid for our primary key, basically: `rowid INTEGER PRIMARY KEY ASC` let count = self.conn.lock().expect("conn poisoned").execute( "CREATE TABLE records ( \ client_id INTEGER NOT NULL, \ soa_serial INTEGER NOT NULL, \ timestamp TEXT NOT NULL, \ record BLOB NOT NULL \ )", [], )?; // assert_eq!(count, 1); Ok(1) } } /// Returns an iterator over all items in a Journal /// /// Useful for replaying an entire journal into memory to reconstruct a zone from disk pub struct JournalIter<'j> { current_row_id: i64, journal: &'j Journal, } impl<'j> JournalIter<'j> { fn new(journal: &'j Journal) -> Self { JournalIter { current_row_id: 0, journal, } } } impl Iterator for JournalIter<'_> { type Item = Record; fn next(&mut self) -> Option { match self.journal.select_record(self.current_row_id + 1) { Ok(Some((row_id, record))) => { self.current_row_id = row_id; Some(record) } Ok(None) => None, Err(err) => { error!("persistence error while iterating over journal: {}", err); None } } } }