Merge upstream v0.16.26 #196
No files matched your search
@@ -0,0 +1,2 @@
|
||||
[target.'cfg(all(target_arch = "x86_64", not(target_env = "musl")))']
|
||||
rustflags = ["-C", "target-cpu=x86-64-v2"]
|
||||
@@ -12,5 +12,6 @@
|
||||
# because CI builds from a checkout and only the image build has a context.
|
||||
!vendor
|
||||
|
||||
!.cargo
|
||||
!Cargo.lock
|
||||
!Cargo.toml
|
||||
@@ -11,6 +11,7 @@ run.sh
|
||||
!.github
|
||||
!.gitlab-ci.yml
|
||||
!.gitea
|
||||
!.cargo
|
||||
CLAUDE.md
|
||||
|
||||
# The cutover rehearsal writes its fixture and state here.
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.16.26] - 2026-10-11
|
||||
|
||||
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
||||
|
||||
## Added
|
||||
- `sd_notify` support.
|
||||
- JMAP: `Mailbox/set` allows moving a mailbox under a parent that already has a child with the same name, and rejects renames that only change the case of a mailbox name with `alreadyExists`.
|
||||
|
||||
## Changed
|
||||
|
||||
## Fixed
|
||||
- DNSSEC: Use `hickory-resolver` patched fork at https://github.com/stalwartlabs/hickory-dns.
|
||||
- RocksDB: Blob garbage collection rewrites every stored blob on each compaction of the blobs column family, about once per 128MB of new mail.
|
||||
- Milter: End-of-headers is not sent to milters that negotiate `SMFIP_NOHDRS` without `SMFIP_NOEOH`, so the transaction stalls until it times out.
|
||||
- OIDC: When the access token lacks the `claimUsername` claim but has an `email` claim, the account is built from `email` without consulting the userinfo endpoint.
|
||||
- JMAP:
|
||||
- `onSuccessUpdateEmail` and `onSuccessDestroyEmail` entries in `EmailSubmission/set` that name an existing submission by id update or destroy the Email with the same id instead of the submission's own Email.
|
||||
- A `VacationResponse/set` that only sets `isEnabled` on an account without a vacation response activates an empty Sieve script.
|
||||
|
||||
## [0.16.25] - 2026-10-05
|
||||
|
||||
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
||||
|
||||
Generated
+123
-129
@@ -116,7 +116,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
"zerocopy 0.8.59",
|
||||
"zerocopy 0.8.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -188,6 +188,12 @@ dependencies = [
|
||||
"password-hash 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "array-const-fn-init"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bcb85e548c05d407fa6faff46b750ba287714ef32afc0f5e15b4641ffd6affb"
|
||||
|
||||
[[package]]
|
||||
name = "array-init"
|
||||
version = "2.1.0"
|
||||
@@ -330,13 +336,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -402,14 +408,14 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-creds"
|
||||
version = "0.39.1"
|
||||
version = "0.40.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3b85155d265df828f84e53886ed9e427aed979dd8a39f5b8b2162c77e142d7"
|
||||
checksum = "886acd98f2df6e988d5428e92cdf041792ffa87a494d2b5125982502ec726a9c"
|
||||
dependencies = [
|
||||
"attohttpc",
|
||||
"home",
|
||||
"log",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml 0.41.0",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
@@ -443,9 +449,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-region"
|
||||
version = "0.28.1"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0"
|
||||
checksum = "8f38493193e45cb4788ee7d479ee94a0a56721ccd28de79bbd0227bb8c11fee3"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -1100,9 +1106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
|
||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1292,7 +1298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"aes-gcm-siv",
|
||||
"ahash",
|
||||
@@ -1352,6 +1358,7 @@ dependencies = [
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"sd-notify",
|
||||
"sequoia-openpgp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1477,7 +1484,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "coordinator"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"async-nats",
|
||||
"futures",
|
||||
@@ -1490,16 +1497,6 @@ dependencies = [
|
||||
"zenoh",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -1723,9 +1720,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -1889,7 +1886,7 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
|
||||
[[package]]
|
||||
name = "dav"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"calcard",
|
||||
"chrono",
|
||||
@@ -1912,7 +1909,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dav-proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"calcard",
|
||||
"chrono",
|
||||
@@ -2125,7 +2122,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "directory"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"argon2 0.6.0",
|
||||
@@ -2335,9 +2332,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
@@ -2366,7 +2363,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "email"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"aes 0.9.3",
|
||||
"aes-gcm 0.11.1",
|
||||
@@ -2408,9 +2405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encodify"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798c447647dd23f673748f2b868ef309a01dd86aaae999182559d36f06ac82f0"
|
||||
checksum = "e95d6e27686e1d6991a3077f4dbec9d33d18903466d234165ad86cd08c3ebc79"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"simdutf8",
|
||||
@@ -2484,7 +2481,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event_macro"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
@@ -3012,7 +3009,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "groupware"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"calcard",
|
||||
@@ -3040,9 +3037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.19"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
||||
checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -3146,8 +3143,6 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
[[package]]
|
||||
name = "hickory-net"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-lc-rs",
|
||||
@@ -3182,8 +3177,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bitflags 2.13.2",
|
||||
@@ -3206,8 +3199,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -3299,7 +3290,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"base64 0.23.1",
|
||||
@@ -3395,7 +3386,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"compact_str",
|
||||
@@ -3449,9 +3440,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.11.1"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||
checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -3882,7 +3873,7 @@ checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
|
||||
|
||||
[[package]]
|
||||
name = "imap"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"common",
|
||||
@@ -3907,7 +3898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imap_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.23.1",
|
||||
@@ -3922,7 +3913,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inbuxa"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"coordinator",
|
||||
@@ -3930,10 +3921,11 @@ dependencies = [
|
||||
"directory",
|
||||
"email",
|
||||
"groupware",
|
||||
"http 0.16.25",
|
||||
"http 0.16.26",
|
||||
"http_proto",
|
||||
"imap",
|
||||
"jmap",
|
||||
"libc",
|
||||
"managesieve",
|
||||
"migration",
|
||||
"pop3",
|
||||
@@ -4167,9 +4159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.37"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
|
||||
checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-core",
|
||||
@@ -4194,9 +4186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.37"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
|
||||
checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
|
||||
dependencies = [
|
||||
"jiff-core",
|
||||
"proc-macro2",
|
||||
@@ -4206,9 +4198,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
|
||||
checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7"
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb-platform"
|
||||
@@ -4221,7 +4213,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jmap"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"base64 0.23.1",
|
||||
@@ -4304,7 +4296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jmap_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"calcard",
|
||||
@@ -4564,9 +4556,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
@@ -4738,7 +4730,7 @@ version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226"
|
||||
dependencies = [
|
||||
"twox-hash 2.1.4",
|
||||
"twox-hash 2.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4810,7 +4802,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "managesieve"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"compact_str",
|
||||
@@ -4945,7 +4937,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492"
|
||||
|
||||
[[package]]
|
||||
name = "migration"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"email",
|
||||
@@ -4991,9 +4983,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
@@ -5128,7 +5120,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"twox-hash 2.1.4",
|
||||
"twox-hash 2.1.5",
|
||||
"url",
|
||||
"webpki-roots 1.0.9",
|
||||
]
|
||||
@@ -5195,7 +5187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nlp"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashify",
|
||||
@@ -6027,7 +6019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pop3"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"directory",
|
||||
@@ -6102,9 +6094,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
||||
|
||||
[[package]]
|
||||
name = "ppmd-rust"
|
||||
@@ -6118,7 +6110,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy 0.8.59",
|
||||
"zerocopy 0.8.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6129,13 +6121,15 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prefix-trie"
|
||||
version = "0.8.4"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
|
||||
checksum = "6369af434551b9bf8be05ab871ac1263687d6f0c9485e6d781f904f3e06d7d03"
|
||||
dependencies = [
|
||||
"array-const-fn-init",
|
||||
"either",
|
||||
"ipnet",
|
||||
"num-traits",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6276,9 +6270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "psl"
|
||||
version = "2.1.238"
|
||||
version = "2.1.241"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e6aa2cd4e8e062e78ac5d7df6206e2f8e39b624f658efd6f34f9aa1928ac5da"
|
||||
checksum = "c7319b480e204d67e759cad7886178335d26324c7c8573c126cc9dfa7350d064"
|
||||
dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
@@ -6334,16 +6328,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
@@ -6351,6 +6335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6847,7 +6832,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "registry"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashify",
|
||||
@@ -7138,9 +7123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-s3"
|
||||
version = "0.37.2"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeedb13abdaa7e48d391de05b0569b37fa0a7a64a668dff6ffb2141ad0c2527e"
|
||||
checksum = "15d4ed794cdfede6c59f81d485cae6bb63f47a7e35d190148d40754f783073c4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-creds",
|
||||
@@ -7156,7 +7141,7 @@ dependencies = [
|
||||
"maybe-async",
|
||||
"md5",
|
||||
"percent-encoding",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml 0.41.0",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -7276,7 +7261,7 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
@@ -7404,7 +7389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "scim"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.23.1",
|
||||
@@ -7430,7 +7415,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "scim-proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"hashify",
|
||||
"serde",
|
||||
@@ -7456,6 +7441,15 @@ dependencies = [
|
||||
"sha2 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-notify"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e4ef7359e694bfaf1dd27a30f9d760b54c00dfae9f19bd0c05a39bc9128fe76"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -7488,7 +7482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -7765,7 +7759,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "services"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"aes-gcm 0.11.1",
|
||||
"aho-corasick",
|
||||
@@ -8080,7 +8074,7 @@ checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
|
||||
[[package]]
|
||||
name = "smtp"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.23.1",
|
||||
@@ -8171,7 +8165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spam-filter"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"common",
|
||||
"compact_str",
|
||||
@@ -8292,7 +8286,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "store"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"arc-swap",
|
||||
@@ -8470,12 +8464,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
@@ -8552,7 +8546,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"aws-lc-rs",
|
||||
@@ -8574,7 +8568,7 @@ dependencies = [
|
||||
"form_urlencoded",
|
||||
"futures",
|
||||
"groupware",
|
||||
"http 0.16.25",
|
||||
"http 0.16.26",
|
||||
"http_proto",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
@@ -8777,9 +8771,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8931,18 +8925,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.15+spec-1.1.0"
|
||||
version = "0.25.16+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||
checksum = "88f38f3af8bf71841c67a990f4e2b52143b2d0a1a994c72513ff4c14977fc722"
|
||||
dependencies = [
|
||||
"indexmap 2.14.2",
|
||||
"toml_datetime",
|
||||
@@ -8952,9 +8946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
version = "1.1.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
@@ -9167,7 +9161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "trc"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.23.1",
|
||||
@@ -9252,9 +9246,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.4"
|
||||
version = "2.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
|
||||
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
@@ -9276,7 +9270,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "types"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"compact_str",
|
||||
@@ -9310,9 +9304,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
@@ -9445,7 +9439,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utils"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"arcstr",
|
||||
@@ -9476,9 +9470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.1"
|
||||
version = "1.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -9559,9 +9553,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
@@ -10647,11 +10641,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
||||
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.59",
|
||||
"zerocopy-derive 0.8.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10667,9 +10661,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.59"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
||||
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -10699,9 +10693,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
@@ -85,5 +85,12 @@ rpath = false
|
||||
# (vnd.stalwart.*), which scripts `require` and ManageSieve advertises.
|
||||
# vendor/sieve-rs is the published 0.7.3 with those renamed; see its
|
||||
# VENDORED.md. Re-vendor when the version in Cargo.lock moves.
|
||||
#
|
||||
# inbuxa: hickory is upstream's patched branch (DNSSEC fixes), vendored at
|
||||
# the commit upstream pins so builds never fetch from upstream's
|
||||
# repositories; see vendor/hickory-dns/VENDORED.md.
|
||||
[patch.crates-io]
|
||||
sieve-rs = { path = "vendor/sieve-rs" }
|
||||
hickory-proto = { path = "vendor/hickory-dns/hickory-proto" }
|
||||
hickory-net = { path = "vendor/hickory-dns/hickory-net" }
|
||||
hickory-resolver = { path = "vendor/hickory-dns/hickory-resolver" }
|
||||
+34
-20
@@ -4,7 +4,7 @@
|
||||
# *****************
|
||||
# Base image for planner & builder
|
||||
# *****************
|
||||
FROM --platform=$BUILDPLATFORM rust:1.98.1-slim-trixie AS base
|
||||
FROM --platform=$BUILDPLATFORM rust:slim-trixie AS base
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
BINSTALL_DISABLE_TELEMETRY=true \
|
||||
@@ -24,25 +24,24 @@ RUN \
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \
|
||||
apt-get update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates curl git jq xz-utils make libclang-19-dev
|
||||
# Zig is pinned to 0.13.0. Zig 0.14+ changed how its default target ABI is
|
||||
# resolved for `x86_64-linux-gnu`-style triples, and on Stalwart's dep tree
|
||||
# (rocksdb, jemalloc, aws-lc-sys, libfdb_c.so, rust-std 1.87+) this surfaces
|
||||
# as ld.lld undefined references to pthread_*/stat/pow@GLIBC_2.33+. An
|
||||
# explicit glibc suffix (`.2.17`) is NOT a workaround — C deps compiled via
|
||||
# zig cc against Zig 0.16 headers still emit @2.33+ symbols even when the
|
||||
# linker target is 2.17. Zig 0.13 produces binaries with a max glibc ref of
|
||||
# ~2.28 which stays compatible with Debian 11 / RHEL 8. Revisit this pin if
|
||||
# Zig upstream stabilizes a backwards-compatible default or if Stalwart drops
|
||||
# its hard C deps.
|
||||
RUN \
|
||||
ZIG_VERSION=0.13.0 && \
|
||||
curl --retry 5 -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \
|
||||
ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig && \
|
||||
ZIG_VERSION=0.17.0 && \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) ZIG_SHA256=1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026 ;; \
|
||||
aarch64) ZIG_SHA256=9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8 ;; \
|
||||
*) exit 1 ;; \
|
||||
esac && \
|
||||
curl --retry 5 -fsSLo /tmp/zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-$(uname -m)-linux-${ZIG_VERSION}.tar.xz" && \
|
||||
echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - && \
|
||||
tar -J -x -C /usr/local -f /tmp/zig.tar.xz && \
|
||||
rm /tmp/zig.tar.xz && \
|
||||
ln -s "/usr/local/zig-$(uname -m)-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig && \
|
||||
zig version
|
||||
# Install cargo-binstall
|
||||
RUN curl --retry 5 -fL --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
# Install cargo-chef & sccache & cargo-zigbuild
|
||||
RUN cargo binstall --no-confirm [email protected] [email protected] [email protected]
|
||||
RUN cargo binstall --no-confirm [email protected] [email protected]
|
||||
RUN cargo install --locked --git https://github.com/rust-cross/cargo-zigbuild --rev d9f7fd32fbac13cc26d23847835f7f23a5ed3a25 cargo-zigbuild
|
||||
|
||||
# *****************
|
||||
# Planner
|
||||
@@ -61,6 +60,7 @@ WORKDIR /app
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
ARG TARGET
|
||||
ARG BUILD_ENV
|
||||
ARG GLIBC_VERSION=2.28
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
# Install toolchain and specify some env variables
|
||||
RUN \
|
||||
@@ -68,6 +68,13 @@ RUN \
|
||||
rustup target add ${TARGET} && \
|
||||
mkdir -p artifact && \
|
||||
touch /env-cargo && \
|
||||
if [[ "${TARGET}" == *-gnu* ]]; then \
|
||||
echo "export BUILD_TARGET=${TARGET}.${GLIBC_VERSION}" >> /env-cargo; \
|
||||
mkdir -p target && \
|
||||
ln -s "${TARGET}" "target/${TARGET}.${GLIBC_VERSION}"; \
|
||||
else \
|
||||
echo "export BUILD_TARGET=${TARGET}" >> /env-cargo; \
|
||||
fi && \
|
||||
if [ ! -z "${BUILD_ENV}" ]; then \
|
||||
echo "export ${BUILD_ENV}" >> /env-cargo; \
|
||||
echo "Setting up ${BUILD_ENV}"; \
|
||||
@@ -85,15 +92,22 @@ RUN \
|
||||
exit 1; \
|
||||
;; \
|
||||
esac; \
|
||||
target_var="${TARGET^^}"; \
|
||||
echo "export FDB_RUSTFLAGS_VAR=CARGO_TARGET_${target_var//-/_}_RUSTFLAGS" >> /env-cargo; \
|
||||
fi
|
||||
# Install FoundationDB (pinned to latest 7.4.x; Apple publishes 7.4 as
|
||||
# prereleases on GitHub, so the release list is fetched without the
|
||||
# prerelease filter and narrowed by tag name).
|
||||
ARG FDB_VERSION_RANGE="7.4"
|
||||
RUN \
|
||||
--mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \
|
||||
source /env-cargo && \
|
||||
if [ ! -z "${FDB_ARCH}" ]; then \
|
||||
curl --retry 5 -fLso fdb-client.deb "$(curl --retry 5 -fLs 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \
|
||||
auth=() && \
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi && \
|
||||
fdb_url="$(curl --retry 5 -fLs "${auth[@]}" 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \
|
||||
[ -n "${fdb_url}" ] && \
|
||||
curl --retry 5 -fLso fdb-client.deb "${fdb_url}" && \
|
||||
mkdir -p /fdb && \
|
||||
dpkg -x fdb-client.deb /fdb && \
|
||||
mv /fdb/usr/include/foundationdb /usr/include && \
|
||||
@@ -108,7 +122,7 @@ RUN \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
source /env-cargo && \
|
||||
if [ ! -z "${FDB_ARCH}" ]; then \
|
||||
RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats"; \
|
||||
env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats"; \
|
||||
fi
|
||||
RUN \
|
||||
--mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \
|
||||
@@ -116,7 +130,7 @@ RUN \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
source /env-cargo && \
|
||||
cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"
|
||||
cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"
|
||||
# Copy the source code
|
||||
COPY . .
|
||||
ENV RUSTC_WRAPPER="sccache" \
|
||||
@@ -129,7 +143,7 @@ RUN \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
source /env-cargo && \
|
||||
if [ ! -z "${FDB_ARCH}" ]; then \
|
||||
RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats" && \
|
||||
env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats" && \
|
||||
mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa-foundationdb; \
|
||||
fi
|
||||
# Build generic version
|
||||
@@ -139,7 +153,7 @@ RUN \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
source /env-cargo && \
|
||||
cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \
|
||||
cargo zigbuild --release --target ${BUILD_TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \
|
||||
mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa
|
||||
|
||||
# *****************
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "common"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
@@ -86,6 +86,9 @@ quick-xml = "0.41"
|
||||
privdrop = "0.5.6"
|
||||
libc = "0.2.189"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
sd-notify = "0.5"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
socket2 = "0.6"
|
||||
|
||||
|
||||
@@ -214,7 +214,6 @@ impl Resolvers {
|
||||
let config_dnssec = resolver_config.clone();
|
||||
let mut opts_dnssec = opts.clone();
|
||||
opts_dnssec.validate = true;
|
||||
opts_dnssec.num_concurrent_reqs = 1;
|
||||
|
||||
let dnssec = DnssecResolver {
|
||||
resolver: TokioResolver::builder_with_config(
|
||||
@@ -344,7 +343,6 @@ impl Default for Resolvers {
|
||||
let config_dnssec = config.clone();
|
||||
let mut opts_dnssec = opts.clone();
|
||||
opts_dnssec.validate = true;
|
||||
opts_dnssec.num_concurrent_reqs = 1;
|
||||
|
||||
Self {
|
||||
dns: MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"),
|
||||
|
||||
@@ -25,6 +25,8 @@ pub mod first_party;
|
||||
pub mod granted_permissions; // inbuxa: permissions added after roles were stored
|
||||
pub mod restore;
|
||||
pub mod spam_rules; // inbuxa: rules bundled with the server
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod systemd;
|
||||
|
||||
pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes();
|
||||
pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ServiceState {
|
||||
Ready,
|
||||
Reloading,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
impl ServiceState {
|
||||
pub fn notify(self) {
|
||||
use sd_notify::{NotifyState, notify};
|
||||
|
||||
let _ = match self {
|
||||
ServiceState::Ready => notify(&[NotifyState::Ready]),
|
||||
ServiceState::Stopping => notify(&[NotifyState::Stopping]),
|
||||
ServiceState::Reloading => NotifyState::monotonic_usec_now()
|
||||
.and_then(|now| notify(&[NotifyState::Reloading, now])),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coordinator"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dav-proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dav"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "directory"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -87,12 +87,14 @@ impl OpenIdDirectory {
|
||||
self.validate_scopes(&token_data.claims)?;
|
||||
|
||||
let mut claims = token_data.claims;
|
||||
let missing_username =
|
||||
is_claim_missing(&claims, Some(&self.config.claim_email));
|
||||
let jwt_email = self.resolve_email(&claims).ok();
|
||||
let missing_profile =
|
||||
is_claim_missing(&claims, self.config.claim_name.as_ref())
|
||||
|| is_claim_missing(&claims, self.config.claim_groups.as_ref());
|
||||
|
||||
if jwt_email.is_none() || missing_profile {
|
||||
if jwt_email.is_none() || missing_username || missing_profile {
|
||||
match self.fetch_userinfo(token).await {
|
||||
Ok(userinfo) => {
|
||||
if let (Some(base), Value::Object(extra)) =
|
||||
@@ -111,8 +113,8 @@ impl OpenIdDirectory {
|
||||
}
|
||||
|
||||
let email = match jwt_email {
|
||||
Some(email) => email,
|
||||
None => self.resolve_email(&claims)?,
|
||||
Some(email) if !missing_username => email,
|
||||
_ => self.resolve_email(&claims)?,
|
||||
};
|
||||
return self.build_account(email, &claims);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "email"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "groupware"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "http_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "http"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imap_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imap"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jmap_proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jmap"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -589,14 +589,14 @@ impl MailboxSet for Server {
|
||||
if !changes.name.is_empty() {
|
||||
// Obtain parent mailbox id
|
||||
let lower_name = changes.name.to_lowercase();
|
||||
if update
|
||||
.as_ref()
|
||||
.is_none_or(|(_, m)| m.inner.name != changes.name)
|
||||
&& let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| {
|
||||
m.name.to_lowercase() == lower_name
|
||||
&& m.parent_id().map_or(0, |id| id + 1) == changes.parent_id
|
||||
})
|
||||
{
|
||||
let current_id = update.as_ref().map(|(document_id, _)| *document_id);
|
||||
if update.as_ref().is_none_or(|(_, m)| {
|
||||
m.inner.name != changes.name || m.inner.parent_id != changes.parent_id
|
||||
}) && let Some(existing) = cached_mailboxes.mailboxes.items.iter().find(|m| {
|
||||
Some(m.document_id) != current_id
|
||||
&& m.parent_id().map_or(0, |id| id + 1) == changes.parent_id
|
||||
&& m.name.to_lowercase() == lower_name
|
||||
}) {
|
||||
return Ok(Err(SetError::already_exists()
|
||||
.with_existing_id(Id::from(existing.document_id))
|
||||
.with_description(format!(
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
use crate::registry::mapping::{RegistrySetResponse, map_bootstrap_error};
|
||||
#[cfg(target_os = "linux")]
|
||||
use common::manager::systemd::ServiceState;
|
||||
use common::{
|
||||
Server,
|
||||
config::mailstore::spamfilter::SpamFilterAction,
|
||||
@@ -88,8 +90,13 @@ pub(crate) async fn action_set(
|
||||
Action::ReloadBlockedIps => ObjectType::BlockedIp,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
ServiceState::Reloading.notify();
|
||||
let result =
|
||||
Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await?;
|
||||
Box::pin(set.server.reload_registry(RegistryChange::Reload(object))).await;
|
||||
#[cfg(target_os = "linux")]
|
||||
ServiceState::Ready.notify();
|
||||
let result = result?;
|
||||
|
||||
if !result.has_errors() {
|
||||
set.server
|
||||
|
||||
@@ -81,6 +81,7 @@ impl EmailSubmissionSet for Server {
|
||||
|
||||
// Process creates
|
||||
let mut success_email_ids = HashMap::new();
|
||||
let mut changed_email_ids = HashMap::new();
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (id, object) in request.unwrap_create() {
|
||||
match self
|
||||
@@ -263,6 +264,8 @@ impl EmailSubmissionSet for Server {
|
||||
// Update record
|
||||
let mut new_submission = submission.inner.clone();
|
||||
new_submission.undo_status = UndoStatus::Canceled;
|
||||
let email_id =
|
||||
Id::from_parts(new_submission.thread_id, new_submission.email_id);
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::EmailSubmission)
|
||||
@@ -274,6 +277,7 @@ impl EmailSubmissionSet for Server {
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
changed_email_ids.insert(id, email_id);
|
||||
response.updated.append(id, None);
|
||||
} else {
|
||||
response.not_updated.append(
|
||||
@@ -315,19 +319,21 @@ impl EmailSubmissionSet for Server {
|
||||
.await?
|
||||
{
|
||||
// Update record
|
||||
let submission = submission
|
||||
.to_unarchived::<EmailSubmission>()
|
||||
.caused_by(trc::location!())?;
|
||||
let email_id = Id::from_parts(
|
||||
u32::from(submission.inner.thread_id),
|
||||
u32::from(submission.inner.email_id),
|
||||
);
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::EmailSubmission)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new().with_current(
|
||||
submission
|
||||
.to_unarchived::<EmailSubmission>()
|
||||
.caused_by(trc::location!())?,
|
||||
),
|
||||
)
|
||||
.custom(ObjectIndexBuilder::<_, ()>::new().with_current(submission))
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
changed_email_ids.insert(id, email_id);
|
||||
response.destroyed.push(id);
|
||||
} else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
@@ -345,18 +351,34 @@ impl EmailSubmissionSet for Server {
|
||||
}
|
||||
|
||||
// On success
|
||||
if (request
|
||||
let resolve_email_id = |id: MaybeIdReference<Id>| match id {
|
||||
MaybeIdReference::Id(id) => changed_email_ids.get(&id).copied(),
|
||||
MaybeIdReference::Reference(id_ref) => success_email_ids.get(&id_ref).copied(),
|
||||
MaybeIdReference::Invalid(_) => None,
|
||||
};
|
||||
let update = request
|
||||
.arguments
|
||||
.on_success_update_email
|
||||
.map(|update| {
|
||||
update
|
||||
.into_iter()
|
||||
.filter_map(|(id, value)| {
|
||||
resolve_email_id(id).map(|id| (MaybeInvalid::Value(id), value))
|
||||
})
|
||||
.collect::<VecMap<_, _>>()
|
||||
})
|
||||
.filter(|update| !update.is_empty());
|
||||
let destroy = request
|
||||
.arguments
|
||||
.on_success_destroy_email
|
||||
.as_ref()
|
||||
.is_some_and(|p| !p.is_empty())
|
||||
|| request
|
||||
.arguments
|
||||
.on_success_update_email
|
||||
.as_ref()
|
||||
.is_some_and(|p| !p.is_empty()))
|
||||
&& response.has_changes()
|
||||
{
|
||||
.map(|ids| {
|
||||
ids.into_iter()
|
||||
.filter_map(resolve_email_id)
|
||||
.map(MaybeInvalid::Value)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|ids| !ids.is_empty());
|
||||
if update.is_some() || destroy.is_some() {
|
||||
*next_call = Call {
|
||||
id: String::new(),
|
||||
name: MethodName::new(MethodObject::Email, MethodFunction::Set),
|
||||
@@ -364,38 +386,8 @@ impl EmailSubmissionSet for Server {
|
||||
account_id: request.account_id,
|
||||
if_in_state: None,
|
||||
create: None,
|
||||
update: request.arguments.on_success_update_email.map(|update| {
|
||||
update
|
||||
.into_iter()
|
||||
.filter_map(|(id, value)| {
|
||||
(
|
||||
match id {
|
||||
MaybeIdReference::Id(id) => MaybeInvalid::Value(id),
|
||||
MaybeIdReference::Reference(id_ref) => {
|
||||
MaybeInvalid::Value(*(success_email_ids.get(&id_ref)?))
|
||||
}
|
||||
MaybeIdReference::Invalid(id) => MaybeInvalid::Invalid(id),
|
||||
},
|
||||
value,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
destroy: request.arguments.on_success_destroy_email.map(|ids| {
|
||||
MaybeResultReference::Value(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| match id {
|
||||
MaybeIdReference::Id(id) => Some(id),
|
||||
MaybeIdReference::Reference(id_ref) => {
|
||||
success_email_ids.get(&id_ref).copied()
|
||||
}
|
||||
MaybeIdReference::Invalid(_) => None,
|
||||
})
|
||||
.map(MaybeInvalid::Value)
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
update,
|
||||
destroy: destroy.map(MaybeResultReference::Value),
|
||||
arguments: Default::default(),
|
||||
}))),
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ impl VacationResponseSet for Server {
|
||||
|
||||
// Parse properties
|
||||
let mut is_active = document_id.is_some_and(|id| active_script_id == Some(id));
|
||||
let mut build_script = create_id.is_some();
|
||||
let mut build_script = create_id.is_some() || sieve.blob_hash.is_empty();
|
||||
let vacation = sieve.vacation_response.as_mut().unwrap();
|
||||
|
||||
for (property, mut value) in changes.into_expanded_object() {
|
||||
|
||||
@@ -7,7 +7,7 @@ keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"]
|
||||
categories = ["email"]
|
||||
# Upstream offers AGPL-3.0-only OR LicenseRef-SEL; inbuxa takes the AGPL only.
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
@@ -41,6 +41,9 @@ migration = { path = "../migration" }
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
rustls = { version = "0.23.43", default-features = false, features = ["std", "aws_lc_rs", "tls12"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "freebsd")))'.dependencies]
|
||||
libc = "0.2.189"
|
||||
|
||||
[target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies]
|
||||
tikv-jemallocator = "0.7"
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use std::arch::x86_64::__cpuid;
|
||||
|
||||
const LEAF_FEATURES: u32 = 0x0000_0001;
|
||||
const LEAF_EXTENDED_MAX: u32 = 0x8000_0000;
|
||||
const LEAF_EXTENDED_FEATURES: u32 = 0x8000_0001;
|
||||
|
||||
const SSE3: u32 = 1 << 0;
|
||||
const SSSE3: u32 = 1 << 9;
|
||||
const CMPXCHG16B: u32 = 1 << 13;
|
||||
const SSE4_1: u32 = 1 << 19;
|
||||
const SSE4_2: u32 = 1 << 20;
|
||||
const POPCNT: u32 = 1 << 23;
|
||||
const LAHF_SAHF: u32 = 1 << 0;
|
||||
|
||||
const FEATURES_X86_64_V2: u32 = SSE3 | SSSE3 | CMPXCHG16B | SSE4_1 | SSE4_2 | POPCNT;
|
||||
const EXTENDED_FEATURES_X86_64_V2: u32 = LAHF_SAHF;
|
||||
|
||||
const UNSUPPORTED_CPU: &[u8] = b"This build of inbuxa requires an x86-64-v2 compatible CPU (SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF). If inbuxa runs in a virtual machine, set its CPU type to 'host' or 'x86-64-v2'.\n";
|
||||
const EXIT_UNSUPPORTED_CPU: i32 = 1;
|
||||
|
||||
#[used]
|
||||
#[unsafe(link_section = ".init_array.00001")]
|
||||
static ASSERT_X86_64_V2: extern "C" fn() = assert_x86_64_v2;
|
||||
|
||||
extern "C" fn assert_x86_64_v2() {
|
||||
let features = __cpuid(LEAF_FEATURES).ecx;
|
||||
let extended_features = if __cpuid(LEAF_EXTENDED_MAX).eax >= LEAF_EXTENDED_FEATURES {
|
||||
__cpuid(LEAF_EXTENDED_FEATURES).ecx
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if features & FEATURES_X86_64_V2 != FEATURES_X86_64_V2
|
||||
|| extended_features & EXTENDED_FEATURES_X86_64_V2 != EXTENDED_FEATURES_X86_64_V2
|
||||
{
|
||||
unsafe {
|
||||
libc::write(
|
||||
libc::STDERR_FILENO,
|
||||
UNSUPPORTED_CPU.as_ptr().cast(),
|
||||
UNSUPPORTED_CPU.len(),
|
||||
);
|
||||
libc::_exit(EXIT_UNSUPPORTED_CPU);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@
|
||||
#![warn(clippy::cast_possible_wrap)]
|
||||
#![warn(clippy::cast_sign_loss)]
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use common::manager::systemd::ServiceState;
|
||||
use common::{
|
||||
BuildServer, Inner,
|
||||
config::server::{Listener, ServerProtocol},
|
||||
@@ -29,6 +31,12 @@ use tokio::sync::watch;
|
||||
use trc::Collector;
|
||||
use utils::wait_for_shutdown;
|
||||
|
||||
#[cfg(all(
|
||||
target_arch = "x86_64",
|
||||
target_feature = "sse4.2",
|
||||
any(target_os = "linux", target_os = "freebsd")
|
||||
))]
|
||||
mod cpu;
|
||||
#[cfg(feature = "dev_mode")]
|
||||
pub mod test_data;
|
||||
|
||||
@@ -106,9 +114,15 @@ async fn main() -> std::io::Result<()> {
|
||||
let inner = init.inner.clone();
|
||||
spawn_broadcast_subscriber(init.inner, shutdown_rx);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
ServiceState::Ready.notify();
|
||||
|
||||
// Wait for shutdown signal
|
||||
wait_for_shutdown().await;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
ServiceState::Stopping.notify();
|
||||
|
||||
// inbuxa: hand back the task locks this node holds, so other nodes can
|
||||
// run those tasks now rather than when the locks expire
|
||||
services::task_manager::lock::release_task_locks(&inner.build_server()).await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managesieve"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "migration"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nlp"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "pop3"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "registry"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scim-proto"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scim"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "services"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -6,7 +6,7 @@ homepage = "https://inbuxa.org"
|
||||
keywords = ["smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -252,8 +252,9 @@ impl<T: AsyncRead + AsyncWrite + Unpin> MilterClient<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write EndOfHeaders
|
||||
if !self.has_option(SMFIP_NOEOH) {
|
||||
self.write(Command::EndOfHeader).await?;
|
||||
if !self.has_option(SMFIP_NR_EOH) {
|
||||
return self.read().await?.into_action();
|
||||
|
||||
@@ -436,7 +436,9 @@ impl TlsaLookup for Server {
|
||||
}
|
||||
|
||||
// inbuxa: hickory 0.26.3 calls some valid answers bogus, and the queue then
|
||||
// retries those hosts until the message expires. Two cases seen in production:
|
||||
// retries those hosts until the message expires. Two cases seen in production
|
||||
// (the hickory vendored since upstream v0.16.26 gets both right in the
|
||||
// `--ignored` tests below; this stays as the fallback):
|
||||
//
|
||||
// - A zone delegated beneath an unsigned zone, such as `l.google.com` under
|
||||
// `google.com`. To prove the delegation insecure, hickory wants an SOA
|
||||
@@ -844,7 +846,10 @@ mod tests {
|
||||
}
|
||||
|
||||
// Needs the network: a signed MX pointing into a zone delegated beneath an
|
||||
// unsigned one. Run with `--ignored` to check a hickory upgrade.
|
||||
// unsigned one. Run with `--ignored` to check a hickory upgrade. hickory
|
||||
// 0.26.3 called it bogus and the recheck proved it insecure; the vendored
|
||||
// hickory proves it itself. Either way the answer has to come out insecure.
|
||||
// The TLSA test below prints whether hickory alone gets its case right.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn validated_lookup_proves_delegation_below_unsigned_zone() {
|
||||
@@ -870,12 +875,17 @@ mod tests {
|
||||
};
|
||||
let (dnssec, plain) = (build(true), build(false));
|
||||
|
||||
let validated =
|
||||
validated_lookup(&dnssec, &plain, name("aspmx.l.google.com."), RecordType::A)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(validated.insecure);
|
||||
let query = name("aspmx.l.google.com.");
|
||||
let validated = validated_lookup(&dnssec, &plain, query.clone(), RecordType::A)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!validated.lookup.answers().is_empty());
|
||||
let status = if validated.insecure {
|
||||
DnssecStatus::Insecure
|
||||
} else {
|
||||
tlsa_base_status(&query, validated.lookup.answers(), RecordType::A)
|
||||
};
|
||||
assert_eq!(status, DnssecStatus::Insecure);
|
||||
}
|
||||
|
||||
// Needs the network: a TLSA name that is a signed CNAME to the zone apex,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "spam-filter"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "store"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -12,7 +12,7 @@ registry = { path = "../registry" }
|
||||
rocksdb = { version = "0.25", optional = true, features = ["multi-threaded-cf"] }
|
||||
foundationdb = { version = "0.11", features = ["embedded-fdb-include", "fdb-7_4"], optional = true }
|
||||
rusqlite = { version = "0.40", features = ["bundled"], optional = true }
|
||||
rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"], optional = true }
|
||||
rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"], optional = true }
|
||||
reqwest_s3 = { package = "reqwest", version = "0.12", default-features = false, features = ["rustls-tls-native-roots"], optional = true }
|
||||
azure_core = { version = "0.21.0", optional = true }
|
||||
azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
|
||||
|
||||
@@ -74,9 +74,8 @@ impl RocksDbStore {
|
||||
let mut cf_opts = cf_options(CfProfile::Blob, &cache, write_buffer_size);
|
||||
cf_opts.set_enable_blob_files(true);
|
||||
cf_opts.set_min_blob_size(config.blob_size);
|
||||
cf_opts.set_enable_blob_gc(true);
|
||||
cf_opts.set_enable_blob_gc(false);
|
||||
cf_opts.set_blob_gc_age_cutoff(1.0);
|
||||
cf_opts.set_blob_gc_force_threshold(0.5);
|
||||
cfs.push(ColumnFamilyDescriptor::new(CF_BLOBS, cf_opts));
|
||||
|
||||
// Other cfs
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use super::{CF_INDEXES, CF_LOGS, CfHandle, RocksDbStore, into_error};
|
||||
use crate::{
|
||||
Deserialize, IndexKey, Key, LogKey, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER,
|
||||
SUBSPACE_QUOTA,
|
||||
Deserialize, IndexKey, Key, LogKey, SUBSPACE_BLOBS, SUBSPACE_COUNTER,
|
||||
SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_QUOTA,
|
||||
backend::deserialize_i64_le,
|
||||
write::{
|
||||
AssignedIds, Batch, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, MergeResult, Operation,
|
||||
@@ -16,8 +16,8 @@ use crate::{
|
||||
};
|
||||
use rand::RngExt;
|
||||
use rocksdb::{
|
||||
BoundColumnFamily, ErrorKind, IteratorMode, OptimisticTransactionDB,
|
||||
OptimisticTransactionOptions, WriteOptions,
|
||||
BottommostLevelCompaction, BoundColumnFamily, CompactOptions, ErrorKind, IteratorMode,
|
||||
OptimisticTransactionDB, OptimisticTransactionOptions, WriteOptions,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
@@ -25,6 +25,9 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const BLOB_GC_MIN_GARBAGE: u64 = 256 * 1024 * 1024;
|
||||
const BLOB_GC_GARBAGE_RATIO: f64 = 0.25;
|
||||
|
||||
impl RocksDbStore {
|
||||
pub(crate) async fn write(&self, mut batch: Batch<'_>) -> trc::Result<AssignedIds> {
|
||||
let db = self.db.clone();
|
||||
@@ -114,6 +117,28 @@ impl RocksDbStore {
|
||||
}
|
||||
}
|
||||
|
||||
let cf = db.subspace_handle(SUBSPACE_BLOBS);
|
||||
let total = db
|
||||
.property_int_value_cf(&cf, "rocksdb.live-blob-file-size")
|
||||
.map_err(into_error)?
|
||||
.unwrap_or_default();
|
||||
let garbage = db
|
||||
.property_int_value_cf(&cf, "rocksdb.live-blob-file-garbage-size")
|
||||
.map_err(into_error)?
|
||||
.unwrap_or_default();
|
||||
|
||||
if garbage >= BLOB_GC_MIN_GARBAGE
|
||||
&& garbage as f64 >= total as f64 * BLOB_GC_GARBAGE_RATIO
|
||||
{
|
||||
db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "true")])
|
||||
.map_err(into_error)?;
|
||||
let mut opts = CompactOptions::default();
|
||||
opts.set_bottommost_level_compaction(BottommostLevelCompaction::Force);
|
||||
db.compact_range_cf_opt(&cf, None::<&[u8]>, None::<&[u8]>, &opts);
|
||||
db.set_options_cf(&cf, &[("enable_blob_garbage_collection", "false")])
|
||||
.map_err(into_error)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "trc"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "event_macro"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "types"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "utils"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proc_macros"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ target "docker-metadata-action" {}
|
||||
target "build" {
|
||||
secret = [
|
||||
"type=env,id=ACTIONS_RESULTS_URL",
|
||||
"type=env,id=ACTIONS_RUNTIME_TOKEN"
|
||||
"type=env,id=ACTIONS_RUNTIME_TOKEN",
|
||||
"type=env,id=GITHUB_TOKEN"
|
||||
]
|
||||
args = {
|
||||
TARGET = "${TARGET}"
|
||||
|
||||
@@ -5,7 +5,7 @@ ConditionPathExists=__PATH__/etc/config.json
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=notify
|
||||
LimitNOFILE=65536
|
||||
KillMode=process
|
||||
KillSignal=SIGINT
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tests"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
@@ -89,7 +89,7 @@ sha2 = "0.11"
|
||||
zip = "8.6" # inbuxa: reading legal hold exports
|
||||
time = "0.3"
|
||||
testcontainers = { version = "0.28", features = ["reusable-containers"] }
|
||||
rust-s3 = { version = "0.37", default-features = false, features = ["tokio-rustls-tls"] }
|
||||
rust-s3 = { version = "0.38", default-features = false, features = ["tokio-rustls-tls"] }
|
||||
|
||||
[target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies]
|
||||
tikv-jemallocator = "0.7"
|
||||
|
||||
@@ -133,6 +133,72 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "stalwart-userinfo-username",
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "stalwart-userinfo-username-secret",
|
||||
"redirectUris": [
|
||||
"*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"*"
|
||||
],
|
||||
"publicClient": false,
|
||||
"protocol": "openid-connect",
|
||||
"directAccessGrantsEnabled": true,
|
||||
"standardFlowEnabled": true,
|
||||
"serviceAccountsEnabled": true,
|
||||
"defaultClientScopes": [
|
||||
"openid"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"email",
|
||||
"profile",
|
||||
"roles"
|
||||
],
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "email-claim",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"user.attribute": "email",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "email",
|
||||
"userinfo.token.claim": "true",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "login-userinfo-only",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"claim.name": "login",
|
||||
"claim.value": "jdoe",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "false",
|
||||
"userinfo.token.claim": "true",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "audience",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"included.client.audience": "stalwart",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
|
||||
@@ -229,6 +229,37 @@ pub async fn test(test: &TestServer) {
|
||||
"{result:?}",
|
||||
);
|
||||
|
||||
// Moving a mailbox under a parent that has a sibling with the same name fails
|
||||
let nested_id = client
|
||||
.mailbox_create("level 2", Some(&id_map["l.1"]), Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
let result = client
|
||||
.mailbox_move(&nested_id, Some(&id_map["inbox"]))
|
||||
.await;
|
||||
assert!(
|
||||
matches!(
|
||||
result,
|
||||
Err(Error::Set(SetError {
|
||||
type_: SetErrorType::AlreadyExists,
|
||||
..
|
||||
}))
|
||||
),
|
||||
"{result:?}",
|
||||
);
|
||||
client.mailbox_destroy(&nested_id, true).await.unwrap();
|
||||
|
||||
// Changing only the case of a mailbox name succeeds
|
||||
client
|
||||
.mailbox_rename(&id_map["l.2"], "LEVEL 2")
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.mailbox_rename(&id_map["l.2"], "Level 2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Circular relationship
|
||||
let mut request = client.build();
|
||||
request
|
||||
|
||||
@@ -363,7 +363,10 @@ async fn update_preserves_uids(
|
||||
assert_eq!(uids[&INBOX_ID], inbox_uid);
|
||||
assert_ne!(uids[&test_mailbox_document_id], 0);
|
||||
|
||||
client.mailbox_destroy(&test_mailbox_id, true).await.unwrap();
|
||||
client
|
||||
.mailbox_destroy(&test_mailbox_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn message_uids(
|
||||
|
||||
@@ -510,6 +510,117 @@ pub async fn test(test: &TestServer) {
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// onSuccess actions keyed by an existing submission id act on its own email
|
||||
let mut held = Vec::with_capacity(2);
|
||||
for subject in ["held 1", "held 2"] {
|
||||
let draft_id = client
|
||||
.email_import(
|
||||
format!("From: [email protected]\r\nTo: [email protected]\r\nSubject: {subject}\r\n\r\ntest")
|
||||
.into_bytes(),
|
||||
[&mailbox_id],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
let submission_id = client
|
||||
.email_submission_create_envelope(
|
||||
&draft_id,
|
||||
&identity_id,
|
||||
Address::new("[email protected]")
|
||||
.parameter("HOLDUNTIL", Some(hold_until_date.to_string())),
|
||||
["[email protected]"],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
held.push((submission_id, draft_id));
|
||||
}
|
||||
let [(submission_id_1, draft_id_1), (submission_id_2, draft_id_2)] =
|
||||
<[_; 2]>::try_from(held).unwrap();
|
||||
let mut found_filler = false;
|
||||
for _ in 0..64 {
|
||||
if client
|
||||
.email_get(&submission_id_1, None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
found_filler = true;
|
||||
break;
|
||||
}
|
||||
client
|
||||
.email_import(
|
||||
b"From: [email protected]\r\nSubject: filler\r\n\r\ntest".to_vec(),
|
||||
[&mailbox_id_2],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
assert!(found_filler, "No email shares the submission document id");
|
||||
assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await;
|
||||
|
||||
let mut request = client.build();
|
||||
let set_request = request.set_email_submission();
|
||||
set_request
|
||||
.update(&submission_id_1)
|
||||
.undo_status(UndoStatus::Canceled);
|
||||
set_request
|
||||
.arguments()
|
||||
.on_success_destroy_email_id(&submission_id_1);
|
||||
request.send().await.unwrap().unwrap_method_responses();
|
||||
assert!(
|
||||
client
|
||||
.email_get(&draft_id_1, None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await;
|
||||
|
||||
// onSuccessUpdateEmail is skipped for a submission whose update failed
|
||||
let mut request = client.build();
|
||||
let set_request = request.set_email_submission();
|
||||
set_request
|
||||
.update(&submission_id_1)
|
||||
.undo_status(UndoStatus::Canceled);
|
||||
set_request
|
||||
.update(&submission_id_2)
|
||||
.undo_status(UndoStatus::Canceled);
|
||||
set_request
|
||||
.arguments()
|
||||
.on_success_update_email_id(&submission_id_1)
|
||||
.keyword("$draft", true);
|
||||
set_request
|
||||
.arguments()
|
||||
.on_success_update_email_id(&submission_id_2)
|
||||
.keyword("$draft", true)
|
||||
.mailbox_id(&mailbox_id, false)
|
||||
.mailbox_id(&mailbox_id_2, true);
|
||||
request.send().await.unwrap().unwrap_method_responses();
|
||||
assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await;
|
||||
assert_email_properties(&client, &draft_id_2, &[&mailbox_id_2], &["$draft"]).await;
|
||||
|
||||
// onSuccessDestroyEmail keyed by a destroyed submission id
|
||||
let mut request = client.build();
|
||||
let set_request = request.set_email_submission();
|
||||
set_request.destroy([&submission_id_2]);
|
||||
set_request
|
||||
.arguments()
|
||||
.on_success_destroy_email_id(&submission_id_2);
|
||||
request.send().await.unwrap().unwrap_method_responses();
|
||||
assert!(
|
||||
client
|
||||
.email_get(&draft_id_2, None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_email_properties(&client, &submission_id_1, &[&mailbox_id_2], &[]).await;
|
||||
smtp_settings.lock().do_stop = true;
|
||||
|
||||
// Identities are created and destroyed as the account's addresses change
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection},
|
||||
};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use jmap_client::core::response::VacationResponseSetResponse;
|
||||
use std::time::Instant;
|
||||
|
||||
pub async fn test(test: &TestServer) {
|
||||
@@ -29,6 +30,42 @@ pub async fn test(test: &TestServer) {
|
||||
Instant::now() + std::time::Duration::from_secs(10),
|
||||
);
|
||||
|
||||
// Enabling with only isEnabled on a fresh account uses the default response
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_vacation_response()
|
||||
.update("singleton")
|
||||
.is_enabled(true);
|
||||
request
|
||||
.send_single::<VacationResponseSetResponse>()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated("singleton")
|
||||
.unwrap();
|
||||
let mut lmtp = SmtpConnection::connect().await;
|
||||
lmtp.ingest(
|
||||
"[email protected]",
|
||||
&["[email protected]"],
|
||||
concat!(
|
||||
"From: [email protected]\r\n",
|
||||
"To: [email protected]\r\n",
|
||||
"Subject: Printer\r\n",
|
||||
"\r\n",
|
||||
"PC load letter? What does that even mean?",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
lmtp.quit().await;
|
||||
assert_message_delivery(
|
||||
&mut smtp_rx,
|
||||
MockMessage::new(
|
||||
"<[email protected]>",
|
||||
["<[email protected]>"],
|
||||
"@I am away.",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Let people know that we'll be down in Kokomo
|
||||
client
|
||||
.vacation_response_enable(
|
||||
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
# hickory-dns, vendored
|
||||
|
||||
`hickory-proto`, `hickory-net` and `hickory-resolver` 0.26.3 as upstream
|
||||
builds them since v0.16.26: from its patched branch of
|
||||
[hickory-dns](https://github.com/hickory-dns/hickory-dns), `stalwart/0.26`
|
||||
at commit `f8b92b17fa6c7d09a6959672592106ca1410937b` (2026-10-11), under
|
||||
MIT OR Apache-2.0. INBUXA takes them under MIT. The root `Cargo.toml`
|
||||
patches them in with `[patch.crates-io]`, so builds never fetch from
|
||||
upstream's repositories.
|
||||
|
||||
That branch is hickory's own `release/0.26` (unreleased work after 0.26.3,
|
||||
including DS lookups that check the parent zone when an answer carries no
|
||||
insecure proof) plus nine DNSSEC commits: finding the zone cut with DS
|
||||
queries, accepting opt-out DS proofs for the next closer name, setting the
|
||||
CD bit while validating, searching ancestors when a DS lookup fails,
|
||||
wildcard validation for all RRsets, and a TCP retry for a late truncated
|
||||
UDP answer.
|
||||
|
||||
## Changes
|
||||
|
||||
None. Each directory is `cargo package --no-verify` of that commit (the
|
||||
manifest normalized, so it builds outside hickory's workspace), without
|
||||
`Cargo.lock` and `Cargo.toml.orig`. `.cargo_vcs_info.json` records the
|
||||
commit.
|
||||
|
||||
Both answers hickory 0.26.3 called bogus in production (an insecure
|
||||
delegation below an unsigned zone, and a signed CNAME to a name without the
|
||||
type asked for) validate with this copy: the `--ignored` tests at the end of
|
||||
`crates/smtp/src/outbound/dane/dnssec.rs` show it, the TLSA one through
|
||||
Hetzner's resolver (2026-10-12). `validated_lookup` there still rechecks
|
||||
answers hickory calls bogus, as a fallback.
|
||||
|
||||
## Updating
|
||||
|
||||
When upstream moves its hickory pin, or hickory publishes a release that
|
||||
carries these fixes, re-vendor: clone the branch at the new commit, run
|
||||
`cargo package --no-verify -p hickory-proto -p hickory-net -p
|
||||
hickory-resolver`, unpack each `.crate` here, and drop `Cargo.lock` and
|
||||
`Cargo.toml.orig`. If the version changes, Cargo stops using these copies
|
||||
and only warns that the patch went unused, so check `Cargo.lock` has no
|
||||
`hickory-*` entry with a `source` line. Update the commit above.
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
name = "hickory-net"
|
||||
version = "0.26.3"
|
||||
authors = ["The contributors to Hickory DNS"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = """
|
||||
hickory-net is a safe and secure low-level DNS library. This is the foundational DNS protocol
|
||||
library used by the other higher-level Hickory DNS crates.
|
||||
"""
|
||||
homepage = "https://hickory-dns.org/"
|
||||
documentation = "https://docs.rs/hickory-net"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"dns",
|
||||
"dnssec",
|
||||
]
|
||||
categories = ["network-programming"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/hickory-dns/hickory-dns"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
targets = [
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
]
|
||||
rustdoc-args = [
|
||||
"--cfg",
|
||||
"docsrs",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
skip_optional_dependencies = true
|
||||
max_combination_size = 2
|
||||
denylist = [
|
||||
"__tls",
|
||||
"__https",
|
||||
"__quic",
|
||||
"__h3",
|
||||
"__dnssec",
|
||||
]
|
||||
|
||||
[features]
|
||||
__dnssec = [
|
||||
"dep:bitflags",
|
||||
"dep:rustls-pki-types",
|
||||
"dep:time",
|
||||
"dep:lru-cache",
|
||||
"dep:parking_lot",
|
||||
]
|
||||
__h3 = [
|
||||
"dep:h3",
|
||||
"dep:h3-quinn",
|
||||
"dep:http",
|
||||
]
|
||||
__https = [
|
||||
"dep:bytes",
|
||||
"dep:h2",
|
||||
"dep:http",
|
||||
]
|
||||
__quic = [
|
||||
"dep:bytes",
|
||||
"dep:pin-project-lite",
|
||||
"dep:quinn",
|
||||
]
|
||||
__tls = [
|
||||
"dep:bytes",
|
||||
"dep:rustls",
|
||||
"dep:tokio-rustls",
|
||||
"tokio",
|
||||
]
|
||||
default = ["tokio"]
|
||||
dnssec-aws-lc-rs = [
|
||||
"dep:aws-lc-rs",
|
||||
"aws-lc-rs/aws-lc-sys",
|
||||
"aws-lc-rs/ring-io",
|
||||
"hickory-proto/dnssec-aws-lc-rs",
|
||||
"__dnssec",
|
||||
]
|
||||
dnssec-ring = [
|
||||
"dep:ring",
|
||||
"hickory-proto/dnssec-ring",
|
||||
"__dnssec",
|
||||
]
|
||||
h3-aws-lc-rs = [
|
||||
"quic-aws-lc-rs",
|
||||
"__h3",
|
||||
]
|
||||
h3-ring = [
|
||||
"quic-ring",
|
||||
"__h3",
|
||||
]
|
||||
https-aws-lc-rs = [
|
||||
"tls-aws-lc-rs",
|
||||
"__https",
|
||||
]
|
||||
https-ring = [
|
||||
"tls-ring",
|
||||
"__https",
|
||||
]
|
||||
mdns = ["socket2/all"]
|
||||
quic-aws-lc-rs = [
|
||||
"quinn/rustls-aws-lc-rs",
|
||||
"tls-aws-lc-rs",
|
||||
"__quic",
|
||||
]
|
||||
quic-ring = [
|
||||
"quinn/rustls-ring",
|
||||
"tls-ring",
|
||||
"__quic",
|
||||
]
|
||||
rustls-platform-verifier = ["dep:rustls-platform-verifier"]
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"url/serde",
|
||||
]
|
||||
tls-aws-lc-rs = [
|
||||
"tokio-rustls/aws-lc-rs",
|
||||
"__tls",
|
||||
]
|
||||
tls-ring = [
|
||||
"tokio-rustls/ring",
|
||||
"__tls",
|
||||
]
|
||||
tokio = [
|
||||
"dep:tokio",
|
||||
"tokio/net",
|
||||
"tokio/rt",
|
||||
"tokio/time",
|
||||
"tokio/rt-multi-thread",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "hickory_net"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.async-trait]
|
||||
version = "0.1.43"
|
||||
|
||||
[dependencies.aws-lc-rs]
|
||||
version = "1.12.3"
|
||||
features = ["prebuilt-nasm"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.4.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.bytes]
|
||||
version = "1"
|
||||
optional = true
|
||||
|
||||
[dependencies.cfg-if]
|
||||
version = "1"
|
||||
|
||||
[dependencies.data-encoding]
|
||||
version = "2.2.0"
|
||||
features = ["alloc"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures-channel]
|
||||
version = "0.3.5"
|
||||
features = [
|
||||
"alloc",
|
||||
"std",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures-io]
|
||||
version = "0.3.5"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures-util]
|
||||
version = "0.3.5"
|
||||
features = [
|
||||
"alloc",
|
||||
"async-await-macro",
|
||||
"std",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.h2]
|
||||
version = "0.4.0"
|
||||
features = ["stream"]
|
||||
optional = true
|
||||
|
||||
[dependencies.h3]
|
||||
version = "0.0.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.h3-quinn]
|
||||
version = "0.0.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.hickory-proto]
|
||||
version = "0.26.3"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.http]
|
||||
version = "1.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.idna]
|
||||
version = "1.0.3"
|
||||
features = [
|
||||
"alloc",
|
||||
"compiled_data",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.ipnet]
|
||||
version = "2.11.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.lru-cache]
|
||||
version = "0.1.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.parking_lot]
|
||||
version = "0.12"
|
||||
optional = true
|
||||
|
||||
[dependencies.pin-project-lite]
|
||||
version = "0.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.quinn]
|
||||
version = "0.11.2"
|
||||
features = [
|
||||
"log",
|
||||
"runtime-tokio",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.10.0"
|
||||
features = [
|
||||
"alloc",
|
||||
"std_rng",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.ring]
|
||||
version = "0.17"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
|
||||
[dependencies.rustls]
|
||||
version = "0.23.23"
|
||||
features = [
|
||||
"logging",
|
||||
"std",
|
||||
"tls12",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rustls-pki-types]
|
||||
version = "1.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.rustls-platform-verifier]
|
||||
version = "0.7"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dependencies.socket2]
|
||||
version = "0.6"
|
||||
optional = true
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
default-features = false
|
||||
|
||||
[dependencies.time]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.tinyvec]
|
||||
version = "1.1.1"
|
||||
features = ["alloc"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.36"
|
||||
features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dependencies.tokio-rustls]
|
||||
version = "0.26"
|
||||
features = ["early-data"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1.30"
|
||||
default-features = false
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.5.4"
|
||||
default-features = false
|
||||
|
||||
[dependencies.webpki-roots]
|
||||
version = "1"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies.futures-executor]
|
||||
version = "0.3.5"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1.36"
|
||||
features = [
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
"test-util",
|
||||
]
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.jni]
|
||||
version = "0.22.1"
|
||||
|
||||
[lints.clippy]
|
||||
bool_to_int_with_if = "allow"
|
||||
default_trait_access = "warn"
|
||||
manual_let_else = "warn"
|
||||
or_fun_call = "warn"
|
||||
single_component_path_imports = "allow"
|
||||
unimplemented = "warn"
|
||||
upper_case_acronyms = "allow"
|
||||
use_self = "warn"
|
||||
|
||||
[lints.rust]
|
||||
non_snake_case = "warn"
|
||||
non_upper_case_globals = "warn"
|
||||
rust_2018_idioms = "warn"
|
||||
unnameable_types = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "warn"
|
||||
priority = 0
|
||||
check-cfg = ["cfg(nightly)"]
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2015 The Hickory DNS Developers
|
||||
Copyright (c) 2017 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Overview
|
||||
|
||||
`hickory-net` provides network protocol implementations for DNS message transport.
|
||||
This is a support library used by the other higher-level Hickory DNS crates.
|
||||
|
||||
For higher-level interfaces, refer to the [`hickory-server`] and [`hickory-resolver`]
|
||||
library crates instead.
|
||||
|
||||
[`hickory-server`]: ../server
|
||||
[`hickory-resolver`]: ../resolver
|
||||
|
||||
## Cryptography provider
|
||||
|
||||
Features requiring cryptography require selecting a specific cryptography
|
||||
provider. See the [project README] for more information.
|
||||
|
||||
[project README]: ../../README.md#Cryptography-provider
|
||||
|
||||
## Protocol support
|
||||
|
||||
The following DNS protocols are optionally supported:
|
||||
|
||||
* DNS over TLS (DoT)
|
||||
* DNS over HTTP/2 (DoH)
|
||||
* DNS over QUIC (DoQ)
|
||||
* DNS over HTTP/3 (DoH3)
|
||||
|
||||
In order to use these optional protocols you must enable a cargo feature
|
||||
corresponding to your desired cryptography provider:
|
||||
|
||||
* DoT: `tls-aws-lc-rs` or `tls-ring`.
|
||||
* DoH: `https-aws-lc-rs` or `https-ring`
|
||||
* DoQ: `quic-aws-lc-rs` or `quic-ring`
|
||||
* DoH3: `h3-aws-lc-rs` or `h3-ring`
|
||||
|
||||
## DNSSEC
|
||||
|
||||
In order to use DNSSEC you must enable a cargo feature corresponding to your
|
||||
desired cryptography provider:
|
||||
|
||||
* `dnssec-aws-lc-rs`
|
||||
* `dnssec-ring`
|
||||
|
||||
## Other crate features
|
||||
|
||||
* `tokio` - support for the Tokio async runtime.
|
||||
* `serde` - enable serde serialization support.
|
||||
* `mdns` (experimental) - enable experimental mDNS support.
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2015-2019 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::stream::Stream;
|
||||
|
||||
use super::Client;
|
||||
use crate::dnssec::DnssecDnsHandle;
|
||||
use crate::error::NetError;
|
||||
use crate::proto::dnssec::TrustAnchors;
|
||||
use crate::proto::op::{DnsRequest, DnsResponse};
|
||||
use crate::runtime::{TokioRuntimeProvider, TokioTime};
|
||||
use crate::xfer::{DnsExchangeBackground, DnsHandle, DnsRequestSender};
|
||||
|
||||
/// A DNSSEC Client implemented over futures-rs.
|
||||
///
|
||||
/// This Client is generic and capable of wrapping UDP, TCP, and other underlying DNS protocol
|
||||
/// implementations.
|
||||
pub struct DnssecClient {
|
||||
client: DnssecDnsHandle<Client<TokioRuntimeProvider>>,
|
||||
}
|
||||
|
||||
impl DnssecClient {
|
||||
/// Returns a DNSSEC verifying client with a TrustAnchor that can be replaced
|
||||
pub fn builder<F, S>(connect_future: F) -> AsyncSecureClientBuilder<F>
|
||||
where
|
||||
F: Future<Output = Result<S, NetError>> + 'static + Send + Unpin,
|
||||
S: DnsRequestSender,
|
||||
{
|
||||
AsyncSecureClientBuilder {
|
||||
connect_future,
|
||||
trust_anchor: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a DNSSEC verifying client with the default TrustAnchor
|
||||
pub async fn connect<F, S>(
|
||||
connect_future: F,
|
||||
) -> Result<(Self, DnsExchangeBackground<S, TokioTime>), NetError>
|
||||
where
|
||||
S: DnsRequestSender,
|
||||
F: Future<Output = Result<S, NetError>> + 'static + Send + Unpin,
|
||||
{
|
||||
Self::builder(connect_future).build().await
|
||||
}
|
||||
|
||||
/// Constructs a DNSSEC client with an existing client and the given trust anchors
|
||||
pub fn from_client(
|
||||
client: Client<TokioRuntimeProvider>,
|
||||
trust_anchor: Arc<TrustAnchors>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client: DnssecDnsHandle::with_trust_anchor(client, trust_anchor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DnssecClient {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: self.client.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsHandle for DnssecClient {
|
||||
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send + 'static>>;
|
||||
type Runtime = TokioRuntimeProvider;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
self.client.send(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder to allow a custom trust to be used for validating all signed records
|
||||
pub struct AsyncSecureClientBuilder<F> {
|
||||
connect_future: F,
|
||||
trust_anchor: Option<TrustAnchors>,
|
||||
}
|
||||
|
||||
impl<F, S> AsyncSecureClientBuilder<F>
|
||||
where
|
||||
F: Future<Output = Result<S, NetError>> + 'static + Send + Unpin,
|
||||
S: DnsRequestSender,
|
||||
{
|
||||
/// This variant allows for the trust_anchor to be replaced
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `trust_anchor` - the set of trusted DNSKEY public_keys, by default this only contains the
|
||||
/// root public_key.
|
||||
pub fn trust_anchor(mut self, trust_anchor: TrustAnchors) -> Self {
|
||||
self.trust_anchor = Some(trust_anchor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct the new client
|
||||
pub async fn build(
|
||||
mut self,
|
||||
) -> Result<(DnssecClient, DnsExchangeBackground<S, TokioTime>), NetError> {
|
||||
let trust_anchor = Arc::new(self.trust_anchor.take().unwrap_or_default());
|
||||
let (client, bg) = Client::from_sender(self.connect_future.await?);
|
||||
Ok((DnssecClient::from_client(client, trust_anchor), bg))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
use core::pin::Pin;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::future::FutureExt;
|
||||
use futures_util::lock::Mutex;
|
||||
use futures_util::stream::Stream;
|
||||
|
||||
use super::{
|
||||
ClientHandle,
|
||||
rc_stream::{RcStream, rc_stream},
|
||||
};
|
||||
use crate::{
|
||||
NetError,
|
||||
proto::op::{DnsRequest, DnsResponse, Query},
|
||||
xfer::DnsHandle,
|
||||
};
|
||||
|
||||
// TODO: move to proto
|
||||
/// A ClientHandle for memoized (cached) responses to queries.
|
||||
///
|
||||
/// This wraps a ClientHandle, changing the implementation `send()` to store the response against
|
||||
/// the Message.Query that was sent. This should reduce network traffic especially during things
|
||||
/// like DNSSEC validation. *Warning* this will currently cache for the life of the Client.
|
||||
#[derive(Clone)]
|
||||
#[must_use = "queries can only be sent through a ClientHandle"]
|
||||
pub struct MemoizeClientHandle<H: ClientHandle> {
|
||||
client: H,
|
||||
active_queries: Arc<Mutex<HashMap<Query, RcStream<<H as DnsHandle>::Response>>>>,
|
||||
}
|
||||
|
||||
impl<H> MemoizeClientHandle<H>
|
||||
where
|
||||
H: ClientHandle,
|
||||
{
|
||||
/// Returns a new handle wrapping the specified client
|
||||
pub fn new(client: H) -> Self {
|
||||
Self {
|
||||
client,
|
||||
active_queries: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn inner_send(
|
||||
request: DnsRequest,
|
||||
active_queries: Arc<Mutex<HashMap<Query, RcStream<<H as DnsHandle>::Response>>>>,
|
||||
client: H,
|
||||
) -> impl Stream<Item = Result<DnsResponse, NetError>> {
|
||||
// TODO: what if we want to support multiple queries (non-standard)?
|
||||
let query = request.queries.first().expect("no query!").clone();
|
||||
|
||||
// lock all the currently running queries
|
||||
let mut active_queries = active_queries.lock().await;
|
||||
|
||||
// TODO: we need to consider TTL on the records here at some point
|
||||
// If the query is running, grab that existing one...
|
||||
if let Some(rc_stream) = active_queries.get(&query) {
|
||||
return rc_stream.clone();
|
||||
};
|
||||
|
||||
// Otherwise issue a new query and store in the map
|
||||
active_queries
|
||||
.entry(query)
|
||||
.or_insert_with(|| rc_stream(client.send(request)))
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: ClientHandle> DnsHandle for MemoizeClientHandle<H> {
|
||||
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
|
||||
type Runtime = H::Runtime;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
Box::pin(
|
||||
Self::inner_send(
|
||||
request,
|
||||
Arc::clone(&self.active_queries),
|
||||
self.client.clone(),
|
||||
)
|
||||
.flatten_stream(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#![allow(clippy::dbg_macro, clippy::print_stdout)]
|
||||
|
||||
use core::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::lock::Mutex;
|
||||
use futures_util::stream;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
proto::{
|
||||
op::{DnsRequest, DnsResponse, Message, MessageType, OpCode, Query},
|
||||
rr::RecordType,
|
||||
},
|
||||
runtime::TokioRuntimeProvider,
|
||||
xfer::{DnsHandle, FirstAnswer},
|
||||
};
|
||||
use test_support::subscribe;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestClient {
|
||||
i: Arc<Mutex<u16>>,
|
||||
}
|
||||
|
||||
impl DnsHandle for TestClient {
|
||||
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
|
||||
type Runtime = TokioRuntimeProvider;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
let i = Arc::clone(&self.i);
|
||||
Box::pin(stream::once(async move {
|
||||
let mut i = i.lock().await;
|
||||
let message = Message::new(*i, MessageType::Query, OpCode::Query).into_response();
|
||||
std::println!(
|
||||
"sending {}: {}",
|
||||
*i,
|
||||
request.queries.first().expect("no query!").clone()
|
||||
);
|
||||
|
||||
*i += 1;
|
||||
|
||||
Ok(DnsResponse::from_message(message).unwrap())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memoized() {
|
||||
use futures_executor::block_on;
|
||||
|
||||
subscribe();
|
||||
|
||||
let client = MemoizeClientHandle::new(TestClient {
|
||||
i: Arc::new(Mutex::new(0)),
|
||||
});
|
||||
|
||||
let mut test1 = Message::query();
|
||||
test1.add_query(Query::new().set_query_type(RecordType::A).clone());
|
||||
|
||||
let mut test2 = Message::query();
|
||||
test2.add_query(Query::new().set_query_type(RecordType::AAAA).clone());
|
||||
|
||||
let result = block_on(client.send(DnsRequest::from(test1.clone())).first_answer()).unwrap();
|
||||
assert_eq!(result.id, 0);
|
||||
|
||||
let result = block_on(client.send(DnsRequest::from(test2.clone())).first_answer()).unwrap();
|
||||
assert_eq!(result.id, 1);
|
||||
|
||||
// should get the same result for each...
|
||||
let result = block_on(client.send(DnsRequest::from(test1)).first_answer()).unwrap();
|
||||
assert_eq!(result.id, 0);
|
||||
|
||||
let result = block_on(client.send(DnsRequest::from(test2)).first_answer()).unwrap();
|
||||
assert_eq!(result.id, 1);
|
||||
}
|
||||
}
|
||||
+820
@@ -0,0 +1,820 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2016 Benjamin Fry <[email protected]>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! DNS Client associated classes for performing queries and other operations.
|
||||
|
||||
use core::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_util::{
|
||||
ready,
|
||||
stream::{Stream, StreamExt},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
error::NetError,
|
||||
proto::{
|
||||
ProtoError,
|
||||
op::{
|
||||
DEFAULT_MAX_PAYLOAD_LEN, DnsRequest, DnsRequestOptions, DnsResponse, Edns, Message,
|
||||
OpCode, Query, update_message,
|
||||
},
|
||||
rr::{DNSClass, Name, RData, Record, RecordSet, RecordType, rdata::SOA},
|
||||
},
|
||||
runtime::RuntimeProvider,
|
||||
xfer::{
|
||||
BufDnsStreamHandle, DnsClientStream, DnsExchange, DnsExchangeBackground, DnsExchangeSend,
|
||||
DnsHandle, DnsMultiplexer, DnsRequestSender,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "__dnssec", feature = "tokio"))]
|
||||
pub(crate) mod dnssec_client;
|
||||
#[cfg(all(feature = "__dnssec", feature = "tokio"))]
|
||||
pub use dnssec_client::{AsyncSecureClientBuilder, DnssecClient};
|
||||
|
||||
mod memoize_client_handle;
|
||||
pub use memoize_client_handle::MemoizeClientHandle;
|
||||
|
||||
mod rc_stream;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A DNS Client implemented over futures-rs.
|
||||
///
|
||||
/// This Client is generic and capable of wrapping UDP, TCP, and other underlying DNS protocol
|
||||
/// implementations.
|
||||
#[derive(Clone)]
|
||||
pub struct Client<P> {
|
||||
exchange: DnsExchange<P>,
|
||||
use_edns: bool,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> Client<P> {
|
||||
/// Spawns a new Client Stream. This uses a default timeout of 5 seconds for all requests.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stream` - A stream of bytes that can be used to send/receive DNS messages
|
||||
/// (see TcpClientStream or UdpClientStream)
|
||||
/// * `stream_handle` - The handle for the `stream` on which bytes can be sent/received.
|
||||
pub fn new<S: DnsClientStream>(
|
||||
stream: S,
|
||||
stream_handle: BufDnsStreamHandle,
|
||||
) -> (Self, DnsExchangeBackground<DnsMultiplexer<S>, P::Timer>) {
|
||||
Self::with_timeout(stream, stream_handle, Duration::from_secs(5))
|
||||
}
|
||||
|
||||
/// Spawns a new Client Stream.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stream` - A stream of bytes that can be used to send/receive DNS messages
|
||||
/// (see TcpClientStream or UdpClientStream)
|
||||
/// * `stream_handle` - The handle for the `stream` on which bytes can be sent/received.
|
||||
/// * `timeout_duration` - All requests may fail due to lack of response, this is the time to
|
||||
/// wait for a response before canceling the request.
|
||||
pub fn with_timeout<S: DnsClientStream>(
|
||||
stream: S,
|
||||
stream_handle: BufDnsStreamHandle,
|
||||
timeout_duration: Duration,
|
||||
) -> (Self, DnsExchangeBackground<DnsMultiplexer<S>, P::Timer>) {
|
||||
Self::from_sender(DnsMultiplexer::new(stream, stream_handle).with_timeout(timeout_duration))
|
||||
}
|
||||
|
||||
/// Creates a Client from an existing DnsRequestSender
|
||||
pub fn from_sender<S: DnsRequestSender>(
|
||||
sender: S,
|
||||
) -> (Self, DnsExchangeBackground<S, P::Timer>) {
|
||||
let (exchange, bg) = DnsExchange::from_stream(sender);
|
||||
(
|
||||
Self {
|
||||
exchange,
|
||||
use_edns: true,
|
||||
},
|
||||
bg,
|
||||
)
|
||||
}
|
||||
|
||||
/// (Re-)enable usage of EDNS for outgoing messages
|
||||
pub fn enable_edns(&mut self) {
|
||||
self.use_edns = true;
|
||||
}
|
||||
|
||||
/// Disable usage of EDNS for outgoing messages
|
||||
pub fn disable_edns(&mut self) {
|
||||
self.use_edns = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> DnsHandle for Client<P> {
|
||||
type Response = DnsExchangeSend<P>;
|
||||
type Runtime = P;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
self.exchange.send(request)
|
||||
}
|
||||
|
||||
fn is_using_edns(&self) -> bool {
|
||||
self.use_edns
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ClientHandle for T where T: DnsHandle {}
|
||||
|
||||
/// A trait for implementing high level functions of DNS.
|
||||
pub trait ClientHandle: 'static + Clone + DnsHandle + Send {
|
||||
/// A *classic* DNS query
|
||||
///
|
||||
/// *Note* As of now, this will not recurse on PTR or CNAME record responses, that is up to
|
||||
/// the caller.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - the label to lookup
|
||||
/// * `query_class` - most likely this should always be DNSClass::IN
|
||||
/// * `query_type` - record type to lookup
|
||||
fn query(
|
||||
&mut self,
|
||||
name: Name,
|
||||
query_class: DNSClass,
|
||||
query_type: RecordType,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response> {
|
||||
let mut query = Query::query(name, query_type);
|
||||
query.set_query_class(query_class);
|
||||
let mut options = DnsRequestOptions::default();
|
||||
options.use_edns = self.is_using_edns();
|
||||
ClientResponse(self.lookup(query, options))
|
||||
}
|
||||
|
||||
/// Sends a NOTIFY message to the remote system
|
||||
///
|
||||
/// [RFC 1996](https://tools.ietf.org/html/rfc1996), DNS NOTIFY, August 1996
|
||||
///
|
||||
///
|
||||
/// ```text
|
||||
/// 1. Rationale and Scope
|
||||
///
|
||||
/// 1.1. Slow propagation of new and changed data in a DNS zone can be
|
||||
/// due to a zone's relatively long refresh times. Longer refresh times
|
||||
/// are beneficial in that they reduce load on the Primary Zone Servers, but
|
||||
/// that benefit comes at the cost of long intervals of incoherence among
|
||||
/// authority servers whenever the zone is updated.
|
||||
///
|
||||
/// 1.2. The DNS NOTIFY transaction allows Primary Zone Servers to inform Secondary
|
||||
/// Zone Servers when the zone has changed -- an interrupt as opposed to poll
|
||||
/// model -- which it is hoped will reduce propagation delay while not
|
||||
/// unduly increasing the masters' load. This specification only allows
|
||||
/// slaves to be notified of SOA RR changes, but the architecture of
|
||||
/// NOTIFY is intended to be extensible to other RR types.
|
||||
///
|
||||
/// 1.3. This document intentionally gives more definition to the roles
|
||||
/// of "Primary", "Secondary" and "Stealth" servers, their enumeration in NS
|
||||
/// RRs, and the SOA MNAME field. In that sense, this document can be
|
||||
/// considered an addendum to [RFC1035].
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// The below section describes how the Notify message should be constructed. The function
|
||||
/// implementation accepts a Record, but the actual data of the record should be ignored by the
|
||||
/// server, i.e. the server should make a request subsequent to receiving this Notification for
|
||||
/// the authority record, but could be used to decide to request an update or not:
|
||||
///
|
||||
/// ```text
|
||||
/// 3.7. A NOTIFY request has QDCOUNT>0, ANCOUNT>=0, AUCOUNT>=0,
|
||||
/// ADCOUNT>=0. If ANCOUNT>0, then the answer section represents an
|
||||
/// unsecure hint at the new RRset for this <QNAME,QCLASS,QTYPE>. A
|
||||
/// Secondary receiving such a hint is free to treat equivalence of this
|
||||
/// answer section with its local data as a "no further work needs to be
|
||||
/// done" indication. If ANCOUNT=0, or ANCOUNT>0 and the answer section
|
||||
/// differs from the Secondary's local data, then the Secondary should query its
|
||||
/// known Primaries to retrieve the new data.
|
||||
/// ```
|
||||
///
|
||||
/// Client's should be ready to handle, or be aware of, a server response of NOTIMP:
|
||||
///
|
||||
/// ```text
|
||||
/// 3.12. If a NOTIFY request is received by a Secondary who does not
|
||||
/// implement the NOTIFY opcode, it will respond with a NOTIMP
|
||||
/// (unimplemented feature error) message. A Primary Zone Server who receives
|
||||
/// such a NOTIMP should consider the NOTIFY transaction complete for
|
||||
/// that Secondary.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - the label which is being notified
|
||||
/// * `query_class` - most likely this should always be DNSClass::IN
|
||||
/// * `query_type` - record type which has been updated
|
||||
/// * `rrset` - the new version of the record(s) being notified
|
||||
fn notify<R>(
|
||||
&mut self,
|
||||
name: Name,
|
||||
query_class: DNSClass,
|
||||
query_type: RecordType,
|
||||
rrset: Option<R>,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response>
|
||||
where
|
||||
R: Into<RecordSet>,
|
||||
{
|
||||
debug!("notifying: {} {:?}", name, query_type);
|
||||
|
||||
// build the message
|
||||
let mut message = Message::query();
|
||||
// 3.3. NOTIFY is similar to QUERY in that it has a request message with
|
||||
// the header QR flag "clear" and a response message with QR "set". The
|
||||
// response message contains no useful information, but its reception by
|
||||
// the Primary is an indication that the Secondary has received the NOTIFY
|
||||
// and that the Primary Zone Server can remove the Secondary from any retry queue for
|
||||
// this NOTIFY event.
|
||||
message.metadata.op_code = OpCode::Notify;
|
||||
|
||||
// Extended dns
|
||||
if self.is_using_edns() {
|
||||
message
|
||||
.edns
|
||||
.get_or_insert_with(Edns::new)
|
||||
.set_max_payload(DEFAULT_MAX_PAYLOAD_LEN)
|
||||
.set_version(0);
|
||||
}
|
||||
|
||||
// add the query
|
||||
let mut query: Query = Query::new();
|
||||
query
|
||||
.set_name(name)
|
||||
.set_query_class(query_class)
|
||||
.set_query_type(query_type);
|
||||
message.add_query(query);
|
||||
|
||||
// add the notify message, see https://tools.ietf.org/html/rfc1996, section 3.7
|
||||
if let Some(rrset) = rrset {
|
||||
message.add_answers(rrset.into());
|
||||
}
|
||||
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Sends a record to create on the server, this will fail if the record exists (atomicity
|
||||
/// depends on the server)
|
||||
///
|
||||
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
|
||||
///
|
||||
/// ```text
|
||||
/// 2.4.3 - RRset Does Not Exist
|
||||
///
|
||||
/// No RRs with a specified NAME and TYPE (in the zone and class denoted
|
||||
/// by the Zone Section) can exist.
|
||||
///
|
||||
/// For this prerequisite, a requestor adds to the section a single RR
|
||||
/// whose NAME and TYPE are equal to that of the RRset whose nonexistence
|
||||
/// is required. The RDLENGTH of this record is zero (0), and RDATA
|
||||
/// field is therefore empty. CLASS must be specified as NONE in order
|
||||
/// to distinguish this condition from a valid RR whose RDLENGTH is
|
||||
/// naturally zero (0) (for example, the NULL RR). TTL must be specified
|
||||
/// as zero (0).
|
||||
///
|
||||
/// 2.5.1 - Add To An RRset
|
||||
///
|
||||
/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH
|
||||
/// and RDATA are those being added, and CLASS is the same as the zone
|
||||
/// class. Any duplicate RRs will be silently ignored by the Primary Zone
|
||||
/// Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `rrset` - the record(s) to create
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection)
|
||||
fn create<R>(
|
||||
&mut self,
|
||||
rrset: R,
|
||||
zone_origin: Name,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response>
|
||||
where
|
||||
R: Into<RecordSet>,
|
||||
{
|
||||
let rrset = rrset.into();
|
||||
let message = update_message::create(rrset, zone_origin, self.is_using_edns());
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Appends a record to an existing rrset, optionally require the rrset to exist (atomicity
|
||||
/// depends on the server)
|
||||
///
|
||||
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
|
||||
///
|
||||
/// ```text
|
||||
/// 2.4.1 - RRset Exists (Value Independent)
|
||||
///
|
||||
/// At least one RR with a specified NAME and TYPE (in the zone and class
|
||||
/// specified in the Zone Section) must exist.
|
||||
///
|
||||
/// For this prerequisite, a requestor adds to the section a single RR
|
||||
/// whose NAME and TYPE are equal to that of the zone RRset whose
|
||||
/// existence is required. RDLENGTH is zero and RDATA is therefore
|
||||
/// empty. CLASS must be specified as ANY to differentiate this
|
||||
/// condition from that of an actual RR whose RDLENGTH is naturally zero
|
||||
/// (0) (e.g., NULL). TTL is specified as zero (0).
|
||||
///
|
||||
/// 2.5.1 - Add To An RRset
|
||||
///
|
||||
/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH
|
||||
/// and RDATA are those being added, and CLASS is the same as the zone
|
||||
/// class. Any duplicate RRs will be silently ignored by the Primary Zone
|
||||
/// Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `rrset` - the record(s) to append to an RRSet
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
/// * `must_exist` - if true, the request will fail if the record does not exist
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection). If
|
||||
/// the rrset does not exist and must_exist is false, then the RRSet will be created.
|
||||
fn append<R>(
|
||||
&mut self,
|
||||
rrset: R,
|
||||
zone_origin: Name,
|
||||
must_exist: bool,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response>
|
||||
where
|
||||
R: Into<RecordSet>,
|
||||
{
|
||||
let rrset = rrset.into();
|
||||
let message = update_message::append(rrset, zone_origin, must_exist, self.is_using_edns());
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Compares and if it matches, swaps it for the new value (atomicity depends on the server)
|
||||
///
|
||||
/// ```text
|
||||
/// 2.4.2 - RRset Exists (Value Dependent)
|
||||
///
|
||||
/// A set of RRs with a specified NAME and TYPE exists and has the same
|
||||
/// members with the same RDATAs as the RRset specified here in this
|
||||
/// section. While RRset ordering is undefined and therefore not
|
||||
/// significant to this comparison, the sets be identical in their
|
||||
/// extent.
|
||||
///
|
||||
/// For this prerequisite, a requestor adds to the section an entire
|
||||
/// RRset whose preexistence is required. NAME and TYPE are that of the
|
||||
/// RRset being denoted. CLASS is that of the zone. TTL must be
|
||||
/// specified as zero (0) and is ignored when comparing RRsets for
|
||||
/// identity.
|
||||
///
|
||||
/// 2.5.4 - Delete An RR From An RRset
|
||||
///
|
||||
/// RRs to be deleted are added to the Update Section. The NAME, TYPE,
|
||||
/// RDLENGTH and RDATA must match the RR being deleted. TTL must be
|
||||
/// specified as zero (0) and will otherwise be ignored by the Primary
|
||||
/// Zone Server. CLASS must be specified as NONE to distinguish this from an
|
||||
/// RR addition. If no such RRs exist, then this Update RR will be
|
||||
/// silently ignored by the Primary Zone Server.
|
||||
///
|
||||
/// 2.5.1 - Add To An RRset
|
||||
///
|
||||
/// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH
|
||||
/// and RDATA are those being added, and CLASS is the same as the zone
|
||||
/// class. Any duplicate RRs will be silently ignored by the Primary
|
||||
/// Zone Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current` - the current rrset which must exist for the swap to complete
|
||||
/// * `new` - the new rrset with which to replace the current rrset
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection).
|
||||
fn compare_and_swap<C, N>(
|
||||
&mut self,
|
||||
current: C,
|
||||
new: N,
|
||||
zone_origin: Name,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response>
|
||||
where
|
||||
C: Into<RecordSet>,
|
||||
N: Into<RecordSet>,
|
||||
{
|
||||
let current = current.into();
|
||||
let new = new.into();
|
||||
|
||||
let message =
|
||||
update_message::compare_and_swap(current, new, zone_origin, self.is_using_edns());
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Deletes a record (by rdata) from an rrset, optionally require the rrset to exist.
|
||||
///
|
||||
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
|
||||
///
|
||||
/// ```text
|
||||
/// 2.4.1 - RRset Exists (Value Independent)
|
||||
///
|
||||
/// At least one RR with a specified NAME and TYPE (in the zone and class
|
||||
/// specified in the Zone Section) must exist.
|
||||
///
|
||||
/// For this prerequisite, a requestor adds to the section a single RR
|
||||
/// whose NAME and TYPE are equal to that of the zone RRset whose
|
||||
/// existence is required. RDLENGTH is zero and RDATA is therefore
|
||||
/// empty. CLASS must be specified as ANY to differentiate this
|
||||
/// condition from that of an actual RR whose RDLENGTH is naturally zero
|
||||
/// (0) (e.g., NULL). TTL is specified as zero (0).
|
||||
///
|
||||
/// 2.5.4 - Delete An RR From An RRset
|
||||
///
|
||||
/// RRs to be deleted are added to the Update Section. The NAME, TYPE,
|
||||
/// RDLENGTH and RDATA must match the RR being deleted. TTL must be
|
||||
/// specified as zero (0) and will otherwise be ignored by the Primary
|
||||
/// Zone Server. CLASS must be specified as NONE to distinguish this from an
|
||||
/// RR addition. If no such RRs exist, then this Update RR will be
|
||||
/// silently ignored by the Primary Zone Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `rrset` - the record(s) to delete from a RRSet, the name, type and rdata must match the
|
||||
/// record to delete
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
/// * `signer` - the signer, with private key, to use to sign the request
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection). If
|
||||
/// the rrset does not exist and must_exist is false, then the RRSet will be deleted.
|
||||
fn delete_by_rdata<R>(
|
||||
&mut self,
|
||||
rrset: R,
|
||||
zone_origin: Name,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response>
|
||||
where
|
||||
R: Into<RecordSet>,
|
||||
{
|
||||
let rrset = rrset.into();
|
||||
let message = update_message::delete_by_rdata(rrset, zone_origin, self.is_using_edns());
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Deletes an entire rrset, optionally require the rrset to exist.
|
||||
///
|
||||
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
|
||||
///
|
||||
/// ```text
|
||||
/// 2.4.1 - RRset Exists (Value Independent)
|
||||
///
|
||||
/// At least one RR with a specified NAME and TYPE (in the zone and class
|
||||
/// specified in the Zone Section) must exist.
|
||||
///
|
||||
/// For this prerequisite, a requestor adds to the section a single RR
|
||||
/// whose NAME and TYPE are equal to that of the zone RRset whose
|
||||
/// existence is required. RDLENGTH is zero and RDATA is therefore
|
||||
/// empty. CLASS must be specified as ANY to differentiate this
|
||||
/// condition from that of an actual RR whose RDLENGTH is naturally zero
|
||||
/// (0) (e.g., NULL). TTL is specified as zero (0).
|
||||
///
|
||||
/// 2.5.2 - Delete An RRset
|
||||
///
|
||||
/// One RR is added to the Update Section whose NAME and TYPE are those
|
||||
/// of the RRset to be deleted. TTL must be specified as zero (0) and is
|
||||
/// otherwise not used by the Primary Zone Server. CLASS must be specified as
|
||||
/// ANY. RDLENGTH must be zero (0) and RDATA must therefore be empty.
|
||||
/// If no such RRset exists, then this Update RR will be silently ignored
|
||||
/// by the Primary Zone Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `record` - The name, class and record_type will be used to match and delete the RecordSet
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection). If
|
||||
/// the rrset does not exist and must_exist is false, then the RRSet will be deleted.
|
||||
fn delete_rrset(
|
||||
&mut self,
|
||||
record: Record,
|
||||
zone_origin: Name,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response> {
|
||||
assert!(zone_origin.zone_of(&record.name));
|
||||
let message = update_message::delete_rrset(record, zone_origin, self.is_using_edns());
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Deletes all records at the specified name
|
||||
///
|
||||
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
|
||||
///
|
||||
/// ```text
|
||||
/// 2.5.3 - Delete All RRsets From A Name
|
||||
///
|
||||
/// One RR is added to the Update Section whose NAME is that of the name
|
||||
/// to be cleansed of RRsets. TYPE must be specified as ANY. TTL must
|
||||
/// be specified as zero (0) and is otherwise not used by the Primary
|
||||
/// Zone Server. CLASS must be specified as ANY. RDLENGTH must be zero (0)
|
||||
/// and RDATA must therefore be empty. If no such RRsets exist, then
|
||||
/// this Update RR will be silently ignored by the Primary Zone Server.
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_of_records` - the name of all the record sets to delete
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
/// * `dns_class` - the class of the SOA
|
||||
///
|
||||
/// The update must go to a zone authority (i.e. the server used in the ClientConnection). This
|
||||
/// operation attempts to delete all resource record sets the specified name regardless of
|
||||
/// the record type.
|
||||
fn delete_all(
|
||||
&mut self,
|
||||
name_of_records: Name,
|
||||
zone_origin: Name,
|
||||
dns_class: DNSClass,
|
||||
) -> ClientResponse<<Self as DnsHandle>::Response> {
|
||||
assert!(zone_origin.zone_of(&name_of_records));
|
||||
let message = update_message::delete_all(
|
||||
name_of_records,
|
||||
zone_origin,
|
||||
dns_class,
|
||||
self.is_using_edns(),
|
||||
);
|
||||
ClientResponse(self.send(DnsRequest::from(message)))
|
||||
}
|
||||
|
||||
/// Download all records from a zone, or all records modified since given SOA was observed.
|
||||
/// The request will either be a AXFR Query (ask for full zone transfer) if a SOA was not
|
||||
/// provided, or a IXFR Query (incremental zone transfer) if a SOA was provided.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `zone_origin` - the zone name to update, i.e. SOA name
|
||||
/// * `last_soa` - the last SOA known, if any. If provided, name must match `zone_origin`
|
||||
fn zone_transfer(
|
||||
&mut self,
|
||||
zone_origin: Name,
|
||||
last_soa: Option<SOA>,
|
||||
) -> ClientStreamXfr<<Self as DnsHandle>::Response> {
|
||||
let ixfr = last_soa.is_some();
|
||||
let message = update_message::zone_transfer(zone_origin, last_soa);
|
||||
ClientStreamXfr::new(self.send(DnsRequest::from(message)), ixfr)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream result of a Client Request
|
||||
#[must_use = "stream do nothing unless polled"]
|
||||
pub struct ClientStreamingResponse<R>(pub(crate) R)
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, ProtoError>> + Send + Unpin + 'static;
|
||||
|
||||
impl<R> Stream for ClientStreamingResponse<R>
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, ProtoError>> + Send + Unpin + 'static,
|
||||
{
|
||||
type Item = Result<DnsResponse, ProtoError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(ready!(self.0.poll_next_unpin(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A future result of a Client Request
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct ClientResponse<R>(pub(crate) R)
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static;
|
||||
|
||||
impl<R> Future for ClientResponse<R>
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static,
|
||||
{
|
||||
type Output = Result<DnsResponse, NetError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Poll::Ready(match ready!(self.0.poll_next_unpin(cx)) {
|
||||
Some(r) => r,
|
||||
None => Err(NetError::Timeout),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream result of a zone transfer Client Request
|
||||
/// Accept messages until the end of a zone transfer. For AXFR, it search for a starting and an
|
||||
/// ending SOA. For IXFR, it do so taking into account there will be other SOA inbetween
|
||||
#[must_use = "stream do nothing unless polled"]
|
||||
pub struct ClientStreamXfr<R>
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static,
|
||||
{
|
||||
state: ClientStreamXfrState<R>,
|
||||
}
|
||||
|
||||
impl<R> ClientStreamXfr<R>
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static,
|
||||
{
|
||||
fn new(inner: R, maybe_incr: bool) -> Self {
|
||||
Self {
|
||||
state: ClientStreamXfrState::Start { inner, maybe_incr },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State machine for ClientStreamXfr, implementing almost all logic
|
||||
#[derive(Debug)]
|
||||
enum ClientStreamXfrState<R> {
|
||||
Start {
|
||||
inner: R,
|
||||
maybe_incr: bool,
|
||||
},
|
||||
Second {
|
||||
inner: R,
|
||||
expected_serial: u32,
|
||||
maybe_incr: bool,
|
||||
},
|
||||
Axfr {
|
||||
inner: R,
|
||||
expected_serial: u32,
|
||||
},
|
||||
Ixfr {
|
||||
inner: R,
|
||||
even: bool,
|
||||
expected_serial: u32,
|
||||
},
|
||||
Ended,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl<R> ClientStreamXfrState<R> {
|
||||
/// Helper to get the stream from the enum
|
||||
fn inner(&mut self) -> &mut R {
|
||||
use ClientStreamXfrState::*;
|
||||
match self {
|
||||
Start { inner, .. } => inner,
|
||||
Second { inner, .. } => inner,
|
||||
Axfr { inner, .. } => inner,
|
||||
Ixfr { inner, .. } => inner,
|
||||
Ended | Invalid => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to ingest answer Records
|
||||
// TODO: this is complex enough it should get its own tests
|
||||
fn process(&mut self, answers: &[Record]) -> Result<(), NetError> {
|
||||
use ClientStreamXfrState::*;
|
||||
fn get_serial(r: &Record) -> Option<u32> {
|
||||
match &r.data {
|
||||
RData::SOA(soa) => Some(soa.serial),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
if answers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
match core::mem::replace(self, Invalid) {
|
||||
Start { inner, maybe_incr } => {
|
||||
if let Some(expected_serial) = get_serial(&answers[0]) {
|
||||
*self = Second {
|
||||
inner,
|
||||
maybe_incr,
|
||||
expected_serial,
|
||||
};
|
||||
self.process(&answers[1..])
|
||||
} else {
|
||||
*self = Ended;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Second {
|
||||
inner,
|
||||
maybe_incr,
|
||||
expected_serial,
|
||||
} => {
|
||||
if let Some(serial) = get_serial(&answers[0]) {
|
||||
// maybe IXFR, or empty AXFR
|
||||
if serial == expected_serial {
|
||||
// empty AXFR
|
||||
*self = Ended;
|
||||
if answers.len() == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
// invalid answer : trailing records
|
||||
Err("invalid zone transfer, contains trailing records".into())
|
||||
}
|
||||
} else if maybe_incr {
|
||||
*self = Ixfr {
|
||||
inner,
|
||||
expected_serial,
|
||||
even: true,
|
||||
};
|
||||
self.process(&answers[1..])
|
||||
} else {
|
||||
*self = Ended;
|
||||
Err("invalid zone transfer, expected AXFR, got IXFR".into())
|
||||
}
|
||||
} else {
|
||||
// standard AXFR
|
||||
*self = Axfr {
|
||||
inner,
|
||||
expected_serial,
|
||||
};
|
||||
self.process(&answers[1..])
|
||||
}
|
||||
}
|
||||
Axfr {
|
||||
inner,
|
||||
expected_serial,
|
||||
} => {
|
||||
let soa_count = answers
|
||||
.iter()
|
||||
.filter(|a| a.record_type() == RecordType::SOA)
|
||||
.count();
|
||||
match soa_count {
|
||||
0 => {
|
||||
*self = Axfr {
|
||||
inner,
|
||||
expected_serial,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
1 => {
|
||||
*self = Ended;
|
||||
match answers.last().map(|r| r.record_type()) {
|
||||
Some(RecordType::SOA) => Ok(()),
|
||||
_ => Err("invalid zone transfer, contains trailing records".into()),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
*self = Ended;
|
||||
Err("invalid zone transfer, contains trailing records".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ixfr {
|
||||
inner,
|
||||
even,
|
||||
expected_serial,
|
||||
} => {
|
||||
let even = answers
|
||||
.iter()
|
||||
.fold(even, |even, a| even ^ (a.record_type() == RecordType::SOA));
|
||||
if even {
|
||||
if let Some(serial) = get_serial(answers.last().unwrap()) {
|
||||
if serial == expected_serial {
|
||||
*self = Ended;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
*self = Ixfr {
|
||||
inner,
|
||||
even,
|
||||
expected_serial,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
Ended | Invalid => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Stream for ClientStreamXfr<R>
|
||||
where
|
||||
R: Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static,
|
||||
{
|
||||
type Item = Result<DnsResponse, NetError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
use ClientStreamXfrState::*;
|
||||
|
||||
if matches!(self.state, Ended) {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
let message = ready!(self.state.inner().poll_next_unpin(cx)).map(|response| {
|
||||
let ok = response?;
|
||||
self.state.process(&ok.answers)?;
|
||||
Ok(ok)
|
||||
});
|
||||
Poll::Ready(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::lock::Mutex;
|
||||
use futures_util::stream::{Fuse, Stream, StreamExt};
|
||||
use futures_util::{FutureExt, ready};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[must_use = "stream do nothing unless polled"]
|
||||
pub(crate) struct RcStream<S: Stream>
|
||||
where
|
||||
S: Stream + Send + Unpin,
|
||||
S::Item: Clone + Send,
|
||||
{
|
||||
stream_and_result: Arc<Mutex<(Fuse<S>, Vec<S::Item>)>>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn rc_stream<S>(stream: S) -> RcStream<S>
|
||||
where
|
||||
S: Stream + Unpin + Send,
|
||||
S::Item: Clone + Send,
|
||||
{
|
||||
let stream_and_result = Arc::new(Mutex::new((stream.fuse(), vec![])));
|
||||
|
||||
RcStream {
|
||||
stream_and_result,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Stream for RcStream<S>
|
||||
where
|
||||
S: Stream + Send + Unpin,
|
||||
S::Item: Clone + Send,
|
||||
{
|
||||
type Item = S::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
// try and get a mutable reference to execute the future
|
||||
// at least one caller should be able to get a mut reference... others will
|
||||
// wait for it to complete.
|
||||
let mut stream_and_result = ready!(self.stream_and_result.lock().poll_unpin(cx));
|
||||
let (stream, stored_result) = &mut *stream_and_result;
|
||||
if stored_result.len() > self.pos {
|
||||
let result = stored_result[self.pos].clone();
|
||||
drop(stream_and_result);
|
||||
self.pos += 1;
|
||||
return Poll::Ready(Some(result));
|
||||
}
|
||||
|
||||
// if pending it's either done, or it's actually pending
|
||||
match stream.poll_next_unpin(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(result) => {
|
||||
if let Some(result) = &result {
|
||||
stored_result.push(result.clone());
|
||||
}
|
||||
Poll::Ready(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Clone for RcStream<S>
|
||||
where
|
||||
S: Stream + Send + Unpin,
|
||||
S::Item: Clone + Send + Unpin,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
stream_and_result: Arc::clone(&self.stream_and_result),
|
||||
pos: 0, // index is not kept to allow to read first messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures_executor::block_on;
|
||||
use futures_util::future;
|
||||
use futures_util::stream::once;
|
||||
|
||||
use super::*;
|
||||
use crate::NetError;
|
||||
use crate::xfer::FirstAnswer;
|
||||
|
||||
#[test]
|
||||
fn test_rc_stream() {
|
||||
let future = future::ok::<usize, NetError>(1_usize);
|
||||
|
||||
let rc = rc_stream(once(future));
|
||||
|
||||
let i = block_on(rc.clone().first_answer()).unwrap();
|
||||
assert_eq!(i, 1);
|
||||
|
||||
let i = block_on(rc.first_answer()).unwrap();
|
||||
assert_eq!(i, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rc_stream_failed() {
|
||||
let future = future::err::<usize, NetError>(NetError::Busy);
|
||||
|
||||
let rc = rc_stream(once(future));
|
||||
|
||||
let i = block_on(rc.clone().first_answer()).unwrap_err();
|
||||
assert!(matches!(i, NetError::Busy));
|
||||
|
||||
let i = block_on(rc.first_answer()).unwrap_err();
|
||||
assert!(matches!(i, NetError::Busy));
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use futures_util::stream::iter;
|
||||
use test_support::subscribe;
|
||||
|
||||
use super::ClientStreamXfrState::*;
|
||||
use super::*;
|
||||
use crate::{
|
||||
proto::rr::{
|
||||
RData,
|
||||
rdata::{A, SOA},
|
||||
},
|
||||
runtime::TokioRuntimeProvider,
|
||||
};
|
||||
|
||||
// Keep this in sync with the example in the README.
|
||||
#[tokio::test]
|
||||
async fn readme_example() {
|
||||
subscribe();
|
||||
|
||||
use core::net::SocketAddr;
|
||||
|
||||
use crate::client::{Client, ClientHandle};
|
||||
use crate::proto::rr::{DNSClass, Name, RecordType};
|
||||
use crate::runtime::TokioRuntimeProvider;
|
||||
use crate::udp::UdpClientStream;
|
||||
|
||||
let address = SocketAddr::from(([8, 8, 8, 8], 53));
|
||||
let conn = UdpClientStream::builder(address, TokioRuntimeProvider::default()).build();
|
||||
let (mut client, bg) = Client::<TokioRuntimeProvider>::from_sender(conn);
|
||||
tokio::spawn(bg);
|
||||
|
||||
// Specify the name, note the final '.' which specifies it's an FQDN
|
||||
let name = Name::from_str("www.example.com.").unwrap();
|
||||
|
||||
// NOTE: see 'Setup a connection' example above
|
||||
// Send the query and get a message response, see RecordType for all supported options
|
||||
let response = client
|
||||
.query(name, DNSClass::IN, RecordType::A)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Messages are the packets sent between client and server in DNS, DnsResponse's can be
|
||||
// dereferenced to a Message. There are many fields to a Message, It's beyond the scope
|
||||
// of these examples to explain them. See hickory_dns::op::message::Message for more details.
|
||||
// generally we will be interested in the Message::answers
|
||||
let answers = &response.answers;
|
||||
|
||||
// Records are generic objects which can contain any data.
|
||||
// In order to access it we need to first check what type of record it is
|
||||
// In this case we are interested in A, IPv4 address
|
||||
let a_data = answers
|
||||
.iter()
|
||||
.flat_map(|record| match record.data {
|
||||
RData::A(addr) => Some(addr),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!a_data.is_empty());
|
||||
}
|
||||
|
||||
fn soa_record(serial: u32) -> Record {
|
||||
let soa = RData::SOA(SOA::new(
|
||||
Name::from_ascii("example.com.").unwrap(),
|
||||
Name::from_ascii("admin.example.com.").unwrap(),
|
||||
serial,
|
||||
60,
|
||||
60,
|
||||
60,
|
||||
60,
|
||||
));
|
||||
Record::from_rdata(Name::from_ascii("example.com.").unwrap(), 600, soa)
|
||||
}
|
||||
|
||||
fn a_record(ip: u8) -> Record {
|
||||
let a = RData::A(A::new(0, 0, 0, ip));
|
||||
Record::from_rdata(Name::from_ascii("www.example.com.").unwrap(), 600, a)
|
||||
}
|
||||
|
||||
fn get_stream_testcase(
|
||||
records: Vec<Vec<Record>>,
|
||||
) -> impl Stream<Item = Result<DnsResponse, NetError>> + Send + Unpin + 'static {
|
||||
let stream = records.into_iter().map(|r| {
|
||||
Ok({
|
||||
let mut m = Message::query();
|
||||
m.insert_answers(r);
|
||||
DnsResponse::from_message(m.into_response()).unwrap()
|
||||
})
|
||||
});
|
||||
iter(stream)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_valid_axfr() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![vec![
|
||||
soa_record(3),
|
||||
a_record(1),
|
||||
a_record(2),
|
||||
soa_record(3),
|
||||
]]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 4);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_valid_axfr_multipart() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![
|
||||
vec![soa_record(3)],
|
||||
vec![a_record(1)],
|
||||
vec![soa_record(3)],
|
||||
vec![a_record(2)], // will be ignored as connection is dropped before reading this message
|
||||
]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Second { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Axfr { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_empty_axfr() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![vec![soa_record(3)], vec![soa_record(3)]]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Second { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_axfr_with_ixfr_reply() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![vec![
|
||||
soa_record(3),
|
||||
soa_record(2),
|
||||
a_record(1),
|
||||
soa_record(3),
|
||||
a_record(2),
|
||||
soa_record(3),
|
||||
]]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
stream.next().await.unwrap().unwrap_err();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_axfr_with_non_xfr_reply() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![
|
||||
vec![a_record(1)], // assume this is an error response, not a zone transfer
|
||||
vec![a_record(2)],
|
||||
]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_invalid_axfr_multipart() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![
|
||||
vec![soa_record(3)],
|
||||
vec![a_record(1)],
|
||||
vec![soa_record(3), a_record(2)],
|
||||
vec![soa_record(3)],
|
||||
]);
|
||||
let mut stream = ClientStreamXfr::new(stream, false);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Second { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Axfr { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
stream.next().await.unwrap().unwrap_err();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_valid_ixfr() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![vec![
|
||||
soa_record(3),
|
||||
soa_record(2),
|
||||
a_record(1),
|
||||
soa_record(3),
|
||||
a_record(2),
|
||||
soa_record(3),
|
||||
]]);
|
||||
let mut stream = ClientStreamXfr::new(stream, true);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 6);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_xfr_valid_ixfr_multipart() {
|
||||
subscribe();
|
||||
let stream = get_stream_testcase(vec![
|
||||
vec![soa_record(3)],
|
||||
vec![soa_record(2)],
|
||||
vec![a_record(1)],
|
||||
vec![soa_record(3)],
|
||||
vec![a_record(2)],
|
||||
vec![soa_record(3)],
|
||||
vec![a_record(3)], //
|
||||
]);
|
||||
let mut stream = ClientStreamXfr::new(stream, true);
|
||||
assert!(matches!(stream.state, Start { .. }));
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Second { .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ixfr { even: true, .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ixfr { even: true, .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ixfr { even: false, .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ixfr { even: false, .. }));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
let response = stream.next().await.unwrap().unwrap();
|
||||
assert!(matches!(stream.state, Ended));
|
||||
assert_eq!(response.answers.len(), 1);
|
||||
|
||||
assert!(stream.next().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_client() {
|
||||
subscribe();
|
||||
use crate::client::{Client, ClientHandle};
|
||||
use crate::{
|
||||
proto::rr::{DNSClass, Name, RData, RecordType},
|
||||
tcp::TcpClientStream,
|
||||
};
|
||||
use core::str::FromStr;
|
||||
|
||||
// Since we used UDP in the previous examples, let's change things up a bit and use TCP here
|
||||
let addr = SocketAddr::from(([8, 8, 8, 8], 53));
|
||||
let (future, sender) = TcpClientStream::new(addr, None, None, TokioRuntimeProvider::new());
|
||||
|
||||
// Create a new client, the bg is a background future which handles
|
||||
// the multiplexing of the DNS requests to the server.
|
||||
// the client is a handle to an unbounded queue for sending requests via the
|
||||
// background. The background must be scheduled to run before the client can
|
||||
// send any dns requests
|
||||
let (mut client, bg) = Client::<TokioRuntimeProvider>::new(future.await.unwrap(), sender);
|
||||
|
||||
// make sure to run the background task
|
||||
tokio::spawn(bg);
|
||||
|
||||
// Create a query future
|
||||
let query = client.query(
|
||||
Name::from_str("dns.google.").unwrap(),
|
||||
DNSClass::IN,
|
||||
RecordType::A,
|
||||
);
|
||||
|
||||
// wait for its response
|
||||
let (message_returned, buffer) = query.await.unwrap().into_parts();
|
||||
|
||||
// extract A records in a stable order we can assert on
|
||||
let assert_a_records_match = |answers: &[Record], expected: &[A]| {
|
||||
let mut a_records = answers
|
||||
.iter()
|
||||
.filter_map(|record| match &record.data {
|
||||
RData::A(addr) => Some(*addr),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
a_records.sort_by_key(|a| u32::from(**a));
|
||||
assert_eq!(a_records, expected);
|
||||
};
|
||||
|
||||
let expected_answers = vec![A::new(8, 8, 4, 4), A::new(8, 8, 8, 8)];
|
||||
assert_a_records_match(&message_returned.answers, &expected_answers);
|
||||
|
||||
let message_parsed = Message::from_vec(&buffer)
|
||||
.expect("buffer was parsed already by Client so we should be able to do it again");
|
||||
|
||||
assert_a_records_match(&message_parsed.answers, &expected_answers);
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
use core::fmt;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
error::NetError,
|
||||
proto::{
|
||||
ProtoError,
|
||||
dnssec::{Algorithm, Proof},
|
||||
op::Query,
|
||||
rr::{Name, RecordType},
|
||||
},
|
||||
};
|
||||
|
||||
/// The error kind for dnssec errors that get returned in the crate
|
||||
#[allow(unreachable_pub)]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ProofErrorKind {
|
||||
/// An error with an arbitrary message, stored as String
|
||||
#[error("{0}")]
|
||||
Msg(String),
|
||||
|
||||
/// Algorithm mismatch between rrsig and dnskey
|
||||
#[error("algorithm mismatch rrsig: {rrsig} dnskey: {dnskey}")]
|
||||
AlgorithmMismatch {
|
||||
/// Algorithm specified in the RRSIG
|
||||
rrsig: Algorithm,
|
||||
/// Algorithm supported in the DNSKEY
|
||||
dnskey: Algorithm,
|
||||
},
|
||||
|
||||
/// A DnsKey verification of rrset and rrsig failed
|
||||
#[error("dnskey and rrset failed to verify: {name} key_tag: {key_tag}")]
|
||||
DnsKeyVerifyRrsig {
|
||||
/// The name/label of the DNSKEY
|
||||
name: Name,
|
||||
/// The key tag derived from the DNSKEY
|
||||
key_tag: u16,
|
||||
/// The Error that occurred during validation
|
||||
error: ProtoError,
|
||||
},
|
||||
|
||||
/// There was no DNSKEY found for verifying the DNSSEC of the zone
|
||||
#[error("no dnskey was found: {name}")]
|
||||
DnskeyNotFound {
|
||||
/// The name of the missing DNSKEY
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// A DnsKey was revoked and could not be used for validation
|
||||
#[error("dnskey revoked: {name}, key_tag: {key_tag}")]
|
||||
DnsKeyRevoked {
|
||||
/// The name of the DNSKEY that was revoked
|
||||
name: Name,
|
||||
/// The key tag derived from the DNSKEY
|
||||
key_tag: u16,
|
||||
},
|
||||
|
||||
/// The DNSKEY is not covered by a DS record
|
||||
#[error("dnskey has no ds: {name}")]
|
||||
DnsKeyHasNoDs {
|
||||
/// The name of the DNSKEY
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// DS record exists but not a DNSKEY that matches
|
||||
#[error("ds record exists, but no dnskey: {name}")]
|
||||
DsRecordsButNoDnskey {
|
||||
/// Name of the missing DNSKEY
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// DS record parent exists, but child does not
|
||||
#[error("ds record should exist: {name}")]
|
||||
DsRecordShouldExist {
|
||||
/// Name fo the missing DS key
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// DS record does not exist, and this was proven with an NSEC or an
|
||||
/// insecure parent zone
|
||||
#[error("ds record does not exist: {name}")]
|
||||
DsResponseInsecure {
|
||||
/// The name of the DS record
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// An error ocurred while calculating the DNSKEY key tag
|
||||
#[error("internal error computing the key tag for: {name}")]
|
||||
ErrorComputingKeyTag {
|
||||
/// The name of the DNSKEY record
|
||||
name: Name,
|
||||
},
|
||||
|
||||
/// The DNSKEY used was not verified as secure
|
||||
#[error("dnskey insecure: {name}, key_tag: {key_tag}")]
|
||||
InsecureDnsKey {
|
||||
/// The name of the DNSKEY
|
||||
name: Name,
|
||||
/// The key tag derived from the DNSKEY
|
||||
key_tag: u16,
|
||||
},
|
||||
|
||||
/// The DnsKey is not marked as a zone key
|
||||
#[error("not a zone signing key: {name} key_tag: {key_tag}")]
|
||||
NotZoneDnsKey {
|
||||
/// Name of the DNSKEY
|
||||
name: Name,
|
||||
/// The key tag derived from the DNSKEY
|
||||
key_tag: u16,
|
||||
},
|
||||
|
||||
/// There was a protocol error when looking up DNSSEC records
|
||||
#[error("communication failure for query: {query}: {net}")]
|
||||
Net {
|
||||
/// Query that failed
|
||||
query: Query,
|
||||
/// Reasons fo the failure
|
||||
net: NetError,
|
||||
},
|
||||
|
||||
/// The RRSIGs for the rrset are not present.
|
||||
/// It's indeterminate if DS records can't be found
|
||||
/// It's bogus if the DS records are present
|
||||
#[error("rrsigs are not present for: {name} record_type: {record_type}")]
|
||||
RrsigsNotPresent {
|
||||
/// Name that RRSIGS are missing for
|
||||
name: Name,
|
||||
/// The record type in question
|
||||
record_type: RecordType,
|
||||
},
|
||||
|
||||
/// The RRSIGs could not be verified or failed validation
|
||||
#[error("rrsigs were not able to be verified: {name}, type: {record_type}")]
|
||||
RrsigsUnverified {
|
||||
/// Name that RRSIGS failed for
|
||||
name: Name,
|
||||
/// The record type in question
|
||||
record_type: RecordType,
|
||||
},
|
||||
|
||||
/// Unknown or reserved key algorithm
|
||||
#[error("unknown or reserved key algorithm")]
|
||||
UnknownKeyAlgorithm,
|
||||
|
||||
/// Unsupported key algorithms
|
||||
#[error("unsupported key algorithms")]
|
||||
UnsupportedKeyAlgorithm,
|
||||
}
|
||||
|
||||
/// The error type for dnssec errors that get returned in the crate
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub struct ProofError {
|
||||
/// The proof derived from the failed state
|
||||
pub proof: Proof,
|
||||
/// The kind of error
|
||||
pub kind: Box<ProofErrorKind>,
|
||||
}
|
||||
|
||||
impl ProofError {
|
||||
/// Create an error with the given Proof and Associated Error
|
||||
pub fn new(proof: Proof, kind: ProofErrorKind) -> Self {
|
||||
Self {
|
||||
proof,
|
||||
kind: Box::new(kind),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the kind of the error
|
||||
pub fn kind(&self) -> &ProofErrorKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Returns an error related to the absence of a DS record
|
||||
pub fn ds_should_exist(name: Name) -> Self {
|
||||
Self {
|
||||
proof: Proof::Bogus,
|
||||
kind: Box::new(ProofErrorKind::DsRecordShouldExist { name }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProofError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}: {}", self.proof, self.kind)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
//! Tests for [`DnssecDnsHandle`] against a mock upstream returning canned responses.
|
||||
|
||||
use core::mem;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::Ipv4Addr,
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex, PoisonError},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use data_encoding::BASE32_DNSSEC;
|
||||
use futures_util::{
|
||||
future,
|
||||
stream::{self, Stream},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::DnssecDnsHandle;
|
||||
use crate::{
|
||||
error::{DnsError, NetError},
|
||||
proto::{
|
||||
dnssec::{
|
||||
DigestType, DnssecSigner, Nsec3HashAlgorithm, Proof, PublicKeyBuf, SigningKey,
|
||||
TrustAnchors,
|
||||
crypto::Ed25519SigningKey,
|
||||
rdata::{DNSKEY, DNSSECRData, DS, NSEC, NSEC3, RRSIG},
|
||||
},
|
||||
op::{DnsRequest, DnsRequestOptions, DnsResponse, Message, Query, ResponseCode},
|
||||
rr::{
|
||||
DNSClass, LowerName, Name, RData, Record, RecordSet, RecordType,
|
||||
rdata::{
|
||||
A, CNAME, NS, SOA, TLSA,
|
||||
tlsa::{CertUsage, Matching, Selector},
|
||||
},
|
||||
},
|
||||
},
|
||||
runtime::TokioRuntimeProvider,
|
||||
xfer::{DnsHandle, FirstAnswer},
|
||||
};
|
||||
use test_support::subscribe;
|
||||
|
||||
/// A server answering a DS query for an insecure zone with the child zone's own SOA record
|
||||
/// (instead of the parent's) must not cause a validation loop, and the zone must be recognized
|
||||
/// as insecure if its parent zone is insecure.
|
||||
///
|
||||
/// Regression test for <https://github.com/hickory-dns/hickory-dns/issues/3974>.
|
||||
#[tokio::test]
|
||||
async fn insecure_delegation_with_child_soa_in_ds_response() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&child)]);
|
||||
let (handle, queries) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Insecure);
|
||||
assert_eq!(
|
||||
queries.count(&child, RecordType::DS),
|
||||
1,
|
||||
"the DS query for the child zone should only be sent once"
|
||||
);
|
||||
}
|
||||
|
||||
/// A server answering a DS query for an insecure zone with an empty authority section must
|
||||
/// still allow the zone to be recognized as insecure if its parent zone is insecure.
|
||||
///
|
||||
/// Regression test for <https://github.com/hickory-dns/hickory-dns/issues/3974>.
|
||||
#[tokio::test]
|
||||
async fn insecure_delegation_with_empty_ds_response() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
fixture.respond(&child, RecordType::DS, [], []);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Insecure);
|
||||
}
|
||||
|
||||
/// The expected case: a DS query for an insecure zone is answered with the parent zone's SOA
|
||||
/// record.
|
||||
#[tokio::test]
|
||||
async fn insecure_delegation_with_parent_soa_in_ds_response() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
let parent = Name::from_ascii("example.com.").unwrap();
|
||||
fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&parent)]);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Insecure);
|
||||
}
|
||||
|
||||
/// If the parent zone is secure, an unauthenticated DS response for the child zone must not be
|
||||
/// accepted as proof of an insecure delegation, regardless of the SOA record it carries.
|
||||
#[tokio::test]
|
||||
async fn bogus_delegation_with_child_soa_in_ds_response() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&child)]);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Bogus);
|
||||
}
|
||||
|
||||
/// If the parent zone is secure, an empty DS response for the child zone must not be accepted
|
||||
/// as proof of an insecure delegation.
|
||||
#[tokio::test]
|
||||
async fn bogus_delegation_with_empty_ds_response() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
fixture.respond(&child, RecordType::DS, [], []);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Bogus);
|
||||
}
|
||||
|
||||
/// A validating handle sets the CD bit on every query it sends upstream: the query being
|
||||
/// validated and the DNSKEY and DS queries it makes through `lookup()`.
|
||||
///
|
||||
/// RFC 6840 section 5.9: validating resolvers SHOULD set the CD bit on every upstream query.
|
||||
/// Regression test for <https://github.com/hickory-dns/hickory-dns/issues/3966>.
|
||||
#[tokio::test]
|
||||
async fn validating_queries_set_checking_disabled() -> Result<(), NetError> {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let child = Name::from_ascii("child.example.com.")?;
|
||||
let parent = Name::from_ascii("example.com.")?;
|
||||
fixture.respond(&child, RecordType::DS, [], [unsigned_soa(&parent)]);
|
||||
let (handle, queries) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await?;
|
||||
assert_eq!(
|
||||
response.answers.first().map(|record| record.proof),
|
||||
Some(Proof::Insecure)
|
||||
);
|
||||
|
||||
let logged = mem::take(&mut *queries.0.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
for record_type in [RecordType::A, RecordType::DNSKEY, RecordType::DS] {
|
||||
assert!(
|
||||
logged
|
||||
.iter()
|
||||
.any(|entry| entry.query.query_type == record_type),
|
||||
"expected at least one {record_type} query upstream"
|
||||
);
|
||||
}
|
||||
let cleared = logged
|
||||
.iter()
|
||||
.filter(|entry| !entry.checking_disabled)
|
||||
.map(|entry| &entry.query)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
cleared.is_empty(),
|
||||
"queries sent upstream with CD=0: {cleared:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A zone without NS records at its apex must not be mistaken for part of its signed parent zone.
|
||||
///
|
||||
/// Regression test for <https://github.com/hickory-dns/hickory-dns/issues/4027>.
|
||||
#[tokio::test]
|
||||
async fn insecure_delegation_without_apex_ns() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let com = Name::from_ascii("com.").unwrap();
|
||||
let example = Name::from_ascii("example.com.").unwrap();
|
||||
let www = Name::from_ascii("www.example.com.").unwrap();
|
||||
fixture.respond(&com, RecordType::NS, [ns(&com)], []);
|
||||
fixture.respond(&example, RecordType::NS, [], [unsigned_soa(&example)]);
|
||||
fixture.respond(&www, RecordType::NS, [], [unsigned_soa(&example)]);
|
||||
fixture.respond(&www, RecordType::DS, [], [unsigned_soa(&example)]);
|
||||
fixture.respond(&www, RecordType::A, [a(&www)], []);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = handle
|
||||
.lookup(
|
||||
Query::query(www, RecordType::A),
|
||||
DnsRequestOptions::default(),
|
||||
)
|
||||
.first_answer()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Insecure);
|
||||
}
|
||||
|
||||
// A CNAME'd TLSA answer (RFC 7672 3.1.2) with the target's NSEC record in the authority section.
|
||||
#[tokio::test]
|
||||
async fn positive_answer_with_unrelated_nsec() {
|
||||
subscribe();
|
||||
|
||||
let fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let target = Name::from_ascii("tlsa201._dane.example.com.").unwrap();
|
||||
let nsec = NSEC::new(
|
||||
Name::from_ascii("tlsa202._dane.example.com.").unwrap(),
|
||||
[RecordType::TLSA, RecordType::RRSIG, RecordType::NSEC],
|
||||
);
|
||||
let authorities = fixture.sign_example([Record::from_rdata(
|
||||
target,
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::NSEC(nsec)),
|
||||
)]);
|
||||
let (handle, _) = fixture.with_cname_tlsa(authorities).build();
|
||||
|
||||
assert_secure(lookup_tlsa(&handle).await);
|
||||
}
|
||||
|
||||
// Same as above, with an unrelated NSEC3 record in the authority section.
|
||||
#[tokio::test]
|
||||
async fn positive_answer_with_unrelated_nsec3() {
|
||||
subscribe();
|
||||
|
||||
let fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let owner_hash = nsec3_hash(&Name::from_ascii("tlsa201._dane.example.com.").unwrap());
|
||||
let mut next_hash = owner_hash.clone();
|
||||
for byte in next_hash.iter_mut().rev() {
|
||||
*byte = byte.wrapping_add(1);
|
||||
if *byte != 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let nsec3 = NSEC3::new(
|
||||
Nsec3HashAlgorithm::SHA1,
|
||||
false,
|
||||
0,
|
||||
Vec::new(),
|
||||
next_hash,
|
||||
[RecordType::TLSA, RecordType::RRSIG],
|
||||
);
|
||||
let owner = Name::from_ascii(BASE32_DNSSEC.encode(&owner_hash))
|
||||
.unwrap()
|
||||
.append_domain(fixture.example.signer_name())
|
||||
.unwrap();
|
||||
let authorities = fixture.sign_example([Record::from_rdata(
|
||||
owner,
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::NSEC3(nsec3)),
|
||||
)]);
|
||||
let (handle, _) = fixture.with_cname_tlsa(authorities).build();
|
||||
|
||||
assert_secure(lookup_tlsa(&handle).await);
|
||||
}
|
||||
|
||||
// A wildcard expansion whose authority section only has an unrelated NSEC record.
|
||||
#[tokio::test]
|
||||
async fn wildcard_answer_with_unrelated_nsec() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let authorities = fixture.unrelated_nsec();
|
||||
let answers = fixture.wildcard_a(&wildcard_host());
|
||||
fixture.respond(&wildcard_host(), RecordType::A, answers, authorities);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await);
|
||||
}
|
||||
|
||||
// A wildcard expansion without any denial records.
|
||||
#[tokio::test]
|
||||
async fn wildcard_answer_without_nsec() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let answers = fixture.wildcard_a(&wildcard_host());
|
||||
fixture.respond(&wildcard_host(), RecordType::A, answers, []);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await);
|
||||
}
|
||||
|
||||
// A wildcard expansion with an NSEC record covering the next closer name.
|
||||
#[tokio::test]
|
||||
async fn wildcard_answer_with_next_closer_proof() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let nsec = NSEC::new(
|
||||
Name::from_ascii("z.wild.example.com.").unwrap(),
|
||||
[RecordType::A, RecordType::RRSIG, RecordType::NSEC],
|
||||
);
|
||||
let authorities = fixture.sign_example([Record::from_rdata(
|
||||
Name::from_ascii("a.wild.example.com.").unwrap(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::NSEC(nsec)),
|
||||
)]);
|
||||
let answers = fixture.wildcard_a(&wildcard_host());
|
||||
fixture.respond(&wildcard_host(), RecordType::A, answers, authorities);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
assert_secure(lookup(&handle, wildcard_host(), RecordType::A).await);
|
||||
}
|
||||
|
||||
// A wildcard-expanded CNAME, not the queried RRset itself, still needs a next closer proof.
|
||||
#[tokio::test]
|
||||
async fn wildcard_cname_with_unrelated_nsec() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let target = Name::from_ascii("www.example.com.").unwrap();
|
||||
let cname = Record::from_rdata(
|
||||
Name::from_ascii("*.wild.example.com.").unwrap(),
|
||||
TTL,
|
||||
RData::CNAME(CNAME(target.clone())),
|
||||
);
|
||||
let mut answers = fixture.expand_wildcard(cname, &wildcard_host());
|
||||
answers.extend(fixture.sign_example([a(&target)]));
|
||||
let authorities = fixture.unrelated_nsec();
|
||||
fixture.respond(&wildcard_host(), RecordType::A, answers, authorities);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
assert_bogus(lookup(&handle, wildcard_host(), RecordType::A).await);
|
||||
}
|
||||
|
||||
fn assert_secure(result: Result<DnsResponse, NetError>) {
|
||||
let response = result.expect("response should validate");
|
||||
assert!(!response.answers.is_empty());
|
||||
for record in &response.answers {
|
||||
assert_eq!(record.proof, Proof::Secure, "{record}");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_bogus(result: Result<DnsResponse, NetError>) {
|
||||
match result {
|
||||
Err(NetError::Dns(DnsError::Nsec { proof, .. })) => assert_eq!(proof, Proof::Bogus),
|
||||
other => panic!("expected a bogus NSEC proof, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn wildcard_host() -> Name {
|
||||
Name::from_ascii("host.wild.example.com.").unwrap()
|
||||
}
|
||||
|
||||
fn nsec3_hash(name: &Name) -> Vec<u8> {
|
||||
Nsec3HashAlgorithm::SHA1
|
||||
.hash(&[], name, 0)
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
async fn lookup_tlsa(handle: &DnssecDnsHandle<MockHandle>) -> Result<DnsResponse, NetError> {
|
||||
lookup(
|
||||
handle,
|
||||
Name::from_ascii("_25._tcp.mx.example.com.").unwrap(),
|
||||
RecordType::TLSA,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn lookup(
|
||||
handle: &DnssecDnsHandle<MockHandle>,
|
||||
name: Name,
|
||||
record_type: RecordType,
|
||||
) -> Result<DnsResponse, NetError> {
|
||||
handle
|
||||
.lookup(
|
||||
Query::query(name, record_type),
|
||||
DnsRequestOptions::default(),
|
||||
)
|
||||
.first_answer()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Failed DS queries below an insecure delegation must not prevent the zone from being
|
||||
/// recognized as insecure.
|
||||
#[tokio::test]
|
||||
async fn insecure_delegation_with_failed_ds_queries() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Insecure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
let www = Name::from_ascii("www.child.example.com.").unwrap();
|
||||
fixture.fail(&www, RecordType::DS);
|
||||
fixture.fail(&child, RecordType::DS);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Insecure);
|
||||
}
|
||||
|
||||
/// Failed DS queries below a secure delegation must not be treated as an insecure delegation.
|
||||
#[tokio::test]
|
||||
async fn bogus_delegation_with_failed_ds_queries() {
|
||||
subscribe();
|
||||
|
||||
let mut fixture = MockHandleBuilder::new(Parent::Secure);
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
let www = Name::from_ascii("www.child.example.com.").unwrap();
|
||||
fixture.fail(&www, RecordType::DS);
|
||||
fixture.fail(&child, RecordType::DS);
|
||||
let (handle, _) = fixture.build();
|
||||
|
||||
let response = lookup_child_a(&handle).await.unwrap();
|
||||
assert_eq!(response.answers[0].proof, Proof::Bogus);
|
||||
}
|
||||
|
||||
async fn lookup_child_a(handle: &DnssecDnsHandle<MockHandle>) -> Result<DnsResponse, NetError> {
|
||||
let name = Name::from_ascii("www.child.example.com.").unwrap();
|
||||
handle
|
||||
.lookup(
|
||||
Query::query(name, RecordType::A),
|
||||
DnsRequestOptions::default(),
|
||||
)
|
||||
.first_answer()
|
||||
.await
|
||||
}
|
||||
|
||||
/// A signed root zone and a signed `com.` zone, delegating to `example.com.`, which in turn
|
||||
/// delegates to `child.example.com.`. Both `example.com.` and `child.example.com.` are unsigned
|
||||
/// zones, and `www.child.example.com.` has an A record.
|
||||
///
|
||||
/// If the `parent` configuration value is [`Parent::Secure`], `com.` zone will include a DS
|
||||
/// record for the `example.com.` zone, establishing a secure delegation, even though the
|
||||
/// `example.com.` zone's records are unsigned.
|
||||
struct MockHandleBuilder {
|
||||
root: DnssecSigner,
|
||||
example: DnssecSigner,
|
||||
responses: HashMap<(LowerName, RecordType), Message>,
|
||||
}
|
||||
|
||||
impl MockHandleBuilder {
|
||||
fn new(parent: Parent) -> Self {
|
||||
let key =
|
||||
Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap();
|
||||
let public_key = key.to_public_key().unwrap();
|
||||
let root = DnssecSigner::new(
|
||||
DNSKEY::from_key(&public_key),
|
||||
Box::new(key),
|
||||
Name::root(),
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
let key =
|
||||
Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap();
|
||||
let example_public_key = key.to_public_key().unwrap();
|
||||
let example = DnssecSigner::new(
|
||||
DNSKEY::from_key(&example_public_key),
|
||||
Box::new(key),
|
||||
Name::from_ascii("example.com.").unwrap(),
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
let mut new = Self {
|
||||
root,
|
||||
example,
|
||||
responses: HashMap::default(),
|
||||
};
|
||||
|
||||
let key =
|
||||
Ed25519SigningKey::from_pkcs8(&Ed25519SigningKey::generate_pkcs8().unwrap()).unwrap();
|
||||
let com_public_key = key.to_public_key().unwrap();
|
||||
let com = DnssecSigner::new(
|
||||
DNSKEY::from_key(&com_public_key),
|
||||
Box::new(key),
|
||||
Name::from_ascii("com.").unwrap(),
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
let example = new.example.signer_name().clone();
|
||||
let child = Name::from_ascii("child.example.com.").unwrap();
|
||||
let www = Name::from_ascii("www.child.example.com.").unwrap();
|
||||
|
||||
new.respond(
|
||||
&new.root.signer_name().clone(),
|
||||
RecordType::DNSKEY,
|
||||
sign([dnskey(&new.root)], &new.root),
|
||||
[],
|
||||
);
|
||||
new.respond(
|
||||
com.signer_name(),
|
||||
RecordType::DS,
|
||||
sign([ds(&com, &com_public_key)], &new.root),
|
||||
[],
|
||||
);
|
||||
new.respond(
|
||||
com.signer_name(),
|
||||
RecordType::DNSKEY,
|
||||
sign([dnskey(&com)], &com),
|
||||
[],
|
||||
);
|
||||
new.respond(&example, RecordType::NS, [ns(&example)], []);
|
||||
|
||||
match parent {
|
||||
Parent::Insecure => {
|
||||
let mut authorities = sign([unsigned_soa(com.signer_name())], &com);
|
||||
authorities.extend(sign(
|
||||
[Record::from_rdata(
|
||||
example.clone(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::NSEC(NSEC::new(
|
||||
Name::from_ascii("example0.com.").unwrap(),
|
||||
[RecordType::NS, RecordType::RRSIG, RecordType::NSEC],
|
||||
))),
|
||||
)],
|
||||
&com,
|
||||
));
|
||||
new.respond(&example, RecordType::DS, [], authorities);
|
||||
}
|
||||
Parent::Secure => {
|
||||
let records = sign([ds(&new.example, &example_public_key)], &com);
|
||||
new.respond(&example, RecordType::DS, records, []);
|
||||
let records = sign([dnskey(&new.example)], &new.example);
|
||||
new.respond(&example, RecordType::DNSKEY, records, []);
|
||||
}
|
||||
}
|
||||
|
||||
new.respond(&child, RecordType::NS, [ns(&child)], []);
|
||||
new.respond(&www, RecordType::NS, [], []);
|
||||
new.respond(&www, RecordType::DS, [], [unsigned_soa(&child)]);
|
||||
new.respond(&www, RecordType::A, [a(&www)], []);
|
||||
new
|
||||
}
|
||||
|
||||
fn respond(
|
||||
&mut self,
|
||||
name: &Name,
|
||||
record_type: RecordType,
|
||||
answers: impl IntoIterator<Item = Record>,
|
||||
authorities: impl IntoIterator<Item = Record>,
|
||||
) {
|
||||
let mut message = Message::query();
|
||||
message.metadata.response_code = ResponseCode::NoError;
|
||||
message.add_answers(answers);
|
||||
message.add_authorities(authorities);
|
||||
self.responses
|
||||
.insert((LowerName::from(name), record_type), message);
|
||||
}
|
||||
|
||||
fn sign_example(&self, records: impl IntoIterator<Item = Record>) -> Vec<Record> {
|
||||
sign(records, &self.example)
|
||||
}
|
||||
|
||||
/// Sign a record owned by a wildcard name, and rename it as if it were expanded to `name`.
|
||||
fn expand_wildcard(&self, record: Record, name: &Name) -> Vec<Record> {
|
||||
let mut records = self.sign_example([record]);
|
||||
for record in &mut records {
|
||||
record.name = name.clone();
|
||||
}
|
||||
records
|
||||
}
|
||||
|
||||
fn wildcard_a(&self, name: &Name) -> Vec<Record> {
|
||||
self.expand_wildcard(a(&Name::from_ascii("*.wild.example.com.").unwrap()), name)
|
||||
}
|
||||
|
||||
fn unrelated_nsec(&self) -> Vec<Record> {
|
||||
let nsec = NSEC::new(
|
||||
Name::from_ascii("tlsa202._dane.example.com.").unwrap(),
|
||||
[RecordType::TLSA, RecordType::RRSIG, RecordType::NSEC],
|
||||
);
|
||||
self.sign_example([Record::from_rdata(
|
||||
Name::from_ascii("tlsa201._dane.example.com.").unwrap(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::NSEC(nsec)),
|
||||
)])
|
||||
}
|
||||
|
||||
/// Answer `_25._tcp.mx.example.com. TLSA` with a CNAME to `tlsa201._dane.example.com.` and
|
||||
/// the TLSA record at the target.
|
||||
fn with_cname_tlsa(mut self, authorities: Vec<Record>) -> Self {
|
||||
let alias = Name::from_ascii("_25._tcp.mx.example.com.").unwrap();
|
||||
let target = Name::from_ascii("tlsa201._dane.example.com.").unwrap();
|
||||
let mut answers = self.sign_example([Record::from_rdata(
|
||||
alias.clone(),
|
||||
TTL,
|
||||
RData::CNAME(CNAME(target.clone())),
|
||||
)]);
|
||||
answers.extend(self.sign_example([Record::from_rdata(
|
||||
target,
|
||||
TTL,
|
||||
RData::TLSA(TLSA::new(
|
||||
CertUsage::DaneEe,
|
||||
Selector::Spki,
|
||||
Matching::Sha256,
|
||||
vec![0; 32],
|
||||
)),
|
||||
)]));
|
||||
self.respond(&alias, RecordType::TLSA, answers, authorities);
|
||||
self
|
||||
}
|
||||
|
||||
fn fail(&mut self, name: &Name, record_type: RecordType) {
|
||||
let mut message = Message::query();
|
||||
message.metadata.response_code = ResponseCode::ServFail;
|
||||
self.responses
|
||||
.insert((LowerName::from(name), record_type), message);
|
||||
}
|
||||
|
||||
fn build(self) -> (DnssecDnsHandle<MockHandle>, QueryLog) {
|
||||
let Self {
|
||||
root, responses, ..
|
||||
} = self;
|
||||
let queries = QueryLog::default();
|
||||
let handle = MockHandle {
|
||||
responses: Arc::new(responses),
|
||||
queries: queries.clone(),
|
||||
};
|
||||
|
||||
let mut anchors = TrustAnchors::empty();
|
||||
anchors.insert(&root.key().to_public_key().unwrap());
|
||||
|
||||
(
|
||||
DnssecDnsHandle::with_trust_anchor(handle, Arc::new(anchors)),
|
||||
queries,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Parent {
|
||||
Insecure,
|
||||
Secure,
|
||||
}
|
||||
|
||||
/// Sign the given RRset with this zone's key, returning the records and their RRSIG.
|
||||
fn sign(records: impl IntoIterator<Item = Record>, signer: &DnssecSigner) -> Vec<Record> {
|
||||
let mut records = records.into_iter().collect::<Vec<_>>();
|
||||
let first = records.first().expect("RRset must not be empty");
|
||||
let mut rrset = RecordSet::with_ttl(first.name.clone(), first.record_type(), TTL);
|
||||
for record in &records {
|
||||
rrset.insert(record.clone(), 0);
|
||||
}
|
||||
|
||||
let inception = OffsetDateTime::now_utc() - Duration::from_secs(3600);
|
||||
let rrsig = RRSIG::from_rrset(&rrset, DNSClass::IN, inception, signer).unwrap();
|
||||
records.push(Record::from_rdata(
|
||||
first.name.clone(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::RRSIG(rrsig)),
|
||||
));
|
||||
records
|
||||
}
|
||||
|
||||
fn ds(signer: &DnssecSigner, pub_key: &PublicKeyBuf) -> Record {
|
||||
let ds = DS::from_key(pub_key, signer.signer_name(), DigestType::SHA256).unwrap();
|
||||
Record::from_rdata(
|
||||
signer.signer_name().clone(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::DS(ds)),
|
||||
)
|
||||
}
|
||||
|
||||
fn dnskey(signer: &DnssecSigner) -> Record {
|
||||
Record::from_rdata(
|
||||
signer.signer_name().clone(),
|
||||
TTL,
|
||||
RData::DNSSEC(DNSSECRData::DNSKEY(signer.to_dnskey())),
|
||||
)
|
||||
}
|
||||
|
||||
fn unsigned_soa(name: &Name) -> Record {
|
||||
Record::from_rdata(
|
||||
name.clone(),
|
||||
TTL,
|
||||
RData::SOA(SOA::new(
|
||||
Name::from_ascii("ns.")
|
||||
.unwrap()
|
||||
.append_domain(name)
|
||||
.unwrap(),
|
||||
Name::from_ascii("admin.")
|
||||
.unwrap()
|
||||
.append_domain(name)
|
||||
.unwrap(),
|
||||
1,
|
||||
3600,
|
||||
3600,
|
||||
3600,
|
||||
3600,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn ns(name: &Name) -> Record {
|
||||
Record::from_rdata(
|
||||
name.clone(),
|
||||
TTL,
|
||||
RData::NS(NS(Name::from_ascii("ns.")
|
||||
.unwrap()
|
||||
.append_domain(name)
|
||||
.unwrap())),
|
||||
)
|
||||
}
|
||||
|
||||
fn a(name: &Name) -> Record {
|
||||
Record::from_rdata(name.clone(), TTL, RData::A(A(Ipv4Addr::new(192, 0, 2, 1))))
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct QueryLog(Arc<Mutex<Vec<LoggedQuery>>>);
|
||||
|
||||
/// A query the mock upstream received, with the CD bit of the request that carried it.
|
||||
struct LoggedQuery {
|
||||
query: Query,
|
||||
checking_disabled: bool,
|
||||
}
|
||||
|
||||
impl QueryLog {
|
||||
fn count(&self, name: &Name, record_type: RecordType) -> usize {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|logged| &logged.query.name == name && logged.query.query_type == record_type)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockHandle {
|
||||
responses: Arc<HashMap<(LowerName, RecordType), Message>>,
|
||||
queries: QueryLog,
|
||||
}
|
||||
|
||||
impl DnsHandle for MockHandle {
|
||||
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
|
||||
type Runtime = TokioRuntimeProvider;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
let query = request.queries[0].clone();
|
||||
self.queries
|
||||
.0
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.push(LoggedQuery {
|
||||
query: query.clone(),
|
||||
checking_disabled: request.metadata.checking_disabled,
|
||||
});
|
||||
|
||||
let key = (LowerName::from(&query.name), query.query_type);
|
||||
let Some(message) = self.responses.get(&key) else {
|
||||
return Box::pin(stream::once(future::err(NetError::from(format!(
|
||||
"no canned response for {query}"
|
||||
)))));
|
||||
};
|
||||
|
||||
if message.response_code != ResponseCode::NoError {
|
||||
return Box::pin(stream::once(future::err(NetError::from(
|
||||
DnsError::ResponseCode(message.response_code),
|
||||
))));
|
||||
}
|
||||
|
||||
let mut message = message.clone();
|
||||
message.metadata.id = request.metadata.id;
|
||||
message.add_query(query);
|
||||
let response = DnsResponse::from_message(message.into_response()).unwrap();
|
||||
Box::pin(stream::once(future::ok(response)))
|
||||
}
|
||||
}
|
||||
|
||||
const TTL: u32 = 3600;
|
||||
+3605
File diff suppressed because it is too large.
Load diff
+1712
File diff suppressed because it is too large.
Load diff
+605
@@ -0,0 +1,605 @@
|
||||
// Copyright 2015-2020 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! Error types for the crate
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use core::num::ParseIntError;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
use http::header::ToStrError;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::proto::ProtoError;
|
||||
#[cfg(feature = "__dnssec")]
|
||||
use crate::proto::dnssec::Proof;
|
||||
use crate::proto::op::{DnsResponse, Query, ResponseCode};
|
||||
use crate::proto::rr::RData;
|
||||
use crate::proto::rr::{DNSClass, Name, Record, RecordRef, RecordType, rdata::SOA};
|
||||
use crate::proto::serialize::binary::DecodeError;
|
||||
|
||||
/// The error type for network protocol errors (UDP, TCP, QUIC, H2, H3)
|
||||
#[non_exhaustive]
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum NetError {
|
||||
/// The underlying resource is too busy
|
||||
///
|
||||
/// This is a signal that an internal resource is too busy. The intended action should be tried
|
||||
/// again, ideally after waiting for a little while for the situation to improve. Alternatively,
|
||||
/// the action could be tried on another resource (for example, in a name server pool).
|
||||
#[error("resource too busy")]
|
||||
Busy,
|
||||
|
||||
/// Unable to decode HTTP header value to string
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
#[error("header decode error: {0}")]
|
||||
Decode(Arc<ToStrError>),
|
||||
|
||||
/// Semantic DNS errors
|
||||
#[error("DNS error: {0}")]
|
||||
Dns(#[from] DnsError),
|
||||
|
||||
/// An HTTP/2 related error
|
||||
#[error("H2 error: {0}")]
|
||||
#[cfg(feature = "__https")]
|
||||
H2(Arc<h2::Error>),
|
||||
|
||||
/// An HTTP/3 related error
|
||||
#[error("H3 error: {0}")]
|
||||
#[cfg(feature = "__h3")]
|
||||
H3(Arc<h3::error::StreamError>),
|
||||
|
||||
/// An error with an arbitrary message, referenced as &'static str
|
||||
#[error("{0}")]
|
||||
Message(&'static str),
|
||||
|
||||
/// An error with an arbitrary message, stored as String
|
||||
#[error("{0}")]
|
||||
Msg(String),
|
||||
|
||||
/// Unable to parse header value as number
|
||||
#[error("unable to parse number: {0}")]
|
||||
ParseInt(#[from] ParseIntError),
|
||||
|
||||
/// No connections available
|
||||
#[error("no connections available")]
|
||||
NoConnections,
|
||||
|
||||
/// Protocol error from higher layers
|
||||
#[error("protocol error: {0}")]
|
||||
Proto(#[from] ProtoError),
|
||||
|
||||
// foreign
|
||||
/// An error got returned from IO
|
||||
#[error("io error: {0}")]
|
||||
Io(Arc<io::Error>),
|
||||
|
||||
/// A request was too large
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
#[error("request too large")]
|
||||
RequestTooLarge,
|
||||
|
||||
/// A request timed out
|
||||
#[error("request timed out")]
|
||||
Timeout,
|
||||
|
||||
/// A Quinn (Quic) connection error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("error creating quic connection: {0}")]
|
||||
QuinnConnect(#[from] quinn::ConnectError),
|
||||
|
||||
/// A Quinn (QUIC) connection error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("error with quic connection: {0}")]
|
||||
QuinnConnection(#[from] quinn::ConnectionError),
|
||||
|
||||
/// A Quinn (QUIC) write error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("error writing to quic connection: {0}")]
|
||||
QuinnWriteError(#[from] quinn::WriteError),
|
||||
|
||||
/// A Quinn (QUIC) read error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("error writing to quic read: {0}")]
|
||||
QuinnReadError(#[from] quinn::ReadExactError),
|
||||
|
||||
/// A Quinn (QUIC) stream error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("referenced a closed QUIC stream: {0}")]
|
||||
QuinnStreamError(#[from] quinn::ClosedStream),
|
||||
|
||||
/// A Quinn (QUIC) configuration error occurred
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("error constructing quic configuration: {0}")]
|
||||
QuinnConfigError(#[from] quinn::ConfigError),
|
||||
|
||||
/// QUIC TLS config must include an AES-128-GCM cipher suite
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("QUIC TLS config must include an AES-128-GCM cipher suite")]
|
||||
QuinnTlsConfigError(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
|
||||
|
||||
/// Unknown QUIC stream used
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("an unknown quic stream was used")]
|
||||
QuinnUnknownStreamError,
|
||||
|
||||
/// A quic message id should always be 0
|
||||
#[cfg(feature = "__quic")]
|
||||
#[error("quic messages should always be 0, got: {0}")]
|
||||
QuicMessageIdNot0(u16),
|
||||
|
||||
/// A Rustls error occurred
|
||||
#[cfg(feature = "__tls")]
|
||||
#[error("rustls construction error: {0}")]
|
||||
RustlsError(#[from] rustls::Error),
|
||||
|
||||
/// Case randomization is enabled, and a server did not echo a query name back with the same
|
||||
/// case.
|
||||
#[error("case of query name in response did not match")]
|
||||
QueryCaseMismatch,
|
||||
|
||||
/// An upstream response contained a record whose class did not match the IN query class.
|
||||
#[error("foreign class record in response: {record_name} {record_class} {record_type}")]
|
||||
ForeignClassRecord {
|
||||
/// The name of the offending record.
|
||||
record_name: Name,
|
||||
/// The class of the offending record.
|
||||
record_class: DNSClass,
|
||||
/// The type of the offending record.
|
||||
record_type: RecordType,
|
||||
},
|
||||
|
||||
/// Received a truncated response
|
||||
#[error("response was truncated; TCP already tried or no other transport available")]
|
||||
Truncated,
|
||||
}
|
||||
|
||||
impl NetError {
|
||||
/// Returns true if the domain does not exist
|
||||
#[inline]
|
||||
pub fn is_nx_domain(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Dns(DnsError::NoRecordsFound(NoRecords {
|
||||
response_code: ResponseCode::NXDomain,
|
||||
..
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if the error represents NoRecordsFound
|
||||
#[inline]
|
||||
pub fn is_no_records_found(&self) -> bool {
|
||||
matches!(self, Self::Dns(DnsError::NoRecordsFound { .. }))
|
||||
}
|
||||
|
||||
/// Returns true for a transport-level connection error: the connection was
|
||||
/// closed, reset, or refused, as opposed to a timeout or a server-side (DNS)
|
||||
/// error.
|
||||
pub fn is_connection_closed(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "__https")]
|
||||
Self::H2(err) => {
|
||||
err.is_io() || err.is_go_away() || err.reason() == Some(h2::Reason::REFUSED_STREAM)
|
||||
}
|
||||
#[cfg(feature = "__h3")]
|
||||
Self::H3(err) => err.is_h3_no_error(),
|
||||
Self::Io(err) => matches!(
|
||||
err.kind(),
|
||||
io::ErrorKind::ConnectionReset
|
||||
| io::ErrorKind::ConnectionAborted
|
||||
| io::ErrorKind::BrokenPipe
|
||||
| io::ErrorKind::NotConnected
|
||||
| io::ErrorKind::UnexpectedEof
|
||||
),
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnConnection(err) => matches!(
|
||||
err,
|
||||
quinn::ConnectionError::ConnectionClosed(_)
|
||||
| quinn::ConnectionError::ApplicationClosed(_)
|
||||
| quinn::ConnectionError::Reset
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the SOA record, if the error contains one
|
||||
#[inline]
|
||||
pub fn into_soa(self) -> Option<Box<Record<SOA>>> {
|
||||
match self {
|
||||
Self::Dns(DnsError::NoRecordsFound(NoRecords { soa, .. })) => soa,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a representative string of the error for use as a metrics label.
|
||||
pub fn as_metrics_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Busy => "busy",
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
Self::Decode(_) => "http_header_decode",
|
||||
Self::Dns(dns_err) => dns_err.as_metrics_label(),
|
||||
#[cfg(feature = "__https")]
|
||||
Self::H2(_) => "http2",
|
||||
#[cfg(feature = "__h3")]
|
||||
Self::H3(_) => "http3",
|
||||
Self::ParseInt(_) => "parse_header_value",
|
||||
Self::NoConnections => "no_connections",
|
||||
Self::Proto(_) => "proto",
|
||||
Self::Io(e) => {
|
||||
use std::io::ErrorKind::*;
|
||||
match e.kind() {
|
||||
NotFound => "io_not_found",
|
||||
PermissionDenied => "io_permission_denied",
|
||||
ConnectionRefused => "io_connection_refused",
|
||||
ConnectionReset => "io_connection_reset",
|
||||
HostUnreachable => "io_host_unreachable",
|
||||
NetworkUnreachable => "io_network_unreachable",
|
||||
ConnectionAborted => "io_connection_aborted",
|
||||
NotConnected => "io_not_connected",
|
||||
AddrInUse => "io_addr_in_use",
|
||||
AddrNotAvailable => "io_addr_not_available",
|
||||
NetworkDown => "io_network_down",
|
||||
BrokenPipe => "io_broken_pipe",
|
||||
AlreadyExists => "io_already_exists",
|
||||
WouldBlock => "io_would_block",
|
||||
InvalidInput => "io_invalid_input",
|
||||
InvalidData => "io_invalid_data",
|
||||
TimedOut => "io_timed_out",
|
||||
WriteZero => "io_write_zero",
|
||||
QuotaExceeded => "io_quota_exceeded",
|
||||
ResourceBusy => "io_resource_busy",
|
||||
Deadlock => "io_deadlock",
|
||||
Interrupted => "io_interrupted",
|
||||
Unsupported => "io_unsupported",
|
||||
OutOfMemory => "io_out_of_memory",
|
||||
Other => "io_other",
|
||||
|
||||
// It's highly unlikely that any of these would actually be returned.
|
||||
NotADirectory => "io_not_a_directory",
|
||||
IsADirectory => "io_is_a_directory",
|
||||
DirectoryNotEmpty => "io_directory_not_empty",
|
||||
ReadOnlyFilesystem => "io_read_only_filesystem",
|
||||
StaleNetworkFileHandle => "io_stale_network_file_handle",
|
||||
StorageFull => "io_storage_full",
|
||||
NotSeekable => "io_not_seekable",
|
||||
FileTooLarge => "io_file_too_large",
|
||||
ExecutableFileBusy => "io_executable_file_busy",
|
||||
CrossesDevices => "io_crosses_devices",
|
||||
TooManyLinks => "io_too_many_links",
|
||||
InvalidFilename => "io_invalid_filename",
|
||||
ArgumentListTooLong => "io_argument_list_too_long",
|
||||
UnexpectedEof => "io_unexpected_eof",
|
||||
|
||||
_ => "io_unknown",
|
||||
}
|
||||
}
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
Self::RequestTooLarge => "request_too_large",
|
||||
Self::Timeout => "timeout",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnConnect(_) => "quic_connect",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnConnection(_) => "quic_connection",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnWriteError(_) => "quic_write",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnReadError(_) => "quic_read",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnStreamError(_) => "quic_stream",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnConfigError(_) => "quic_config",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnTlsConfigError(_) => "quic_tls_config_error",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuinnUnknownStreamError => "quic_unknown_stream",
|
||||
#[cfg(feature = "__quic")]
|
||||
Self::QuicMessageIdNot0(_) => "quic_message_id_not_0",
|
||||
#[cfg(feature = "__tls")]
|
||||
Self::RustlsError(_) => "tls",
|
||||
Self::QueryCaseMismatch => "query_case_mismatch",
|
||||
Self::ForeignClassRecord { .. } => "foreign_class_record",
|
||||
Self::Truncated => "truncated",
|
||||
|
||||
// Don't report these because the format is arbitrary, and in the case of Msg, dynamic.
|
||||
Self::Message(_) | Self::Msg(_) => "message",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoRecords> for NetError {
|
||||
fn from(no_records: NoRecords) -> Self {
|
||||
Self::Dns(DnsError::NoRecordsFound(no_records))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for NetError {
|
||||
fn from(e: DecodeError) -> Self {
|
||||
Self::Proto(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "__h3")]
|
||||
impl From<h3::error::StreamError> for NetError {
|
||||
fn from(e: h3::error::StreamError) -> Self {
|
||||
Self::H3(Arc::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "__https")]
|
||||
impl From<h2::Error> for NetError {
|
||||
fn from(e: h2::Error) -> Self {
|
||||
Self::H2(Arc::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
impl From<ToStrError> for NetError {
|
||||
fn from(e: ToStrError) -> Self {
|
||||
Self::Decode(Arc::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for NetError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
match e.kind() {
|
||||
io::ErrorKind::TimedOut => Self::Timeout,
|
||||
_ => Self::Io(Arc::new(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for NetError {
|
||||
fn from(msg: String) -> Self {
|
||||
Self::Msg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for NetError {
|
||||
fn from(msg: &'static str) -> Self {
|
||||
Self::Message(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Semantic DNS errors
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum DnsError {
|
||||
/// Received an error response code from the server
|
||||
#[error("error response: {0}")]
|
||||
ResponseCode(ResponseCode),
|
||||
/// No records were found for a query
|
||||
#[error("no records found for {:?}", .0.query)]
|
||||
NoRecordsFound(NoRecords),
|
||||
/// No Records and there is a corresponding DNSSEC Proof for NSEC
|
||||
#[cfg(feature = "__dnssec")]
|
||||
#[non_exhaustive]
|
||||
#[error("DNSSEC Negative Record Response for {query}, {proof}")]
|
||||
Nsec {
|
||||
/// Query for which the NSEC was returned
|
||||
query: Box<Query>,
|
||||
/// Response for which the NSEC was returned
|
||||
response: Box<DnsResponse>,
|
||||
/// DNSSEC proof of the record
|
||||
proof: Proof,
|
||||
},
|
||||
/// DNSSEC validation failure
|
||||
#[error("DNSSEC validation failed")]
|
||||
DnssecBogus,
|
||||
}
|
||||
|
||||
impl DnsError {
|
||||
/// A conversion to determine if the response is an error
|
||||
pub fn from_response(response: DnsResponse) -> Result<DnsResponse, Self> {
|
||||
use ResponseCode::*;
|
||||
debug!("response: {}", *response);
|
||||
|
||||
match response.response_code {
|
||||
Refused => Err(Self::ResponseCode(Refused)),
|
||||
code @ ServFail
|
||||
| code @ FormErr
|
||||
| code @ NotImp
|
||||
| code @ YXDomain
|
||||
| code @ YXRRSet
|
||||
| code @ NXRRSet
|
||||
| code @ NotAuth
|
||||
| code @ NotZone
|
||||
| code @ BADVERS
|
||||
| code @ BADSIG
|
||||
| code @ BADKEY
|
||||
| code @ BADTIME
|
||||
| code @ BADMODE
|
||||
| code @ BADNAME
|
||||
| code @ BADALG
|
||||
| code @ BADTRUNC
|
||||
| code @ BADCOOKIE => Err(Self::ResponseCode(code)),
|
||||
// Some NXDOMAIN responses contain CNAME referrals, that will not be an error
|
||||
code @ NXDomain |
|
||||
// No answers are available, CNAME referrals are not failures
|
||||
code @ NoError
|
||||
if !response.contains_answer() && !response.truncation => {
|
||||
// TODO: if authoritative, this is cacheable, store a TTL (currently that requires time, need a "now" here)
|
||||
// let valid_until = if response.authoritative() { now + response.negative_ttl() };
|
||||
let soa = response.soa().as_ref().map(RecordRef::to_owned);
|
||||
|
||||
// Collect any referral nameservers and associated glue records
|
||||
let mut referral_name_servers = vec![];
|
||||
for ns in response.authorities.iter().filter(|ns| ns.record_type() == RecordType::NS) {
|
||||
let glue = response
|
||||
.additionals
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let RData::NS(ns_data) = &ns.data {
|
||||
if record.name == **ns_data && matches!(&record.data, RData::A(_) | RData::AAAA(_)) {
|
||||
return Some(Record::to_owned(record));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect::<Vec<Record>>();
|
||||
referral_name_servers.push(ForwardNSData { ns: Record::to_owned(ns), glue: glue.into() })
|
||||
}
|
||||
|
||||
let option_ns = if !referral_name_servers.is_empty() {
|
||||
Some(referral_name_servers.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let authorities = if !response.authorities.is_empty() {
|
||||
Some(response.authorities.to_owned().into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let negative_ttl = response.negative_ttl();
|
||||
let query = response.into_message().queries.drain(..).next().unwrap_or_default();
|
||||
|
||||
Err(Self::NoRecordsFound(NoRecords {
|
||||
query: Box::new(query),
|
||||
soa: soa.map(Box::new),
|
||||
ns: option_ns,
|
||||
negative_ttl,
|
||||
response_code: code,
|
||||
authorities,
|
||||
}))
|
||||
}
|
||||
NXDomain
|
||||
| NoError
|
||||
| Unknown(_) => Ok(response),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the DNS error as a representative string for use as a metrics label.
|
||||
pub fn as_metrics_label(&self) -> &'static str {
|
||||
use hickory_proto::op::ResponseCode::*;
|
||||
match self {
|
||||
Self::ResponseCode(NoError) => "dns_response_code_noerror",
|
||||
Self::ResponseCode(FormErr) => "dns_response_code_formerror",
|
||||
Self::ResponseCode(ServFail) => "dns_response_code_servfail",
|
||||
Self::ResponseCode(NXDomain) => "dns_response_code_nxdomain",
|
||||
Self::ResponseCode(NotImp) => "dns_response_code_notimp",
|
||||
Self::ResponseCode(Refused) => "dns_response_code_refused",
|
||||
Self::ResponseCode(YXDomain) => "dns_response_code_yxdomain",
|
||||
Self::ResponseCode(YXRRSet) => "dns_response_code_yxrrset",
|
||||
Self::ResponseCode(NXRRSet) => "dns_response_code_nxrrset",
|
||||
Self::ResponseCode(NotAuth) => "dns_response_code_notauth",
|
||||
Self::ResponseCode(NotZone) => "dns_response_code_notzone",
|
||||
Self::ResponseCode(BADVERS) => "dns_response_code_badvers",
|
||||
Self::ResponseCode(BADSIG) => "dns_response_code_badsig",
|
||||
Self::ResponseCode(BADKEY) => "dns_response_code_badkey",
|
||||
Self::ResponseCode(BADTIME) => "dns_response_code_badtime",
|
||||
Self::ResponseCode(BADMODE) => "dns_response_code_badmode",
|
||||
Self::ResponseCode(BADNAME) => "dns_response_code_badname",
|
||||
Self::ResponseCode(BADALG) => "dns_response_code_badalg",
|
||||
Self::ResponseCode(BADTRUNC) => "dns_response_code_badtrunc",
|
||||
Self::ResponseCode(BADCOOKIE) => "dns_response_code_badcookie",
|
||||
Self::ResponseCode(Unknown(_)) => "dns_response_code_unknown",
|
||||
Self::NoRecordsFound(_) => "dns_no_records",
|
||||
#[cfg(feature = "__dnssec")]
|
||||
Self::Nsec { .. } => "dns_nsec",
|
||||
Self::DnssecBogus => "dns_dnssec_bogus",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response where no records were found
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct NoRecords {
|
||||
/// The query for which no records were found.
|
||||
pub query: Box<Query>,
|
||||
/// If an SOA is present, then this is an authoritative response or a referral to another nameserver, see the negative_type field.
|
||||
pub soa: Option<Box<Record<SOA>>>,
|
||||
/// Nameservers may be present in addition to or in lieu of an SOA for a referral
|
||||
/// The tuple struct layout is vec[(Nameserver, [vec of glue records])]
|
||||
pub ns: Option<Arc<[ForwardNSData]>>,
|
||||
/// negative ttl, as determined from DnsResponse::negative_ttl
|
||||
/// this will only be present if the SOA was also present.
|
||||
pub negative_ttl: Option<u32>,
|
||||
/// ResponseCode, if `NXDOMAIN`, the domain does not exist (and no other types).
|
||||
/// If `NoError`, then the domain exists but there exist either other types at the same label, or subzones of that label.
|
||||
pub response_code: ResponseCode,
|
||||
/// Authority records from the query. These are important to preserve for DNSSEC validation.
|
||||
pub authorities: Option<Arc<[Record]>>,
|
||||
}
|
||||
|
||||
impl NoRecords {
|
||||
/// Construct a new [`NoRecords`] from a query and a response code
|
||||
pub fn new(query: impl Into<Box<Query>>, response_code: ResponseCode) -> Self {
|
||||
Self {
|
||||
query: query.into(),
|
||||
soa: None,
|
||||
ns: None,
|
||||
negative_ttl: None,
|
||||
response_code,
|
||||
authorities: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data needed to process a NS-record-based referral.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ForwardNSData {
|
||||
/// The referant NS record
|
||||
pub ns: Record,
|
||||
/// Any glue records associated with the referant NS record.
|
||||
pub glue: Arc<[Record]>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_connection_closed_io_kinds() {
|
||||
for kind in [
|
||||
io::ErrorKind::ConnectionReset,
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
io::ErrorKind::BrokenPipe,
|
||||
io::ErrorKind::NotConnected,
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
] {
|
||||
assert!(
|
||||
NetError::from(io::Error::from(kind)).is_connection_closed(),
|
||||
"{kind:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
!NetError::from(io::Error::from(io::ErrorKind::PermissionDenied))
|
||||
.is_connection_closed()
|
||||
);
|
||||
assert!(!NetError::Timeout.is_connection_closed());
|
||||
assert!(!NetError::Message("server error").is_connection_closed());
|
||||
}
|
||||
|
||||
#[cfg(feature = "__https")]
|
||||
#[test]
|
||||
fn is_connection_closed_h2() {
|
||||
// REFUSED_STREAM: the server never processed the request, so it's safe to retry.
|
||||
assert!(NetError::from(h2::Error::from(h2::Reason::REFUSED_STREAM)).is_connection_closed());
|
||||
// A protocol error mid-exchange is not a clean connection-closed signal.
|
||||
assert!(
|
||||
!NetError::from(h2::Error::from(h2::Reason::INTERNAL_ERROR)).is_connection_closed()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "__quic")]
|
||||
#[test]
|
||||
fn is_connection_closed_quic_excludes_timeout() {
|
||||
assert!(NetError::QuinnConnection(quinn::ConnectionError::Reset).is_connection_closed());
|
||||
assert!(
|
||||
!NetError::QuinnConnection(quinn::ConnectionError::TimedOut).is_connection_closed()
|
||||
);
|
||||
}
|
||||
}
|
||||
+776
@@ -0,0 +1,776 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! TLS protocol related components for DNS over HTTPS (DoH)
|
||||
|
||||
use core::fmt::Debug;
|
||||
use core::future::Future;
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::str::FromStr;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
use futures_util::stream::Stream;
|
||||
use h2::client::SendRequest;
|
||||
use http::header::{self, CONTENT_LENGTH};
|
||||
use http::{Method, Request};
|
||||
use rustls::ClientConfig;
|
||||
use rustls::pki_types::ServerName;
|
||||
use tokio::time::timeout;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::error::NetError;
|
||||
use crate::http::{RequestContext, SetHeaders, Version, fetch_body};
|
||||
use crate::proto::op::{DnsRequest, DnsResponse};
|
||||
use crate::runtime::iocompat::AsyncIoStdAsTokio;
|
||||
use crate::runtime::{DnsTcpStream, RuntimeProvider, Spawn};
|
||||
use crate::xfer::{CONNECT_TIMEOUT, DnsExchange, DnsRequestSender, DnsResponseStream};
|
||||
|
||||
/// A DNS client connection for DNS-over-HTTPS
|
||||
#[derive(Clone)]
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct HttpsClientStream {
|
||||
context: Arc<RequestContext>,
|
||||
h2: SendRequest<Bytes>,
|
||||
is_shutdown: bool,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl HttpsClientStream {
|
||||
/// Constructs a new HttpsClientStreamBuilder with the associated ClientConfig
|
||||
pub fn builder<P: RuntimeProvider>(
|
||||
client_config: Arc<ClientConfig>,
|
||||
provider: P,
|
||||
) -> HttpsClientStreamBuilder<P> {
|
||||
HttpsClientStreamBuilder {
|
||||
provider,
|
||||
client_config,
|
||||
bind_addr: None,
|
||||
set_headers: None,
|
||||
request_timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsRequestSender for HttpsClientStream {
|
||||
/// This indicates that the HTTP message was successfully sent, and we now have the response.RecvStream
|
||||
///
|
||||
/// If the request fails, this will return the error, and it should be assumed that the Stream portion of
|
||||
/// this will have no date.
|
||||
///
|
||||
/// ```text
|
||||
/// RFC 8484 DNS Queries over HTTPS (DoH) October 2018
|
||||
///
|
||||
///
|
||||
/// 4.2. The HTTP Response
|
||||
///
|
||||
/// The only response type defined in this document is "application/dns-
|
||||
/// message", but it is possible that other response formats will be
|
||||
/// defined in the future. A DoH server MUST be able to process
|
||||
/// "application/dns-message" request messages.
|
||||
///
|
||||
/// Different response media types will provide more or less information
|
||||
/// from a DNS response. For example, one response type might include
|
||||
/// information from the DNS header bytes while another might omit it.
|
||||
/// The amount and type of information that a media type gives are solely
|
||||
/// up to the format, which is not defined in this protocol.
|
||||
///
|
||||
/// Each DNS request-response pair is mapped to one HTTP exchange. The
|
||||
/// responses may be processed and transported in any order using HTTP's
|
||||
/// multi-streaming functionality (see Section 5 of [RFC7540]).
|
||||
///
|
||||
/// Section 5.1 discusses the relationship between DNS and HTTP response
|
||||
/// caching.
|
||||
///
|
||||
/// 4.2.1. Handling DNS and HTTP Errors
|
||||
///
|
||||
/// DNS response codes indicate either success or failure for the DNS
|
||||
/// query. A successful HTTP response with a 2xx status code (see
|
||||
/// Section 6.3 of [RFC7231]) is used for any valid DNS response,
|
||||
/// regardless of the DNS response code. For example, a successful 2xx
|
||||
/// HTTP status code is used even with a DNS message whose DNS response
|
||||
/// code indicates failure, such as SERVFAIL or NXDOMAIN.
|
||||
///
|
||||
/// HTTP responses with non-successful HTTP status codes do not contain
|
||||
/// replies to the original DNS question in the HTTP request. DoH
|
||||
/// clients need to use the same semantic processing of non-successful
|
||||
/// HTTP status codes as other HTTP clients. This might mean that the
|
||||
/// DoH client retries the query with the same DoH server, such as if
|
||||
/// there are authorization failures (HTTP status code 401; see
|
||||
/// Section 3.1 of [RFC7235]). It could also mean that the DoH client
|
||||
/// retries with a different DoH server, such as for unsupported media
|
||||
/// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or
|
||||
/// where the server cannot generate a representation suitable for the
|
||||
/// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so
|
||||
/// on.
|
||||
/// ```
|
||||
fn send_message(&mut self, mut request: DnsRequest) -> DnsResponseStream {
|
||||
if self.is_shutdown {
|
||||
panic!("can not send messages after stream is shutdown")
|
||||
}
|
||||
|
||||
// per the RFC, a zero id allows for the HTTP packet to be cached better
|
||||
request.metadata.id = 0;
|
||||
|
||||
let bytes = match request.to_vec() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => return NetError::from(err).into(),
|
||||
};
|
||||
|
||||
let send_fut = send(self.h2.clone(), Bytes::from(bytes), self.context.clone());
|
||||
|
||||
let Some(request_timeout) = self.request_timeout else {
|
||||
return Box::pin(send_fut).into();
|
||||
};
|
||||
|
||||
Box::pin(async move {
|
||||
timeout(request_timeout, send_fut)
|
||||
.await
|
||||
.map_err(|_| NetError::Timeout)?
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
self.is_shutdown = true;
|
||||
}
|
||||
|
||||
fn is_shutdown(&self) -> bool {
|
||||
self.is_shutdown
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for HttpsClientStream {
|
||||
type Item = Result<(), NetError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.is_shutdown {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// just checking if the connection is ok
|
||||
match self.h2.poll_ready(cx) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Some(Ok(()))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(NetError::from(format!(
|
||||
"h2 stream errored: {e}",
|
||||
))))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A HTTPS connection builder for DNS-over-HTTPS
|
||||
#[derive(Clone)]
|
||||
pub struct HttpsClientStreamBuilder<P> {
|
||||
provider: P,
|
||||
client_config: Arc<ClientConfig>,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
set_headers: Option<Arc<dyn SetHeaders>>,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> HttpsClientStreamBuilder<P> {
|
||||
/// Sets the address to connect from.
|
||||
pub fn bind_addr(&mut self, bind_addr: SocketAddr) {
|
||||
self.bind_addr = Some(bind_addr);
|
||||
}
|
||||
|
||||
/// Set the [`SetHeaders`] trait object used to inject dynamic headers into the DoH request
|
||||
pub fn set_headers(&mut self, headers: Arc<dyn SetHeaders>) {
|
||||
self.set_headers.replace(headers);
|
||||
}
|
||||
|
||||
/// Set the per-request timeout applied to each DNS send over the H2 connection.
|
||||
///
|
||||
/// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with
|
||||
/// [`NetError::Timeout`] if no response arrives within this duration.
|
||||
pub fn request_timeout(&mut self, timeout: Duration) {
|
||||
self.request_timeout = Some(timeout);
|
||||
}
|
||||
|
||||
/// Creates a new [`DnsExchange`] wrapping the [`HttpsClientStream`] from this builder
|
||||
pub async fn exchange(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
let mut handle = self.provider.create_handle();
|
||||
let stream = self.build(name_server, server_name, path).await?;
|
||||
let (exchange, bg) = DnsExchange::from_stream(stream);
|
||||
handle.spawn_bg(bg);
|
||||
Ok(exchange)
|
||||
}
|
||||
|
||||
/// Creates a new HttpsStream to the specified name_server
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `dns_name` - The DNS name associated with a certificate
|
||||
/// * `http_endpoint` - The HTTP endpoint where the remote DNS resolver provides service, typically `/dns-query`
|
||||
pub fn build(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> impl Future<Output = Result<HttpsClientStream, NetError>> + Send + 'static {
|
||||
let request_timeout = self.request_timeout;
|
||||
let connect_fut = connect(
|
||||
self.provider.connect_tcp(name_server, self.bind_addr, None),
|
||||
self.client_config,
|
||||
name_server,
|
||||
server_name,
|
||||
path,
|
||||
self.set_headers,
|
||||
);
|
||||
async move {
|
||||
let mut stream = connect_fut.await?;
|
||||
stream.request_timeout = request_timeout;
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new HttpsStream with existing connection
|
||||
pub fn connect(
|
||||
tcp: impl Future<Output = Result<impl DnsTcpStream, io::Error>> + Send + 'static,
|
||||
mut client_config: Arc<ClientConfig>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
query_path: Arc<str>,
|
||||
set_headers: Option<Arc<dyn SetHeaders>>,
|
||||
) -> impl Future<Output = Result<HttpsClientStream, NetError>> + Send + 'static {
|
||||
// ensure the ALPN protocol is set correctly
|
||||
if client_config.alpn_protocols.is_empty() {
|
||||
let mut client_cfg = (*client_config).clone();
|
||||
client_cfg.alpn_protocols = vec![ALPN_H2.to_vec()];
|
||||
|
||||
client_config = Arc::new(client_cfg);
|
||||
}
|
||||
|
||||
let context = Arc::new(RequestContext {
|
||||
version: Version::Http2,
|
||||
server_name,
|
||||
query_path,
|
||||
set_headers,
|
||||
});
|
||||
|
||||
async move {
|
||||
let tls_server_name = match ServerName::try_from(&*context.server_name) {
|
||||
Ok(dns_name) => dns_name.to_owned(),
|
||||
Err(err) => {
|
||||
return Err(NetError::from(format!(
|
||||
"bad server name {:?}: {err}",
|
||||
context.server_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let tcp = tcp.await?;
|
||||
let future = timeout(
|
||||
CONNECT_TIMEOUT,
|
||||
TlsConnector::from(client_config).connect(tls_server_name, AsyncIoStdAsTokio(tcp)),
|
||||
);
|
||||
|
||||
let tls = match future.await {
|
||||
Ok(Ok(tls)) => tls,
|
||||
Ok(Err(err)) => return Err(NetError::from(err)),
|
||||
Err(_) => return Err(NetError::Timeout),
|
||||
};
|
||||
|
||||
let mut handshake = h2::client::Builder::new();
|
||||
handshake.enable_push(false);
|
||||
let (h2, driver) = handshake.handshake(tls).await?;
|
||||
|
||||
debug!("h2 connection established to: {name_server}");
|
||||
tokio::spawn(async {
|
||||
if let Err(e) = driver.await {
|
||||
warn!("h2 connection failed: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(HttpsClientStream {
|
||||
h2,
|
||||
context,
|
||||
is_shutdown: false,
|
||||
request_timeout: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
h2: SendRequest<Bytes>,
|
||||
message: Bytes,
|
||||
cx: Arc<RequestContext>,
|
||||
) -> Result<DnsResponse, NetError> {
|
||||
let mut h2 = h2.ready().await?;
|
||||
|
||||
// build up the http request
|
||||
let request = cx.build(message.remaining())?;
|
||||
|
||||
debug!("request: {:#?}", request);
|
||||
|
||||
// Send the request
|
||||
let (response_future, mut send_stream) = h2.send_request(request, false)?;
|
||||
send_stream.send_data(message, true)?;
|
||||
|
||||
let mut response_stream = response_future.await?;
|
||||
|
||||
debug!("got response: {:#?}", response_stream);
|
||||
|
||||
// get the length of packet
|
||||
let content_length = response_stream
|
||||
.headers()
|
||||
.get(CONTENT_LENGTH)
|
||||
.map(|v| v.to_str())
|
||||
.transpose()
|
||||
.map_err(|e| NetError::from(format!("bad headers received: {e}")))?
|
||||
.map(usize::from_str)
|
||||
.transpose()
|
||||
.map_err(|e| NetError::from(format!("bad headers received: {e}")))?;
|
||||
|
||||
// read the response body
|
||||
let response_bytes = fetch_body(response_stream.body_mut(), content_length).await?;
|
||||
|
||||
// Was it a successful request?
|
||||
if !response_stream.status().is_success() {
|
||||
let error_string = String::from_utf8_lossy(response_bytes.as_ref());
|
||||
|
||||
// TODO: make explicit error type
|
||||
return Err(NetError::from(format!(
|
||||
"http unsuccessful code: {}, message: {}",
|
||||
response_stream.status(),
|
||||
error_string
|
||||
)));
|
||||
} else {
|
||||
// verify content type
|
||||
{
|
||||
// in the case that the ContentType is not specified, we assume it's the standard DNS format
|
||||
let content_type = response_stream
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.map(|h| {
|
||||
h.to_str().map_err(|err| {
|
||||
// TODO: make explicit error type
|
||||
NetError::from(format!("ContentType header not a string: {err}"))
|
||||
})
|
||||
})
|
||||
.unwrap_or(Ok(crate::http::MIME_APPLICATION_DNS))?;
|
||||
|
||||
if content_type != crate::http::MIME_APPLICATION_DNS {
|
||||
return Err(NetError::from(format!(
|
||||
"ContentType unsupported (must be '{}'): '{}'",
|
||||
crate::http::MIME_APPLICATION_DNS,
|
||||
content_type
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// and finally convert the bytes into a DNS message
|
||||
DnsResponse::from_buffer(response_bytes.to_vec()).map_err(NetError::from)
|
||||
}
|
||||
|
||||
/// Given an HTTP request, return a future that will result in the next sequence of bytes.
|
||||
///
|
||||
/// To allow downstream clients to do something interesting with the lifetime of the bytes, this doesn't
|
||||
/// perform a conversion to a Message, only collects all the bytes.
|
||||
pub async fn message_from<R>(
|
||||
this_server_name: Option<Arc<str>>,
|
||||
this_server_endpoint: Arc<str>,
|
||||
request: Request<R>,
|
||||
) -> Result<BytesMut, NetError>
|
||||
where
|
||||
R: Stream<Item = Result<Bytes, h2::Error>> + 'static + Send + Debug + Unpin,
|
||||
{
|
||||
debug!("Received request: {:#?}", request);
|
||||
|
||||
let this_server_name = this_server_name.as_deref();
|
||||
match crate::http::verify(
|
||||
Version::Http2,
|
||||
this_server_name,
|
||||
&this_server_endpoint,
|
||||
&request,
|
||||
) {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
// attempt to get the content length
|
||||
let mut content_length = None;
|
||||
if let Some(length) = request.headers().get(CONTENT_LENGTH) {
|
||||
let length = usize::from_str(length.to_str()?)?;
|
||||
debug!("got message length: {}", length);
|
||||
content_length = Some(length);
|
||||
}
|
||||
|
||||
match *request.method() {
|
||||
Method::GET => Err(format!("GET unimplemented: {}", request.method()).into()),
|
||||
Method::POST => fetch_body(request.into_body(), content_length).await,
|
||||
_ => Err(format!("bad method: {}", request.method()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
const ALPN_H2: &[u8] = b"h2";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::net::SocketAddr;
|
||||
|
||||
use rustls::KeyLogFile;
|
||||
use test_support::subscribe;
|
||||
|
||||
use super::*;
|
||||
use crate::proto::op::{DnsRequestOptions, Edns, Message, Query};
|
||||
use crate::proto::rr::{Name, RData, RecordType};
|
||||
use crate::runtime::TokioRuntimeProvider;
|
||||
use crate::tls::client_config;
|
||||
use crate::xfer::FirstAnswer;
|
||||
|
||||
#[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))]
|
||||
#[tokio::test]
|
||||
async fn test_https_google() {
|
||||
subscribe();
|
||||
|
||||
let google = SocketAddr::from(([8, 8, 8, 8], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut client_config = client_config_h2();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider);
|
||||
let connect = https_builder.build(google, Arc::from("dns.google"), Arc::from("/dns-query"));
|
||||
|
||||
let mut https = connect.await.expect("https connect failed");
|
||||
|
||||
let response = https
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = https
|
||||
.send_message(request.clone())
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))]
|
||||
#[tokio::test]
|
||||
async fn test_https_google_with_pure_ip_address_server() {
|
||||
subscribe();
|
||||
|
||||
let google = SocketAddr::from(([8, 8, 8, 8], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut client_config = client_config_h2();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider);
|
||||
let connect = https_builder.build(
|
||||
google,
|
||||
Arc::from(google.ip().to_string()),
|
||||
Arc::from("/dns-query"),
|
||||
);
|
||||
|
||||
let mut https = connect.await.expect("https connect failed");
|
||||
|
||||
let response = https
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = https
|
||||
.send_message(request.clone())
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "webpki-roots", feature = "rustls-platform-verifier"))]
|
||||
#[tokio::test]
|
||||
#[ignore = "cloudflare has been unreliable as a public test service"]
|
||||
async fn test_https_cloudflare() {
|
||||
subscribe();
|
||||
|
||||
let cloudflare = SocketAddr::from(([1, 1, 1, 1], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let client_config = client_config_h2();
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
let https_builder = HttpsClientStream::builder(Arc::new(client_config), provider);
|
||||
let connect = https_builder.build(
|
||||
cloudflare,
|
||||
Arc::from("cloudflare-dns.com"),
|
||||
Arc::from("/dns-query"),
|
||||
);
|
||||
|
||||
let mut https = connect.await.expect("https connect failed");
|
||||
|
||||
let response = https
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = https
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
fn client_config_h2() -> ClientConfig {
|
||||
let mut config = client_config().unwrap();
|
||||
config.alpn_protocols = vec![ALPN_H2.to_vec()];
|
||||
config
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_from_post() {
|
||||
subscribe();
|
||||
let message = Message::query();
|
||||
let msg_bytes = message.to_vec().unwrap();
|
||||
let len = msg_bytes.len();
|
||||
let stream = TestBytesStream(vec![Ok(Bytes::from(msg_bytes))]);
|
||||
let cx = RequestContext {
|
||||
version: Version::Http2,
|
||||
server_name: Arc::from("ns.example.com"),
|
||||
query_path: Arc::from("/dns-query"),
|
||||
set_headers: None,
|
||||
};
|
||||
|
||||
let request = cx.build(len).unwrap();
|
||||
let request = request.map(|()| stream);
|
||||
|
||||
let bytes = message_from(
|
||||
Some(Arc::from("ns.example.com")),
|
||||
"/dns-query".into(),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg_from_post = Message::from_vec(bytes.as_ref()).expect("bytes failed");
|
||||
assert_eq!(message, msg_from_post);
|
||||
}
|
||||
|
||||
/// Verifies that `request_timeout` cancels a stalled DNS-over-H2 request.
|
||||
///
|
||||
/// Uses an in-process `tokio::io::duplex` pipe — no TLS or external servers needed.
|
||||
/// The server half accepts the H2 request but never sends a response, simulating
|
||||
/// a hung upstream DoH server. The test asserts that `send_message` returns
|
||||
/// `Err(NetError::Timeout)` within the configured timeout.
|
||||
///
|
||||
/// This test also serves as the representative coverage for the identical
|
||||
/// `request_timeout` logic in `QuicClientStream` and `H3ClientStream`: all three
|
||||
/// apply the same `tokio::time::timeout` wrapper in `send_message`. In-process
|
||||
/// QUIC/H3 tests are impractical because QUIC mandates TLS with no plain-transport
|
||||
/// equivalent of the duplex trick used here.
|
||||
#[tokio::test]
|
||||
async fn test_request_timeout() {
|
||||
use std::str::FromStr;
|
||||
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
subscribe();
|
||||
|
||||
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
|
||||
|
||||
// Both H2 handshakes must run concurrently to exchange SETTINGS frames.
|
||||
let (client_result, server_result) = tokio::join!(
|
||||
h2::client::Builder::new()
|
||||
.enable_push(false)
|
||||
.handshake(client_io),
|
||||
h2::server::handshake(server_io),
|
||||
);
|
||||
let (h2_send, h2_conn) = client_result.expect("client h2 handshake");
|
||||
let mut server_conn = server_result.expect("server h2 handshake");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = h2_conn.await;
|
||||
});
|
||||
|
||||
// Accept the incoming request but hold the `SendResponse` without calling
|
||||
// `send_response` — the client's `response_future.await` will block forever.
|
||||
tokio::spawn(async move {
|
||||
if let Some(Ok((_req, respond))) = server_conn.next().await {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
drop(respond);
|
||||
}
|
||||
});
|
||||
|
||||
let context = Arc::new(RequestContext {
|
||||
version: Version::Http2,
|
||||
server_name: Arc::from("test.example"),
|
||||
query_path: Arc::from("/dns-query"),
|
||||
set_headers: None,
|
||||
});
|
||||
|
||||
let mut stream = HttpsClientStream {
|
||||
h2: h2_send,
|
||||
context,
|
||||
is_shutdown: false,
|
||||
request_timeout: Some(Duration::from_millis(50)),
|
||||
};
|
||||
|
||||
let mut msg = Message::query();
|
||||
msg.add_query(Query::query(
|
||||
Name::from_str("example.com.").unwrap(),
|
||||
RecordType::A,
|
||||
));
|
||||
let request = DnsRequest::new(msg, DnsRequestOptions::default());
|
||||
|
||||
let result = stream.send_message(request).first_answer().await;
|
||||
assert!(
|
||||
matches!(result, Err(NetError::Timeout)),
|
||||
"expected Timeout, got: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestBytesStream(Vec<Result<Bytes, h2::Error>>);
|
||||
|
||||
impl Stream for TestBytesStream {
|
||||
type Item = Result<Bytes, h2::Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.0.pop() {
|
||||
Some(Ok(bytes)) => Poll::Ready(Some(Ok(bytes))),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(err))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::fmt::{self, Display};
|
||||
use core::future::{Future, poll_fn};
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::str::FromStr;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
use futures_util::stream::Stream;
|
||||
use h3::client::SendRequest;
|
||||
use h3_quinn::OpenStreams;
|
||||
use http::header::{self, CONTENT_LENGTH};
|
||||
use quinn::{Endpoint, EndpointConfig, TransportConfig};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::timeout;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::{ALPN_H3, BodyStream};
|
||||
use crate::error::NetError;
|
||||
use crate::http::{RequestContext, SetHeaders, Version, fetch_body};
|
||||
use crate::proto::ProtoError;
|
||||
use crate::proto::op::{DnsRequest, DnsResponse};
|
||||
use crate::quic::connect_quic;
|
||||
use crate::runtime::{RuntimeProvider, Spawn};
|
||||
use crate::tls::client_config;
|
||||
use crate::udp::UdpSocket;
|
||||
use crate::xfer::{DnsExchange, DnsRequestSender, DnsResponseStream};
|
||||
|
||||
/// A DNS client connection for DNS-over-HTTP/3
|
||||
#[derive(Clone)]
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct H3ClientStream {
|
||||
// Corresponds to the dns-name of the HTTP/3 server
|
||||
name_server: SocketAddr,
|
||||
send_request: SendRequest<OpenStreams, Bytes>,
|
||||
context: Arc<RequestContext>,
|
||||
shutdown_tx: mpsc::Sender<()>,
|
||||
is_shutdown: bool,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl H3ClientStream {
|
||||
/// Builder for H3ClientStream
|
||||
pub fn builder() -> H3ClientStreamBuilder {
|
||||
H3ClientStreamBuilder {
|
||||
crypto_config: None,
|
||||
transport_config: Arc::new(super::transport()),
|
||||
bind_addr: None,
|
||||
set_headers: None,
|
||||
disable_grease: false,
|
||||
request_timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn inner_send(
|
||||
mut h3: SendRequest<OpenStreams, Bytes>,
|
||||
message: Bytes,
|
||||
cx: Arc<RequestContext>,
|
||||
) -> Result<DnsResponse, NetError> {
|
||||
// build up the http request
|
||||
let request = cx.build(message.remaining())?;
|
||||
debug!("request: {:#?}", request);
|
||||
|
||||
// Send the request
|
||||
let mut stream = h3.send_request(request).await?;
|
||||
stream.send_data(message).await?;
|
||||
stream.finish().await?;
|
||||
|
||||
let response = stream.recv_response().await?;
|
||||
debug!("got response: {:#?}", response);
|
||||
|
||||
// get the length of packet
|
||||
let content_length = response
|
||||
.headers()
|
||||
.get(CONTENT_LENGTH)
|
||||
.map(|v| v.to_str())
|
||||
.transpose()
|
||||
.map_err(|e| NetError::from(format!("bad headers received: {e}")))?
|
||||
.map(usize::from_str)
|
||||
.transpose()
|
||||
.map_err(|e| NetError::from(format!("bad headers received: {e}")))?;
|
||||
|
||||
// Was it a successful request?
|
||||
let response_bytes = fetch_body(
|
||||
BodyStream::from(|cx: &mut Context<'_>| stream.poll_recv_data(cx)),
|
||||
content_length,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_string = String::from_utf8_lossy(response_bytes.as_ref());
|
||||
|
||||
// TODO: make explicit error type
|
||||
return Err(NetError::from(format!(
|
||||
"http unsuccessful code: {}, message: {}",
|
||||
response.status(),
|
||||
error_string
|
||||
)));
|
||||
}
|
||||
|
||||
// in the case that the ContentType is not specified, we assume it's the standard DNS format
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.map(|h| {
|
||||
h.to_str().map_err(|err| {
|
||||
// TODO: make explicit error type
|
||||
NetError::from(format!("ContentType header not a string: {err}"))
|
||||
})
|
||||
})
|
||||
.unwrap_or(Ok(crate::http::MIME_APPLICATION_DNS))?;
|
||||
|
||||
if content_type != crate::http::MIME_APPLICATION_DNS {
|
||||
return Err(NetError::from(format!(
|
||||
"ContentType unsupported (must be '{}'): '{}'",
|
||||
crate::http::MIME_APPLICATION_DNS,
|
||||
content_type
|
||||
)));
|
||||
}
|
||||
|
||||
// and finally convert the bytes into a DNS message
|
||||
DnsResponse::from_buffer(response_bytes.to_vec()).map_err(NetError::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsRequestSender for H3ClientStream {
|
||||
/// This indicates that the HTTP message was successfully sent, and we now have the response.RecvStream
|
||||
///
|
||||
/// If the request fails, this will return the error, and it should be assumed that the Stream portion of
|
||||
/// this will have no date.
|
||||
///
|
||||
/// ```text
|
||||
/// RFC 8484 DNS Queries over HTTPS (DoH) October 2018
|
||||
///
|
||||
///
|
||||
/// 4.2. The HTTP Response
|
||||
///
|
||||
/// The only response type defined in this document is "application/dns-
|
||||
/// message", but it is possible that other response formats will be
|
||||
/// defined in the future. A DoH server MUST be able to process
|
||||
/// "application/dns-message" request messages.
|
||||
///
|
||||
/// Different response media types will provide more or less information
|
||||
/// from a DNS response. For example, one response type might include
|
||||
/// information from the DNS header bytes while another might omit it.
|
||||
/// The amount and type of information that a media type gives are solely
|
||||
/// up to the format, which is not defined in this protocol.
|
||||
///
|
||||
/// Each DNS request-response pair is mapped to one HTTP exchange. The
|
||||
/// responses may be processed and transported in any order using HTTP's
|
||||
/// multi-streaming functionality (see Section 5 of [RFC7540]).
|
||||
///
|
||||
/// Section 5.1 discusses the relationship between DNS and HTTP response
|
||||
/// caching.
|
||||
///
|
||||
/// 4.2.1. Handling DNS and HTTP Errors
|
||||
///
|
||||
/// DNS response codes indicate either success or failure for the DNS
|
||||
/// query. A successful HTTP response with a 2xx status code (see
|
||||
/// Section 6.3 of [RFC7231]) is used for any valid DNS response,
|
||||
/// regardless of the DNS response code. For example, a successful 2xx
|
||||
/// HTTP status code is used even with a DNS message whose DNS response
|
||||
/// code indicates failure, such as SERVFAIL or NXDOMAIN.
|
||||
///
|
||||
/// HTTP responses with non-successful HTTP status codes do not contain
|
||||
/// replies to the original DNS question in the HTTP request. DoH
|
||||
/// clients need to use the same semantic processing of non-successful
|
||||
/// HTTP status codes as other HTTP clients. This might mean that the
|
||||
/// DoH client retries the query with the same DoH server, such as if
|
||||
/// there are authorization failures (HTTP status code 401; see
|
||||
/// Section 3.1 of [RFC7235]). It could also mean that the DoH client
|
||||
/// retries with a different DoH server, such as for unsupported media
|
||||
/// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or
|
||||
/// where the server cannot generate a representation suitable for the
|
||||
/// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so
|
||||
/// on.
|
||||
/// ```
|
||||
fn send_message(&mut self, mut request: DnsRequest) -> DnsResponseStream {
|
||||
if self.is_shutdown {
|
||||
panic!("can not send messages after stream is shutdown")
|
||||
}
|
||||
|
||||
// per the RFC, a zero id allows for the HTTP packet to be cached better
|
||||
request.metadata.id = 0;
|
||||
|
||||
let bytes = match request.to_vec() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => return NetError::from(err).into(),
|
||||
};
|
||||
|
||||
let send_fut = Self::inner_send(
|
||||
self.send_request.clone(),
|
||||
Bytes::from(bytes),
|
||||
self.context.clone(),
|
||||
);
|
||||
|
||||
let Some(request_timeout) = self.request_timeout else {
|
||||
return Box::pin(send_fut).into();
|
||||
};
|
||||
|
||||
Box::pin(async move {
|
||||
timeout(request_timeout, send_fut)
|
||||
.await
|
||||
.map_err(|_| NetError::Timeout)?
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
self.is_shutdown = true;
|
||||
}
|
||||
|
||||
fn is_shutdown(&self) -> bool {
|
||||
self.is_shutdown
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for H3ClientStream {
|
||||
type Item = Result<(), NetError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.is_shutdown {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// just checking if the connection is ok
|
||||
if self.shutdown_tx.is_closed() {
|
||||
return Poll::Ready(Some(Err(NetError::from(
|
||||
"h3 connection is already shutdown",
|
||||
))));
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for H3ClientStream {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(
|
||||
formatter,
|
||||
"H3({},{})",
|
||||
self.name_server, self.context.server_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A H3 connection builder for DNS-over-HTTP/3
|
||||
#[derive(Clone)]
|
||||
pub struct H3ClientStreamBuilder {
|
||||
crypto_config: Option<rustls::ClientConfig>,
|
||||
transport_config: Arc<TransportConfig>,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
set_headers: Option<Arc<dyn SetHeaders>>,
|
||||
disable_grease: bool,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl H3ClientStreamBuilder {
|
||||
/// Constructs a new H3ClientStreamBuilder with the associated ClientConfig
|
||||
pub fn crypto_config(mut self, crypto_config: rustls::ClientConfig) -> Self {
|
||||
self.crypto_config = Some(crypto_config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to connect from.
|
||||
pub fn bind_addr(mut self, bind_addr: SocketAddr) -> Self {
|
||||
self.bind_addr = Some(bind_addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`SetHeaders`] trait object used to inject dynamic headers into the DoH request
|
||||
pub fn set_headers(&mut self, headers: Arc<dyn SetHeaders>) {
|
||||
self.set_headers.replace(headers);
|
||||
}
|
||||
|
||||
/// Sets whether to disable GREASE
|
||||
pub fn disable_grease(mut self, disable_grease: bool) -> Self {
|
||||
self.disable_grease = disable_grease;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the per-request timeout applied to each DNS send over the H3 connection.
|
||||
///
|
||||
/// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with
|
||||
/// [`NetError::Timeout`] if no response arrives within this duration.
|
||||
pub fn request_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.request_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new H3Stream to the specified name_server
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `server_name` - The DNS name associated with a certificate
|
||||
pub fn build(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> impl Future<Output = Result<H3ClientStream, NetError>> + Send + 'static {
|
||||
self.connect(name_server, server_name, path)
|
||||
}
|
||||
|
||||
/// Creates a new [`DnsExchange`] wrapping the [`H3ClientStream`] from this builder
|
||||
pub async fn exchange<P: RuntimeProvider>(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
provider: P,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
let stream = self
|
||||
.connect_with_future(socket, name_server, server_name, path)
|
||||
.await?;
|
||||
let (exchange, bg) = DnsExchange::from_stream(stream);
|
||||
provider.create_handle().spawn_bg(bg);
|
||||
Ok(exchange)
|
||||
}
|
||||
|
||||
/// Creates a new H3Stream with existing connection
|
||||
pub fn build_with_future(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> impl Future<Output = Result<H3ClientStream, NetError>> + Send + 'static {
|
||||
self.connect_with_future(socket, name_server, server_name, path)
|
||||
}
|
||||
|
||||
async fn connect_with_future(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> Result<H3ClientStream, NetError> {
|
||||
let endpoint = Endpoint::new_with_abstract_socket(
|
||||
EndpointConfig::default(),
|
||||
None,
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
self.connect_inner(endpoint, name_server, server_name, path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> Result<H3ClientStream, NetError> {
|
||||
let connect = if let Some(bind_addr) = self.bind_addr {
|
||||
<tokio::net::UdpSocket as UdpSocket>::connect_with_bind(name_server, bind_addr)
|
||||
} else {
|
||||
<tokio::net::UdpSocket as UdpSocket>::connect(name_server)
|
||||
};
|
||||
|
||||
let socket = connect.await?;
|
||||
let socket = socket.into_std()?;
|
||||
let endpoint = Endpoint::new(
|
||||
EndpointConfig::default(),
|
||||
None,
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
self.connect_inner(endpoint, name_server, server_name, path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn connect_inner(
|
||||
self,
|
||||
endpoint: Endpoint,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
path: Arc<str>,
|
||||
) -> Result<H3ClientStream, NetError> {
|
||||
let quic_connection = connect_quic(
|
||||
name_server,
|
||||
server_name.clone(),
|
||||
ALPN_H3,
|
||||
match self.crypto_config {
|
||||
Some(crypto_config) => crypto_config,
|
||||
None => client_config()?,
|
||||
},
|
||||
self.transport_config,
|
||||
endpoint,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let h3_connection = h3_quinn::Connection::new(quic_connection);
|
||||
let (mut driver, send_request) = h3::client::builder()
|
||||
.send_grease(!self.disable_grease)
|
||||
.build(h3_connection)
|
||||
.await
|
||||
.map_err(|e| ProtoError::from(format!("h3 connection failed: {e}")))?;
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
// TODO: hand this back for others to run rather than spawning here?
|
||||
debug!("h3 connection is ready: {}", name_server);
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
error = poll_fn(|cx| driver.poll_close(cx)) => {
|
||||
// `poll_close()` strangely unconditionally returns a `ConnectionError`
|
||||
if !error.is_h3_no_error() {
|
||||
warn!(%error, "h3 connection failed to close")
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.recv() => {
|
||||
debug!("h3 connection is shutting down: {}", name_server);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(H3ClientStream {
|
||||
name_server,
|
||||
send_request,
|
||||
context: Arc::new(RequestContext {
|
||||
version: Version::Http3,
|
||||
server_name,
|
||||
query_path: path,
|
||||
set_headers: self.set_headers,
|
||||
}),
|
||||
shutdown_tx,
|
||||
is_shutdown: false,
|
||||
request_timeout: self.request_timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
test,
|
||||
any(feature = "rustls-platform-verifier", feature = "webpki-roots")
|
||||
))]
|
||||
mod tests {
|
||||
use core::net::SocketAddr;
|
||||
use core::str::FromStr;
|
||||
use std::println;
|
||||
|
||||
use rustls::KeyLogFile;
|
||||
use test_support::subscribe;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::*;
|
||||
use crate::proto::op::{DnsRequestOptions, Edns, Message, Query};
|
||||
use crate::proto::rr::{Name, RData, RecordType};
|
||||
use crate::xfer::FirstAnswer;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_h3_google() {
|
||||
subscribe();
|
||||
|
||||
let google = SocketAddr::from(([8, 8, 8, 8], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut client_config = client_config().unwrap();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let mut h3 = H3ClientStream::builder()
|
||||
.crypto_config(client_config)
|
||||
.build(google, Arc::from("dns.google"), Arc::from("/dns-query"))
|
||||
.await
|
||||
.expect("h3 connect failed");
|
||||
|
||||
let response = h3
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = h3
|
||||
.send_message(request.clone())
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_h3_google_with_pure_ip_address_server() {
|
||||
subscribe();
|
||||
|
||||
let google = SocketAddr::from(([8, 8, 8, 8], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut client_config = client_config().unwrap();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let mut h3 = H3ClientStream::builder()
|
||||
.crypto_config(client_config)
|
||||
.build(
|
||||
google,
|
||||
Arc::from(google.ip().to_string()),
|
||||
Arc::from("/dns-query"),
|
||||
)
|
||||
.await
|
||||
.expect("h3 connect failed");
|
||||
|
||||
let response = h3
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = h3
|
||||
.send_message(request.clone())
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_h3_cloudflare() {
|
||||
subscribe();
|
||||
|
||||
let cloudflare = SocketAddr::from(([1, 1, 1, 1], 443));
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut client_config = client_config().unwrap();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let mut h3 = H3ClientStream::builder()
|
||||
.crypto_config(client_config)
|
||||
// Currently CF is using a broken GREASE implementation, see <https://github.com/hyperium/h3/issues/206>.
|
||||
.disable_grease(true)
|
||||
.build(
|
||||
cloudflare,
|
||||
Arc::from("cloudflare-dns.com"),
|
||||
Arc::from("/dns-query"),
|
||||
)
|
||||
.await
|
||||
.expect("h3 connect failed");
|
||||
|
||||
let response = h3
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::A(_)))
|
||||
);
|
||||
|
||||
//
|
||||
// assert that the connection works for a second query
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
request.metadata.recursion_desired = true;
|
||||
let mut edns = Edns::new();
|
||||
edns.set_version(0);
|
||||
edns.set_max_payload(1232);
|
||||
request.edns = Some(edns);
|
||||
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let response = h3
|
||||
.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
|
||||
assert!(
|
||||
response
|
||||
.answers
|
||||
.iter()
|
||||
.any(|record| matches!(record.data, RData::AAAA(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[allow(clippy::print_stdout)]
|
||||
async fn test_h3_client_stream_clonable() {
|
||||
subscribe();
|
||||
|
||||
// use google
|
||||
let google = SocketAddr::from(([8, 8, 8, 8], 443));
|
||||
|
||||
let mut client_config = client_config().unwrap();
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
let h3 = H3ClientStream::builder()
|
||||
.crypto_config(client_config)
|
||||
.build(google, Arc::from("dns.google"), Arc::from("/dns-query"))
|
||||
.await
|
||||
.expect("h3 connect failed");
|
||||
|
||||
// prepare request
|
||||
let mut request = Message::query();
|
||||
let query = Query::query(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
request.add_query(query);
|
||||
let request = DnsRequest::new(request, DnsRequestOptions::default());
|
||||
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
for i in 0..50 {
|
||||
let mut h3 = h3.clone();
|
||||
let request = request.clone();
|
||||
|
||||
join_set.spawn(async move {
|
||||
let start = std::time::Instant::now();
|
||||
h3.send_message(request)
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("send_message failed");
|
||||
println!("request[{i}] completed: {:?}", start.elapsed());
|
||||
});
|
||||
}
|
||||
|
||||
let total = join_set.len();
|
||||
let mut idx = 0usize;
|
||||
while join_set.join_next().await.is_some() {
|
||||
println!("join_set completed {idx}/{total}");
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! HTTP/3 related server items
|
||||
|
||||
use core::net::SocketAddr;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use h3::server::{Connection, RequestStream};
|
||||
use h3_quinn::{BidiStream, Endpoint};
|
||||
use http::Request;
|
||||
use quinn::crypto::rustls::QuicServerConfig;
|
||||
use quinn::{EndpointConfig, ServerConfig};
|
||||
use rustls::server::ResolvesServerCert;
|
||||
use rustls::server::ServerConfig as TlsServerConfig;
|
||||
use rustls::version::TLS13;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{error::NetError, tls::default_provider, udp::UdpSocket};
|
||||
|
||||
use super::ALPN_H3;
|
||||
|
||||
/// A DNS-over-HTTP/3 Server, see H3ClientStream for the client counterpart
|
||||
pub struct H3Server {
|
||||
endpoint: Endpoint,
|
||||
}
|
||||
|
||||
impl H3Server {
|
||||
/// Construct the new Acceptor with the associated pkcs12 data
|
||||
pub async fn new(
|
||||
name_server: SocketAddr,
|
||||
server_cert_resolver: Arc<dyn ResolvesServerCert>,
|
||||
) -> Result<Self, NetError> {
|
||||
// setup a new socket for the server to use
|
||||
let socket = <tokio::net::UdpSocket as UdpSocket>::bind(name_server).await?;
|
||||
Self::with_socket(socket, server_cert_resolver)
|
||||
}
|
||||
|
||||
/// Construct the new server with an existing socket and default TLS config.
|
||||
pub fn with_socket(
|
||||
socket: tokio::net::UdpSocket,
|
||||
server_cert_resolver: Arc<dyn ResolvesServerCert>,
|
||||
) -> Result<Self, NetError> {
|
||||
let mut config = TlsServerConfig::builder_with_provider(Arc::new(default_provider()))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.expect("TLS1.3 not supported")
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(server_cert_resolver);
|
||||
|
||||
config.alpn_protocols = vec![ALPN_H3.to_vec()];
|
||||
|
||||
Self::with_socket_and_tls_config(socket, Arc::new(config))
|
||||
}
|
||||
|
||||
/// Construct the new server with an existing socket and custom TLS config.
|
||||
///
|
||||
/// The TLS configuration should support TLS 1.3 and have the H3 ALPN protocol enabled.
|
||||
pub fn with_socket_and_tls_config(
|
||||
socket: tokio::net::UdpSocket,
|
||||
tls_config: Arc<TlsServerConfig>,
|
||||
) -> Result<Self, NetError> {
|
||||
let mut server_config =
|
||||
ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(tls_config).unwrap()));
|
||||
server_config.transport = Arc::new(super::transport());
|
||||
|
||||
let socket = socket.into_std()?;
|
||||
|
||||
let endpoint = Endpoint::new(
|
||||
EndpointConfig::default(),
|
||||
Some(server_config),
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
|
||||
Ok(Self { endpoint })
|
||||
}
|
||||
|
||||
/// Accept the next incoming connection.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A remote connection that could accept many potential requests and the remote socket address
|
||||
pub async fn accept(&mut self) -> Result<Option<(H3Connection, SocketAddr)>, NetError> {
|
||||
let Some(connecting) = self.endpoint.accept().await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let remote_addr = connecting.remote_address();
|
||||
let future = async {
|
||||
let connection = connecting.await?;
|
||||
let connection = Connection::new(h3_quinn::Connection::new(connection))
|
||||
.await
|
||||
.map_err(|e| NetError::from(format!("h3 connection failed: {e}")))?;
|
||||
Ok::<_, NetError>(H3Connection { connection })
|
||||
};
|
||||
let Ok(result) = timeout(Duration::from_secs(5), future).await else {
|
||||
return Err(NetError::from("h3 timeout expired during handshake"));
|
||||
};
|
||||
Ok(Some((result?, remote_addr)))
|
||||
}
|
||||
|
||||
/// Returns the address this server is listening on
|
||||
///
|
||||
/// This can be useful in tests, where a random port can be associated with the server by binding on `127.0.0.1:0` and then getting the
|
||||
/// associated port address with this function.
|
||||
pub fn local_addr(&self) -> Result<SocketAddr, io::Error> {
|
||||
self.endpoint.local_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// A HTTP/3 connection.
|
||||
pub struct H3Connection {
|
||||
connection: Connection<h3_quinn::Connection, Bytes>,
|
||||
}
|
||||
|
||||
impl H3Connection {
|
||||
/// Accept the next request from the client
|
||||
pub async fn accept(
|
||||
&mut self,
|
||||
) -> Option<Result<(Request<()>, RequestStream<BidiStream<Bytes>, Bytes>), NetError>> {
|
||||
match self.connection.accept().await {
|
||||
Ok(Some(resolver)) => match resolver.resolve_request().await {
|
||||
Ok((request, stream)) => Some(Ok((request, stream))),
|
||||
Err(e) => Some(Err(NetError::from(format!(
|
||||
"h3 request resolution failed: {e}"
|
||||
)))),
|
||||
},
|
||||
Ok(None) => None,
|
||||
Err(e) => Some(Err(NetError::from(format!("h3 request failed: {e}")))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdown the connection.
|
||||
pub async fn shutdown(&mut self) -> Result<(), NetError> {
|
||||
self.connection
|
||||
.shutdown(0)
|
||||
.await
|
||||
.map_err(|e| NetError::from(format!("h3 connection shutdown failed: {e}")))
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! TLS protocol related components for DNS over HTTP/3 (DoH3)
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Buf;
|
||||
use futures_util::Stream;
|
||||
use quinn::{TransportConfig, VarInt};
|
||||
|
||||
use crate::NetError;
|
||||
|
||||
mod h3_client_stream;
|
||||
pub use h3_client_stream::{H3ClientStream, H3ClientStreamBuilder};
|
||||
pub mod h3_server;
|
||||
|
||||
const ALPN_H3: &[u8] = b"h3";
|
||||
|
||||
/// Returns a default endpoint configuration for DNS-over-QUIC
|
||||
fn transport() -> TransportConfig {
|
||||
let mut transport_config = TransportConfig::default();
|
||||
|
||||
transport_config.datagram_receive_buffer_size(None);
|
||||
transport_config.datagram_send_buffer_size(0);
|
||||
// clients never accept new bidirectional streams
|
||||
transport_config.max_concurrent_bidi_streams(VarInt::from_u32(3));
|
||||
// - SETTINGS
|
||||
// - QPACK encoder
|
||||
// - QPACK decoder
|
||||
// - RESERVED (GREASE)
|
||||
transport_config.max_concurrent_uni_streams(VarInt::from_u32(4));
|
||||
|
||||
transport_config
|
||||
}
|
||||
|
||||
/// [`Stream`] adapter for h3 body streaming.
|
||||
pub struct BodyStream<T>(T);
|
||||
|
||||
impl<T, B> Stream for BodyStream<T>
|
||||
where
|
||||
T: FnMut(&mut Context<'_>) -> Poll<Result<Option<B>, h3::error::StreamError>> + Unpin,
|
||||
B: Buf,
|
||||
{
|
||||
type Item = Result<B, NetError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let Poll::Ready(result) = (this.0)(cx) else {
|
||||
return Poll::Pending;
|
||||
};
|
||||
|
||||
Poll::Ready(match result {
|
||||
Ok(Some(buf)) => Some(Ok(buf)),
|
||||
Ok(None) => None,
|
||||
Err(e) => Some(Err(NetError::from(format!(
|
||||
"h3 stream receive data failed: {e}"
|
||||
)))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, B> From<T> for BodyStream<T>
|
||||
where
|
||||
T: FnMut(&mut Context<'_>) -> Poll<Result<Option<B>, h3::error::StreamError>> + Unpin,
|
||||
B: Buf,
|
||||
{
|
||||
fn from(stream: T) -> Self {
|
||||
Self(stream)
|
||||
}
|
||||
}
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! HTTP protocol related components for DNS over HTTP/2 (DoH) and HTTP/3 (DoH3)
|
||||
|
||||
use core::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use http::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use http::{HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header, uri};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::NetError;
|
||||
|
||||
pub(crate) struct RequestContext {
|
||||
pub(crate) version: Version,
|
||||
pub(crate) server_name: Arc<str>,
|
||||
pub(crate) query_path: Arc<str>,
|
||||
pub(crate) set_headers: Option<Arc<dyn SetHeaders>>,
|
||||
}
|
||||
|
||||
impl RequestContext {
|
||||
/// Create a new Request for an http dns-message request
|
||||
///
|
||||
/// ```text
|
||||
/// RFC 8484 DNS Queries over HTTPS (DoH) October 2018
|
||||
///
|
||||
/// The URI Template defined in this document is processed without any
|
||||
/// variables when the HTTP method is POST. When the HTTP method is GET,
|
||||
/// the single variable "dns" is defined as the content of the DNS
|
||||
/// request (as described in Section 6), encoded with base64url
|
||||
/// [RFC4648].
|
||||
/// ```
|
||||
pub(crate) fn build(&self, message_len: usize) -> Result<Request<()>, NetError> {
|
||||
let mut parts = uri::Parts::default();
|
||||
parts.path_and_query = Some(
|
||||
uri::PathAndQuery::try_from(&*self.query_path)
|
||||
.map_err(|e| NetError::from(format!("invalid DoH path: {e}")))?,
|
||||
);
|
||||
parts.scheme = Some(uri::Scheme::HTTPS);
|
||||
parts.authority = Some(
|
||||
uri::Authority::from_str(&self.server_name)
|
||||
.map_err(|e| NetError::from(format!("invalid authority: {e}")))?,
|
||||
);
|
||||
|
||||
let url =
|
||||
Uri::from_parts(parts).map_err(|e| NetError::from(format!("uri parse error: {e}")))?;
|
||||
|
||||
// TODO: add user agent to TypedHeaders
|
||||
let mut request = Request::builder()
|
||||
.method("POST")
|
||||
.uri(url)
|
||||
.version(self.version.to_http())
|
||||
.header(CONTENT_TYPE, MIME_APPLICATION_DNS)
|
||||
.header(ACCEPT, MIME_APPLICATION_DNS)
|
||||
.header(CONTENT_LENGTH, message_len);
|
||||
|
||||
if let Some(headers) = &self.set_headers {
|
||||
if let Some(map) = request.headers_mut() {
|
||||
headers.set_headers(map)?;
|
||||
}
|
||||
}
|
||||
|
||||
request
|
||||
.body(())
|
||||
.map_err(|e| NetError::from(format!("http stream errored: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the request is well-formed for the name-server and supported protocols
|
||||
pub fn verify<T>(
|
||||
version: Version,
|
||||
name_server: Option<&str>,
|
||||
query_path: &str,
|
||||
request: &Request<T>,
|
||||
) -> Result<(), NetError> {
|
||||
// Verify all HTTP parameters
|
||||
let uri = request.uri();
|
||||
|
||||
// validate path
|
||||
if uri.path() != query_path {
|
||||
return Err(format!("bad path: {}, expected: {}", uri.path(), query_path).into());
|
||||
}
|
||||
|
||||
// we only accept HTTPS
|
||||
if Some(&uri::Scheme::HTTPS) != uri.scheme() {
|
||||
return Err("must be HTTPS scheme".into());
|
||||
}
|
||||
|
||||
// the authority must match our nameserver name
|
||||
if let Some(name_server) = name_server {
|
||||
if let Some(authority) = uri.authority() {
|
||||
if authority.host() != name_server {
|
||||
return Err("incorrect authority".into());
|
||||
}
|
||||
} else {
|
||||
return Err("no authority in HTTPS request".into());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: switch to mime::APPLICATION_DNS when that stabilizes
|
||||
match request.headers().get(CONTENT_TYPE).map(|v| v.to_str()) {
|
||||
Some(Ok(ctype)) if ctype == MIME_APPLICATION_DNS => {}
|
||||
_ => return Err("unsupported content type".into()),
|
||||
};
|
||||
|
||||
// TODO: switch to mime::APPLICATION_DNS when that stabilizes
|
||||
match request.headers().get(ACCEPT).map(|v| v.to_str()) {
|
||||
Some(Ok(ctype)) => {
|
||||
let mut found = false;
|
||||
for mime_and_quality in ctype.split(',') {
|
||||
let mut parts = mime_and_quality.splitn(2, ';');
|
||||
match parts.next() {
|
||||
Some(mime) if mime.trim() == MIME_APPLICATION_DNS => {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
Some(mime) if mime.trim() == "application/*" => {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return Err("does not accept content type".into());
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => return Err(e.into()),
|
||||
None => return Err("Accept is unspecified".into()),
|
||||
};
|
||||
|
||||
if request.version() != version.to_http() {
|
||||
let message = match version {
|
||||
#[cfg(feature = "__https")]
|
||||
Version::Http2 => "only HTTP/2 supported",
|
||||
#[cfg(feature = "__h3")]
|
||||
Version::Http3 => "only HTTP/3 supported",
|
||||
};
|
||||
return Err(message.into());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"verified request from: {}",
|
||||
request
|
||||
.headers()
|
||||
.get(header::USER_AGENT)
|
||||
.map(|h| h.to_str().unwrap_or("bad user agent"))
|
||||
.unwrap_or("unknown user agent")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch the body of the request from the stream
|
||||
pub async fn fetch_body<E: Into<NetError>>(
|
||||
mut stream: impl Stream<Item = Result<impl Buf, E>> + Unpin,
|
||||
length: Option<usize>,
|
||||
) -> Result<BytesMut, NetError> {
|
||||
let mut bytes = BytesMut::with_capacity(512);
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(Ok(frame)) => match bytes.len() + frame.remaining() > MAX_REQUEST_SIZE {
|
||||
true => return Err(NetError::RequestTooLarge),
|
||||
false => bytes.put(frame),
|
||||
},
|
||||
Some(Err(err)) => return Err(err.into()),
|
||||
None => match length {
|
||||
Some(length) if bytes.len() == length => return Ok(bytes),
|
||||
Some(_) => return Err("body size does not match expected content-length".into()),
|
||||
None => return Ok(bytes),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_REQUEST_SIZE: usize = u16::MAX as usize;
|
||||
|
||||
/// Create a new Response for an http dns-message request
|
||||
///
|
||||
/// ```text
|
||||
/// RFC 8484 DNS Queries over HTTPS (DoH) October 2018
|
||||
///
|
||||
/// 4.2.1. Handling DNS and HTTP Errors
|
||||
///
|
||||
/// DNS response codes indicate either success or failure for the DNS
|
||||
/// query. A successful HTTP response with a 2xx status code (see
|
||||
/// Section 6.3 of [RFC7231]) is used for any valid DNS response,
|
||||
/// regardless of the DNS response code. For example, a successful 2xx
|
||||
/// HTTP status code is used even with a DNS message whose DNS response
|
||||
/// code indicates failure, such as SERVFAIL or NXDOMAIN.
|
||||
///
|
||||
/// HTTP responses with non-successful HTTP status codes do not contain
|
||||
/// replies to the original DNS question in the HTTP request. DoH
|
||||
/// clients need to use the same semantic processing of non-successful
|
||||
/// HTTP status codes as other HTTP clients. This might mean that the
|
||||
/// DoH client retries the query with the same DoH server, such as if
|
||||
/// there are authorization failures (HTTP status code 401; see
|
||||
/// Section 3.1 of [RFC7235]). It could also mean that the DoH client
|
||||
/// retries with a different DoH server, such as for unsupported media
|
||||
/// types (HTTP status code 415; see Section 6.5.13 of [RFC7231]), or
|
||||
/// where the server cannot generate a representation suitable for the
|
||||
/// client (HTTP status code 406; see Section 6.5.6 of [RFC7231]), and so
|
||||
/// on.
|
||||
/// ```
|
||||
pub fn response(version: Version, message_len: usize) -> Result<Response<()>, NetError> {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.version(version.to_http())
|
||||
.header(CONTENT_TYPE, MIME_APPLICATION_DNS)
|
||||
.header(CONTENT_LENGTH, message_len)
|
||||
.body(())
|
||||
.map_err(|e| NetError::from(format!("invalid response: {e}")))
|
||||
}
|
||||
|
||||
/// Represents a version of the HTTP spec.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Version {
|
||||
/// HTTP/2 for DoH.
|
||||
#[cfg(feature = "__https")]
|
||||
Http2,
|
||||
/// HTTP/3 for DoH3.
|
||||
#[cfg(feature = "__h3")]
|
||||
Http3,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
fn to_http(self) -> http::Version {
|
||||
match self {
|
||||
#[cfg(feature = "__https")]
|
||||
Self::Http2 => http::Version::HTTP_2,
|
||||
#[cfg(feature = "__h3")]
|
||||
Self::Http3 => http::Version::HTTP_3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to update HTTP headers on requests
|
||||
///
|
||||
/// For instance a DoH server may require authentication based
|
||||
/// on per-request HTTP headers and this trait allows their addition.
|
||||
pub trait SetHeaders: Send + Sync + 'static {
|
||||
/// Get a set of headers to add to the query
|
||||
fn set_headers(&self, headers: &mut HeaderMap<HeaderValue>) -> Result<(), NetError>;
|
||||
}
|
||||
|
||||
pub(crate) const MIME_APPLICATION_DNS: &str = "application/dns-message";
|
||||
|
||||
/// The default query path for DNS-over-HTTPS if none was given.
|
||||
pub const DEFAULT_DNS_QUERY_PATH: &str = "/dns-query";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream;
|
||||
use http::{
|
||||
HeaderMap,
|
||||
header::{HeaderName, HeaderValue},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "__https")]
|
||||
fn test_new_verify_h2() {
|
||||
let cx = RequestContext {
|
||||
version: Version::Http2,
|
||||
server_name: Arc::from("ns.example.com"),
|
||||
query_path: Arc::from("/dns-query"),
|
||||
set_headers: None,
|
||||
};
|
||||
|
||||
let request = cx.build(512).expect("error converting to http");
|
||||
assert!(
|
||||
verify(
|
||||
Version::Http2,
|
||||
Some("ns.example.com"),
|
||||
"/dns-query",
|
||||
&request
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "__https")]
|
||||
fn test_additional_headers() {
|
||||
let cx = RequestContext {
|
||||
version: Version::Http2,
|
||||
server_name: Arc::from("ns.example.com"),
|
||||
query_path: Arc::from("/dns-query"),
|
||||
set_headers: Some(Arc::new(vec![(
|
||||
HeaderName::from_static("test-header"),
|
||||
HeaderValue::from_static("test-header-value"),
|
||||
)]) as Arc<dyn SetHeaders>),
|
||||
};
|
||||
|
||||
let request = cx.build(512).expect("error converting to http");
|
||||
assert!(
|
||||
verify(
|
||||
Version::Http2,
|
||||
Some("ns.example.com"),
|
||||
"/dns-query",
|
||||
&request
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.headers()
|
||||
.get(HeaderName::from_static("test-header"))
|
||||
.expect("header to be set"),
|
||||
HeaderValue::from_static("test-header-value")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "__h3")]
|
||||
fn test_new_verify_h3() {
|
||||
let cx = RequestContext {
|
||||
version: Version::Http3,
|
||||
server_name: Arc::from("ns.example.com"),
|
||||
query_path: Arc::from("/dns-query"),
|
||||
set_headers: None,
|
||||
};
|
||||
|
||||
let request = cx.build(512).expect("error converting to http");
|
||||
assert!(
|
||||
verify(
|
||||
Version::Http3,
|
||||
Some("ns.example.com"),
|
||||
"/dns-query",
|
||||
&request
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
impl SetHeaders for Vec<(HeaderName, HeaderValue)> {
|
||||
fn set_headers(&self, map: &mut HeaderMap<HeaderValue>) -> Result<(), NetError> {
|
||||
for (name, value) in self.iter() {
|
||||
map.insert(name.clone(), value.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_accumulates_chunks() {
|
||||
let mut body = chunks(&[512, 512, 200]);
|
||||
let bytes = fetch_body(&mut body, None).await.unwrap();
|
||||
assert_eq!(bytes.len(), 1224);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_stops_at_content_length() {
|
||||
let mut body = chunks(&[512, 512]);
|
||||
assert!(fetch_body(&mut body, Some(512)).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_short_body_errors() {
|
||||
let mut body = chunks(&[512]);
|
||||
assert!(fetch_body(&mut body, Some(1024)).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_single_oversized_chunk() {
|
||||
// a single chunk exceeding the limit is rejected without being buffered
|
||||
let mut body = chunks(&[MAX_REQUEST_SIZE + 1]);
|
||||
assert!(matches!(
|
||||
fetch_body(&mut body, None).await,
|
||||
Err(NetError::RequestTooLarge)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_accumulated_oversize_rejected() {
|
||||
// no single chunk is too large, but together they exceed the limit
|
||||
let half = MAX_REQUEST_SIZE / 2 + 1;
|
||||
let mut body = chunks(&[half, half]);
|
||||
assert!(matches!(
|
||||
fetch_body(&mut body, None).await,
|
||||
Err(NetError::RequestTooLarge)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_body_at_limit_ok() {
|
||||
// a body exactly at the limit is still accepted
|
||||
let mut body = chunks(&[MAX_REQUEST_SIZE]);
|
||||
let bytes = fetch_body(&mut body, None).await.unwrap();
|
||||
assert_eq!(bytes.len(), MAX_REQUEST_SIZE);
|
||||
}
|
||||
|
||||
fn chunks(lengths: &[usize]) -> impl Stream<Item = Result<Bytes, NetError>> + Unpin {
|
||||
stream::iter(
|
||||
lengths
|
||||
.iter()
|
||||
.map(|&len| Ok(Bytes::from(vec![0u8; len])))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//! Networking library for Hickory DNS
|
||||
|
||||
#![warn(clippy::dbg_macro, clippy::print_stdout, missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use hickory_proto as proto;
|
||||
|
||||
pub mod client;
|
||||
|
||||
#[cfg(feature = "__dnssec")]
|
||||
pub mod dnssec;
|
||||
|
||||
mod error;
|
||||
pub use error::{DnsError, ForwardNSData, NetError, NoRecords};
|
||||
|
||||
#[cfg(feature = "__https")]
|
||||
pub mod h2;
|
||||
#[cfg(feature = "__h3")]
|
||||
pub mod h3;
|
||||
#[cfg(any(feature = "__https", feature = "__h3"))]
|
||||
pub mod http;
|
||||
#[cfg(feature = "mdns")]
|
||||
pub mod multicast;
|
||||
#[cfg(all(feature = "__quic", feature = "tokio"))]
|
||||
pub mod quic;
|
||||
pub mod runtime;
|
||||
pub mod tcp;
|
||||
#[cfg(feature = "__tls")]
|
||||
pub mod tls;
|
||||
pub mod udp;
|
||||
pub mod xfer;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use crate::xfer::BufDnsStreamHandle;
|
||||
#[doc(hidden)]
|
||||
pub use crate::xfer::dns_handle::{DnsHandle, DnsStreamHandle};
|
||||
#[doc(hidden)]
|
||||
pub use crate::xfer::dns_multiplexer::DnsMultiplexer;
|
||||
#[doc(hidden)]
|
||||
pub use crate::xfer::retry_dns_handle::RetryDnsHandle;
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::net::{Ipv4Addr, SocketAddr};
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use std::future::Future;
|
||||
|
||||
use futures_util::stream::{Stream, StreamExt, TryStreamExt};
|
||||
|
||||
use crate::BufDnsStreamHandle;
|
||||
use crate::error::NetError;
|
||||
use crate::multicast::mdns_stream::{MDNS_IPV4, MDNS_IPV6};
|
||||
use crate::multicast::{MdnsQueryType, MdnsStream};
|
||||
use crate::proto::op::SerialMessage;
|
||||
use crate::runtime::TokioTime;
|
||||
use crate::xfer::DnsClientStream;
|
||||
|
||||
/// A UDP client stream of DNS binary packets
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct MdnsClientStream {
|
||||
mdns_stream: MdnsStream,
|
||||
}
|
||||
|
||||
impl MdnsClientStream {
|
||||
/// associates the socket to the well-known ipv4 multicast address
|
||||
pub fn new_ipv4(
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
) -> (
|
||||
impl Future<Output = Result<Self, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
Self::new(*MDNS_IPV4, mdns_query_type, packet_ttl, ipv4_if, None)
|
||||
}
|
||||
|
||||
/// associates the socket to the well-known ipv6 multicast address
|
||||
pub fn new_ipv6(
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv6_if: Option<u32>,
|
||||
) -> (
|
||||
impl Future<Output = Result<Self, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
Self::new(*MDNS_IPV6, mdns_query_type, packet_ttl, None, ipv6_if)
|
||||
}
|
||||
|
||||
/// it is expected that the resolver wrapper will be responsible for creating and managing
|
||||
/// new UdpClients such that each new client would have a random port (reduce chance of cache
|
||||
/// poisoning)
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// a tuple of a Future Stream which will handle sending and receiving messages, and a
|
||||
/// handle which can be used to send messages into the stream.
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
mdns_addr: SocketAddr,
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
ipv6_if: Option<u32>,
|
||||
) -> (
|
||||
impl Future<Output = Result<Self, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (stream_future, sender) =
|
||||
MdnsStream::new(mdns_addr, mdns_query_type, packet_ttl, ipv4_if, ipv6_if);
|
||||
|
||||
(
|
||||
async {
|
||||
Ok(Self {
|
||||
mdns_stream: stream_future.await?,
|
||||
})
|
||||
},
|
||||
sender,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsClientStream for MdnsClientStream {
|
||||
type Time = TokioTime;
|
||||
|
||||
fn name_server_addr(&self) -> SocketAddr {
|
||||
self.mdns_stream.multicast_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for MdnsClientStream {
|
||||
type Item = Result<SerialMessage, NetError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mdns_stream = &mut self.as_mut().mdns_stream;
|
||||
mdns_stream.map_err(NetError::from).poll_next_unpin(cx)
|
||||
// match ready!(self.mdns_stream.poll_next_unpin(cx).map_err(ProtoError::from)) {
|
||||
// Some(serial_message) => {
|
||||
// // TODO: for mDNS queries could come from anywhere. It's not clear that there is anything
|
||||
// // we can validate in this case.
|
||||
// Poll::Ready(Some(Ok(serial_message)))
|
||||
// }
|
||||
// None => Poll::Ready(None),
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::future::Future;
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use std::io;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use futures_util::{
|
||||
FutureExt,
|
||||
future::{self, BoxFuture},
|
||||
ready,
|
||||
stream::{Stream, StreamExt},
|
||||
};
|
||||
use socket2::{self, Socket};
|
||||
use tokio::net::UdpSocket;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::BufDnsStreamHandle;
|
||||
use crate::multicast::MdnsQueryType;
|
||||
use crate::proto::op::SerialMessage;
|
||||
use crate::runtime::TokioRuntimeProvider;
|
||||
use crate::udp::UdpStream;
|
||||
|
||||
pub(crate) const MDNS_PORT: u16 = 5353;
|
||||
/// mDNS ipv4 address, see [multicast-addresses](https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml)
|
||||
pub static MDNS_IPV4: LazyLock<SocketAddr> =
|
||||
LazyLock::new(|| SocketAddr::new(Ipv4Addr::new(224, 0, 0, 251).into(), MDNS_PORT));
|
||||
/// link-local mDNS ipv6 address, see [ipv6-multicast-addresses](https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml)
|
||||
pub static MDNS_IPV6: LazyLock<SocketAddr> = LazyLock::new(|| {
|
||||
SocketAddr::new(
|
||||
Ipv6Addr::new(0xFF02, 0, 0, 0, 0, 0, 0, 0x00FB).into(),
|
||||
MDNS_PORT,
|
||||
)
|
||||
});
|
||||
|
||||
/// A UDP stream of DNS binary packets
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct MdnsStream {
|
||||
/// Multicast address used for mDNS queries
|
||||
multicast_addr: SocketAddr,
|
||||
/// This is used for sending and (directly) receiving messages
|
||||
datagram: Option<UdpStream<TokioRuntimeProvider>>,
|
||||
// FIXME: like UdpStream, this Arc is unnecessary, only needed for temp async/await capture below
|
||||
/// In one-shot multicast, this will not join the multicast group
|
||||
multicast: Option<Arc<UdpSocket>>,
|
||||
/// Receiving portion of the MdnsStream
|
||||
rcving_mcast: Option<BoxFuture<'static, io::Result<SerialMessage>>>,
|
||||
}
|
||||
|
||||
impl MdnsStream {
|
||||
/// associates the socket to the well-known ipv4 multicast address
|
||||
pub fn new_ipv4(
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<Self, io::Error>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
Self::new(*MDNS_IPV4, mdns_query_type, packet_ttl, ipv4_if, None)
|
||||
}
|
||||
|
||||
/// associates the socket to the well-known ipv6 multicast address
|
||||
pub fn new_ipv6(
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv6_if: Option<u32>,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<Self, io::Error>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
Self::new(*MDNS_IPV6, mdns_query_type, packet_ttl, None, ipv6_if)
|
||||
}
|
||||
|
||||
/// Returns the address of the multicast network in use
|
||||
pub fn multicast_addr(&self) -> SocketAddr {
|
||||
self.multicast_addr
|
||||
}
|
||||
|
||||
/// This method is available for specifying a custom Multicast address to use.
|
||||
///
|
||||
/// In general this operates nearly identically to UDP, except that it automatically joins
|
||||
/// the default multicast DNS addresses. See <https://tools.ietf.org/html/rfc6762#section-5>
|
||||
/// for details.
|
||||
///
|
||||
/// When sending ipv6 multicast packets, the interface being used is required,
|
||||
/// this will panic if the interface is not specified for all MdnsQueryType except Passive
|
||||
/// (which does not allow sending data)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `multicast_addr` - address to use for multicast requests
|
||||
/// * `mdns_query_type` - true if the querier using this socket will only perform standard DNS queries over multicast.
|
||||
/// * `ipv4_if` - Address to bind to for sending multicast packets, defaults to `0.0.0.0` if not specified (not relevant for ipv6)
|
||||
/// * `ipv6_if` - Interface index for the interface to be used when sending ipv6 packets.
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// a tuple of a Future Stream which will handle sending and receiving messages, and a
|
||||
/// handle which can be used to send messages into the stream.
|
||||
pub fn new(
|
||||
multicast_addr: SocketAddr,
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
ipv6_if: Option<u32>,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<Self, io::Error>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(multicast_addr);
|
||||
let multicast_socket = match Self::join_multicast(&multicast_addr, mdns_query_type) {
|
||||
Ok(socket) => socket,
|
||||
Err(err) => return (Box::pin(future::err(err)), message_sender),
|
||||
};
|
||||
|
||||
// TODO: allow the bind address to be specified...
|
||||
// constructs a future for getting the next randomly bound port to a UdpSocket
|
||||
let next_socket = Self::next_bound_local_address(
|
||||
&multicast_addr,
|
||||
mdns_query_type,
|
||||
packet_ttl,
|
||||
ipv4_if,
|
||||
ipv6_if,
|
||||
);
|
||||
|
||||
// while 0 is meant to keep the packet on localhost, linux regards this as an error,
|
||||
// while macOS (BSD?) and Windows allow it.
|
||||
if let Some(ttl) = packet_ttl {
|
||||
assert!(ttl > 0, "TTL must be greater than 0");
|
||||
}
|
||||
|
||||
// This set of futures collapses the next udp socket into a stream which can be used for
|
||||
// sending and receiving udp packets.
|
||||
let stream = Box::pin(async move {
|
||||
let datagram = if let Some(socket) = next_socket.await? {
|
||||
socket.set_nonblocking(true)?;
|
||||
Some(UdpStream::from_parts(
|
||||
UdpSocket::from_std(socket)?,
|
||||
outbound_messages,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let multicast = if let Some(multicast_socket) = multicast_socket {
|
||||
Some(Arc::new(UdpSocket::from_std(multicast_socket)?))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
multicast_addr,
|
||||
datagram,
|
||||
multicast,
|
||||
rcving_mcast: None,
|
||||
})
|
||||
});
|
||||
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
||||
/// On Windows, unlike all Unix variants, it is improper to bind to the multicast address
|
||||
///
|
||||
/// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms737550(v=vs.85).aspx
|
||||
#[cfg(windows)]
|
||||
fn bind_multicast(socket: &Socket, multicast_addr: &SocketAddr) -> io::Result<()> {
|
||||
let multicast_addr = match multicast_addr {
|
||||
SocketAddr::V4(addr) => SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), addr.port()),
|
||||
SocketAddr::V6(addr) => SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), addr.port()),
|
||||
};
|
||||
socket.bind(&socket2::SockAddr::from(multicast_addr))
|
||||
}
|
||||
|
||||
/// On unixes we bind to the multicast address, which causes multicast packets to be filtered
|
||||
#[cfg(unix)]
|
||||
fn bind_multicast(socket: &Socket, multicast_addr: &SocketAddr) -> io::Result<()> {
|
||||
socket.bind(&socket2::SockAddr::from(*multicast_addr))
|
||||
}
|
||||
|
||||
/// Returns a socket joined to the multicast address
|
||||
fn join_multicast(
|
||||
multicast_addr: &SocketAddr,
|
||||
mdns_query_type: MdnsQueryType,
|
||||
) -> Result<Option<std::net::UdpSocket>, io::Error> {
|
||||
if !mdns_query_type.join_multicast() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let ip_addr = multicast_addr.ip();
|
||||
// it's an error to not use a proper mDNS address
|
||||
if !ip_addr.is_multicast() {
|
||||
return Err(io::Error::other(format!(
|
||||
"expected multicast address for binding: {ip_addr}"
|
||||
)));
|
||||
}
|
||||
|
||||
// binding the UdpSocket to the multicast address tells the OS to filter all packets on this socket to just this
|
||||
// multicast address
|
||||
// TODO: allow the binding interface to be specified
|
||||
let socket = match &ip_addr {
|
||||
IpAddr::V4(mdns_v4) => {
|
||||
let socket = Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::UDP),
|
||||
)?;
|
||||
socket.join_multicast_v4(mdns_v4, &Ipv4Addr::UNSPECIFIED)?;
|
||||
socket
|
||||
}
|
||||
IpAddr::V6(mdns_v6) => {
|
||||
let socket = Socket::new(
|
||||
socket2::Domain::IPV6,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::UDP),
|
||||
)?;
|
||||
|
||||
socket.set_only_v6(true)?;
|
||||
socket.join_multicast_v6(mdns_v6, 0)?;
|
||||
socket
|
||||
}
|
||||
};
|
||||
|
||||
socket.set_nonblocking(true)?;
|
||||
socket.set_reuse_address(true)?;
|
||||
#[cfg(unix)] // this is currently restricted to Unix's in socket2
|
||||
socket.set_reuse_port(true)?;
|
||||
Self::bind_multicast(&socket, multicast_addr)?;
|
||||
|
||||
debug!("joined {multicast_addr}");
|
||||
Ok(Some(std::net::UdpSocket::from(socket)))
|
||||
}
|
||||
|
||||
/// Creates a future for randomly binding to a local socket address for client connections.
|
||||
fn next_bound_local_address(
|
||||
multicast_addr: &SocketAddr,
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
ipv6_if: Option<u32>,
|
||||
) -> NextRandomUdpSocket {
|
||||
let bind_address: IpAddr = match multicast_addr {
|
||||
SocketAddr::V4(..) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
SocketAddr::V6(..) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
|
||||
};
|
||||
|
||||
NextRandomUdpSocket {
|
||||
bind_address,
|
||||
mdns_query_type,
|
||||
packet_ttl,
|
||||
ipv4_if,
|
||||
ipv6_if,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for MdnsStream {
|
||||
type Item = io::Result<SerialMessage>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
assert!(self.datagram.is_some() || self.multicast.is_some());
|
||||
|
||||
// we poll the datagram socket first, if available, since it's a direct response or direct request
|
||||
if let Some(datagram) = self.as_mut().datagram.as_mut() {
|
||||
match datagram.poll_next_unpin(cx) {
|
||||
Poll::Ready(ready) => return Poll::Ready(ready),
|
||||
Poll::Pending => (), // drop through
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let msg = if let Some(receiving) = self.rcving_mcast.as_mut() {
|
||||
// TODO: should we drop this packet if it's not from the same src as dest?
|
||||
let msg = ready!(receiving.as_mut().poll_unpin(cx))?;
|
||||
|
||||
Some(Poll::Ready(Some(Ok(msg))))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.rcving_mcast = None;
|
||||
|
||||
if let Some(msg) = msg {
|
||||
return msg;
|
||||
}
|
||||
|
||||
// let socket = Arc::clone(socket);
|
||||
if let Some(socket) = &self.multicast {
|
||||
let socket = Arc::clone(socket);
|
||||
let receive_future = async {
|
||||
let socket = socket;
|
||||
let mut buf = [0u8; 2_048];
|
||||
let (len, src) = socket.recv_from(&mut buf).await?;
|
||||
|
||||
Ok(SerialMessage::new(
|
||||
buf.iter().take(len).cloned().collect(),
|
||||
src,
|
||||
))
|
||||
};
|
||||
|
||||
self.rcving_mcast = Some(Box::pin(receive_future));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
struct NextRandomUdpSocket {
|
||||
bind_address: IpAddr,
|
||||
mdns_query_type: MdnsQueryType,
|
||||
packet_ttl: Option<u32>,
|
||||
ipv4_if: Option<Ipv4Addr>,
|
||||
ipv6_if: Option<u32>,
|
||||
}
|
||||
|
||||
impl NextRandomUdpSocket {
|
||||
fn prepare_sender(&self, socket: std::net::UdpSocket) -> io::Result<std::net::UdpSocket> {
|
||||
let addr = socket.local_addr()?;
|
||||
debug!("preparing sender on: {addr}");
|
||||
|
||||
let socket = Socket::from(socket);
|
||||
|
||||
// TODO: TTL doesn't work on ipv6
|
||||
match addr {
|
||||
SocketAddr::V4(..) => {
|
||||
socket.set_multicast_loop_v4(true)?;
|
||||
socket.set_multicast_if_v4(&self.ipv4_if.unwrap_or(Ipv4Addr::UNSPECIFIED))?;
|
||||
if let Some(ttl) = self.packet_ttl {
|
||||
socket.set_ttl_v4(ttl)?;
|
||||
socket.set_multicast_ttl_v4(ttl)?;
|
||||
}
|
||||
}
|
||||
SocketAddr::V6(..) => {
|
||||
let ipv6_if = self.ipv6_if.unwrap_or_else(|| {
|
||||
panic!("for ipv6 multicasting the interface must be specified")
|
||||
});
|
||||
|
||||
socket.set_multicast_loop_v6(true)?;
|
||||
socket.set_multicast_if_v6(ipv6_if)?;
|
||||
if let Some(ttl) = self.packet_ttl {
|
||||
socket.set_unicast_hops_v6(ttl)?;
|
||||
socket.set_multicast_hops_v6(ttl)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(std::net::UdpSocket::from(socket))
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for NextRandomUdpSocket {
|
||||
// TODO: clean this up, the RandomUdpSocket shouldn't care about the query type
|
||||
type Output = io::Result<Option<std::net::UdpSocket>>;
|
||||
|
||||
/// polls until there is an available next random UDP port.
|
||||
///
|
||||
/// if there is no port available after 10 attempts, returns NotReady
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// non-one-shot, i.e. continuous, always use one of the well-known mdns ports and bind to the multicast addr
|
||||
if !self.mdns_query_type.sender() {
|
||||
debug!("skipping sending stream");
|
||||
Poll::Ready(Ok(None))
|
||||
} else if self.mdns_query_type.bind_on_5353() {
|
||||
let addr = SocketAddr::new(self.bind_address, MDNS_PORT);
|
||||
debug!("binding sending stream to {}", addr);
|
||||
let socket = std::net::UdpSocket::bind(addr)?;
|
||||
let socket = self.prepare_sender(socket)?;
|
||||
|
||||
Poll::Ready(Ok(Some(socket)))
|
||||
} else {
|
||||
// TODO: this is basically identical to UdpStream from here... share some code? (except for the port restriction)
|
||||
// one-shot queries look very similar to UDP socket, but can't listen on 5353
|
||||
|
||||
for attempt in 0..10 {
|
||||
// Per RFC 6056 Section 2.1:
|
||||
//
|
||||
// The dynamic port range defined by IANA consists of the 49152-65535
|
||||
// range, and is meant for the selection of ephemeral ports.
|
||||
let port = rand::random_range(49152_u16..=u16::MAX);
|
||||
|
||||
// see one_shot usage info: https://tools.ietf.org/html/rfc6762#section-5
|
||||
// the MDNS_PORT is used to signal to remote processes that this is capable of receiving multicast packets
|
||||
// i.e. is joined to the multicast address.
|
||||
if port == MDNS_PORT {
|
||||
trace!("unlucky, got MDNS_PORT");
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr = SocketAddr::new(self.bind_address, port);
|
||||
debug!("binding sending stream to {}", addr);
|
||||
|
||||
match std::net::UdpSocket::bind(addr) {
|
||||
Ok(socket) => {
|
||||
let socket = self.prepare_sender(socket)?;
|
||||
return Poll::Ready(Ok(Some(socket)));
|
||||
}
|
||||
Err(err) => debug!("unable to bind port, attempt: {}: {}", attempt, err),
|
||||
}
|
||||
}
|
||||
|
||||
debug!("could not get next random port, delaying");
|
||||
|
||||
// TODO: this replaced a task::current().notify, is it correct?
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
#![allow(clippy::dbg_macro, clippy::print_stdout)]
|
||||
|
||||
use futures_util::future::Either;
|
||||
use std::println;
|
||||
use std::sync::LazyLock;
|
||||
use test_support::subscribe;
|
||||
use tokio::runtime;
|
||||
|
||||
use super::*;
|
||||
use crate::xfer::dns_handle::DnsStreamHandle;
|
||||
|
||||
// TODO: is there a better way?
|
||||
const BASE_TEST_PORT: u16 = 5379;
|
||||
|
||||
/// 250 appears to be unused/unregistered
|
||||
static TEST_MDNS_IPV4: LazyLock<IpAddr> =
|
||||
LazyLock::new(|| Ipv4Addr::new(224, 0, 0, 250).into());
|
||||
/// FA appears to be unused/unregistered
|
||||
static TEST_MDNS_IPV6: LazyLock<IpAddr> =
|
||||
LazyLock::new(|| Ipv6Addr::new(0xFF02, 0, 0, 0, 0, 0, 0, 0x00FA).into());
|
||||
|
||||
// one_shot tests are basically clones from the udp tests
|
||||
#[tokio::test]
|
||||
#[allow(unused_qualifications)] // https://github.com/rust-lang/rust/issues/149873
|
||||
async fn test_next_random_socket() {
|
||||
subscribe();
|
||||
|
||||
let (stream, _) = MdnsStream::new(
|
||||
SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT),
|
||||
MdnsQueryType::OneShot,
|
||||
Some(1),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let result = stream.await;
|
||||
|
||||
if let Err(error) = result {
|
||||
println!("Random address error: {error:#?}");
|
||||
panic!("failed to get next random address");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: reenable after breakage in async/await
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test_one_shot_mdns_ipv4() {
|
||||
subscribe();
|
||||
one_shot_mdns_test(SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_one_shot_mdns_ipv6() {
|
||||
subscribe();
|
||||
one_shot_mdns_test(SocketAddr::new(*TEST_MDNS_IPV6, BASE_TEST_PORT + 2));
|
||||
}
|
||||
|
||||
// as there are probably unexpected responses coming on the standard addresses
|
||||
fn one_shot_mdns_test(mdns_addr: SocketAddr) {
|
||||
use core::time::Duration;
|
||||
|
||||
let client_done = Arc::new(core::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
let test_bytes: &'static [u8; 8] = b"DEADBEEF";
|
||||
let send_recv_times = 10;
|
||||
let client_done_clone = client_done.clone();
|
||||
|
||||
// an in and out server
|
||||
let server_handle = std::thread::Builder::new()
|
||||
.name("test_one_shot_mdns:server".to_string())
|
||||
.spawn(move || {
|
||||
let server_loop = runtime::Runtime::new().unwrap();
|
||||
let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
|
||||
// TTLs are 0 so that multicast test packets never leave the test host...
|
||||
// FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases...
|
||||
let (server_stream_future, mut server_sender) = MdnsStream::new(
|
||||
mdns_addr,
|
||||
MdnsQueryType::OneShotJoin,
|
||||
Some(1),
|
||||
None,
|
||||
Some(5),
|
||||
);
|
||||
|
||||
// For one-shot responses we are competing with a system mDNS responder, we will respond from a different port...
|
||||
let mut server_stream = server_loop
|
||||
.block_on(server_stream_future)
|
||||
.expect("could not create mDNS listener")
|
||||
.into_future();
|
||||
|
||||
for _ in 0..=send_recv_times {
|
||||
if client_done_clone.load(core::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
// wait for some bytes...
|
||||
match server_loop.block_on(
|
||||
future::lazy(|_| future::select(server_stream, timeout)).flatten(),
|
||||
) {
|
||||
Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => {
|
||||
let (buffer_and_addr, stream_tmp): (
|
||||
Option<Result<SerialMessage, io::Error>>,
|
||||
MdnsStream,
|
||||
) = buffer_and_addr_stream_tmp;
|
||||
|
||||
server_stream = stream_tmp.into_future();
|
||||
timeout = timeout_tmp;
|
||||
let (buffer, addr) = buffer_and_addr
|
||||
.expect("no msg received")
|
||||
.expect("error receiving msg")
|
||||
.into_parts();
|
||||
|
||||
assert_eq!(&buffer, test_bytes);
|
||||
//println!("server got data! {}", addr);
|
||||
|
||||
// bounce them right back...
|
||||
server_sender
|
||||
.send(SerialMessage::new(test_bytes.to_vec(), addr))
|
||||
.expect("could not send to client");
|
||||
}
|
||||
Either::Right(((), buffer_and_addr_stream_tmp)) => {
|
||||
server_stream = buffer_and_addr_stream_tmp;
|
||||
timeout =
|
||||
future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
}
|
||||
}
|
||||
|
||||
// let the server turn for a bit... send the message
|
||||
server_loop.block_on(tokio::time::sleep(Duration::from_millis(100)));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
let io_loop = runtime::Runtime::new().unwrap();
|
||||
|
||||
// FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases...
|
||||
let (stream, mut sender) =
|
||||
MdnsStream::new(mdns_addr, MdnsQueryType::OneShot, Some(1), None, Some(5));
|
||||
let mut stream = io_loop.block_on(stream).unwrap().into_future();
|
||||
let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
let mut successes = 0;
|
||||
|
||||
for _ in 0..send_recv_times {
|
||||
// test once
|
||||
sender
|
||||
.send(SerialMessage::new(test_bytes.to_vec(), mdns_addr))
|
||||
.unwrap();
|
||||
|
||||
println!("client sending data!");
|
||||
|
||||
// TODO: this lazy isn't needed is it?
|
||||
match io_loop.block_on(future::lazy(|_| future::select(stream, timeout)).flatten()) {
|
||||
Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => {
|
||||
let (buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp;
|
||||
stream = stream_tmp.into_future();
|
||||
timeout = timeout_tmp;
|
||||
|
||||
let (buffer, _addr) = buffer_and_addr
|
||||
.expect("no msg received")
|
||||
.expect("error receiving msg")
|
||||
.into_parts();
|
||||
println!("client got data!");
|
||||
|
||||
assert_eq!(&buffer, test_bytes);
|
||||
successes += 1;
|
||||
}
|
||||
Either::Right(((), buffer_and_addr_stream_tmp)) => {
|
||||
stream = buffer_and_addr_stream_tmp;
|
||||
timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client_done.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
println!("successes: {successes}");
|
||||
assert!(successes >= 1);
|
||||
server_handle.join().expect("server thread failed");
|
||||
}
|
||||
|
||||
// FIXME: reenable after breakage in async/await
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test_passive_mdns() {
|
||||
subscribe();
|
||||
passive_mdns_test(
|
||||
MdnsQueryType::Passive,
|
||||
SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 3),
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: reenable after breakage in async/await
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test_oneshot_join_mdns() {
|
||||
subscribe();
|
||||
passive_mdns_test(
|
||||
MdnsQueryType::OneShotJoin,
|
||||
SocketAddr::new(*TEST_MDNS_IPV4, BASE_TEST_PORT + 4),
|
||||
)
|
||||
}
|
||||
|
||||
// as there are probably unexpected responses coming on the standard addresses
|
||||
fn passive_mdns_test(mdns_query_type: MdnsQueryType, mdns_addr: SocketAddr) {
|
||||
use core::time::Duration;
|
||||
|
||||
let server_got_packet = Arc::new(core::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
let test_bytes: &'static [u8; 8] = b"DEADBEEF";
|
||||
let send_recv_times = 10;
|
||||
let server_got_packet_clone = server_got_packet.clone();
|
||||
|
||||
// an in and out server
|
||||
let _server_handle = std::thread::Builder::new()
|
||||
.name("test_one_shot_mdns:server".to_string())
|
||||
.spawn(move || {
|
||||
let io_loop = runtime::Runtime::new().unwrap();
|
||||
let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
|
||||
// TTLs are 0 so that multicast test packets never leave the test host...
|
||||
// FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases...
|
||||
let (server_stream_future, _server_sender) =
|
||||
MdnsStream::new(mdns_addr, mdns_query_type, Some(1), None, Some(5));
|
||||
|
||||
// For one-shot responses we are competing with a system mDNS responder, we will respond from a different port...
|
||||
let mut server_stream = io_loop
|
||||
.block_on(server_stream_future)
|
||||
.expect("could not create mDNS listener")
|
||||
.into_future();
|
||||
|
||||
for _ in 0..=send_recv_times {
|
||||
// wait for some bytes...
|
||||
match io_loop.block_on(
|
||||
future::lazy(|_| future::select(server_stream, timeout)).flatten(),
|
||||
) {
|
||||
Either::Left((_buffer_and_addr_stream_tmp, _timeout_tmp)) => {
|
||||
// let (buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp;
|
||||
|
||||
// server_stream = stream_tmp.into_future();
|
||||
// timeout = timeout_tmp;
|
||||
// let (buffer, addr) = buffer_and_addr.expect("no buffer received");
|
||||
|
||||
// assert_eq!(&buffer, test_bytes);
|
||||
// println!("server got data! {}", addr);
|
||||
|
||||
server_got_packet_clone
|
||||
.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
Either::Right(((), buffer_and_addr_stream_tmp)) => {
|
||||
server_stream = buffer_and_addr_stream_tmp;
|
||||
timeout =
|
||||
future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
}
|
||||
}
|
||||
|
||||
// let the server turn for a bit... send the message
|
||||
io_loop.block_on(tokio::time::sleep(Duration::from_millis(100)));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
let io_loop = runtime::Runtime::new().unwrap();
|
||||
// FIXME: this is hardcoded to index 5 for ipv6, which isn't going to be correct in most cases...
|
||||
let (stream, mut sender) =
|
||||
MdnsStream::new(mdns_addr, MdnsQueryType::OneShot, Some(1), None, Some(5));
|
||||
let mut stream = io_loop.block_on(stream).unwrap().into_future();
|
||||
let mut timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
|
||||
for _ in 0..send_recv_times {
|
||||
// test once
|
||||
sender
|
||||
.send(SerialMessage::new(test_bytes.to_vec(), mdns_addr))
|
||||
.unwrap();
|
||||
|
||||
println!("client sending data!");
|
||||
|
||||
// TODO: this lazy is probably unnecessary?
|
||||
let run_result =
|
||||
io_loop.block_on(future::lazy(|_| future::select(stream, timeout)).flatten());
|
||||
|
||||
if server_got_packet.load(core::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match run_result {
|
||||
Either::Left((buffer_and_addr_stream_tmp, timeout_tmp)) => {
|
||||
let (_buffer_and_addr, stream_tmp) = buffer_and_addr_stream_tmp;
|
||||
stream = stream_tmp.into_future();
|
||||
timeout = timeout_tmp;
|
||||
}
|
||||
Either::Right(((), buffer_and_addr_stream_tmp)) => {
|
||||
stream = buffer_and_addr_stream_tmp;
|
||||
timeout = future::lazy(|_| tokio::time::sleep(Duration::from_millis(100)))
|
||||
.flatten()
|
||||
.boxed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panic!("server never got packet.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! Multicast protocol related components for DNS
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
mod mdns_client_stream;
|
||||
#[cfg(feature = "tokio")]
|
||||
mod mdns_stream;
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
pub use self::mdns_client_stream::MdnsClientStream;
|
||||
#[cfg(feature = "tokio")]
|
||||
pub use self::mdns_stream::{MDNS_IPV4, MDNS_IPV6, MdnsStream};
|
||||
|
||||
/// See [rfc6762](https://tools.ietf.org/html/rfc6762#section-5) details on these different types.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MdnsQueryType {
|
||||
/// The querier using this socket will only perform standard DNS queries over multicast. (clients only)
|
||||
///
|
||||
/// Effectively treats mDNS as essentially no different than any other DNS query; one request followed by one response.
|
||||
/// Only one UDP socket will be created.
|
||||
OneShot,
|
||||
/// The querier is fully compliant with [rfc6762](https://tools.ietf.org/html/rfc6762#section-5). (servers, clients)
|
||||
///
|
||||
/// mDNS capable clients will sent messages with many queries, and they will expect many responses. Two UDP sockets will be
|
||||
/// created, one for receiving multicast traffic, the other used for sending queries and direct responses. This requires
|
||||
/// port 5353 to be available on the system (many modern OSes already have mDNSResponders running taking this port).
|
||||
Continuous,
|
||||
/// The querier operates under the OneShot semantics, but also joins the multicast group. (non-compliant servers, clients)
|
||||
///
|
||||
/// This is not defined in the mDNS RFC, but allows for a multicast client to join the group, receiving all multicast network
|
||||
/// traffic. This is useful where listening for all mDNS traffic is of interest, but because another mDNS process may have
|
||||
/// already taken the known port, 5353. Query responses will come from and to the standard UDP socket with a random port,
|
||||
/// multicast traffic will come from the multicast socket. This will create two sockets.
|
||||
OneShotJoin,
|
||||
/// The querier operates under the OneShot semantics, but also joins the multicast group. (servers)
|
||||
///
|
||||
/// Not defined in the RFC, allows for a passive listener to receive all mDNS traffic.
|
||||
Passive,
|
||||
}
|
||||
|
||||
impl MdnsQueryType {
|
||||
/// This will be sending packets, i.e. a standard UDP socket will be created
|
||||
pub fn sender(self) -> bool {
|
||||
match self {
|
||||
Self::Passive => false,
|
||||
Self::OneShot | Self::OneShotJoin => true,
|
||||
Self::Continuous => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this process can bind to *:5353
|
||||
pub fn bind_on_5353(self) -> bool {
|
||||
match self {
|
||||
Self::OneShot | Self::OneShotJoin | Self::Passive => false,
|
||||
Self::Continuous => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this mDNS client should join, listen, on the multicast address
|
||||
pub fn join_multicast(self) -> bool {
|
||||
match self {
|
||||
Self::OneShot => false,
|
||||
Self::Continuous | Self::OneShotJoin | Self::Passive => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! QUIC protocol related components for DNS over QUIC (DoQ)
|
||||
|
||||
mod quic_client_stream;
|
||||
mod quic_config;
|
||||
mod quic_server;
|
||||
mod quic_stream;
|
||||
|
||||
#[cfg(feature = "__h3")]
|
||||
pub(crate) use self::quic_client_stream::connect_quic;
|
||||
pub use self::quic_client_stream::{QuicClientStream, QuicClientStreamBuilder};
|
||||
pub use self::quic_server::{QuicServer, QuicStreams};
|
||||
pub use self::quic_stream::{DoqErrorCode, QuicStream};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,407 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::{
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::stream::Stream;
|
||||
use quinn::{
|
||||
ClientConfig, Connection, Endpoint, TransportConfig, VarInt, crypto::rustls::QuicClientConfig,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{
|
||||
error::NetError,
|
||||
proto::op::{DnsRequest, DnsResponse},
|
||||
quic::quic_stream::{DoqErrorCode, QuicStream},
|
||||
runtime::{RuntimeProvider, Spawn},
|
||||
tls::client_config,
|
||||
udp::UdpSocket,
|
||||
xfer::{CONNECT_TIMEOUT, DnsExchange, DnsRequestSender, DnsResponseStream},
|
||||
};
|
||||
|
||||
use super::{quic_config, quic_stream};
|
||||
|
||||
/// A DNS client connection for DNS-over-QUIC
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
#[derive(Clone)]
|
||||
pub struct QuicClientStream {
|
||||
quic_connection: Connection,
|
||||
server_name: Arc<str>,
|
||||
name_server: SocketAddr,
|
||||
is_shutdown: bool,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Display for QuicClientStream {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(formatter, "QUIC({},{})", self.name_server, self.server_name)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuicClientStream {
|
||||
/// Builder for QuicClientStream
|
||||
pub fn builder() -> QuicClientStreamBuilder {
|
||||
QuicClientStreamBuilder::default()
|
||||
}
|
||||
|
||||
async fn inner_send(
|
||||
connection: Connection,
|
||||
message: DnsRequest,
|
||||
) -> Result<DnsResponse, NetError> {
|
||||
let (send_stream, recv_stream) = connection.open_bi().await.map_err(NetError::from)?;
|
||||
|
||||
// RFC: The mapping specified here requires that the client selects a separate
|
||||
// QUIC stream for each query. The server then uses the same stream to provide all the response messages for that query.
|
||||
let mut stream = QuicStream::new(send_stream, recv_stream);
|
||||
|
||||
stream.send(message.into_parts().0).await?;
|
||||
|
||||
// The client MUST send the DNS query over the selected stream,
|
||||
// and MUST indicate through the STREAM FIN mechanism that no further data will be sent on that stream.
|
||||
stream.finish().await?;
|
||||
|
||||
stream.receive().await
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsRequestSender for QuicClientStream {
|
||||
/// The send loop for QUIC in DNS stipulates that a new QUIC "stream" should be opened and use for sending data.
|
||||
///
|
||||
/// It should be closed after receiving the response. TODO: AXFR/IXFR support...
|
||||
///
|
||||
/// ```text
|
||||
/// RFC 9250 DNS over Dedicated QUIC Connections
|
||||
///
|
||||
/// 4.2. Stream Mapping and Usage
|
||||
///
|
||||
/// The mapping of DNS traffic over QUIC streams takes advantage of the
|
||||
/// QUIC stream features detailed in Section 2 of [RFC9000], the QUIC
|
||||
/// transport specification.
|
||||
///
|
||||
/// DNS query/response traffic [RFC1034] [RFC1035] follows a simple
|
||||
/// pattern in which the client sends a query, and the server provides
|
||||
/// one or more responses (multiple responses can occur in zone
|
||||
/// transfers).
|
||||
///
|
||||
/// The mapping specified here requires that the client select a separate
|
||||
/// QUIC stream for each query. The server then uses the same stream to
|
||||
/// provide all the response messages for that query. In order for
|
||||
/// multiple responses to be parsed, a 2-octet length field is used in
|
||||
/// exactly the same way as the 2-octet length field defined for DNS over
|
||||
/// TCP [RFC1035]. The practical result of this is that the content of
|
||||
/// each QUIC stream is exactly the same as the content of a TCP
|
||||
/// connection that would manage exactly one query.
|
||||
///
|
||||
/// All DNS messages (queries and responses) sent over DoQ connections
|
||||
/// MUST be encoded as a 2-octet length field followed by the message
|
||||
/// content as specified in [RFC1035].
|
||||
///
|
||||
/// The client MUST select the next available client-initiated
|
||||
/// bidirectional stream for each subsequent query on a QUIC connection,
|
||||
/// in conformance with the QUIC transport specification [RFC9000].
|
||||
/// Packet losses and other network events might cause queries to arrive
|
||||
/// in a different order. Servers SHOULD process queries as they arrive,
|
||||
/// as not doing so would cause unnecessary delays.
|
||||
///
|
||||
/// The client MUST send the DNS query over the selected stream and MUST
|
||||
/// indicate through the STREAM FIN mechanism that no further data will
|
||||
/// be sent on that stream.
|
||||
///
|
||||
/// The server MUST send the response(s) on the same stream and MUST
|
||||
/// indicate, after the last response, through the STREAM FIN mechanism
|
||||
/// that no further data will be sent on that stream.
|
||||
///
|
||||
/// Therefore, a single DNS transaction consumes a single bidirectional
|
||||
/// client-initiated stream. This means that the client's first query
|
||||
/// occurs on QUIC stream 0, the second on 4, and so on (see Section 2.1
|
||||
/// of [RFC9000]).
|
||||
///
|
||||
/// Servers MAY defer processing of a query until the STREAM FIN has been
|
||||
/// indicated on the stream selected by the client.
|
||||
///
|
||||
/// Servers and clients MAY monitor the number of "dangling" streams.
|
||||
/// These are open streams where the following events have not occurred
|
||||
/// after implementation-defined timeouts:
|
||||
///
|
||||
/// * the expected queries or responses have not been received or,
|
||||
///
|
||||
/// * the expected queries or responses have been received but not the
|
||||
/// STREAM FIN
|
||||
///
|
||||
/// Implementations MAY impose a limit on the number of such dangling
|
||||
/// streams. If limits are encountered, implementations MAY close the
|
||||
/// connection.
|
||||
///
|
||||
/// 4.2.1. DNS Message IDs
|
||||
///
|
||||
/// When sending queries over a QUIC connection, the DNS Message ID MUST
|
||||
/// be set to 0. The stream mapping for DoQ allows for unambiguous
|
||||
/// correlation of queries and responses, so the Message ID field is not
|
||||
/// required.
|
||||
///
|
||||
/// This has implications for proxying DoQ messages to and from other
|
||||
/// transports. For example, proxies may have to manage the fact that
|
||||
/// DoQ can support a larger number of outstanding queries on a single
|
||||
/// connection than, for example, DNS over TCP, because DoQ is not
|
||||
/// limited by the Message ID space. This issue already exists for DoH,
|
||||
/// where a Message ID of 0 is recommended.
|
||||
///
|
||||
/// When forwarding a DNS message from DoQ over another transport, a DNS
|
||||
/// Message ID MUST be generated according to the rules of the protocol
|
||||
/// that is in use. When forwarding a DNS message from another transport
|
||||
/// over DoQ, the Message ID MUST be set to 0.
|
||||
/// ```
|
||||
fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream {
|
||||
if self.is_shutdown {
|
||||
panic!("can not send messages after stream is shutdown")
|
||||
}
|
||||
|
||||
let send_fut = Self::inner_send(self.quic_connection.clone(), request);
|
||||
|
||||
let Some(request_timeout) = self.request_timeout else {
|
||||
return Box::pin(send_fut).into();
|
||||
};
|
||||
|
||||
Box::pin(async move {
|
||||
timeout(request_timeout, send_fut)
|
||||
.await
|
||||
.map_err(|_| NetError::Timeout)?
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
self.is_shutdown = true;
|
||||
self.quic_connection
|
||||
.close(DoqErrorCode::NoError.into(), b"Shutdown");
|
||||
}
|
||||
|
||||
fn is_shutdown(&self) -> bool {
|
||||
self.is_shutdown
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for QuicClientStream {
|
||||
type Item = Result<(), NetError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.is_shutdown {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A QUIC connection builder for DNS-over-QUIC
|
||||
#[derive(Clone)]
|
||||
pub struct QuicClientStreamBuilder {
|
||||
crypto_config: Option<rustls::ClientConfig>,
|
||||
transport_config: Arc<TransportConfig>,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl QuicClientStreamBuilder {
|
||||
/// Constructs a new TlsStreamBuilder with the associated ClientConfig
|
||||
pub fn crypto_config(mut self, crypto_config: rustls::ClientConfig) -> Self {
|
||||
self.crypto_config = Some(crypto_config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the address to connect from.
|
||||
pub fn bind_addr(mut self, bind_addr: SocketAddr) -> Self {
|
||||
self.bind_addr = Some(bind_addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the per-request timeout applied to each DNS send over the QUIC connection.
|
||||
///
|
||||
/// When set, each call to [`DnsRequestSender::send_message`] will be cancelled with
|
||||
/// [`NetError::Timeout`] if no response arrives within this duration.
|
||||
pub fn request_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.request_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new QuicStream to the specified name_server
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `server_name` - The DNS name associated with a certificate
|
||||
pub fn build(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
) -> impl Future<Output = Result<QuicClientStream, NetError>> + Send + 'static {
|
||||
self.connect(name_server, server_name)
|
||||
}
|
||||
|
||||
/// Creates a new [`DnsExchange`] wrapping the [`QuicClientStream`] from this builder
|
||||
pub async fn exchange<P: RuntimeProvider>(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
provider: P,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
let stream = self
|
||||
.connect_with_future(socket, name_server, server_name)
|
||||
.await?;
|
||||
let (exchange, bg) = DnsExchange::from_stream(stream);
|
||||
provider.create_handle().spawn_bg(bg);
|
||||
Ok(exchange)
|
||||
}
|
||||
|
||||
/// Create a QuicStream with existing connection
|
||||
pub fn build_with_future(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
) -> impl Future<Output = Result<QuicClientStream, NetError>> + Send + 'static {
|
||||
self.connect_with_future(socket, name_server, server_name)
|
||||
}
|
||||
|
||||
async fn connect_with_future(
|
||||
self,
|
||||
socket: Arc<dyn quinn::AsyncUdpSocket>,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
) -> Result<QuicClientStream, NetError> {
|
||||
let endpoint_config = quic_config::endpoint();
|
||||
let endpoint = Endpoint::new_with_abstract_socket(
|
||||
endpoint_config,
|
||||
None,
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
self.connect_inner(endpoint, name_server, server_name).await
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
self,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
) -> Result<QuicClientStream, NetError> {
|
||||
let connect = if let Some(bind_addr) = self.bind_addr {
|
||||
<tokio::net::UdpSocket as UdpSocket>::connect_with_bind(name_server, bind_addr)
|
||||
} else {
|
||||
<tokio::net::UdpSocket as UdpSocket>::connect(name_server)
|
||||
};
|
||||
|
||||
let socket = connect.await?;
|
||||
let socket = socket.into_std()?;
|
||||
let endpoint_config = quic_config::endpoint();
|
||||
let endpoint = Endpoint::new(endpoint_config, None, socket, Arc::new(quinn::TokioRuntime))?;
|
||||
self.connect_inner(endpoint, name_server, server_name).await
|
||||
}
|
||||
|
||||
async fn connect_inner(
|
||||
self,
|
||||
endpoint: Endpoint,
|
||||
name_server: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
) -> Result<QuicClientStream, NetError> {
|
||||
// ensure the ALPN protocol is set correctly
|
||||
let crypto_config = if let Some(crypto_config) = self.crypto_config {
|
||||
crypto_config
|
||||
} else {
|
||||
client_config().map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("failed to initialize default TLS client config: {e}"),
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let quic_connection = connect_quic(
|
||||
name_server,
|
||||
server_name.clone(),
|
||||
quic_stream::DOQ_ALPN,
|
||||
crypto_config,
|
||||
self.transport_config,
|
||||
endpoint,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(QuicClientStream {
|
||||
quic_connection,
|
||||
server_name,
|
||||
name_server,
|
||||
is_shutdown: false,
|
||||
request_timeout: self.request_timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_quic(
|
||||
addr: SocketAddr,
|
||||
server_name: Arc<str>,
|
||||
protocol: &[u8],
|
||||
mut crypto_config: rustls::ClientConfig,
|
||||
transport_config: Arc<TransportConfig>,
|
||||
mut endpoint: Endpoint,
|
||||
) -> Result<Connection, NetError> {
|
||||
if crypto_config.alpn_protocols.is_empty() {
|
||||
crypto_config.alpn_protocols = vec![protocol.to_vec()];
|
||||
}
|
||||
let early_data_enabled = crypto_config.enable_early_data;
|
||||
|
||||
let mut client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(crypto_config)?));
|
||||
client_config.transport_config(transport_config.clone());
|
||||
|
||||
endpoint.set_default_client_config(client_config);
|
||||
|
||||
let connecting = endpoint.connect(addr, &server_name)?;
|
||||
// TODO: for Client/Dynamic update, don't use RTT, for queries, do use it.
|
||||
|
||||
Ok(if early_data_enabled {
|
||||
match connecting.into_0rtt() {
|
||||
Ok((new_connection, _)) => new_connection,
|
||||
Err(connecting) => connect_with_timeout(connecting).await?,
|
||||
}
|
||||
} else {
|
||||
connect_with_timeout(connecting).await?
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_with_timeout(connecting: quinn::Connecting) -> Result<Connection, io::Error> {
|
||||
match timeout(CONNECT_TIMEOUT, connecting).await {
|
||||
Ok(Ok(connection)) => Ok(connection),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
format!("QUIC handshake timed out after {CONNECT_TIMEOUT:?}",),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QuicClientStreamBuilder {
|
||||
fn default() -> Self {
|
||||
let mut transport_config = quic_config::transport();
|
||||
// clients never accept new bidirectional streams
|
||||
transport_config.max_concurrent_bidi_streams(VarInt::from_u32(0));
|
||||
|
||||
Self {
|
||||
crypto_config: None,
|
||||
transport_config: Arc::new(transport_config),
|
||||
bind_addr: None,
|
||||
request_timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use quinn::{EndpointConfig, TransportConfig, VarInt};
|
||||
|
||||
/// Returns a default endpoint configuration for DNS-over-QUIC
|
||||
pub(crate) fn endpoint() -> EndpointConfig {
|
||||
// set some better EndpointConfig defaults for DoQ
|
||||
let mut endpoint_config = EndpointConfig::default();
|
||||
|
||||
// all DNS packets have a maximum size of u16 due to DoQ and 1035 rfc
|
||||
// TODO: the RFC claims max == u16::max, but this matches the max in some test servers.
|
||||
endpoint_config
|
||||
.max_udp_payload_size(0x45ac)
|
||||
.expect("max udp payload size exceeded");
|
||||
|
||||
endpoint_config
|
||||
}
|
||||
|
||||
/// Returns a default endpoint configuration for DNS-over-QUIC
|
||||
pub(crate) fn transport() -> TransportConfig {
|
||||
let mut transport_config = TransportConfig::default();
|
||||
|
||||
transport_config.max_concurrent_uni_streams(VarInt::from_u32(0));
|
||||
transport_config.datagram_receive_buffer_size(None);
|
||||
transport_config.datagram_send_buffer_size(0);
|
||||
|
||||
transport_config
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::net::SocketAddr;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use quinn::crypto::rustls::QuicServerConfig;
|
||||
use quinn::{Connection, Endpoint, ServerConfig};
|
||||
use rustls::server::ResolvesServerCert;
|
||||
use rustls::server::ServerConfig as TlsServerConfig;
|
||||
use rustls::version::TLS13;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{error::NetError, tls::default_provider, udp::UdpSocket};
|
||||
|
||||
use super::{
|
||||
quic_config,
|
||||
quic_stream::{self, QuicStream},
|
||||
};
|
||||
|
||||
/// A DNS-over-QUIC Server, see QuicClientStream for the client counterpart
|
||||
pub struct QuicServer {
|
||||
endpoint: Endpoint,
|
||||
}
|
||||
|
||||
impl QuicServer {
|
||||
/// Construct the new Acceptor with the associated pkcs12 data
|
||||
pub async fn new(
|
||||
name_server: SocketAddr,
|
||||
server_cert_resolver: Arc<dyn ResolvesServerCert>,
|
||||
) -> Result<Self, NetError> {
|
||||
// setup a new socket for the server to use
|
||||
let socket = <tokio::net::UdpSocket as UdpSocket>::bind(name_server).await?;
|
||||
Self::with_socket(socket, server_cert_resolver)
|
||||
}
|
||||
|
||||
/// Construct the new server with an existing socket and a default TLS configuration
|
||||
pub fn with_socket(
|
||||
socket: tokio::net::UdpSocket,
|
||||
server_cert_resolver: Arc<dyn ResolvesServerCert>,
|
||||
) -> Result<Self, NetError> {
|
||||
let mut config = TlsServerConfig::builder_with_provider(Arc::new(default_provider()))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap() // The ring default provider is guaranteed to support TLS 1.3
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(server_cert_resolver);
|
||||
|
||||
config.alpn_protocols = vec![quic_stream::DOQ_ALPN.to_vec()];
|
||||
|
||||
Self::with_socket_and_tls_config(socket, Arc::new(config))
|
||||
}
|
||||
|
||||
/// Construct the new server with an existing socket and a custom TLS configuration
|
||||
///
|
||||
/// The caller must ensure the `TlsServerConfig` has the appropriate DoQ ALPN protocol enabled.
|
||||
pub fn with_socket_and_tls_config(
|
||||
socket: tokio::net::UdpSocket,
|
||||
tls_config: Arc<TlsServerConfig>,
|
||||
) -> Result<Self, NetError> {
|
||||
let mut server_config =
|
||||
ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(tls_config)?));
|
||||
server_config.transport = Arc::new(quic_config::transport());
|
||||
|
||||
let socket = socket.into_std()?;
|
||||
|
||||
let endpoint_config = quic_config::endpoint();
|
||||
let endpoint = Endpoint::new(
|
||||
endpoint_config,
|
||||
Some(server_config),
|
||||
socket,
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
|
||||
Ok(Self { endpoint })
|
||||
}
|
||||
|
||||
/// Get the next incoming stream
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A remote connection that could have many potential bi-directional streams and the remote socket address
|
||||
pub async fn next(&mut self) -> Result<Option<(QuicStreams, SocketAddr)>, NetError> {
|
||||
let Some(connecting) = self.endpoint.accept().await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let remote_addr = connecting.remote_address();
|
||||
let connection = timeout(Duration::from_secs(5), connecting)
|
||||
.await
|
||||
.map_err(|_| NetError::from("quic timeout expired during handshake"))??;
|
||||
Ok(Some((QuicStreams { connection }, remote_addr)))
|
||||
}
|
||||
|
||||
/// Returns the address this server is listening on
|
||||
///
|
||||
/// This can be useful in tests, where a random port can be associated with the server by binding on `127.0.0.1:0` and then getting the
|
||||
/// associated port address with this function.
|
||||
pub fn local_addr(&self) -> Result<SocketAddr, io::Error> {
|
||||
self.endpoint.local_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream of bi-directional QUIC streams
|
||||
pub struct QuicStreams {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl QuicStreams {
|
||||
/// Get the next bi directional stream from the client
|
||||
pub async fn next(&mut self) -> Option<Result<QuicStream, NetError>> {
|
||||
match self.connection.accept_bi().await {
|
||||
Ok((send_stream, receive_stream)) => {
|
||||
Some(Ok(QuicStream::new(send_stream, receive_stream)))
|
||||
}
|
||||
Err(e) => Some(Err(NetError::from(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use quinn::{RecvStream, SendStream, VarInt};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
error::NetError,
|
||||
proto::{
|
||||
ProtoError,
|
||||
op::{DnsResponse, Message},
|
||||
},
|
||||
};
|
||||
|
||||
/// ```text
|
||||
/// 4.1. Connection Establishment
|
||||
///
|
||||
/// DoQ connections are established as described in the QUIC transport
|
||||
/// specification [RFC9000]. During connection establishment, DoQ
|
||||
/// support is indicated by selecting the Application-Layer Protocol
|
||||
/// Negotiation (ALPN) token "doq" in the crypto handshake.
|
||||
/// ```
|
||||
pub(crate) const DOQ_ALPN: &[u8] = b"doq";
|
||||
|
||||
/// [RFC 9250](https://www.rfc-editor.org/rfc/rfc9250.html#name-doq-error-codes)
|
||||
/// ```text
|
||||
/// 4.3. DoQ Error Codes
|
||||
///
|
||||
/// The following error codes are defined for use when abruptly
|
||||
/// terminating streams, for use as application protocol error codes when
|
||||
/// aborting reading of streams, or for immediately closing connections:
|
||||
///
|
||||
/// DOQ_NO_ERROR (0x0): No error. This is used when the connection or
|
||||
/// stream needs to be closed, but there is no error to signal.
|
||||
///
|
||||
/// DOQ_INTERNAL_ERROR (0x1): The DoQ implementation encountered an
|
||||
/// internal error and is incapable of pursuing the transaction or the
|
||||
/// connection.
|
||||
///
|
||||
/// DOQ_PROTOCOL_ERROR (0x2): The DoQ implementation encountered a
|
||||
/// protocol error and is forcibly aborting the connection.
|
||||
///
|
||||
/// DOQ_REQUEST_CANCELLED (0x3): A DoQ client uses this to signal that
|
||||
/// it wants to cancel an outstanding transaction.
|
||||
///
|
||||
/// DOQ_EXCESSIVE_LOAD (0x4): A DoQ implementation uses this to signal
|
||||
/// when closing a connection due to excessive load.
|
||||
///
|
||||
/// DOQ_UNSPECIFIED_ERROR (0x5): A DoQ implementation uses this in the
|
||||
/// absence of a more specific error code.
|
||||
///
|
||||
/// DOQ_ERROR_RESERVED (0xd098ea5e): An alternative error code used for
|
||||
/// tests.
|
||||
/// ```
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DoqErrorCode {
|
||||
/// No error. This is used when the connection or stream needs to be closed, but there is no error to signal.
|
||||
NoError,
|
||||
/// The DoQ implementation encountered an internal error and is incapable of pursuing the transaction or the connection.
|
||||
InternalError,
|
||||
/// The DoQ implementation encountered a protocol error and is forcibly aborting the connection.
|
||||
ProtocolError,
|
||||
/// A DoQ client uses this to signal that it wants to cancel an outstanding transaction.
|
||||
RequestCancelled,
|
||||
/// A DoQ implementation uses this to signal when closing a connection due to excessive load.
|
||||
ExcessiveLoad,
|
||||
/// An alternative error code used for tests.
|
||||
ErrorReserved,
|
||||
/// Unknown Error code
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
// not using repr(u32) above because of the Unknown
|
||||
const NO_ERROR: u32 = 0x0;
|
||||
const INTERNAL_ERROR: u32 = 0x1;
|
||||
const PROTOCOL_ERROR: u32 = 0x2;
|
||||
const REQUEST_CANCELLED: u32 = 0x3;
|
||||
const EXCESSIVE_LOAD: u32 = 0x4;
|
||||
const ERROR_RESERVED: u32 = 0xd098ea5e;
|
||||
|
||||
impl From<DoqErrorCode> for VarInt {
|
||||
fn from(doq_error: DoqErrorCode) -> Self {
|
||||
use DoqErrorCode::*;
|
||||
|
||||
match doq_error {
|
||||
NoError => Self::from_u32(NO_ERROR),
|
||||
InternalError => Self::from_u32(INTERNAL_ERROR),
|
||||
ProtocolError => Self::from_u32(PROTOCOL_ERROR),
|
||||
RequestCancelled => Self::from_u32(REQUEST_CANCELLED),
|
||||
ExcessiveLoad => Self::from_u32(EXCESSIVE_LOAD),
|
||||
ErrorReserved => Self::from_u32(ERROR_RESERVED),
|
||||
Unknown(code) => Self::from_u32(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VarInt> for DoqErrorCode {
|
||||
fn from(doq_error: VarInt) -> Self {
|
||||
let code: u32 = if let Ok(code) = doq_error.into_inner().try_into() {
|
||||
code
|
||||
} else {
|
||||
return Self::ProtocolError;
|
||||
};
|
||||
|
||||
match code {
|
||||
NO_ERROR => Self::NoError,
|
||||
INTERNAL_ERROR => Self::InternalError,
|
||||
PROTOCOL_ERROR => Self::ProtocolError,
|
||||
REQUEST_CANCELLED => Self::RequestCancelled,
|
||||
EXCESSIVE_LOAD => Self::ExcessiveLoad,
|
||||
ERROR_RESERVED => Self::ErrorReserved,
|
||||
_ => Self::Unknown(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single bi-directional stream
|
||||
pub struct QuicStream {
|
||||
send_stream: SendStream,
|
||||
receive_stream: RecvStream,
|
||||
}
|
||||
|
||||
impl QuicStream {
|
||||
pub(crate) fn new(send_stream: SendStream, receive_stream: RecvStream) -> Self {
|
||||
Self {
|
||||
send_stream,
|
||||
receive_stream,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the DNS message to the other side
|
||||
pub async fn send(&mut self, mut message: Message) -> Result<(), NetError> {
|
||||
// RFC: When sending queries over a QUIC connection, the DNS Message ID MUST be set to 0.
|
||||
// The stream mapping for DoQ allows for unambiguous correlation of queries and responses,
|
||||
// so the Message ID field is not required.
|
||||
|
||||
message.metadata.id = 0;
|
||||
|
||||
let bytes = Bytes::from(message.to_vec()?);
|
||||
|
||||
self.send_bytes(bytes).await
|
||||
}
|
||||
|
||||
/// Send pre-encoded bytes, warning, QUIC requires the message id to be 0.
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> Result<(), NetError> {
|
||||
// In order for multiple responses to be parsed, a 2-octet length field is used in exactly
|
||||
// the same way as the 2-octet length field defined for DNS over TCP [RFC1035]. The
|
||||
// practical result of this is that the content of each QUIC stream is exactly the same as
|
||||
// the content of a TCP connection that would manage exactly one query.
|
||||
//
|
||||
// All DNS messages (queries and responses) sent over DoQ connections MUST be encoded as a
|
||||
// 2-octet length field followed by the message content as specified in [RFC1035].
|
||||
let bytes_len = u16::try_from(bytes.len())
|
||||
.map_err(|_e| NetError::from(ProtoError::MaxBufferSizeExceeded(bytes.len())))?;
|
||||
let len = bytes_len.to_be_bytes().to_vec();
|
||||
let len = Bytes::from(len);
|
||||
|
||||
debug!("received packet len: {} bytes: {:x?}", bytes_len, bytes);
|
||||
self.send_stream.write_all_chunks(&mut [len, bytes]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// finishes the send stream, i.e. there will be no more data sent to the remote
|
||||
pub async fn finish(&mut self) -> Result<(), NetError> {
|
||||
self.send_stream.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive a single packet
|
||||
pub async fn receive(&mut self) -> Result<DnsResponse, NetError> {
|
||||
let bytes = self.receive_bytes().await?;
|
||||
let message = Message::from_vec(&bytes)?;
|
||||
|
||||
// assert that the message id is 0, this is a bad dns-over-quic packet if not
|
||||
if message.id != 0 {
|
||||
if let Err(error) = self.reset(DoqErrorCode::ProtocolError) {
|
||||
debug!(%error, "stream already closed");
|
||||
}
|
||||
return Err(NetError::QuicMessageIdNot0(message.id));
|
||||
}
|
||||
|
||||
Ok(DnsResponse::from_buffer(bytes.to_vec())?)
|
||||
}
|
||||
|
||||
// TODO: we should change the protocol handlers to work with Messages since some require things like 0 for the Message ID.
|
||||
/// Receive a single packet as raw bytes
|
||||
pub async fn receive_bytes(&mut self) -> Result<BytesMut, NetError> {
|
||||
// following above, the data should be first the length, followed by the message(s)
|
||||
let mut len = [0u8; 2];
|
||||
self.receive_stream.read_exact(&mut len).await?;
|
||||
let len = u16::from_be_bytes(len) as usize;
|
||||
|
||||
// RFC: DoQ queries and responses are sent on QUIC streams, which in theory can carry up to
|
||||
// 2^62 bytes. However, DNS messages are restricted in practice to a maximum size of 65535
|
||||
// bytes. This maximum size is enforced by the use of a 2-octet message length field in DNS
|
||||
// over TCP [RFC1035] and DoT [RFC7858], and by the definition of the
|
||||
// "application/dns-message" for DoH [RFC8484]. DoQ enforces the same restriction.
|
||||
let mut bytes = BytesMut::with_capacity(512);
|
||||
let mut left = len;
|
||||
while left > 0 {
|
||||
match self.receive_stream.read_chunk(left, true).await {
|
||||
Ok(Some(chunk)) => {
|
||||
left -= chunk.bytes.len();
|
||||
bytes.put(chunk.bytes);
|
||||
}
|
||||
Ok(None) => {
|
||||
return Err(NetError::QuinnReadError(
|
||||
quinn::ReadExactError::FinishedEarly(bytes.len()),
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(NetError::QuinnReadError(e.into())),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Reset the sending stream due to some error
|
||||
pub fn reset(&mut self, code: DoqErrorCode) -> Result<(), NetError> {
|
||||
self.send_stream
|
||||
.reset(code.into())
|
||||
.map_err(|_| NetError::QuinnUnknownStreamError)
|
||||
}
|
||||
|
||||
/// Stop the receiving stream due to some error
|
||||
pub fn stop(&mut self, code: DoqErrorCode) -> Result<(), NetError> {
|
||||
self.receive_stream
|
||||
.stop(code.into())
|
||||
.map_err(|_| NetError::QuinnUnknownStreamError)
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright 2015-2022 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![allow(clippy::print_stdout)] // this is a test module
|
||||
|
||||
use core::{net::SocketAddr, str::FromStr};
|
||||
use std::sync::Arc;
|
||||
use std::{env, path::Path, println};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use rustls::{
|
||||
ClientConfig, KeyLogFile,
|
||||
pki_types::{
|
||||
CertificateDer, PrivateKeyDer,
|
||||
pem::{self, PemObject},
|
||||
},
|
||||
sign::{CertifiedKey, SingleCertAndKey},
|
||||
};
|
||||
use test_support::subscribe;
|
||||
|
||||
use crate::{
|
||||
proto::{
|
||||
op::{Message, Query},
|
||||
rr::{Name, RecordType},
|
||||
},
|
||||
quic::QuicClientStreamBuilder,
|
||||
tls::default_provider,
|
||||
xfer::DnsRequestSender,
|
||||
};
|
||||
|
||||
use super::quic_server::QuicServer;
|
||||
|
||||
async fn server_responder(mut server: QuicServer) {
|
||||
while let Some((mut conn, addr)) = server
|
||||
.next()
|
||||
.await
|
||||
.expect("failed to get next quic session")
|
||||
{
|
||||
println!("received client request {addr}");
|
||||
|
||||
while let Some(stream) = conn.next().await {
|
||||
let mut stream = stream.expect("new client stream failed");
|
||||
|
||||
let bytes = stream.receive_bytes().await.expect("failed to receive");
|
||||
let client_message = Message::from_vec(&bytes).expect("failed to parse message");
|
||||
|
||||
// just respond with the same message converted to a response.
|
||||
stream
|
||||
.send(client_message.into_response())
|
||||
.await
|
||||
.expect("failed to send response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_quic_stream() {
|
||||
subscribe();
|
||||
|
||||
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
|
||||
println!("using server src path: {server_path}");
|
||||
|
||||
let ca = read_certs(format!("{server_path}/tests/test-data/ca.pem")).unwrap();
|
||||
let cert_chain = read_certs(format!("{server_path}/tests/test-data/cert.pem")).unwrap();
|
||||
|
||||
let key =
|
||||
PrivateKeyDer::from_pem_file(format!("{server_path}/tests/test-data/cert.key")).unwrap();
|
||||
|
||||
let certificate_and_key = SingleCertAndKey::from(
|
||||
CertifiedKey::from_der(cert_chain, key, &default_provider()).unwrap(),
|
||||
);
|
||||
|
||||
// All testing is only done on local addresses, construct the server
|
||||
let quic_ns = QuicServer::new(
|
||||
SocketAddr::from(([127, 0, 0, 1], 0)),
|
||||
Arc::new(certificate_and_key),
|
||||
)
|
||||
.await
|
||||
.expect("failed to initialize QuicServer");
|
||||
|
||||
// kick off the server
|
||||
let server_addr = quic_ns.local_addr().expect("no address");
|
||||
println!("testing quic on: {server_addr}");
|
||||
let server_join = tokio::spawn(server_responder(quic_ns));
|
||||
|
||||
// now construct the client
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
let (_, ignored) = roots.add_parsable_certificates(ca.into_iter());
|
||||
assert_eq!(ignored, 0);
|
||||
|
||||
let mut client_config = ClientConfig::builder_with_provider(Arc::new(default_provider()))
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap()
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
|
||||
client_config.key_log = Arc::new(KeyLogFile::new());
|
||||
|
||||
println!("starting quic connect");
|
||||
let builder = QuicClientStreamBuilder::default().crypto_config(client_config);
|
||||
let mut client_stream = builder
|
||||
.build(server_addr, Arc::from("ns.example.com"))
|
||||
.await
|
||||
.expect("failed to connect");
|
||||
|
||||
println!("connected client to server");
|
||||
|
||||
// create a test message, send and then receive...
|
||||
let mut message = Message::query();
|
||||
message.add_query(Query::query(
|
||||
Name::from_str("www.example.test.").unwrap(),
|
||||
RecordType::AAAA,
|
||||
));
|
||||
message.metadata.id = 0; // RFC: DNS over QUIC requires the Message ID to be 0
|
||||
|
||||
// TODO: we should make the finalizer easier to call so this round-trip serialization isn't necessary.
|
||||
let bytes = message.to_vec().unwrap();
|
||||
let message = Message::from_vec(&bytes).unwrap();
|
||||
|
||||
let response = client_stream
|
||||
.send_message(message.clone().into())
|
||||
.next()
|
||||
.await
|
||||
.expect("no response received")
|
||||
.expect("failed to read response");
|
||||
|
||||
assert_eq!(*response, message.into_response());
|
||||
|
||||
// and finally kill the server
|
||||
server_join.abort();
|
||||
}
|
||||
|
||||
fn read_certs(cert_path: impl AsRef<Path>) -> Result<Vec<CertificateDer<'static>>, pem::Error> {
|
||||
CertificateDer::pem_file_iter(cert_path)?.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
//! Abstractions to deal with different async runtimes.
|
||||
|
||||
use core::future::Future;
|
||||
use core::marker::Send;
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::time::Duration;
|
||||
#[cfg(feature = "__quic")]
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{
|
||||
future::poll_fn,
|
||||
io,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_io::{AsyncRead, AsyncWrite};
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
use tokio::runtime::Runtime;
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Spawn a background task, if it was present
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
pub fn spawn_bg<F: Future<Output = R> + Send + 'static, R: Send + 'static>(
|
||||
runtime: &Runtime,
|
||||
background: F,
|
||||
) -> JoinHandle<R> {
|
||||
runtime.spawn(background)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[doc(hidden)]
|
||||
pub mod iocompat {
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use std::io::{self, IoSlice};
|
||||
|
||||
use futures_io::{AsyncRead, AsyncWrite};
|
||||
use tokio::io::{AsyncRead as TokioAsyncRead, AsyncWrite as TokioAsyncWrite, ReadBuf};
|
||||
|
||||
/// Conversion from `tokio::io::{AsyncRead, AsyncWrite}` to `std::io::{AsyncRead, AsyncWrite}`
|
||||
pub struct AsyncIoTokioAsStd<T: TokioAsyncRead + TokioAsyncWrite>(pub T);
|
||||
|
||||
impl<T: TokioAsyncRead + TokioAsyncWrite + Unpin> Unpin for AsyncIoTokioAsStd<T> {}
|
||||
impl<R: TokioAsyncRead + TokioAsyncWrite + Unpin> AsyncRead for AsyncIoTokioAsStd<R> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let mut buf = ReadBuf::new(buf);
|
||||
let polled = Pin::new(&mut self.0).poll_read(cx, &mut buf);
|
||||
|
||||
polled.map_ok(|_| buf.filled().len())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: TokioAsyncRead + TokioAsyncWrite + Unpin> AsyncWrite for AsyncIoTokioAsStd<W> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.0).poll_write(cx, buf)
|
||||
}
|
||||
fn poll_write_vectored(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[IoSlice<'_>],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.0).poll_write_vectored(cx, bufs)
|
||||
}
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.0).poll_flush(cx)
|
||||
}
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.0).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversion from `std::io::{AsyncRead, AsyncWrite}` to `tokio::io::{AsyncRead, AsyncWrite}`
|
||||
pub struct AsyncIoStdAsTokio<T: AsyncRead + AsyncWrite>(pub T);
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin> Unpin for AsyncIoStdAsTokio<T> {}
|
||||
impl<R: AsyncRead + AsyncWrite + Unpin> TokioAsyncRead for AsyncIoStdAsTokio<R> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().0)
|
||||
.poll_read(cx, buf.initialized_mut())
|
||||
.map_ok(|len| buf.advance(len))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: AsyncRead + AsyncWrite + Unpin> TokioAsyncWrite for AsyncIoStdAsTokio<W> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
Pin::new(&mut self.get_mut().0).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Pin::new(&mut self.get_mut().0).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), io::Error>> {
|
||||
Pin::new(&mut self.get_mut().0).poll_close(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[allow(unreachable_pub)]
|
||||
mod tokio_runtime {
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(feature = "__quic")]
|
||||
use quinn::Runtime;
|
||||
use tokio::net::{TcpSocket, TcpStream, UdpSocket as TokioUdpSocket};
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tracing::debug;
|
||||
|
||||
use super::iocompat::AsyncIoTokioAsStd;
|
||||
use super::*;
|
||||
use crate::xfer::CONNECT_TIMEOUT;
|
||||
|
||||
/// A handle to the Tokio runtime
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TokioHandle {
|
||||
join_set: Arc<Mutex<JoinSet<()>>>,
|
||||
}
|
||||
|
||||
impl Spawn for TokioHandle {
|
||||
fn spawn_bg(&mut self, future: impl Future<Output = ()> + Send + 'static) {
|
||||
let mut join_set = self.join_set.lock().unwrap();
|
||||
join_set.spawn(future);
|
||||
reap_tasks(&mut join_set);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Tokio Runtime for async execution
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TokioRuntimeProvider(TokioHandle);
|
||||
|
||||
impl TokioRuntimeProvider {
|
||||
/// Create a Tokio runtime
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeProvider for TokioRuntimeProvider {
|
||||
type Handle = TokioHandle;
|
||||
type Timer = TokioTime;
|
||||
type Udp = TokioUdpSocket;
|
||||
type Tcp = AsyncIoTokioAsStd<TcpStream>;
|
||||
|
||||
fn create_handle(&self) -> Self::Handle {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
fn connect_tcp(
|
||||
&self,
|
||||
server_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
wait_for: Option<Duration>,
|
||||
) -> Pin<Box<dyn Send + Future<Output = Result<Self::Tcp, io::Error>>>> {
|
||||
Box::pin(async move {
|
||||
let socket = match server_addr {
|
||||
SocketAddr::V4(_) => TcpSocket::new_v4(),
|
||||
SocketAddr::V6(_) => TcpSocket::new_v6(),
|
||||
}?;
|
||||
|
||||
if let Some(bind_addr) = bind_addr {
|
||||
socket.bind(bind_addr)?;
|
||||
}
|
||||
|
||||
socket.set_nodelay(true)?;
|
||||
let future = socket.connect(server_addr);
|
||||
let wait_for = wait_for.unwrap_or(CONNECT_TIMEOUT);
|
||||
match timeout(wait_for, future).await {
|
||||
Ok(Ok(socket)) => Ok(AsyncIoTokioAsStd(socket)),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => {
|
||||
debug!(%server_addr, "TCP connect timeout");
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"TCP connect timed out",
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn bind_udp(
|
||||
&self,
|
||||
local_addr: SocketAddr,
|
||||
_server_addr: SocketAddr,
|
||||
) -> Pin<Box<dyn Send + Future<Output = Result<Self::Udp, io::Error>>>> {
|
||||
Box::pin(async move { tokio::net::UdpSocket::bind(local_addr).await })
|
||||
}
|
||||
|
||||
#[cfg(feature = "__quic")]
|
||||
fn quic_binder(&self) -> Option<&dyn QuicSocketBinder> {
|
||||
Some(&TokioQuicSocketBinder)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reap finished tasks from a `JoinSet`, without awaiting or blocking.
|
||||
fn reap_tasks(join_set: &mut JoinSet<()>) {
|
||||
while join_set.try_join_next().is_some() {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "__quic")]
|
||||
struct TokioQuicSocketBinder;
|
||||
|
||||
#[cfg(feature = "__quic")]
|
||||
impl QuicSocketBinder for TokioQuicSocketBinder {
|
||||
fn bind_quic(
|
||||
&self,
|
||||
local_addr: SocketAddr,
|
||||
_server_addr: SocketAddr,
|
||||
) -> Result<Arc<dyn quinn::AsyncUdpSocket>, io::Error> {
|
||||
let socket = std::net::UdpSocket::bind(local_addr)?;
|
||||
quinn::TokioRuntime.wrap_udp_socket(socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
pub use tokio_runtime::{TokioHandle, TokioRuntimeProvider};
|
||||
|
||||
/// RuntimeProvider defines which async runtime that handles IO and timers.
|
||||
pub trait RuntimeProvider: Clone + Send + Sync + Unpin + 'static {
|
||||
/// Handle to the executor;
|
||||
type Handle: Clone + Send + Spawn + Sync + Unpin;
|
||||
|
||||
/// Timer
|
||||
type Timer: Time;
|
||||
|
||||
/// UdpSocket
|
||||
type Udp: DnsUdpSocket;
|
||||
|
||||
/// TcpStream
|
||||
type Tcp: DnsTcpStream;
|
||||
|
||||
/// Create a runtime handle
|
||||
fn create_handle(&self) -> Self::Handle;
|
||||
|
||||
/// Create a TCP connection with custom configuration.
|
||||
fn connect_tcp(
|
||||
&self,
|
||||
server_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Pin<Box<dyn Send + Future<Output = Result<Self::Tcp, io::Error>>>>;
|
||||
|
||||
/// Create a UDP socket bound to `local_addr`. The returned value should **not** be connected to `server_addr`.
|
||||
/// *Notice: the future should be ready once returned at best effort. Otherwise UDP DNS may need much more retries.*
|
||||
fn bind_udp(
|
||||
&self,
|
||||
local_addr: SocketAddr,
|
||||
server_addr: SocketAddr,
|
||||
) -> Pin<Box<dyn Send + Future<Output = Result<Self::Udp, io::Error>>>>;
|
||||
|
||||
/// Yields an object that knows how to bind a QUIC socket.
|
||||
//
|
||||
// Use some indirection here to avoid exposing the `quinn` crate in the public API
|
||||
// even for runtimes that might not (want to) provide QUIC support.
|
||||
fn quic_binder(&self) -> Option<&dyn QuicSocketBinder> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for DnsUdpSocket
|
||||
#[async_trait]
|
||||
pub trait DnsUdpSocket
|
||||
where
|
||||
Self: Send + Sync + Sized + Unpin,
|
||||
{
|
||||
/// Time implementation used for this type
|
||||
type Time: Time;
|
||||
|
||||
/// Poll once Receive data from the socket and returns the number of bytes read and the address from
|
||||
/// where the data came on success.
|
||||
fn poll_recv_from(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<(usize, SocketAddr)>>;
|
||||
|
||||
/// Receive data from the socket and returns the number of bytes read and the address from
|
||||
/// where the data came on success.
|
||||
async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
poll_fn(|cx| self.poll_recv_from(cx, buf)).await
|
||||
}
|
||||
|
||||
/// Poll once to send data to the given address.
|
||||
fn poll_send_to(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
target: SocketAddr,
|
||||
) -> Poll<io::Result<usize>>;
|
||||
|
||||
/// Send data to the given address.
|
||||
async fn send_to(&self, buf: &[u8], target: SocketAddr) -> io::Result<usize> {
|
||||
poll_fn(|cx| self.poll_send_to(cx, buf, target)).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Noop trait for when the `quinn` dependency is not available.
|
||||
#[cfg(not(feature = "__quic"))]
|
||||
pub trait QuicSocketBinder {}
|
||||
|
||||
/// Create a UDP socket for QUIC usage.
|
||||
/// This trait is designed for customization.
|
||||
#[cfg(feature = "__quic")]
|
||||
pub trait QuicSocketBinder {
|
||||
/// Create a UDP socket for QUIC usage.
|
||||
fn bind_quic(
|
||||
&self,
|
||||
_local_addr: SocketAddr,
|
||||
_server_addr: SocketAddr,
|
||||
) -> Result<Arc<dyn quinn::AsyncUdpSocket>, io::Error>;
|
||||
}
|
||||
|
||||
/// Trait for TCP connection
|
||||
pub trait DnsTcpStream: AsyncRead + AsyncWrite + Unpin + Send + Sync + Sized + 'static {
|
||||
/// Timer type to use with this TCP stream type
|
||||
type Time: Time;
|
||||
}
|
||||
|
||||
/// A type defines the Handle which can spawn future.
|
||||
pub trait Spawn {
|
||||
/// Spawn a future in the background
|
||||
fn spawn_bg(&mut self, future: impl Future<Output = ()> + Send + 'static);
|
||||
}
|
||||
|
||||
/// Generic Time for Delay and Timeout.
|
||||
// This trait is created to allow to use different types of time systems. It's used in Fuchsia OS, please be mindful when update it.
|
||||
#[async_trait]
|
||||
pub trait Time: Send + Sync + Unpin {
|
||||
/// Return a type that implements `Future` that will wait until the specified duration has
|
||||
/// elapsed.
|
||||
async fn delay_for(duration: Duration);
|
||||
|
||||
/// Return a type that implement `Future` to complete before the specified duration has elapsed.
|
||||
async fn timeout<F: 'static + Future + Send>(
|
||||
duration: Duration,
|
||||
future: F,
|
||||
) -> Result<F::Output, io::Error>;
|
||||
|
||||
/// Get the current time as a Unix timestamp.
|
||||
///
|
||||
/// This returns the number of seconds since the Unix epoch.
|
||||
fn current_time() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
/// New type which is implemented using tokio::time::{Delay, Timeout}
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TokioTime;
|
||||
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
#[async_trait]
|
||||
impl Time for TokioTime {
|
||||
async fn delay_for(duration: Duration) {
|
||||
tokio::time::sleep(duration).await
|
||||
}
|
||||
|
||||
async fn timeout<F: 'static + Future + Send>(
|
||||
duration: Duration,
|
||||
future: F,
|
||||
) -> Result<F::Output, io::Error> {
|
||||
tokio::time::timeout(duration, future)
|
||||
.await
|
||||
.map_err(move |_| io::Error::new(io::ErrorKind::TimedOut, "future timed out"))
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Benjamin Fry <benjaminfry@me.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! TCP protocol related components for DNS
|
||||
mod tcp_client_stream;
|
||||
mod tcp_stream;
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::print_stdout)]
|
||||
mod tests;
|
||||
|
||||
pub use self::tcp_client_stream::TcpClientStream;
|
||||
pub use self::tcp_stream::TcpStream;
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll, ready};
|
||||
use core::time::Duration;
|
||||
use std::future::Future;
|
||||
|
||||
use futures_util::{StreamExt, stream::Stream};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error::NetError;
|
||||
use crate::proto::op::SerialMessage;
|
||||
#[cfg(feature = "tokio")]
|
||||
use crate::runtime::TokioTime;
|
||||
#[cfg(feature = "tokio")]
|
||||
use crate::runtime::iocompat::AsyncIoTokioAsStd;
|
||||
use crate::runtime::{DnsTcpStream, RuntimeProvider, Spawn};
|
||||
use crate::tcp::TcpStream;
|
||||
use crate::xfer::{DnsClientStream, DnsExchange};
|
||||
use crate::{BufDnsStreamHandle, DnsMultiplexer};
|
||||
|
||||
/// Tcp client stream
|
||||
///
|
||||
/// Use with `hickory_client::client::DnsMultiplexer` impls
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct TcpClientStream<S>
|
||||
where
|
||||
S: DnsTcpStream,
|
||||
{
|
||||
tcp_stream: TcpStream<S>,
|
||||
}
|
||||
|
||||
impl<S: DnsTcpStream> TcpClientStream<S> {
|
||||
/// Create a new [`DnsExchange`] wrapped around a multiplexed [`TcpClientStream`]
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `remote_addr` - Address of the remote nameserver
|
||||
/// * `bind_addr` - Optional local address to bind to
|
||||
/// * `timeout` - Timeout for requests
|
||||
/// * `max_active_requests` - Optional limit on concurrent in-flight requests.
|
||||
/// If `None`, uses the default (32).
|
||||
/// * `provider` - Runtime provider for spawning background tasks
|
||||
pub async fn exchange<P: RuntimeProvider<Tcp = S>>(
|
||||
remote_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
timeout: Duration,
|
||||
max_active_requests: Option<usize>,
|
||||
provider: P,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
let mut handle = provider.create_handle();
|
||||
let (future, sender) = Self::new(remote_addr, bind_addr, Some(timeout), provider);
|
||||
|
||||
// TODO: need config for Signer...
|
||||
let mut multiplexer = DnsMultiplexer::new(future.await?, sender).with_timeout(timeout);
|
||||
if let Some(max) = max_active_requests {
|
||||
multiplexer = multiplexer.with_max_active_requests(max);
|
||||
}
|
||||
let (exchange, bg) = DnsExchange::from_stream(multiplexer);
|
||||
handle.spawn_bg(bg);
|
||||
Ok(exchange)
|
||||
}
|
||||
|
||||
/// Create a new TcpClientStream
|
||||
pub fn new<P: RuntimeProvider<Tcp = S>>(
|
||||
peer_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
timeout: Option<Duration>,
|
||||
provider: P,
|
||||
) -> (
|
||||
impl Future<Output = Result<Self, NetError>> + Send + 'static,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr);
|
||||
(
|
||||
async move {
|
||||
let tcp = provider.connect_tcp(peer_addr, bind_addr, timeout).await?;
|
||||
Ok(Self::from_stream(TcpStream::from_stream_with_receiver(
|
||||
tcp,
|
||||
peer_addr,
|
||||
outbound_messages,
|
||||
)))
|
||||
},
|
||||
sender,
|
||||
)
|
||||
}
|
||||
|
||||
/// Wraps the TcpStream in TcpClientStream
|
||||
pub fn from_stream(tcp_stream: TcpStream<S>) -> Self {
|
||||
Self { tcp_stream }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: DnsTcpStream> DnsClientStream for TcpClientStream<S> {
|
||||
type Time = S::Time;
|
||||
|
||||
fn name_server_addr(&self) -> SocketAddr {
|
||||
self.tcp_stream.peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: DnsTcpStream> Stream for TcpClientStream<S> {
|
||||
type Item = Result<SerialMessage, NetError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let message = match ready!(self.tcp_stream.poll_next_unpin(cx)) {
|
||||
Some(Ok(t)) => t,
|
||||
Some(Err(e)) => return Poll::Ready(Some(Err(NetError::from(e)))),
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
|
||||
// this is busted if the tcp connection doesn't have a peer
|
||||
let peer = self.tcp_stream.peer_addr();
|
||||
if message.addr() != peer {
|
||||
// TODO: this should be an error, right?
|
||||
warn!("{} does not match name_server: {}", message.addr(), peer)
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Ok(message)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
impl<T> DnsTcpStream for AsyncIoTokioAsStd<T>
|
||||
where
|
||||
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + Sized + 'static,
|
||||
{
|
||||
type Time = TokioTime;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "tokio")]
|
||||
mod tests {
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use test_support::subscribe;
|
||||
|
||||
use crate::runtime::TokioRuntimeProvider;
|
||||
use crate::tcp::tests::tcp_client_stream_test;
|
||||
#[tokio::test]
|
||||
async fn test_tcp_stream_ipv4() {
|
||||
subscribe();
|
||||
tcp_client_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tcp_stream_ipv6() {
|
||||
subscribe();
|
||||
tcp_client_stream_test(
|
||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! This module contains all the TCP structures for demuxing TCP into streams of DNS packets.
|
||||
|
||||
use core::mem;
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
use std::io;
|
||||
|
||||
use futures_io::IoSlice;
|
||||
use futures_util::stream::Stream;
|
||||
use futures_util::{self, future::Future, ready};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::proto::op::SerialMessage;
|
||||
use crate::runtime::{DnsTcpStream, Time};
|
||||
use crate::xfer::{BufDnsStreamHandle, StreamReceiver};
|
||||
|
||||
/// Current state while writing to the remote of the TCP connection
|
||||
enum WriteTcpState {
|
||||
/// Currently writing the length of bytes to of the buffer.
|
||||
LenBytes {
|
||||
/// Current position in the length buffer being written
|
||||
pos: usize,
|
||||
/// Length of the buffer
|
||||
length: [u8; 2],
|
||||
/// Buffer to write after the length
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
/// Currently writing the buffer to the remote
|
||||
Bytes {
|
||||
/// Current position in the buffer written
|
||||
pos: usize,
|
||||
/// Buffer to write to the remote
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
/// Currently flushing the bytes to the remote
|
||||
Flushing,
|
||||
}
|
||||
|
||||
/// Current state of a TCP stream as it's being read.
|
||||
pub(crate) enum ReadTcpState {
|
||||
/// Currently reading the length of the TCP packet
|
||||
LenBytes {
|
||||
/// Current position in the buffer
|
||||
pos: usize,
|
||||
/// Buffer of the length to read
|
||||
bytes: [u8; 2],
|
||||
},
|
||||
/// Currently reading the bytes of the DNS packet
|
||||
Bytes {
|
||||
/// Current position while reading the buffer
|
||||
pos: usize,
|
||||
/// buffer being read into
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A Stream used for sending data to and from a remote DNS endpoint (client or server).
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct TcpStream<S: DnsTcpStream> {
|
||||
socket: S,
|
||||
outbound_messages: StreamReceiver,
|
||||
send_state: Option<WriteTcpState>,
|
||||
read_state: ReadTcpState,
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl<S: DnsTcpStream> TcpStream<S> {
|
||||
/// Returns the address of the peer connection.
|
||||
pub fn peer_addr(&self) -> SocketAddr {
|
||||
self.peer_addr
|
||||
}
|
||||
|
||||
fn pollable_split(
|
||||
&mut self,
|
||||
) -> (
|
||||
&mut S,
|
||||
&mut StreamReceiver,
|
||||
&mut Option<WriteTcpState>,
|
||||
&mut ReadTcpState,
|
||||
) {
|
||||
(
|
||||
&mut self.socket,
|
||||
&mut self.outbound_messages,
|
||||
&mut self.send_state,
|
||||
&mut self.read_state,
|
||||
)
|
||||
}
|
||||
|
||||
/// Initializes a TcpStream with an established connection.
|
||||
///
|
||||
/// Uses the default buffer size (32) for the outbound message queue.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stream` - the established IO stream for communication
|
||||
/// * `peer_addr` - address of the remote peer
|
||||
pub fn from_stream(stream: S, peer_addr: SocketAddr) -> (Self, BufDnsStreamHandle) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr);
|
||||
let stream = Self::from_stream_with_receiver(stream, peer_addr, outbound_messages);
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
||||
/// Initializes a TcpStream with an established connection and explicit buffer size.
|
||||
///
|
||||
/// Use this when you need a larger buffer to handle high message rates without
|
||||
/// dropping messages due to backpressure.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stream` - the established IO stream for communication
|
||||
/// * `peer_addr` - address of the remote peer
|
||||
/// * `buffer_size` - maximum number of messages that can be queued for sending
|
||||
pub fn from_stream_with_buffer_size(
|
||||
stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
buffer_size: usize,
|
||||
) -> (Self, BufDnsStreamHandle) {
|
||||
let (message_sender, outbound_messages) =
|
||||
BufDnsStreamHandle::with_buffer_size(peer_addr, buffer_size);
|
||||
let stream = Self::from_stream_with_receiver(stream, peer_addr, outbound_messages);
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
||||
/// Wraps a stream where a sender and receiver have already been established
|
||||
pub fn from_stream_with_receiver(
|
||||
socket: S,
|
||||
peer_addr: SocketAddr,
|
||||
outbound_messages: StreamReceiver,
|
||||
) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
outbound_messages,
|
||||
send_state: None,
|
||||
read_state: ReadTcpState::LenBytes {
|
||||
pos: 0,
|
||||
bytes: [0u8; 2],
|
||||
},
|
||||
peer_addr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new future of the eventually establish a IO stream connection or fail trying
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `future` - underlying stream future which this tcp stream relies on
|
||||
/// * `name_server` - the IP and Port of the DNS server to connect to
|
||||
/// * `timeout` - connection timeout
|
||||
pub fn with_future<F: Future<Output = Result<S, io::Error>> + Send + 'static>(
|
||||
future: F,
|
||||
name_server: SocketAddr,
|
||||
timeout: Duration,
|
||||
) -> (
|
||||
impl Future<Output = Result<Self, io::Error>> + Send,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(name_server);
|
||||
let stream_fut = Self::connect_with_future(future, name_server, timeout, outbound_messages);
|
||||
|
||||
(stream_fut, message_sender)
|
||||
}
|
||||
|
||||
async fn connect_with_future<F: Future<Output = Result<S, io::Error>> + Send + 'static>(
|
||||
future: F,
|
||||
name_server: SocketAddr,
|
||||
timeout: Duration,
|
||||
outbound_messages: StreamReceiver,
|
||||
) -> Result<Self, io::Error> {
|
||||
let socket = S::Time::timeout(timeout, future).await??;
|
||||
debug!("TCP connection established to: {}", name_server);
|
||||
Ok(Self {
|
||||
socket,
|
||||
outbound_messages,
|
||||
send_state: None,
|
||||
read_state: ReadTcpState::LenBytes {
|
||||
pos: 0,
|
||||
bytes: [0u8; 2],
|
||||
},
|
||||
peer_addr: name_server,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: DnsTcpStream> Stream for TcpStream<S> {
|
||||
type Item = io::Result<SerialMessage>;
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let peer = self.peer_addr;
|
||||
let (socket, outbound_messages, send_state, read_state) = self.pollable_split();
|
||||
let mut socket = Pin::new(socket);
|
||||
let mut outbound_messages = Pin::new(outbound_messages);
|
||||
|
||||
// this will not accept incoming data while there is data to send
|
||||
// makes this self throttling.
|
||||
// TODO: it might be interesting to try and split the sending and receiving futures.
|
||||
loop {
|
||||
// in the case we are sending, send it all?
|
||||
if send_state.is_some() {
|
||||
// sending...
|
||||
match send_state {
|
||||
Some(WriteTcpState::LenBytes { pos, length, bytes }) => {
|
||||
let wrote = ready!(socket.as_mut().poll_write_vectored(
|
||||
cx,
|
||||
&[IoSlice::new(&length[*pos..]), IoSlice::new(bytes)]
|
||||
))?;
|
||||
*pos += wrote;
|
||||
}
|
||||
Some(WriteTcpState::Bytes { pos, bytes }) => {
|
||||
let wrote = ready!(socket.as_mut().poll_write(cx, &bytes[*pos..]))?;
|
||||
*pos += wrote;
|
||||
}
|
||||
Some(WriteTcpState::Flushing) => {
|
||||
ready!(socket.as_mut().poll_flush(cx))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// get current state
|
||||
let current_state = send_state.take();
|
||||
|
||||
// switch states
|
||||
match current_state {
|
||||
Some(WriteTcpState::LenBytes { pos, length, bytes }) => {
|
||||
if pos < length.len() {
|
||||
*send_state = Some(WriteTcpState::LenBytes { pos, length, bytes });
|
||||
} else if pos < length.len() + bytes.len() {
|
||||
*send_state = Some(WriteTcpState::Bytes {
|
||||
pos: pos - length.len(),
|
||||
bytes,
|
||||
});
|
||||
} else {
|
||||
*send_state = Some(WriteTcpState::Flushing);
|
||||
}
|
||||
}
|
||||
Some(WriteTcpState::Bytes { pos, bytes }) => {
|
||||
if pos < bytes.len() {
|
||||
*send_state = Some(WriteTcpState::Bytes { pos, bytes });
|
||||
} else {
|
||||
// At this point we successfully delivered the entire message.
|
||||
// flush
|
||||
*send_state = Some(WriteTcpState::Flushing);
|
||||
}
|
||||
}
|
||||
Some(WriteTcpState::Flushing) => {
|
||||
// At this point we successfully delivered the entire message.
|
||||
send_state.take();
|
||||
}
|
||||
None => (),
|
||||
};
|
||||
} else {
|
||||
// then see if there is more to send
|
||||
match outbound_messages.as_mut().poll_next(cx)
|
||||
// .map_err(|()| io::Error::new(io::ErrorKind::Other, "unknown"))?
|
||||
{
|
||||
// already handled above, here to make sure the poll() pops the next message
|
||||
Poll::Ready(Some(message)) => {
|
||||
// if there is no peer, this connection should die...
|
||||
let (buffer, dst) = message.into();
|
||||
|
||||
// This is an error if the destination is not our peer (this is TCP after all)
|
||||
// This will kill the connection...
|
||||
if peer != dst {
|
||||
return Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("mismatched peer: {peer} and dst: {dst}"),
|
||||
))));
|
||||
}
|
||||
|
||||
// will return if the socket will block
|
||||
// the length is 16 bits
|
||||
let len = u16::to_be_bytes(buffer.len() as u16);
|
||||
|
||||
debug!("sending message len: {} to: {}", buffer.len(), dst);
|
||||
*send_state = Some(WriteTcpState::LenBytes {
|
||||
pos: 0,
|
||||
length: len,
|
||||
bytes: buffer,
|
||||
});
|
||||
}
|
||||
// now we get to drop through to the receives...
|
||||
// TODO: should we also return None if there are no more messages to send?
|
||||
Poll::Pending => break,
|
||||
Poll::Ready(None) => {
|
||||
debug!("no messages to send");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut ret_buf = None;
|
||||
|
||||
// this will loop while there is data to read, or the data has been read, or an IO
|
||||
// event would block
|
||||
while ret_buf.is_none() {
|
||||
// Evaluates the next state. If None is the result, then no state change occurs,
|
||||
// if Some(_) is returned, then that will be used as the next state.
|
||||
let new_state: Option<ReadTcpState> = match read_state {
|
||||
ReadTcpState::LenBytes { pos, bytes } => {
|
||||
// debug!("reading length {}", bytes.len());
|
||||
let read = ready!(socket.as_mut().poll_read(cx, &mut bytes[*pos..]))?;
|
||||
if read == 0 {
|
||||
// the Stream was closed!
|
||||
debug!("zero bytes read, stream closed?");
|
||||
//try!(self.socket.shutdown(Shutdown::Both)); // TODO: add generic shutdown function
|
||||
|
||||
if *pos == 0 {
|
||||
// Since this is the start of the next message, we have a clean end
|
||||
return Poll::Ready(None);
|
||||
} else {
|
||||
return Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"closed while reading length",
|
||||
))));
|
||||
}
|
||||
}
|
||||
trace!("in ReadTcpState::LenBytes: {}", pos);
|
||||
*pos += read;
|
||||
|
||||
if *pos < bytes.len() {
|
||||
trace!("remain ReadTcpState::LenBytes: {}", pos);
|
||||
None
|
||||
} else {
|
||||
let length = u16::from_be_bytes(*bytes);
|
||||
trace!("got length: {}", length);
|
||||
let mut bytes = vec![0; length as usize];
|
||||
bytes.resize(length as usize, 0);
|
||||
|
||||
trace!("move ReadTcpState::Bytes: {}", bytes.len());
|
||||
Some(ReadTcpState::Bytes { pos: 0, bytes })
|
||||
}
|
||||
}
|
||||
ReadTcpState::Bytes { pos, bytes } => {
|
||||
let read = ready!(socket.as_mut().poll_read(cx, &mut bytes[*pos..]))?;
|
||||
if read == 0 {
|
||||
// the Stream was closed!
|
||||
trace!("zero bytes read for message, stream closed?");
|
||||
|
||||
// Since this is the start of the next message, we have a clean end
|
||||
// try!(self.socket.shutdown(Shutdown::Both)); // TODO: add generic shutdown function
|
||||
return Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"closed while reading message",
|
||||
))));
|
||||
}
|
||||
|
||||
trace!("in ReadTcpState::Bytes: {}", bytes.len());
|
||||
*pos += read;
|
||||
|
||||
if *pos < bytes.len() {
|
||||
trace!("remain ReadTcpState::Bytes: {}", bytes.len());
|
||||
None
|
||||
} else {
|
||||
trace!("reset ReadTcpState::LenBytes: {}", 0);
|
||||
Some(ReadTcpState::LenBytes {
|
||||
pos: 0,
|
||||
bytes: [0u8; 2],
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this will move to the next state,
|
||||
// if it was a completed receipt of bytes, then it will move out the bytes
|
||||
if let Some(state) = new_state {
|
||||
if let ReadTcpState::Bytes { pos, bytes } = mem::replace(read_state, state) {
|
||||
assert_eq!(pos, bytes.len());
|
||||
ret_buf = Some(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the buffer is ready, return it, if not we're Pending
|
||||
if let Some(buffer) = ret_buf {
|
||||
let src_addr = self.peer_addr;
|
||||
Poll::Ready(Some(Ok(SerialMessage::new(buffer, src_addr))))
|
||||
} else {
|
||||
debug!("bottomed out");
|
||||
// at a minimum the outbound_messages should have been polled,
|
||||
// which will wake this future up later...
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "tokio")]
|
||||
mod tests {
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use test_support::subscribe;
|
||||
|
||||
use crate::runtime::TokioRuntimeProvider;
|
||||
use crate::tcp::tests::tcp_stream_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tcp_stream_ipv4() {
|
||||
subscribe();
|
||||
tcp_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tcp_stream_ipv6() {
|
||||
subscribe();
|
||||
tcp_stream_test(
|
||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
use core::net::{IpAddr, SocketAddr};
|
||||
use core::sync::atomic::AtomicBool;
|
||||
use std::io::{Read, Write};
|
||||
use std::println;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::stream::StreamExt;
|
||||
|
||||
use crate::proto::op::SerialMessage;
|
||||
use crate::runtime::RuntimeProvider;
|
||||
use crate::tcp::TcpStream;
|
||||
use crate::xfer::dns_handle::DnsStreamHandle;
|
||||
|
||||
const TEST_BYTES: &[u8; 8] = b"DEADBEEF";
|
||||
const TEST_BYTES_LEN: usize = 8;
|
||||
const SEND_RECV_TIMES: usize = 4;
|
||||
|
||||
fn tcp_server_setup(
|
||||
server_name: &str,
|
||||
server_addr: IpAddr,
|
||||
) -> (Arc<AtomicBool>, std::thread::JoinHandle<()>, SocketAddr) {
|
||||
let succeeded = Arc::new(AtomicBool::new(false));
|
||||
let succeeded_clone = succeeded.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("thread_killer".to_string())
|
||||
.spawn(move || {
|
||||
let succeeded = succeeded_clone;
|
||||
for _ in 0..15 {
|
||||
std::thread::sleep(core::time::Duration::from_secs(1));
|
||||
if succeeded.load(core::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Thread Killer has been awoken, killing process");
|
||||
std::process::exit(-1);
|
||||
})
|
||||
.expect("Thread spawning failed");
|
||||
|
||||
// TODO: need a timeout on listen
|
||||
let server = std::net::TcpListener::bind(SocketAddr::new(server_addr, 0))
|
||||
.expect("Unable to bind a TCP socket");
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
|
||||
// an in and out server
|
||||
let server_handle = std::thread::Builder::new()
|
||||
.name(server_name.to_string())
|
||||
.spawn(move || {
|
||||
let (mut socket, _) = server.accept().expect("accept failed");
|
||||
|
||||
socket
|
||||
.set_read_timeout(Some(core::time::Duration::from_secs(5)))
|
||||
.unwrap(); // should receive something within 5 seconds...
|
||||
socket
|
||||
.set_write_timeout(Some(core::time::Duration::from_secs(5)))
|
||||
.unwrap(); // should receive something within 5 seconds...
|
||||
|
||||
for _ in 0..SEND_RECV_TIMES {
|
||||
// wait for some bytes...
|
||||
let mut len_bytes = [0_u8; 2];
|
||||
socket
|
||||
.read_exact(&mut len_bytes)
|
||||
.expect("SERVER: receive failed");
|
||||
let length =
|
||||
(u16::from(len_bytes[0]) << 8) & 0xFF00 | u16::from(len_bytes[1]) & 0x00FF;
|
||||
assert_eq!(length as usize, TEST_BYTES_LEN);
|
||||
|
||||
let mut buffer = [0_u8; TEST_BYTES_LEN];
|
||||
socket.read_exact(&mut buffer).unwrap();
|
||||
|
||||
// println!("read bytes iter: {}", i);
|
||||
assert_eq!(&buffer, TEST_BYTES);
|
||||
|
||||
// bounce them right back...
|
||||
socket
|
||||
.write_all(&len_bytes)
|
||||
.expect("SERVER: send length failed");
|
||||
socket
|
||||
.write_all(&buffer)
|
||||
.expect("SERVER: send buffer failed");
|
||||
// println!("wrote bytes iter: {}", i);
|
||||
std::thread::yield_now();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
(succeeded, server_handle, server_addr)
|
||||
}
|
||||
|
||||
/// Test tcp_stream.
|
||||
pub(super) async fn tcp_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) {
|
||||
let (succeeded, server_handle, server_addr) =
|
||||
tcp_server_setup("test_tcp_stream:server", server_addr);
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
|
||||
let tcp = provider
|
||||
.connect_tcp(server_addr, None, None)
|
||||
.await
|
||||
.expect("connect failed");
|
||||
let (mut stream, mut sender) = TcpStream::from_stream(tcp, server_addr);
|
||||
|
||||
for _ in 0..SEND_RECV_TIMES {
|
||||
// test once
|
||||
sender
|
||||
.send(SerialMessage::new(TEST_BYTES.to_vec(), server_addr))
|
||||
.expect("send failed");
|
||||
|
||||
let (buffer, stream_tmp) = stream.into_future().await;
|
||||
stream = stream_tmp;
|
||||
let message = buffer
|
||||
.expect("no buffer received")
|
||||
.expect("error receiving buffer");
|
||||
assert_eq!(message.bytes(), TEST_BYTES);
|
||||
}
|
||||
|
||||
succeeded.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
server_handle.join().expect("server thread failed");
|
||||
}
|
||||
|
||||
/// Test tcp_client_stream.
|
||||
pub(super) async fn tcp_client_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) {
|
||||
let (succeeded, server_handle, server_addr) =
|
||||
tcp_server_setup("test_tcp_client_stream:server", server_addr);
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
|
||||
let tcp = provider
|
||||
.connect_tcp(server_addr, None, None)
|
||||
.await
|
||||
.expect("connect failed");
|
||||
let (mut stream, mut sender) = TcpStream::from_stream(tcp, server_addr);
|
||||
|
||||
for _ in 0..SEND_RECV_TIMES {
|
||||
// test once
|
||||
sender
|
||||
.send(SerialMessage::new(TEST_BYTES.to_vec(), server_addr))
|
||||
.expect("send failed");
|
||||
let (buffer, stream_tmp) = stream.into_future().await;
|
||||
stream = stream_tmp;
|
||||
let buffer = buffer
|
||||
.expect("no buffer received")
|
||||
.expect("error receiving buffer");
|
||||
assert_eq!(buffer.bytes(), TEST_BYTES);
|
||||
}
|
||||
|
||||
succeeded.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
server_handle.join().expect("server thread failed");
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
// Copyright 2015-2021 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! TLS protocol related components for DNS over TLS
|
||||
|
||||
use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::future::BoxFuture;
|
||||
#[cfg(not(feature = "rustls-platform-verifier"))]
|
||||
use rustls::RootCertStore;
|
||||
use rustls::{
|
||||
ClientConfig,
|
||||
crypto::{self, CryptoProvider},
|
||||
pki_types::ServerName,
|
||||
};
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use tokio::time::timeout;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
error::NetError,
|
||||
runtime::{
|
||||
DnsTcpStream, RuntimeProvider, Spawn,
|
||||
iocompat::{AsyncIoStdAsTokio, AsyncIoTokioAsStd},
|
||||
},
|
||||
tcp::{TcpClientStream, TcpStream},
|
||||
xfer::{BufDnsStreamHandle, CONNECT_TIMEOUT, DnsExchange, DnsMultiplexer, StreamReceiver},
|
||||
};
|
||||
|
||||
/// Type of TlsClientStream used with Rustls
|
||||
pub type TlsClientStream<S> =
|
||||
TcpClientStream<AsyncIoTokioAsStd<tokio_rustls::client::TlsStream<AsyncIoStdAsTokio<S>>>>;
|
||||
|
||||
/// Create a new [`DnsExchange`] wrapped around a multiplexed [`TlsClientStream`]
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `remote_addr` - Address of the remote nameserver
|
||||
/// * `server_name` - TLS server name for certificate validation
|
||||
/// * `config` - TLS client configuration
|
||||
/// * `timeout` - Timeout for requests
|
||||
/// * `max_active_requests` - Optional limit on concurrent in-flight requests.
|
||||
/// If `None`, uses the default (32).
|
||||
/// * `provider` - Runtime provider for spawning background tasks
|
||||
pub async fn tls_exchange<P: RuntimeProvider<Tcp = S>, S: DnsTcpStream>(
|
||||
remote_addr: SocketAddr,
|
||||
server_name: ServerName<'static>,
|
||||
config: ClientConfig,
|
||||
timeout: Duration,
|
||||
max_active_requests: Option<usize>,
|
||||
provider: P,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
tls_exchange_with_bind_addr(
|
||||
remote_addr,
|
||||
None,
|
||||
server_name,
|
||||
config,
|
||||
timeout,
|
||||
max_active_requests,
|
||||
provider,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a new [`DnsExchange`] wrapped around a multiplexed [`TlsClientStream`],
|
||||
/// optionally binding the underlying TCP socket to a local address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `remote_addr` - Address of the remote nameserver
|
||||
/// * `bind_addr` - Local address to bind the outgoing TCP socket to. When `None`, the OS picks.
|
||||
/// * `server_name` - TLS server name for certificate validation
|
||||
/// * `config` - TLS client configuration
|
||||
/// * `timeout` - Timeout for requests
|
||||
/// * `max_active_requests` - Optional limit on concurrent in-flight requests.
|
||||
/// If `None`, uses the default (32).
|
||||
/// * `provider` - Runtime provider for spawning background tasks
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn tls_exchange_with_bind_addr<P: RuntimeProvider<Tcp = S>, S: DnsTcpStream>(
|
||||
remote_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
server_name: ServerName<'static>,
|
||||
mut config: ClientConfig,
|
||||
timeout: Duration,
|
||||
max_active_requests: Option<usize>,
|
||||
provider: P,
|
||||
) -> Result<DnsExchange<P>, NetError> {
|
||||
// The port (853) of DOT is for dns dedicated, SNI is unnecessary. (ISP block by the SNI name)
|
||||
config.enable_sni = false;
|
||||
|
||||
let stream = provider
|
||||
.connect_tcp(remote_addr, bind_addr, Some(timeout))
|
||||
.await?;
|
||||
let (future, sender) = tls_client_connect_with_future(
|
||||
stream,
|
||||
remote_addr,
|
||||
server_name.to_owned(),
|
||||
Arc::new(config),
|
||||
);
|
||||
|
||||
let mut multiplexer = DnsMultiplexer::new(future.await?, sender).with_timeout(timeout);
|
||||
if let Some(max) = max_active_requests {
|
||||
multiplexer = multiplexer.with_max_active_requests(max);
|
||||
}
|
||||
let (exchange, bg) = DnsExchange::<P>::from_stream(multiplexer);
|
||||
provider.create_handle().spawn_bg(bg);
|
||||
Ok(exchange)
|
||||
}
|
||||
|
||||
/// Creates a new TlsStream to the specified name_server
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `bind_addr` - IP and port to connect from
|
||||
/// * `dns_name` - The DNS name associated with a certificate
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn tls_client_connect<P: RuntimeProvider>(
|
||||
name_server: SocketAddr,
|
||||
server_name: ServerName<'static>,
|
||||
client_config: Arc<ClientConfig>,
|
||||
provider: P,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<TlsClientStream<P::Tcp>, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
tls_client_connect_with_bind_addr(name_server, None, server_name, client_config, provider)
|
||||
}
|
||||
|
||||
/// Creates a new TlsStream to the specified name_server connecting from a specific address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name_server` - IP and Port for the remote DNS resolver
|
||||
/// * `bind_addr` - IP and port to connect from
|
||||
/// * `dns_name` - The DNS name associated with a certificate
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn tls_client_connect_with_bind_addr<P: RuntimeProvider>(
|
||||
name_server: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
server_name: ServerName<'static>,
|
||||
client_config: Arc<ClientConfig>,
|
||||
provider: P,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<TlsClientStream<P::Tcp>, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(name_server);
|
||||
let early_data_enabled = client_config.enable_early_data;
|
||||
let tls_connector = TlsConnector::from(client_config).early_data(early_data_enabled);
|
||||
|
||||
// This set of futures collapses the next tcp socket into a stream which can be used for
|
||||
// sending and receiving tcp packets.
|
||||
let stream = async move {
|
||||
let tcp = provider.connect_tcp(name_server, bind_addr, None).await?;
|
||||
connect_tls_stream(
|
||||
tls_connector,
|
||||
tcp,
|
||||
name_server,
|
||||
server_name,
|
||||
outbound_messages,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let new_future = Box::pin(async { Ok(TcpClientStream::from_stream(stream.await?)) });
|
||||
|
||||
(new_future, message_sender)
|
||||
}
|
||||
|
||||
/// Creates a new TlsStream to the specified name_server connecting from a specific address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `future` - A future producing DnsTcpStream
|
||||
/// * `dns_name` - The DNS name associated with a certificate
|
||||
fn tls_client_connect_with_future<S: DnsTcpStream>(
|
||||
stream: S,
|
||||
socket_addr: SocketAddr,
|
||||
server_name: ServerName<'static>,
|
||||
client_config: Arc<ClientConfig>,
|
||||
) -> (
|
||||
impl Future<Output = Result<TlsClientStream<S>, NetError>> + Send + 'static,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(socket_addr);
|
||||
let early_data_enabled = client_config.enable_early_data;
|
||||
let tls_connector = TlsConnector::from(client_config).early_data(early_data_enabled);
|
||||
|
||||
// This set of futures collapses the next tcp socket into a stream which can be used for
|
||||
// sending and receiving tcp packets.
|
||||
let stream = async move {
|
||||
connect_tls_stream(
|
||||
tls_connector,
|
||||
stream,
|
||||
socket_addr,
|
||||
server_name,
|
||||
outbound_messages,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
(
|
||||
async move { Ok(TcpClientStream::from_stream(stream.await?)) },
|
||||
message_sender,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn connect_tls_stream<S: DnsTcpStream>(
|
||||
tls_connector: TlsConnector,
|
||||
stream: S,
|
||||
name_server: SocketAddr,
|
||||
server_name: ServerName<'static>,
|
||||
outbound_messages: StreamReceiver,
|
||||
) -> Result<TcpStream<AsyncIoTokioAsStd<TokioTlsClientStream<S>>>, NetError> {
|
||||
let stream = AsyncIoStdAsTokio(stream);
|
||||
let s = match timeout(CONNECT_TIMEOUT, tls_connector.connect(server_name, stream)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return Err(NetError::from(e)),
|
||||
Err(_) => {
|
||||
debug!(%name_server, "TLS connect timeout");
|
||||
return Err(NetError::Timeout);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TcpStream::from_stream_with_receiver(
|
||||
AsyncIoTokioAsStd(s),
|
||||
name_server,
|
||||
outbound_messages,
|
||||
))
|
||||
}
|
||||
|
||||
/// Initializes a TlsStream with an existing tokio_tls::TlsStream.
|
||||
///
|
||||
/// This is intended for use with a TlsListener and Incoming connections
|
||||
pub fn tls_from_stream<S: DnsTcpStream>(
|
||||
stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
) -> (TlsStream<S>, BufDnsStreamHandle) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(peer_addr);
|
||||
let stream = TcpStream::from_stream_with_receiver(stream, peer_addr, outbound_messages);
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
||||
/// Make a new [`ClientConfig`] with the default settings
|
||||
pub fn client_config() -> Result<ClientConfig, rustls::Error> {
|
||||
let builder = ClientConfig::builder_with_provider(Arc::new(default_provider()))
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
let builder = builder.with_platform_verifier()?;
|
||||
#[cfg(not(feature = "rustls-platform-verifier"))]
|
||||
let builder = builder.with_root_certificates({
|
||||
#[cfg_attr(not(feature = "webpki-roots"), allow(unused_mut))]
|
||||
let mut root_store = RootCertStore::empty();
|
||||
#[cfg(feature = "webpki-roots")]
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
root_store
|
||||
});
|
||||
|
||||
Ok(builder.with_no_client_auth())
|
||||
}
|
||||
|
||||
/// Instantiate a new [`CryptoProvider`] for use with rustls
|
||||
#[cfg(all(feature = "tls-aws-lc-rs", not(feature = "tls-ring")))]
|
||||
pub fn default_provider() -> CryptoProvider {
|
||||
crypto::aws_lc_rs::default_provider()
|
||||
}
|
||||
|
||||
/// Instantiate a new [`CryptoProvider`] for use with rustls
|
||||
#[cfg(feature = "tls-ring")]
|
||||
pub fn default_provider() -> CryptoProvider {
|
||||
crypto::ring::default_provider()
|
||||
}
|
||||
|
||||
/// Predefined type for abstracting the base I/O TlsStream with TokioTls
|
||||
pub type TlsStream<S> = TcpStream<S>;
|
||||
|
||||
/// Predefined type for abstracting the TlsClientStream with TokioTls
|
||||
pub type TokioTlsClientStream<S> = tokio_rustls::client::TlsStream<AsyncIoStdAsTokio<S>>;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Benjamin Fry <benjaminfry@me.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//! UDP protocol related components for DNS
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::print_stdout)]
|
||||
mod tests;
|
||||
mod udp_client_stream;
|
||||
mod udp_stream;
|
||||
|
||||
pub use self::udp_client_stream::{UdpClientStream, UdpClientStreamBuilder};
|
||||
pub use self::udp_stream::{UdpSocket, UdpStream};
|
||||
|
||||
/// Max size for the UDP receive buffer as recommended by
|
||||
/// [RFC6891](https://datatracker.ietf.org/doc/html/rfc6891#section-6.2.5).
|
||||
pub const MAX_RECEIVE_BUFFER_SIZE: usize = 4_096;
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use core::str::FromStr;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use core::time::Duration;
|
||||
use std::net::{ToSocketAddrs, UdpSocket};
|
||||
use std::sync::Arc;
|
||||
use std::{println, process, thread};
|
||||
|
||||
use futures_util::stream::StreamExt;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::NetError;
|
||||
use crate::proto::op::{DnsRequest, DnsRequestOptions, DnsResponse, Message, Query, SerialMessage};
|
||||
use crate::proto::rr::rdata::NULL;
|
||||
use crate::proto::rr::{Name, RData, Record, RecordType};
|
||||
use crate::runtime::RuntimeProvider;
|
||||
use crate::udp::{UdpClientStream, UdpStream};
|
||||
use crate::xfer::dns_handle::DnsStreamHandle;
|
||||
use crate::xfer::{DnsRequestSender, FirstAnswer};
|
||||
|
||||
/// Test next random udpsocket.
|
||||
pub(super) async fn next_random_socket_test(provider: impl RuntimeProvider) {
|
||||
let (stream, _) = UdpStream::new(
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 52),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
provider,
|
||||
);
|
||||
drop(stream.await.expect("failed to get next socket address"));
|
||||
}
|
||||
|
||||
/// Test udp_stream.
|
||||
pub(super) async fn udp_stream_test<P: RuntimeProvider>(server_addr: IpAddr, provider: P) {
|
||||
let stop_thread_killer = start_thread_killer();
|
||||
|
||||
let server = UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap();
|
||||
server
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap(); // should receive something within 5 seconds...
|
||||
server
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap(); // should receive something within 5 seconds...
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
println!("server listening on: {server_addr}");
|
||||
|
||||
let test_bytes: &'static [u8; 8] = b"DEADBEEF";
|
||||
let send_recv_times = 4u32;
|
||||
|
||||
// an in and out server
|
||||
let server_handle = thread::Builder::new()
|
||||
.name("test_udp_stream_ipv4:server".to_string())
|
||||
.spawn(move || {
|
||||
let mut buffer = [0_u8; 512];
|
||||
|
||||
for _ in 0..send_recv_times {
|
||||
// wait for some bytes...
|
||||
//println!("receiving message: {}", _i);
|
||||
let (len, addr) = server.recv_from(&mut buffer).expect("receive failed");
|
||||
|
||||
assert_eq!(&buffer[0..len], test_bytes);
|
||||
|
||||
//println!("sending message len back: {}", len);
|
||||
// bounce them right back...
|
||||
assert_eq!(
|
||||
server.send_to(&buffer[0..len], addr).expect("send failed"),
|
||||
len
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
// the tests should run within 5 seconds... right?
|
||||
// TODO: add timeout here, so that test never hangs...
|
||||
let client_addr = match server_addr {
|
||||
SocketAddr::V4(_) => "127.0.0.1:0",
|
||||
SocketAddr::V6(_) => "[::1]:0",
|
||||
};
|
||||
|
||||
println!("binding client socket");
|
||||
let socket = provider
|
||||
.bind_udp(
|
||||
client_addr.to_socket_addrs().unwrap().next().unwrap(),
|
||||
server_addr,
|
||||
)
|
||||
.await
|
||||
.expect("could not create socket"); // some random address...
|
||||
println!("bound client socket");
|
||||
|
||||
let (mut stream, mut sender) = UdpStream::<P>::with_bound(socket, server_addr);
|
||||
|
||||
for _i in 0..send_recv_times {
|
||||
// test once
|
||||
sender
|
||||
.send(SerialMessage::new(test_bytes.to_vec(), server_addr))
|
||||
.unwrap();
|
||||
//println!("blocking on client stream: {}", _i);
|
||||
let buffer_and_addr = stream.next().await;
|
||||
//println!("got message");
|
||||
let message = buffer_and_addr.expect("no message").expect("io error");
|
||||
assert_eq!(message.bytes(), test_bytes);
|
||||
assert_eq!(message.addr(), server_addr);
|
||||
}
|
||||
|
||||
stop_thread_killer.store(true, Ordering::Relaxed);
|
||||
server_handle.join().expect("server thread failed");
|
||||
}
|
||||
|
||||
/// Test udp_client_stream.
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(super) async fn udp_client_stream_test(server_addr: IpAddr, provider: impl RuntimeProvider) {
|
||||
udp_client_stream_test_inner(
|
||||
server_addr,
|
||||
provider,
|
||||
"udp_client_stream",
|
||||
4,
|
||||
1,
|
||||
|_, _| {},
|
||||
|response| match response {
|
||||
Ok(response) => {
|
||||
let response = Message::from(response);
|
||||
if let RData::NULL(null) = &response.answers[0].data {
|
||||
assert_eq!(null.anything, b"DEADBEEF");
|
||||
true
|
||||
} else {
|
||||
panic!("not a NULL response");
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Test udp_client_stream handling of bad response IDs
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(super) async fn udp_client_stream_bad_id_test(
|
||||
server_addr: IpAddr,
|
||||
provider: impl RuntimeProvider,
|
||||
) {
|
||||
udp_client_stream_test_inner(
|
||||
server_addr,
|
||||
provider,
|
||||
"udp_client_stream_bad_id",
|
||||
1,
|
||||
1,
|
||||
|idx, message| {
|
||||
// Mutate the first response to have the wrong ID
|
||||
if idx == 0 {
|
||||
message.metadata.id = message.id.wrapping_add(1);
|
||||
}
|
||||
},
|
||||
|response| {
|
||||
// The test should pass when we see a timeout after waiting for a response
|
||||
// with a correct transaction ID
|
||||
matches!(response, Err(NetError::Timeout))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn udp_client_stream_empty_question_section_test(
|
||||
server_addr: IpAddr,
|
||||
provider: impl RuntimeProvider,
|
||||
) {
|
||||
udp_client_stream_test_inner(
|
||||
server_addr,
|
||||
provider,
|
||||
"udp_client_stream_empty_question_section_test",
|
||||
1,
|
||||
1,
|
||||
// Send a response with an empty question section.
|
||||
|_, message| message.queries.clear(),
|
||||
// We expect to see a timeout after waiting for a response with a matching question section.
|
||||
|response_res| matches!(response_res, Err(NetError::Timeout)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn udp_client_stream_test_inner(
|
||||
server_addr: IpAddr,
|
||||
provider: impl RuntimeProvider,
|
||||
test_name: &str,
|
||||
request_count: usize,
|
||||
response_count: usize,
|
||||
response_mutator: impl Fn(usize, &mut Message) + Send + 'static,
|
||||
accept_response: impl Fn(Result<DnsResponse, NetError>) -> bool,
|
||||
) {
|
||||
let stop_thread_killer = start_thread_killer();
|
||||
|
||||
let server = UdpSocket::bind(SocketAddr::new(server_addr, 0)).unwrap();
|
||||
server
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap(); // should receive something within 5 seconds...
|
||||
server
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap(); // should write something within 5 seconds...
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
|
||||
let mut query = Message::query();
|
||||
let query_name = Name::from_str("dead.beef.").unwrap();
|
||||
query.add_query(Query::query(query_name.clone(), RecordType::NULL));
|
||||
let test_bytes: &'static [u8; 8] = b"DEADBEEF";
|
||||
|
||||
// Keep the server socket alive until the client is done, so that retry
|
||||
// attempts don't hit a closed port. On Windows, sending UDP to a closed
|
||||
// port triggers ICMP "port unreachable" which causes `recv_from()` to return
|
||||
// a connection reset error instead of blocking until timeout.
|
||||
let server_done = stop_thread_killer.clone();
|
||||
let test_name_server = query_name;
|
||||
let server_handle = thread::Builder::new()
|
||||
.name(format!("{test_name}:server"))
|
||||
.spawn(move || {
|
||||
let mut buffer = [0_u8; 512];
|
||||
|
||||
for i in 0..request_count {
|
||||
// wait for some bytes...
|
||||
debug!("server receiving request {}", i);
|
||||
let (len, addr) = server.recv_from(&mut buffer).expect("receive failed");
|
||||
debug!("server received request {} from: {}", i, addr);
|
||||
|
||||
let request = Message::from_vec(&buffer[0..len]).expect("failed parse of request");
|
||||
assert_eq!(*request.queries[0].name(), test_name_server.clone());
|
||||
assert_eq!(request.queries[0].query_type(), RecordType::NULL);
|
||||
|
||||
for response_idx in 0..response_count {
|
||||
let mut message = request.clone().into_response();
|
||||
message.add_answer(Record::from_rdata(
|
||||
test_name_server.clone(),
|
||||
0,
|
||||
RData::NULL(NULL::with(test_bytes.to_vec())),
|
||||
));
|
||||
|
||||
response_mutator(response_idx, &mut message);
|
||||
|
||||
// bounce them right back...
|
||||
let bytes = message.to_vec().unwrap();
|
||||
server.send_to(&bytes, addr).expect("send failed");
|
||||
debug!("server sent response {response_idx} for request {i}");
|
||||
}
|
||||
thread::yield_now();
|
||||
}
|
||||
|
||||
while !server_done.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// setup the client, which is going to run on the testing thread...
|
||||
|
||||
// the tests should run within 5 seconds... right?
|
||||
// TODO: add timeout here, so that test never hangs...
|
||||
// let timeout = Timeout::new(Duration::from_secs(5));
|
||||
let mut stream = UdpClientStream::builder(server_addr, provider)
|
||||
.with_timeout(Some(Duration::from_millis(500)))
|
||||
.build();
|
||||
let mut worked_once = false;
|
||||
|
||||
for i in 0..request_count {
|
||||
let response_stream =
|
||||
stream.send_message(DnsRequest::new(query.clone(), DnsRequestOptions::default()));
|
||||
println!("client sending request {i}");
|
||||
let response = response_stream.first_answer().await;
|
||||
println!("client got response {i}");
|
||||
|
||||
if accept_response(response) {
|
||||
worked_once = true;
|
||||
}
|
||||
}
|
||||
|
||||
stop_thread_killer.store(true, Ordering::Relaxed);
|
||||
server_handle.join().expect("server thread failed");
|
||||
|
||||
assert!(worked_once);
|
||||
}
|
||||
|
||||
/// Spawn a thread that wil kill the process after 15 seconds unless stopped.
|
||||
///
|
||||
/// The thread killer can be stopped when the testing logic is complete by writing
|
||||
/// a true value to the returned atomic bool.
|
||||
fn start_thread_killer() -> Arc<AtomicBool> {
|
||||
let succeeded = Arc::new(AtomicBool::new(false));
|
||||
let succeeded_clone = succeeded.clone();
|
||||
thread::Builder::new()
|
||||
.name("thread_killer".to_string())
|
||||
.spawn(move || {
|
||||
let succeeded = succeeded_clone;
|
||||
for _ in 0..15 {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
if succeeded.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Thread Killer has been awoken, killing process");
|
||||
process::exit(-1);
|
||||
})
|
||||
.unwrap();
|
||||
succeeded
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
// Copyright 2015-2016 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::fmt::{self, Display};
|
||||
use core::net::SocketAddr;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::{FutureExt, Stream, StreamExt, pin_mut, stream::FuturesUnordered};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::error::NetError;
|
||||
use crate::proto::op::{DEFAULT_RETRY_FLOOR, DnsRequest, DnsResponse, Message, SerialMessage};
|
||||
#[cfg(feature = "__dnssec")]
|
||||
use crate::proto::rr::TSigner;
|
||||
use crate::runtime::{DnsUdpSocket, RuntimeProvider, Spawn, Time};
|
||||
use crate::udp::MAX_RECEIVE_BUFFER_SIZE;
|
||||
use crate::udp::udp_stream::NextRandomUdpSocket;
|
||||
use crate::xfer::{DnsExchange, DnsRequestSender, DnsResponseStream};
|
||||
|
||||
/// A UDP client stream of DNS binary packets.
|
||||
///
|
||||
/// It is expected that the resolver wrapper will be responsible for creating and managing a new UDP
|
||||
/// client stream such that each request would have a random port. This is to avoid potential cache
|
||||
/// poisoning due to UDP spoofing attacks.
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct UdpClientStream<P> {
|
||||
name_server: SocketAddr,
|
||||
timeout: Duration,
|
||||
is_shutdown: bool,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: Option<TSigner>,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
avoid_local_ports: Arc<HashSet<u16>>,
|
||||
os_port_selection: bool,
|
||||
provider: P,
|
||||
max_retries: u8,
|
||||
retry_interval_floor: Duration,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpClientStream<P> {
|
||||
/// Construct a new [`UdpClientStream`] via a [`UdpClientStreamBuilder`].
|
||||
pub fn builder(name_server: SocketAddr, provider: P) -> UdpClientStreamBuilder<P> {
|
||||
UdpClientStreamBuilder {
|
||||
name_server,
|
||||
timeout: None,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: None,
|
||||
bind_addr: None,
|
||||
avoid_local_ports: Arc::default(),
|
||||
os_port_selection: false,
|
||||
provider,
|
||||
max_retries: 3,
|
||||
// This is the default value to use for the retry interval floor, which acts as a lower
|
||||
// bound on the retry interval.
|
||||
retry_interval_floor: DEFAULT_RETRY_FLOOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Display for UdpClientStream<P> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(formatter, "UDP({})", self.name_server)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> DnsRequestSender for UdpClientStream<P> {
|
||||
fn send_message(&mut self, request: DnsRequest) -> DnsResponseStream {
|
||||
if self.is_shutdown {
|
||||
panic!("can not send messages after stream is shutdown")
|
||||
}
|
||||
|
||||
let retry_interval_time = request.options().retry_interval;
|
||||
let request = UdpRequest::new(request, self);
|
||||
|
||||
let max_retries = self.max_retries;
|
||||
let retry_interval = if retry_interval_time < self.retry_interval_floor {
|
||||
self.retry_interval_floor
|
||||
} else {
|
||||
retry_interval_time
|
||||
};
|
||||
|
||||
P::Timer::timeout(
|
||||
self.timeout,
|
||||
retry::<P>(request, retry_interval, max_retries.into()),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
self.is_shutdown = true;
|
||||
}
|
||||
|
||||
fn is_shutdown(&self) -> bool {
|
||||
self.is_shutdown
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: is this impl necessary? there's nothing being driven here...
|
||||
impl<P> Stream for UdpClientStream<P> {
|
||||
type Item = Result<(), NetError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
// Technically the Stream doesn't actually do anything.
|
||||
if self.is_shutdown {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request context for data send_udp_message needs via the retry handler closure
|
||||
struct UdpRequest<P> {
|
||||
avoid_local_ports: Arc<HashSet<u16>>,
|
||||
name_server: SocketAddr,
|
||||
request: DnsRequest,
|
||||
provider: P,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: Option<TSigner>,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
now: u64,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
os_port_selection: bool,
|
||||
case_randomization: bool,
|
||||
recv_buf_size: usize,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpRequest<P> {
|
||||
fn new(request: DnsRequest, stream: &UdpClientStream<P>) -> Self {
|
||||
Self {
|
||||
avoid_local_ports: stream.avoid_local_ports.clone(),
|
||||
recv_buf_size: MAX_RECEIVE_BUFFER_SIZE.min(request.max_payload() as usize),
|
||||
case_randomization: request.options().case_randomization,
|
||||
name_server: stream.name_server,
|
||||
// Only smuggle in the signer if we are going to use it.
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: match &stream.signer {
|
||||
Some(signer) if signer.should_sign_message(&request) => stream.signer.clone(),
|
||||
_ => None,
|
||||
},
|
||||
request,
|
||||
provider: stream.provider.clone(),
|
||||
#[cfg(feature = "__dnssec")]
|
||||
now: P::Timer::current_time(),
|
||||
bind_addr: stream.bind_addr,
|
||||
os_port_selection: stream.os_port_selection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> Request for UdpRequest<P> {
|
||||
async fn send(&self) -> Result<DnsResponse, NetError> {
|
||||
let original_query = self.request.original_query();
|
||||
#[cfg_attr(not(feature = "__dnssec"), expect(unused_mut))]
|
||||
let mut request = self.request.clone();
|
||||
|
||||
#[cfg(feature = "__dnssec")]
|
||||
let mut verifier = None;
|
||||
#[cfg(feature = "__dnssec")]
|
||||
if let Some(signer) = &self.signer {
|
||||
match request.finalize(signer, self.now) {
|
||||
Ok(answer_verifier) => verifier = answer_verifier,
|
||||
Err(e) => {
|
||||
debug!("could not sign message: {}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let request_bytes = match request.to_vec() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let msg_id = request.id;
|
||||
let msg = SerialMessage::new(request_bytes, self.name_server);
|
||||
let addr = msg.addr();
|
||||
let final_message = match msg.to_message() {
|
||||
Ok(m) => m,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
debug!(%final_message, "final message");
|
||||
|
||||
let socket = NextRandomUdpSocket::new(
|
||||
addr,
|
||||
self.bind_addr,
|
||||
self.avoid_local_ports.clone(),
|
||||
self.os_port_selection,
|
||||
self.provider.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes = msg.bytes();
|
||||
let len_sent: usize = socket.send_to(bytes, addr).await?;
|
||||
|
||||
if bytes.len() != len_sent {
|
||||
return Err(NetError::from(format!(
|
||||
"Not all bytes of message sent, {} of {}",
|
||||
len_sent,
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Create the receive buffer.
|
||||
trace!(
|
||||
recv_buf_size = self.recv_buf_size,
|
||||
"creating UDP receive buffer"
|
||||
);
|
||||
let mut recv_buf = vec![0; self.recv_buf_size];
|
||||
|
||||
// Wait until we receive a response to our query. Note that outer layers are responsible for
|
||||
// retries and timeouts.
|
||||
loop {
|
||||
let (len, src) = socket.recv_from(&mut recv_buf).await?;
|
||||
|
||||
// Copy the slice of read bytes.
|
||||
let response_bytes = &recv_buf[0..len];
|
||||
let response_buffer = Vec::from(response_bytes);
|
||||
|
||||
// compare expected src to received packet
|
||||
let request_target = msg.addr();
|
||||
|
||||
// We need to check the source port and the message ID before doing anything else,
|
||||
// including fallible message parsing. These two checks are our first screens against
|
||||
// spoofed requests, accounting for about 32 bits of entropy. Any code path that would
|
||||
// break out of the receive loop earlier would allow for a low-effort DoS attack.
|
||||
//
|
||||
// Compare the IP and port directly, as internal information about the link may be
|
||||
// stored in the SocketAddr. See https://github.com/hickory-dns/hickory-dns/issues/2081.
|
||||
if src.ip().to_canonical() != request_target.ip().to_canonical()
|
||||
|| src.port() != request_target.port()
|
||||
{
|
||||
warn!(
|
||||
"ignoring response from {}:{} because it does not match name_server: {}:{}.",
|
||||
src.ip().to_canonical(),
|
||||
src.port(),
|
||||
request_target.ip().to_canonical(),
|
||||
request_target.port(),
|
||||
);
|
||||
|
||||
// await an answer from the correct NameServer
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the message ID.
|
||||
let Some(id_bytes) = response_buffer.first_chunk::<2>() else {
|
||||
warn!(length = response_buffer.len(), "ignoring short message");
|
||||
continue;
|
||||
};
|
||||
let response_id = u16::from_be_bytes(*id_bytes);
|
||||
|
||||
// Validate the message id in the response matches the value chosen for the query.
|
||||
if msg_id != response_id {
|
||||
// on wrong id, attempted poison?
|
||||
warn!(
|
||||
expected_id = msg_id,
|
||||
received_id = response_id,
|
||||
"ignoring response with wrong message id",
|
||||
);
|
||||
|
||||
// await an answer with the correct message id
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut response = DnsResponse::from_buffer(response_buffer)?;
|
||||
|
||||
// Validate the returned query name.
|
||||
//
|
||||
// This currently checks that each response query name was present in the original query, but not that
|
||||
// every original question is present.
|
||||
//
|
||||
// References:
|
||||
//
|
||||
// RFC 1035 7.3:
|
||||
//
|
||||
// The next step is to match the response to a current resolver request.
|
||||
// The recommended strategy is to do a preliminary matching using the ID
|
||||
// field in the domain header, and then to verify that the question section
|
||||
// corresponds to the information currently desired.
|
||||
//
|
||||
// RFC 1035 7.4:
|
||||
//
|
||||
// In general, we expect a resolver to cache all data which it receives in
|
||||
// responses since it may be useful in answering future client requests.
|
||||
// However, there are several types of data which should not be cached:
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// - RR data in responses of dubious reliability. When a resolver
|
||||
// receives unsolicited responses or RR data other than that
|
||||
// requested, it should discard it without caching it.
|
||||
let request_message = Message::from_vec(msg.bytes())?;
|
||||
let request_queries = &request_message.queries;
|
||||
let response_queries = &mut response.queries;
|
||||
|
||||
let question_matches = request_queries.len() == response_queries.len()
|
||||
&& response_queries
|
||||
.iter()
|
||||
.all(|elem| request_queries.contains(elem));
|
||||
if self.case_randomization
|
||||
&& question_matches
|
||||
&& !response_queries.iter().all(|elem| {
|
||||
request_queries
|
||||
.iter()
|
||||
.any(|req_q| req_q == elem && req_q.name().eq_case(elem.name()))
|
||||
})
|
||||
{
|
||||
warn!(
|
||||
"case of question section did not match: we expected '{request_queries:?}', but received '{response_queries:?}' from server {src}"
|
||||
);
|
||||
return Err(NetError::QueryCaseMismatch);
|
||||
}
|
||||
if !question_matches {
|
||||
warn!(
|
||||
"detected forged question section: we expected '{request_queries:?}', but received '{response_queries:?}' from server {src}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// overwrite the query with the original query if case randomization may have been used
|
||||
if self.case_randomization {
|
||||
if let Some(original_query) = original_query {
|
||||
for response_query in response_queries.iter_mut() {
|
||||
if response_query == original_query {
|
||||
*response_query = original_query.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("received message id: {}", response.id);
|
||||
#[cfg(feature = "__dnssec")]
|
||||
if let Some(mut verifier) = verifier {
|
||||
return Ok(verifier.verify(response_bytes)?);
|
||||
}
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder to create a UDP client stream.
|
||||
///
|
||||
/// This is created by [`UdpClientStream::builder`].
|
||||
pub struct UdpClientStreamBuilder<P> {
|
||||
name_server: SocketAddr,
|
||||
timeout: Option<Duration>,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: Option<TSigner>,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
avoid_local_ports: Arc<HashSet<u16>>,
|
||||
os_port_selection: bool,
|
||||
provider: P,
|
||||
max_retries: u8,
|
||||
retry_interval_floor: Duration,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpClientStreamBuilder<P> {
|
||||
/// Sets the connection timeout.
|
||||
pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message finalizer to be applied to queries.
|
||||
#[cfg(feature = "__dnssec")]
|
||||
pub fn with_signer(self, signer: Option<TSigner>) -> Self {
|
||||
Self {
|
||||
name_server: self.name_server,
|
||||
timeout: self.timeout,
|
||||
signer,
|
||||
bind_addr: self.bind_addr,
|
||||
avoid_local_ports: self.avoid_local_ports,
|
||||
os_port_selection: self.os_port_selection,
|
||||
provider: self.provider,
|
||||
max_retries: self.max_retries,
|
||||
retry_interval_floor: self.retry_interval_floor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the local socket address to connect from.
|
||||
///
|
||||
/// If the port number is 0, a random port number will be chosen to defend against spoofing
|
||||
/// attacks. If the port number is nonzero, it will be used instead.
|
||||
pub fn with_bind_addr(mut self, bind_addr: Option<SocketAddr>) -> Self {
|
||||
self.bind_addr = bind_addr;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures a list of local UDP ports that should not be used when making outgoing
|
||||
/// connections.
|
||||
pub fn avoid_local_ports(mut self, avoid_local_ports: Arc<HashSet<u16>>) -> Self {
|
||||
self.avoid_local_ports = avoid_local_ports;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures that OS should provide the ephemeral port, not the Hickory DNS
|
||||
pub fn with_os_port_selection(mut self, os_port_selection: bool) -> Self {
|
||||
self.os_port_selection = os_port_selection;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum number of retries for a single request
|
||||
pub fn with_max_retries(mut self, max_retries: u8) -> Self {
|
||||
self.max_retries = max_retries;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the retry interval floor
|
||||
pub fn with_retry_interval_floor(mut self, floor: u64) -> Self {
|
||||
self.retry_interval_floor = Duration::from_millis(floor);
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrap a [`DnsExchange`] around the built [`UdpClientStream`]
|
||||
pub fn exchange(self) -> DnsExchange<P> {
|
||||
let mut handle = self.provider.create_handle();
|
||||
let stream = self.build();
|
||||
let (exchange, bg) = DnsExchange::from_stream(stream);
|
||||
handle.spawn_bg(bg);
|
||||
exchange
|
||||
}
|
||||
|
||||
/// Construct a new UDP client stream.
|
||||
///
|
||||
/// Returns a future that outputs the client stream.
|
||||
pub fn build(self) -> UdpClientStream<P> {
|
||||
UdpClientStream {
|
||||
name_server: self.name_server,
|
||||
timeout: self.timeout.unwrap_or(Duration::from_secs(5)),
|
||||
is_shutdown: false,
|
||||
#[cfg(feature = "__dnssec")]
|
||||
signer: self.signer,
|
||||
bind_addr: self.bind_addr,
|
||||
avoid_local_ports: self.avoid_local_ports.clone(),
|
||||
os_port_selection: self.os_port_selection,
|
||||
provider: self.provider,
|
||||
max_retries: self.max_retries,
|
||||
retry_interval_floor: self.retry_interval_floor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This implements a retry handler for tasks that might not complete successfully (e.g.,
|
||||
/// DNS requests made via UDP.) It starts a task future immediately, then every
|
||||
/// retry_interval_time period up to a maximum of max_tasks. It will immediately return
|
||||
/// the first task that completes successfully, or an error if no tasks succeed.
|
||||
/// It does not implement an overall timeout to bound the work.
|
||||
async fn retry<Provider: RuntimeProvider>(
|
||||
request: impl Request,
|
||||
retry_interval_time: Duration,
|
||||
max_tasks: usize,
|
||||
) -> Result<DnsResponse, NetError> {
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
let retry_timer = Provider::Timer::delay_for(retry_interval_time).fuse();
|
||||
pin_mut!(retry_timer);
|
||||
|
||||
futures.push(request.send());
|
||||
let mut tasks = 1;
|
||||
|
||||
loop {
|
||||
futures_util::select! {
|
||||
result = futures.next() => {
|
||||
match result {
|
||||
Some(result) => return result,
|
||||
None => return Err(NetError::from("no tasks successful")),
|
||||
}
|
||||
}
|
||||
_ = &mut retry_timer => {
|
||||
if tasks < max_tasks {
|
||||
tasks += 1;
|
||||
futures.push(request.send());
|
||||
retry_timer.set(Provider::Timer::delay_for(retry_interval_time).fuse());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait Request {
|
||||
async fn send(&self) -> Result<DnsResponse, NetError>;
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "tokio"))]
|
||||
mod tests {
|
||||
#![allow(clippy::dbg_macro, clippy::print_stdout)]
|
||||
|
||||
use core::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
str::FromStr,
|
||||
sync::atomic::{AtomicU8, Ordering},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
use tokio::{net::UdpSocket, select, spawn, sync::oneshot, time::sleep};
|
||||
|
||||
use test_support::subscribe;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
proto::{
|
||||
op::{DnsRequestOptions, Query, ResponseCode},
|
||||
rr::{Name, RData, Record, RecordType, rdata::NULL},
|
||||
},
|
||||
runtime::{TokioRuntimeProvider, TokioTime},
|
||||
udp::tests::{
|
||||
udp_client_stream_bad_id_test, udp_client_stream_empty_question_section_test,
|
||||
udp_client_stream_test,
|
||||
},
|
||||
xfer::FirstAnswer,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv4() {
|
||||
subscribe();
|
||||
udp_client_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv4_bad_id() {
|
||||
subscribe();
|
||||
udp_client_stream_bad_id_test(IpAddr::V4(Ipv4Addr::LOCALHOST), TokioRuntimeProvider::new())
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv4_empty_question_section() {
|
||||
subscribe();
|
||||
udp_client_stream_empty_question_section_test(
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv6() {
|
||||
subscribe();
|
||||
udp_client_stream_test(
|
||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv6_bad_id() {
|
||||
subscribe();
|
||||
udp_client_stream_bad_id_test(
|
||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_client_stream_ipv6_empty_question_section() {
|
||||
subscribe();
|
||||
udp_client_stream_empty_question_section_test(
|
||||
IpAddr::V6(Ipv6Addr::LOCALHOST),
|
||||
TokioRuntimeProvider::new(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn retry_handler_test() -> Result<(), NetError> {
|
||||
let mut message = Message::query().into_response();
|
||||
message.metadata.response_code = ResponseCode::NoError;
|
||||
|
||||
let ret = retry::<TokioRuntimeProvider>(
|
||||
FixedResponse {
|
||||
response: DnsResponse::from_message(message.clone())?,
|
||||
},
|
||||
Duration::from_millis(200),
|
||||
5,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(ret.response_code, ResponseCode::NoError);
|
||||
|
||||
// test: retry timer doesn't fire extra tasks before the retry interval
|
||||
let (req, tries) = DelayedResponse::new(
|
||||
DnsResponse::from_message(message.clone()).unwrap(),
|
||||
Duration::from_millis(100),
|
||||
Arc::new(AtomicU8::new(0)),
|
||||
);
|
||||
retry::<TokioRuntimeProvider>(req, Duration::from_millis(200), 5).await?;
|
||||
assert_eq!(tries.load(Ordering::Relaxed), 1);
|
||||
|
||||
// test: retry timer does fire extra tasks after the retry interval
|
||||
let (req, tries) = DelayedResponse::new(
|
||||
DnsResponse::from_message(message.clone()).unwrap(),
|
||||
Duration::from_millis(1500),
|
||||
Arc::new(AtomicU8::new(0)),
|
||||
);
|
||||
retry::<TokioRuntimeProvider>(req, Duration::from_millis(200), 5).await?;
|
||||
assert_eq!(tries.load(Ordering::Relaxed), 5);
|
||||
|
||||
// test: retry timer tasks when nested under a Time::timer
|
||||
let (req, tries) = DelayedResponse::new(
|
||||
DnsResponse::from_message(message.clone()).unwrap(),
|
||||
Duration::from_millis(1000),
|
||||
Arc::new(AtomicU8::new(0)),
|
||||
);
|
||||
let timer_ret = TokioTime::timeout(
|
||||
Duration::from_millis(500),
|
||||
retry::<TokioRuntimeProvider>(req, Duration::from_millis(200), 5),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = timer_ret {
|
||||
assert_eq!(e.kind(), io::ErrorKind::TimedOut);
|
||||
} else {
|
||||
panic!("timer did not timeout");
|
||||
}
|
||||
|
||||
assert_eq!(tries.load(Ordering::Relaxed), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct FixedResponse {
|
||||
response: DnsResponse,
|
||||
}
|
||||
|
||||
impl Request for FixedResponse {
|
||||
async fn send(&self) -> Result<DnsResponse, NetError> {
|
||||
Ok(self.response.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct DelayedResponse {
|
||||
response: DnsResponse,
|
||||
delay: Duration,
|
||||
counter: Arc<AtomicU8>,
|
||||
}
|
||||
|
||||
impl DelayedResponse {
|
||||
fn new(
|
||||
response: DnsResponse,
|
||||
delay: Duration,
|
||||
counter: Arc<AtomicU8>,
|
||||
) -> (Self, Arc<AtomicU8>) {
|
||||
(
|
||||
Self {
|
||||
response,
|
||||
delay,
|
||||
counter: counter.clone(),
|
||||
},
|
||||
counter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Request for DelayedResponse {
|
||||
async fn send(&self) -> Result<DnsResponse, NetError> {
|
||||
let _ = self.counter.fetch_add(1, Ordering::Relaxed);
|
||||
sleep(self.delay).await;
|
||||
Ok(self.response.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// This tests one DoS variant of the TuDoor attack.
|
||||
///
|
||||
/// <https://lixiang521.com/publication/oakland24/sp24spring-tudoor-li.pdf>
|
||||
#[tokio::test]
|
||||
async fn test_ignore_invalid_message() {
|
||||
subscribe();
|
||||
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
|
||||
// Set up server.
|
||||
let server_socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
|
||||
let server_addr = server_socket.local_addr().unwrap();
|
||||
let (shutdown_sender, mut shutdown_receiver) = oneshot::channel();
|
||||
let server_handle = spawn(async move {
|
||||
loop {
|
||||
let mut buffer = [0u8; 4096];
|
||||
let result = select! {
|
||||
result = server_socket.recv_from(&mut buffer) => result,
|
||||
_ = &mut shutdown_receiver => break,
|
||||
};
|
||||
let (len, addr) = result.unwrap();
|
||||
|
||||
let request = Message::from_vec(&buffer[..len]).unwrap();
|
||||
|
||||
// Attacker sends datagrams with invalid DNS packets first.
|
||||
for id in [
|
||||
request.id ^ 1,
|
||||
request.id ^ 2,
|
||||
request.id ^ 4,
|
||||
request.id ^ 8,
|
||||
] {
|
||||
server_socket
|
||||
.send_to(&id.to_be_bytes(), addr)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Name server sends honest response next.
|
||||
let query_name = request.queries[0].name.clone();
|
||||
let mut message = request.into_response();
|
||||
message.add_answer(Record::from_rdata(
|
||||
query_name,
|
||||
0,
|
||||
RData::NULL(NULL::with(b"DEADBEEF".to_vec())),
|
||||
));
|
||||
let bytes = message.to_vec().unwrap();
|
||||
server_socket.send_to(&bytes, addr).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up client.
|
||||
let mut stream = UdpClientStream::builder(server_addr, provider)
|
||||
.with_timeout(Some(Duration::from_millis(500)))
|
||||
.build();
|
||||
|
||||
let mut query_message = Message::query();
|
||||
query_message.add_query(Query::query(
|
||||
Name::from_str("dead.beef.").unwrap(),
|
||||
RecordType::NULL,
|
||||
));
|
||||
let response_stream =
|
||||
stream.send_message(DnsRequest::new(query_message, DnsRequestOptions::default()));
|
||||
let response = response_stream
|
||||
.first_answer()
|
||||
.await
|
||||
.expect("failed to read response in the presence of spoofed invalid messages");
|
||||
let RData::NULL(rdata) = &response.answers[0].data else {
|
||||
panic!("unexpected record type: {response:?}");
|
||||
};
|
||||
assert_eq!(rdata.anything, b"DEADBEEF");
|
||||
|
||||
shutdown_sender.send(()).unwrap();
|
||||
server_handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{
|
||||
future::{BoxFuture, Future},
|
||||
ready,
|
||||
stream::Stream,
|
||||
};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::error::NetError;
|
||||
use crate::proto::op::SerialMessage;
|
||||
use crate::runtime::{DnsUdpSocket, RuntimeProvider};
|
||||
use crate::udp::MAX_RECEIVE_BUFFER_SIZE;
|
||||
use crate::xfer::{BufDnsStreamHandle, StreamReceiver};
|
||||
|
||||
/// Trait for UdpSocket
|
||||
#[async_trait]
|
||||
pub trait UdpSocket: DnsUdpSocket {
|
||||
/// setups up a "client" udp connection that will only receive packets from the associated address
|
||||
async fn connect(addr: SocketAddr) -> io::Result<Self>;
|
||||
|
||||
/// same as connect, but binds to the specified local address for sending address
|
||||
async fn connect_with_bind(addr: SocketAddr, bind_addr: SocketAddr) -> io::Result<Self>;
|
||||
|
||||
/// a "server" UDP socket, that bind to the local listening address, and unbound remote address (can receive from anything)
|
||||
async fn bind(addr: SocketAddr) -> io::Result<Self>;
|
||||
}
|
||||
|
||||
/// A UDP stream of DNS binary packets
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct UdpStream<P: RuntimeProvider> {
|
||||
socket: P::Udp,
|
||||
outbound_messages: StreamReceiver,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpStream<P> {
|
||||
/// This method is intended for client connections, see [`Self::with_bound`] for a method better
|
||||
/// for straight listening. It is expected that the resolver wrapper will be responsible for
|
||||
/// creating and managing new UdpStreams such that each new client would have a random port
|
||||
/// (reduce chance of cache poisoning). This will return a randomly assigned local port, unless
|
||||
/// a nonzero port number is specified in `bind_addr`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `remote_addr` - socket address for the remote connection (used to determine IPv4 or IPv6)
|
||||
/// * `bind_addr` - optional local socket address to connect from (if a nonzero port number is
|
||||
/// specified, it will be used instead of randomly selecting a port)
|
||||
/// * `os_port_selection` - Boolean parameter to specify whether to use the operating system's
|
||||
/// standard UDP port selection logic instead of Hickory's logic to
|
||||
/// securely select a random source port. We do not recommend using
|
||||
/// this option unless absolutely necessary, as the operating system
|
||||
/// may select ephemeral ports from a smaller range than Hickory, which
|
||||
/// can make response poisoning attacks easier to conduct. Some
|
||||
/// operating systems (notably, Windows) might display a user-prompt to
|
||||
/// allow a Hickory-specified port to be used, and setting this option
|
||||
/// will prevent those prompts from being displayed. If os_port_selection
|
||||
/// is true, avoid_local_udp_ports will be ignored.
|
||||
/// * `provider` - async runtime provider, for I/O and timers
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// A tuple of a Future of a Stream which will handle sending and receiving messages, and a
|
||||
/// handle which can be used to send messages into the stream.
|
||||
pub fn new(
|
||||
remote_addr: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
avoid_local_ports: Option<Arc<HashSet<u16>>>,
|
||||
os_port_selection: bool,
|
||||
provider: P,
|
||||
) -> (
|
||||
BoxFuture<'static, Result<Self, NetError>>,
|
||||
BufDnsStreamHandle,
|
||||
) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(remote_addr);
|
||||
|
||||
// constructs a future for getting the next randomly bound port to a UdpSocket
|
||||
let next_socket = NextRandomUdpSocket::new(
|
||||
remote_addr,
|
||||
bind_addr,
|
||||
avoid_local_ports.unwrap_or_default(),
|
||||
os_port_selection,
|
||||
provider,
|
||||
);
|
||||
|
||||
// This set of futures collapses the next udp socket into a stream which can be used for
|
||||
// sending and receiving udp packets.
|
||||
let stream = Box::pin(async {
|
||||
Ok(Self {
|
||||
socket: next_socket.await?,
|
||||
outbound_messages,
|
||||
})
|
||||
});
|
||||
|
||||
(stream, message_sender)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpStream<P> {
|
||||
/// Initialize the Stream with an already bound socket. Generally this should be only used for
|
||||
/// server listening sockets. See [`Self::new`] for a client oriented socket. Specifically,
|
||||
/// this requires there is already a bound socket, whereas `new` makes sure to randomize ports
|
||||
/// for additional cache poison prevention.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `socket` - an already bound UDP socket
|
||||
/// * `remote_addr` - remote side of this connection
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// A tuple of a Stream which will handle sending and receiving messages, and a handle which can
|
||||
/// be used to send messages into the stream.
|
||||
pub fn with_bound(socket: P::Udp, remote_addr: SocketAddr) -> (Self, BufDnsStreamHandle) {
|
||||
let (message_sender, outbound_messages) = BufDnsStreamHandle::new(remote_addr);
|
||||
let stream = Self {
|
||||
socket,
|
||||
outbound_messages,
|
||||
};
|
||||
|
||||
(stream, message_sender)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tokio", feature = "mdns"))]
|
||||
pub(crate) fn from_parts(socket: P::Udp, outbound_messages: StreamReceiver) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
outbound_messages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> UdpStream<P> {
|
||||
fn pollable_split(&mut self) -> (&mut P::Udp, &mut StreamReceiver) {
|
||||
(&mut self.socket, &mut self.outbound_messages)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> Stream for UdpStream<P> {
|
||||
type Item = Result<SerialMessage, io::Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let (socket, outbound_messages) = self.pollable_split();
|
||||
let socket = Pin::new(socket);
|
||||
let mut outbound_messages = Pin::new(outbound_messages);
|
||||
|
||||
// this will not accept incoming data while there is data to send
|
||||
// makes this self throttling.
|
||||
while let Poll::Ready(Some(message)) = outbound_messages.as_mut().poll_peek(cx) {
|
||||
// first try to send
|
||||
let addr = message.addr();
|
||||
|
||||
// this will return if not ready,
|
||||
// meaning that sending will be preferred over receiving...
|
||||
|
||||
// TODO: shouldn't this return the error to send to the sender?
|
||||
if let Err(e) = ready!(socket.poll_send_to(cx, message.bytes(), addr)) {
|
||||
// Drop the UDP packet and continue
|
||||
warn!(
|
||||
"error sending message to {} on udp_socket, dropping response: {}",
|
||||
addr, e
|
||||
);
|
||||
}
|
||||
|
||||
// message sent, need to pop the message
|
||||
assert!(outbound_messages.as_mut().poll_next(cx).is_ready());
|
||||
}
|
||||
|
||||
// For QoS, this will only accept one message and output that
|
||||
// receive all inbound messages
|
||||
|
||||
// TODO: this should match edns settings
|
||||
let mut buf = [0u8; MAX_RECEIVE_BUFFER_SIZE];
|
||||
let (len, src) = ready!(socket.poll_recv_from(cx, &mut buf))?;
|
||||
|
||||
let serial_message = SerialMessage::new(buf.iter().take(len).cloned().collect(), src);
|
||||
Poll::Ready(Some(Ok(serial_message)))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub(crate) struct NextRandomUdpSocket<P: RuntimeProvider> {
|
||||
name_server: SocketAddr,
|
||||
bind_address: SocketAddr,
|
||||
provider: P,
|
||||
/// Number of unsuccessful attempts to pick a port.
|
||||
attempted: usize,
|
||||
#[allow(clippy::type_complexity)]
|
||||
future: Option<Pin<Box<dyn Send + Future<Output = Result<P::Udp, NetError>>>>>,
|
||||
avoid_local_ports: Arc<HashSet<u16>>,
|
||||
os_port_selection: bool,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> NextRandomUdpSocket<P> {
|
||||
/// Creates a future for randomly binding to a local socket address for client connections,
|
||||
/// if no port is specified.
|
||||
///
|
||||
/// If a port is specified in the bind address it is used.
|
||||
pub(crate) fn new(
|
||||
name_server: SocketAddr,
|
||||
bind_addr: Option<SocketAddr>,
|
||||
avoid_local_ports: Arc<HashSet<u16>>,
|
||||
os_port_selection: bool,
|
||||
provider: P,
|
||||
) -> Self {
|
||||
let bind_address = match bind_addr {
|
||||
Some(ba) => ba,
|
||||
None => match name_server {
|
||||
SocketAddr::V4(..) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
SocketAddr::V6(..) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
|
||||
},
|
||||
};
|
||||
|
||||
Self {
|
||||
name_server,
|
||||
bind_address,
|
||||
provider,
|
||||
attempted: 0,
|
||||
future: None,
|
||||
avoid_local_ports,
|
||||
os_port_selection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> Future for NextRandomUdpSocket<P> {
|
||||
type Output = Result<P::Udp, NetError>;
|
||||
|
||||
/// polls until there is an available next random UDP port,
|
||||
/// if no port has been specified in bind_addr.
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
loop {
|
||||
this.future = match this.future.take() {
|
||||
Some(mut future) => match future.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok(socket)) => {
|
||||
debug!("created socket successfully");
|
||||
return Poll::Ready(Ok(socket));
|
||||
}
|
||||
Poll::Ready(Err(NetError::Io(io)))
|
||||
if matches!(
|
||||
io.kind(),
|
||||
io::ErrorKind::PermissionDenied | io::ErrorKind::AddrInUse
|
||||
) && this.attempted < ATTEMPT_RANDOM + 1 =>
|
||||
{
|
||||
debug!("unable to bind port, attempt: {}: {io}", this.attempted);
|
||||
this.attempted += 1;
|
||||
None
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
debug!("failed to bind port: {err}");
|
||||
return Poll::Ready(Err(err));
|
||||
}
|
||||
Poll::Pending => {
|
||||
debug!("unable to bind port, attempt: {}", this.attempted);
|
||||
this.future = Some(future);
|
||||
return Poll::Pending;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let mut bind_addr = this.bind_address;
|
||||
|
||||
if !this.os_port_selection && bind_addr.port() == 0 {
|
||||
while this.attempted < ATTEMPT_RANDOM {
|
||||
// Per RFC 6056 Section 3.2:
|
||||
//
|
||||
// As mentioned in Section 2.1, the dynamic ports consist of the range
|
||||
// 49152-65535. However, ephemeral port selection algorithms should use
|
||||
// the whole range 1024-65535.
|
||||
let port = rand::random_range(1024..=u16::MAX);
|
||||
if this.avoid_local_ports.contains(&port) {
|
||||
// Count this against the total number of attempts to pick a port.
|
||||
// RFC 6056 Section 3.3.2 notes that this algorithm should find a
|
||||
// suitable port in one or two attempts with high probability in
|
||||
// common scenarios. If `avoid_local_ports` is pathologically large,
|
||||
// then incrementing the counter here will prevent an infinite loop.
|
||||
this.attempted += 1;
|
||||
continue;
|
||||
} else {
|
||||
bind_addr = SocketAddr::new(bind_addr.ip(), port);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!(port = bind_addr.port(), "binding UDP socket");
|
||||
let future = this.provider.bind_udp(bind_addr, this.name_server);
|
||||
Some(Box::pin(async move { Ok(future.await?) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ATTEMPT_RANDOM: usize = 10;
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[async_trait]
|
||||
impl UdpSocket for tokio::net::UdpSocket {
|
||||
/// sets up up a "client" udp connection that will only receive packets from the associated address
|
||||
///
|
||||
/// if the addr is ipv4 then it will bind local addr to 0.0.0.0:0, ipv6 \[::\]0
|
||||
async fn connect(addr: SocketAddr) -> io::Result<Self> {
|
||||
let bind_addr: SocketAddr = match addr {
|
||||
SocketAddr::V4(_addr) => (Ipv4Addr::UNSPECIFIED, 0).into(),
|
||||
SocketAddr::V6(_addr) => (Ipv6Addr::UNSPECIFIED, 0).into(),
|
||||
};
|
||||
|
||||
Self::connect_with_bind(addr, bind_addr).await
|
||||
}
|
||||
|
||||
/// same as connect, but binds to the specified local address for sending address
|
||||
async fn connect_with_bind(_addr: SocketAddr, bind_addr: SocketAddr) -> io::Result<Self> {
|
||||
let socket = Self::bind(bind_addr).await?;
|
||||
|
||||
// TODO: research connect more, it appears to break UDP receiving tests, etc...
|
||||
// socket.connect(addr).await?;
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
async fn bind(addr: SocketAddr) -> io::Result<Self> {
|
||||
Self::bind(addr).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
#[async_trait]
|
||||
impl DnsUdpSocket for tokio::net::UdpSocket {
|
||||
type Time = crate::runtime::TokioTime;
|
||||
|
||||
fn poll_recv_from(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<(usize, SocketAddr)>> {
|
||||
let mut buf = tokio::io::ReadBuf::new(buf);
|
||||
let addr = ready!(Self::poll_recv_from(self, cx, &mut buf))?;
|
||||
let len = buf.filled().len();
|
||||
|
||||
Poll::Ready(Ok((len, addr)))
|
||||
}
|
||||
|
||||
fn poll_send_to(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
target: SocketAddr,
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Self::poll_send_to(self, cx, buf, target)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "tokio")]
|
||||
mod tests {
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use test_support::subscribe;
|
||||
|
||||
use crate::{
|
||||
runtime::TokioRuntimeProvider,
|
||||
udp::tests::{next_random_socket_test, udp_stream_test},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_random_socket() {
|
||||
subscribe();
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
next_random_socket_test(provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_stream_ipv4() {
|
||||
subscribe();
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
udp_stream_test(IpAddr::V4(Ipv4Addr::LOCALHOST), provider).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_udp_stream_ipv6() {
|
||||
subscribe();
|
||||
let provider = TokioRuntimeProvider::new();
|
||||
udp_stream_test(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), provider).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2015-2018 Benjamin Fry <[email protected]>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! This module contains all the types for demuxing DNS oriented streams.
|
||||
|
||||
use core::future::Future;
|
||||
use core::marker::PhantomData;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
|
||||
use futures_channel::mpsc;
|
||||
use futures_util::stream::{Peekable, Stream, StreamExt};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::error::NetError;
|
||||
use crate::proto::op::{DnsRequest, DnsResponse};
|
||||
use crate::runtime::RuntimeProvider;
|
||||
use crate::runtime::Time;
|
||||
use crate::xfer::dns_handle::DnsHandle;
|
||||
use crate::xfer::{
|
||||
BufDnsRequestStreamHandle, DEFAULT_STREAM_BUFFER_SIZE, DnsRequestSender, DnsResponseReceiver,
|
||||
OneshotDnsRequest,
|
||||
};
|
||||
|
||||
/// This is a generic Exchange implemented over multiplexed DNS connection providers.
|
||||
///
|
||||
/// The underlying `DnsRequestSender` is expected to multiplex any I/O connections.
|
||||
/// DnsExchange assumes that the underlying stream is responsible for this.
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct DnsExchange<P> {
|
||||
sender: BufDnsRequestStreamHandle<P>,
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> DnsExchange<P> {
|
||||
/// Wraps a [`DnsRequestSender`] in a buffered, cloneable [`DnsHandle`].
|
||||
///
|
||||
/// Returns a `DnsExchange` handle and a background future that must be spawned
|
||||
/// to drive I/O. The handle can be cloned and shared across tasks; requests are
|
||||
/// buffered in an internal channel and forwarded to the underlying stream by the
|
||||
/// background task.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `stream` - Any [`DnsRequestSender`] (e.g.,
|
||||
/// [`UdpClientStream`][crate::udp::UdpClientStream],
|
||||
/// [`TcpClientStream`][crate::tcp::TcpClientStream])
|
||||
pub fn from_stream<S: DnsRequestSender>(
|
||||
stream: S,
|
||||
) -> (Self, DnsExchangeBackground<S, P::Timer>) {
|
||||
let (sender, outbound_messages) = mpsc::channel(DEFAULT_STREAM_BUFFER_SIZE);
|
||||
(
|
||||
Self {
|
||||
sender: BufDnsRequestStreamHandle {
|
||||
sender,
|
||||
_phantom: PhantomData,
|
||||
},
|
||||
},
|
||||
DnsExchangeBackground {
|
||||
io_stream: stream,
|
||||
outbound_messages: outbound_messages.peekable(),
|
||||
marker: PhantomData,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Clone> Clone for DnsExchange<P> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
sender: self.sender.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: RuntimeProvider> DnsHandle for DnsExchange<P> {
|
||||
type Response = DnsExchangeSend<P>;
|
||||
type Runtime = P;
|
||||
|
||||
fn send(&self, request: DnsRequest) -> Self::Response {
|
||||
DnsExchangeSend {
|
||||
result: self.sender.send(request),
|
||||
_sender: self.sender.clone(), // TODO: this shouldn't be necessary, currently the presence of Senders is what allows the background to track current users, it generally is dropped right after send, this makes sure that there is at least one active after send
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Stream that will resolve to Responses after sending the request
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct DnsExchangeSend<P> {
|
||||
result: DnsResponseReceiver,
|
||||
_sender: BufDnsRequestStreamHandle<P>,
|
||||
}
|
||||
|
||||
impl<P: Unpin> Stream for DnsExchangeSend<P> {
|
||||
type Item = Result<DnsResponse, NetError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
// as long as there is no result, poll the exchange
|
||||
self.result.poll_next_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// This background future is responsible for driving all network operations for the DNS protocol.
|
||||
///
|
||||
/// It must be spawned before any DNS messages are sent.
|
||||
#[must_use = "futures do nothing unless polled"]
|
||||
pub struct DnsExchangeBackground<S, TE> {
|
||||
io_stream: S,
|
||||
outbound_messages: Peekable<mpsc::Receiver<OneshotDnsRequest>>,
|
||||
marker: PhantomData<TE>,
|
||||
}
|
||||
|
||||
impl<S, TE> DnsExchangeBackground<S, TE> {
|
||||
fn pollable_split(&mut self) -> (&mut S, &mut Peekable<mpsc::Receiver<OneshotDnsRequest>>) {
|
||||
(&mut self.io_stream, &mut self.outbound_messages)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: DnsRequestSender, TE: Time> Future for DnsExchangeBackground<S, TE> {
|
||||
type Output = ();
|
||||
|
||||
#[allow(clippy::unused_unit)]
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let (io_stream, outbound_messages) = self.pollable_split();
|
||||
let mut io_stream = Pin::new(io_stream);
|
||||
let mut outbound_messages = Pin::new(outbound_messages);
|
||||
|
||||
// this will not accept incoming data while there is data to send
|
||||
// makes this self throttling.
|
||||
loop {
|
||||
// poll the underlying stream, to drive it...
|
||||
match io_stream.as_mut().poll_next(cx) {
|
||||
// The stream is ready
|
||||
Poll::Ready(Some(Ok(()))) => (),
|
||||
Poll::Pending => {
|
||||
if io_stream.is_shutdown() {
|
||||
// the io_stream is in a shutdown state, we are only waiting for final results...
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
// NotReady and not shutdown, see if there are more messages to send
|
||||
()
|
||||
} // underlying stream is complete.
|
||||
Poll::Ready(None) => {
|
||||
debug!("io_stream is done, shutting down");
|
||||
return Poll::Ready(());
|
||||
}
|
||||
Poll::Ready(Some(Err(error))) => {
|
||||
debug!(
|
||||
%error,
|
||||
"io_stream hit an error, shutting down"
|
||||
);
|
||||
|
||||
return Poll::Ready(());
|
||||
}
|
||||
}
|
||||
|
||||
// then see if there is more to send
|
||||
match outbound_messages.as_mut().poll_next(cx) {
|
||||
// already handled above, here to make sure the poll() pops the next message
|
||||
Poll::Ready(Some(dns_request)) => {
|
||||
// if there is no peer, this connection should die...
|
||||
let (dns_request, serial_response): (DnsRequest, _) = dns_request.into_parts();
|
||||
|
||||
// Try to forward the `DnsResponseStream` to the requesting task. If we fail,
|
||||
// it must be because the requesting task has gone away / is no longer
|
||||
// interested. In that case, we can just log a warning, but there's no need
|
||||
// to take any more serious measures (such as shutting down this task).
|
||||
match serial_response.send_response(io_stream.send_message(dns_request)) {
|
||||
Ok(()) => (),
|
||||
Err(_) => {
|
||||
debug!("failed to associate send_message response to the sender");
|
||||
}
|
||||
}
|
||||
}
|
||||
// On not ready, this is our time to return...
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(None) => {
|
||||
// if there is nothing that can use this connection to send messages, then this is done...
|
||||
io_stream.shutdown();
|
||||
|
||||
// now we'll await the stream to shutdown... see io_stream poll above
|
||||
}
|
||||
}
|
||||
|
||||
// else we loop to poll on the outbound_messages
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user