From 70ed7417eda57c0ab7d84cf06fb25600a737e510 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:04:22 +0200 Subject: [PATCH] v1.0.1 (closes #2) --- CHANGELOG.md | 14 + Cargo.lock | 844 ++++++++++++------------------- Cargo.toml | 15 +- src/admin.rs | 2 + src/config/mod.rs | 7 + src/error.rs | 12 +- src/http/proxy.rs | 2 +- src/outbound.rs | 6 +- src/proto/common.rs | 274 +++++++++- src/proto/imap.rs | 1 + src/proto/managesieve.rs | 1 + src/proto/pop3.rs | 1 + src/proto/smtp.rs | 3 +- src/tests/admin.rs | 4 + src/tests/containers/legacy.rs | 27 +- src/tests/containers/mod.rs | 99 +++- src/tests/containers/stalwart.rs | 4 +- src/tests/containers/toolbox.rs | 13 +- src/tests/dest_health.rs | 274 ++++++++++ src/tests/mod.rs | 1 + 20 files changed, 1039 insertions(+), 565 deletions(-) create mode 100644 src/tests/dest_health.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b63334..210290f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [1.0.1] - 2026-07-30 + +### Added +- `server.host_down_min_clients`: number of distinct client addresses a + failure streak must span before client-attributable failures can mark a destination + down. +- `GET /destinations` now also reports `failure_sources` and `host_level_failure`. + +### Changed + +### Fixed +- The circuit breaker no longer marks a destination down because a backend rejects + the connections of a single banned client (#2). + ## [1.0.0] - 2026-06-08 ### Added diff --git a/Cargo.lock b/Cargo.lock index 4f8f699..132244d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,15 +42,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -85,7 +85,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -97,7 +97,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -112,15 +112,15 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" +checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463" dependencies = [ - "filetime", "futures-core", "libc", "portable-atomic", "rustc-hash", + "rustix", "tokio", "tokio-stream", "xattr", @@ -156,18 +156,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -193,9 +193,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -204,14 +204,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -272,6 +273,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "bit-vec" version = "0.9.1" @@ -283,9 +290,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.12.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -310,9 +317,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -335,7 +342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", - "base64", + "base64 0.22.1", "bitflags", "bollard-buildkit-proto", "bollard-stubs", @@ -354,7 +361,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -392,7 +399,7 @@ version = "1.52.1-rc.29.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" dependencies = [ - "base64", + "base64 0.22.1", "bollard-buildkit-proto", "bytes", "prost", @@ -413,12 +420,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -435,15 +442,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.63" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -459,15 +466,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -476,9 +483,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -535,15 +542,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -595,36 +593,36 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" @@ -684,7 +682,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -695,7 +693,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -724,7 +722,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -753,20 +750,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -775,7 +772,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" dependencies = [ - "base64", + "base64 0.22.1", "serde", "serde_json", ] @@ -800,9 +797,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -835,11 +832,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -856,9 +852,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "ferroid" @@ -867,20 +863,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.10.1", + "rand 0.10.2", "web-time", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -904,12 +890,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -933,9 +913,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -948,9 +928,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -958,15 +938,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -986,38 +966,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1060,32 +1040,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1096,9 +1074,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1119,15 +1097,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1136,7 +1105,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1154,14 +1123,14 @@ dependencies = [ "indexmap 2.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", ] @@ -1223,9 +1192,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1233,9 +1202,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1243,9 +1212,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1268,18 +1237,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1299,9 +1268,9 @@ dependencies = [ [[package]] name = "hyper-named-pipe" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" dependencies = [ "hex", "hyper", @@ -1309,7 +1278,6 @@ dependencies = [ "pin-project-lite", "tokio", "tower-service", - "winapi", ] [[package]] @@ -1346,7 +1314,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1484,12 +1452,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1563,13 +1525,13 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jmap-client" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3357aac1ef357587e09f44262f351f7773120fb695e281b34628ef9d84028038" +checksum = "4deab22e057d24e32122f0fc6e2d667a124fdd6a0d8ef3ed4f8a89923c11084f" dependencies = [ "ahash", "async-stream", - "base64", + "base64 0.22.1", "chrono", "futures-util", "maybe-async", @@ -1610,7 +1572,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -1629,28 +1591,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1660,17 +1621,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libsqlite3-sys" @@ -1706,9 +1661,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1718,9 +1673,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mail-parser" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2420e9ce11c2b0583ca97ddff7ab2398c8a613154e9b72e3bafdbf767f1d7" +checksum = "47785d444be4d32c1709171c6219a90f667c0ad0ffe68b4b179e794f31f4f9e8" dependencies = [ "hashify", ] @@ -1748,7 +1703,7 @@ checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1774,9 +1729,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mime" @@ -1792,9 +1747,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1818,7 +1773,7 @@ dependencies = [ "hyper-util", "log", "pin-project-lite", - "rand 0.9.4", + "rand 0.9.5", "regex", "serde_json", "serde_urlencoded", @@ -1881,9 +1836,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1915,11 +1870,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2032,7 +1986,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn", + "syn 2.0.119", ] [[package]] @@ -2041,7 +1995,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -2068,7 +2022,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2085,9 +2039,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "potential_utf" @@ -2113,30 +2067,20 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2144,34 +2088,34 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] [[package]] name = "proxy" -version = "1.0.0" +version = "1.0.1" dependencies = [ "ahash", "arc-swap", "aws-lc-rs", - "base64", + "base64 0.23.0", "futures-util", "globset", "jmap-client", @@ -2226,16 +2170,16 @@ dependencies = [ "byteorder", "hmac 0.10.1", "md-5 0.9.1", - "rand 0.8.6", + "rand 0.8.7", "sha-1", "sha2 0.9.9", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2253,15 +2197,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -2275,23 +2220,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2310,9 +2255,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2321,9 +2266,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2331,12 +2276,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -2384,6 +2329,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rcgen" version = "0.14.8" @@ -2400,9 +2354,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.2.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6b5f4d8ef33944e833e2b1859ad478deab6e431d7337b30ee2efe21f7543" +checksum = "3257df217f7eab0044627a268c9cc6cdb60c0c421c88f83ac41c4e31520b6b84" dependencies = [ "arc-swap", "arcstr", @@ -2437,29 +2391,29 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2469,9 +2423,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -2480,9 +2434,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -2490,7 +2444,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -2539,9 +2493,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2576,9 +2530,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -2613,9 +2567,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -2662,9 +2616,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2704,9 +2658,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -2751,9 +2705,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2761,29 +2715,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2794,13 +2748,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -2826,18 +2780,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "base64", + "base64 0.22.1", "bs58", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -2846,14 +2800,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2871,9 +2825,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -2959,9 +2913,9 @@ dependencies = [ [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -2987,24 +2941,24 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] [[package]] name = "smtp-proto" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c9f5dd7ec5cc6d743f33fcb96de4eb91bb1cc51c5e0ba40cb285a9012043da" +checksum = "4570e312141d42f0cb8d4e532fc8ec96c805b308fcb9012d973558cccd9dee85" [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3012,9 +2966,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -3038,7 +2992,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "cfg-if", "crc", @@ -3078,7 +3032,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.119", ] [[package]] @@ -3101,7 +3055,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.119", "tokio", "url", ] @@ -3139,7 +3093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -3155,7 +3109,7 @@ dependencies = [ "log", "md-5 0.11.0", "memchr", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", @@ -3223,7 +3177,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn", + "syn 2.0.119", ] [[package]] @@ -3234,7 +3188,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3245,9 +3199,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3271,7 +3236,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3287,7 +3252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3335,41 +3300,40 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3379,15 +3343,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3405,9 +3369,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -3420,9 +3384,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3437,13 +3401,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -3458,9 +3422,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -3485,22 +3449,23 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -3522,18 +3487,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -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 = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" @@ -3543,7 +3508,7 @@ checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "h2", "http", @@ -3644,7 +3609,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3703,10 +3668,10 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-pki-types", - "sha1 0.10.6", + "sha1 0.10.7", "thiserror", ] @@ -3743,12 +3708,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.7.1" @@ -3767,7 +3726,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64", + "base64 0.22.1", "log", "percent-encoding", "rustls", @@ -3782,7 +3741,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64", + "base64 0.22.1", "http", "httparse", "log", @@ -3815,11 +3774,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -3869,27 +3828,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3900,9 +3850,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3910,9 +3860,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3920,48 +3870,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -3975,23 +3903,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4009,18 +3925,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -4083,7 +3999,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4094,7 +4010,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4205,18 +4121,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -4224,85 +4131,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -4339,9 +4167,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "yasna" @@ -4372,28 +4200,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4413,15 +4241,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -4453,11 +4281,11 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 46bc42d..b5705d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "proxy" -version = "1.0.0" +version = "1.0.1" edition = "2024" -rust-version = "1.85" license = "AGPL-3.0-only OR LicenseRef-SEL" description = "Multi-protocol e-mail proxy" authors = ["Stalwart Labs LLC "] @@ -17,22 +16,22 @@ name = "proxy" path = "src/main.rs" [dependencies] -tokio = { version = "1.52", features = ["net", "io-util", "rt-multi-thread", "macros", "signal", "sync", "time"] } +tokio = { version = "1.53", features = ["net", "io-util", "rt-multi-thread", "macros", "signal", "sync", "time"] } rustls = { version = "0.23", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } rustls-pemfile = "2.2" -rustls-pki-types = "1.14" +rustls-pki-types = "1.15" aws-lc-rs = "1.17" proxy-header = { version = "0.1.2", features = ["tokio"] } -smtp-proto = "0.2.1" +smtp-proto = "0.2.2" mail-parser = "0.11" -base64 = "0.22" +base64 = "0.23" serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "1" globset = "0.4" -regex = "1.12" -redis = { version = "1.2", features = ["tokio-comp", "aio", "connection-manager"] } +regex = "1.13" +redis = { version = "1.5", features = ["tokio-comp", "aio", "connection-manager"] } sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls-aws-lc-rs", "any", "postgres", "mysql", "sqlite"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/admin.rs b/src/admin.rs index 45e28c4..06bd35c 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -237,6 +237,8 @@ async fn handle( serde_json::json!({ "destination": id, "consecutive_failures": s.consecutive_failures, + "failure_sources": s.failure_sources, + "host_level_failure": s.host_level_failure, "down": s.down, }) }) diff --git a/src/config/mod.rs b/src/config/mod.rs index 3bc63da..b8d2179 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -140,6 +140,8 @@ pub struct ServerConfig { pub backend_connect_retries: u32, #[serde(default = "default_host_down_threshold")] pub host_down_threshold: u32, + #[serde(default = "default_host_down_min_clients")] + pub host_down_min_clients: u32, #[serde(default = "default_host_down_cooldown", with = "duration")] pub host_down_cooldown: Duration, #[serde(default = "default_proxy_ttl")] @@ -159,6 +161,7 @@ impl Default for ServerConfig { backend_timeout: DEFAULT_BACKEND_TIMEOUT, backend_connect_retries: DEFAULT_BACKEND_CONNECT_RETRIES, host_down_threshold: DEFAULT_HOST_DOWN_THRESHOLD, + host_down_min_clients: DEFAULT_HOST_DOWN_MIN_CLIENTS, host_down_cooldown: DEFAULT_HOST_DOWN_COOLDOWN, proxy_ttl: DEFAULT_PROXY_TTL, log_level: DEFAULT_LOG_LEVEL.to_string(), @@ -181,6 +184,7 @@ const DEFAULT_BRIDGE_IDLE: Duration = Duration::from_secs(1800); const DEFAULT_BACKEND_TIMEOUT: Duration = Duration::from_secs(10); const DEFAULT_BACKEND_CONNECT_RETRIES: u32 = 1; const DEFAULT_HOST_DOWN_THRESHOLD: u32 = 5; +const DEFAULT_HOST_DOWN_MIN_CLIENTS: u32 = 2; const DEFAULT_HOST_DOWN_COOLDOWN: Duration = Duration::from_secs(30); const DEFAULT_PROXY_TTL: u32 = 7; const DEFAULT_LOG_LEVEL: &str = "info"; @@ -200,6 +204,9 @@ fn default_backend_connect_retries() -> u32 { fn default_host_down_threshold() -> u32 { DEFAULT_HOST_DOWN_THRESHOLD } +fn default_host_down_min_clients() -> u32 { + DEFAULT_HOST_DOWN_MIN_CLIENTS +} fn default_host_down_cooldown() -> Duration { DEFAULT_HOST_DOWN_COOLDOWN } diff --git a/src/error.rs b/src/error.rs index d5b171e..a1a23cf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,6 +25,9 @@ pub enum ProxyError { #[error("backend connection failed: {0}")] BackendConnect(Cow<'static, str>), + #[error("backend session failed: {0}")] + BackendSession(Cow<'static, str>), + #[error("protocol error: {0}")] Protocol(Cow<'static, str>), @@ -52,8 +55,15 @@ impl ProxyError { ProxyError::BackendConnect(msg.into()) } + pub fn backend_session(msg: impl Into>) -> Self { + ProxyError::BackendSession(msg.into()) + } + pub fn is_retryable(&self) -> bool { - matches!(self, ProxyError::BackendConnect(_) | ProxyError::Io(_)) + matches!( + self, + ProxyError::BackendConnect(_) | ProxyError::BackendSession(_) | ProxyError::Io(_) + ) } pub fn protocol(msg: impl Into>) -> Self { diff --git a/src/http/proxy.rs b/src/http/proxy.rs index 7ae979e..a0a8ec8 100644 --- a/src/http/proxy.rs +++ b/src/http/proxy.rs @@ -502,7 +502,7 @@ async fn serve( let _ = write_client(stream, &error_response(502, "Bad Gateway")).await; return Ok(Disposition::Close); } - let mut backend = match crate::proto::common::establish(ctx, &dest_id, || { + let mut backend = match crate::proto::common::establish(ctx, &dest_id, peer, || { crate::outbound::dial(dest, ep, fwd, &tls_cfg, peer, local) }) .await diff --git a/src/outbound.rs b/src/outbound.rs index 9ac2ea7..0987288 100644 --- a/src/outbound.rs +++ b/src/outbound.rs @@ -134,7 +134,7 @@ pub async fn tls_connect( let tls = connector .connect(name, stream) .await - .map_err(|e| ProxyError::backend_connect(format!("backend TLS handshake failed: {e}")))?; + .map_err(|e| ProxyError::backend_session(format!("backend TLS handshake failed: {e}")))?; Ok(Box::new(tls)) } @@ -160,9 +160,9 @@ pub async fn read_line( let n = stream .read(&mut buf) .await - .map_err(|e| ProxyError::backend_connect(format!("reading backend: {e}")))?; + .map_err(|e| ProxyError::backend_session(format!("reading backend: {e}")))?; if n == 0 { - return Err(ProxyError::backend_connect( + return Err(ProxyError::backend_session( "backend closed before sending a complete line", )); } diff --git a/src/proto/common.rs b/src/proto/common.rs index 5859405..69af222 100644 --- a/src/proto/common.rs +++ b/src/proto/common.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::future::Future; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -141,9 +141,35 @@ pub struct DestHealth { #[derive(Default)] struct HealthState { consecutive_failures: u32, + failure_sources: AHashSet, + host_level_failure: bool, down_until: Option, } +impl HealthState { + fn clear_evidence(&mut self) { + self.failure_sources.clear(); + self.host_level_failure = false; + } +} + +const MAX_TRACKED_FAILURE_SOURCES: usize = 64; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Attribution { + Host, + Client(IpAddr), +} + +impl Attribution { + fn of(err: &ProxyError, peer: IpAddr) -> Self { + match err { + ProxyError::BackendConnect(_) => Attribution::Host, + _ => Attribution::Client(peer), + } + } +} + impl DestHealth { fn gate(&self) -> Result<()> { let mut st = self.inner.lock(); @@ -154,26 +180,39 @@ impl DestHealth { )); } st.down_until = None; + st.clear_evidence(); } Ok(()) } - fn record(&self, ok: bool, threshold: u32, cooldown: Duration) { + fn record_success(&self) { let mut st = self.inner.lock(); - if ok { - st.consecutive_failures = 0; - st.down_until = None; - } else { - st.consecutive_failures = st.consecutive_failures.saturating_add(1); - if threshold > 0 && st.consecutive_failures >= threshold { - st.down_until = Some(Instant::now() + cooldown); + st.consecutive_failures = 0; + st.clear_evidence(); + st.down_until = None; + } + + fn record_failure(&self, at: Attribution, limits: &HealthLimits) { + let mut st = self.inner.lock(); + st.consecutive_failures = st.consecutive_failures.saturating_add(1); + match at { + Attribution::Host => st.host_level_failure = true, + Attribution::Client(ip) => { + if st.failure_sources.len() < MAX_TRACKED_FAILURE_SOURCES { + st.failure_sources.insert(ip); + } } } + let broad = st.host_level_failure || st.failure_sources.len() as u32 >= limits.min_clients; + if limits.threshold > 0 && st.consecutive_failures >= limits.threshold && broad { + st.down_until = Some(Instant::now() + limits.cooldown); + } } pub fn reset(&self) { let mut st = self.inner.lock(); st.consecutive_failures = 0; + st.clear_evidence(); st.down_until = None; } @@ -182,13 +221,33 @@ impl DestHealth { let down = matches!(st.down_until, Some(until) if Instant::now() < until); HealthSnapshot { consecutive_failures: st.consecutive_failures, + failure_sources: st.failure_sources.len() as u32, + host_level_failure: st.host_level_failure, down, } } } +pub struct HealthLimits { + threshold: u32, + min_clients: u32, + cooldown: Duration, +} + +impl HealthLimits { + fn for_server(server: &crate::config::ServerConfig) -> Self { + HealthLimits { + threshold: server.host_down_threshold, + min_clients: server.host_down_min_clients, + cooldown: server.host_down_cooldown, + } + } +} + pub struct HealthSnapshot { pub consecutive_failures: u32, + pub failure_sources: u32, + pub host_level_failure: bool, pub down: bool, } @@ -465,7 +524,12 @@ pub fn assert_egress_encrypted( const BACKEND_RETRY_DELAY: Duration = Duration::from_millis(200); -pub async fn establish(ctx: &Ctx, dest_id: &str, mut open: F) -> Result +pub async fn establish( + ctx: &Ctx, + dest_id: &str, + peer: SocketAddr, + mut open: F, +) -> Result where F: FnMut() -> Fut, Fut: Future>, @@ -475,6 +539,7 @@ where } let server = &ctx.config.server; + let limits = HealthLimits::for_server(server); let mut attempt: u32 = 0; loop { let outcome = match tokio::time::timeout(server.backend_timeout, open()).await { @@ -487,7 +552,7 @@ where match outcome { Ok(value) => { if let Some(health) = ctx.dest_health.get(dest_id) { - health.record(true, server.host_down_threshold, server.host_down_cooldown); + health.record_success(); } return Ok(value); } @@ -504,7 +569,15 @@ where continue; } if let Some(health) = ctx.dest_health.get(dest_id) { - health.record(false, server.host_down_threshold, server.host_down_cooldown); + let at = Attribution::of(&err, peer.ip()); + if matches!(at, Attribution::Client(_)) { + tracing::debug!( + destination = %dest_id, + error = %err, + "backend failure attributed to the client, not counted as host evidence on its own" + ); + } + health.record_failure(at, &limits); } return Err(err); } @@ -659,6 +732,7 @@ pub async fn bridge_authenticated( ctx: &Ctx, dest_id: &str, mut stream: BoxedStream, + peer: SocketAddr, identifier: Option<&str>, client_residual: &[u8], started: Instant, @@ -685,7 +759,7 @@ where return Ok(()); } - let (mut backend, mut backend_residual) = match establish(ctx, dest_id, open).await { + let (mut backend, mut backend_residual) = match establish(ctx, dest_id, peer, open).await { Ok(b) => b, Err(e) => { tracing::warn!(error = %e, protocol = label, destination = %dest_id, "backend unavailable"); @@ -854,29 +928,189 @@ mod tests { )); } + fn limits(threshold: u32, min_clients: u32, cooldown: Duration) -> HealthLimits { + HealthLimits { + threshold, + min_clients, + cooldown, + } + } + + fn client(addr: &str) -> Attribution { + Attribution::Client(addr.parse().unwrap()) + } + #[test] fn dest_health_opens_after_threshold_and_recovers() { let h = DestHealth::default(); - let cooldown = Duration::from_secs(60); + let lim = limits(3, 2, Duration::from_secs(60)); - h.record(false, 3, cooldown); - h.record(false, 3, cooldown); + h.record_failure(Attribution::Host, &lim); + h.record_failure(Attribution::Host, &lim); assert!(h.gate().is_ok()); - h.record(false, 3, cooldown); + h.record_failure(Attribution::Host, &lim); assert!(h.gate().is_err()); - h.record(true, 3, cooldown); + h.record_success(); assert!(h.gate().is_ok()); } #[test] fn dest_health_cooldown_elapses_into_a_probe() { let h = DestHealth::default(); - h.record(false, 1, Duration::from_secs(0)); + h.record_failure(Attribution::Host, &limits(1, 2, Duration::from_secs(0))); assert!(h.gate().is_ok()); } + #[test] + fn attribution_follows_the_failure_stage() { + let ip: IpAddr = "203.0.113.9".parse().unwrap(); + assert_eq!( + Attribution::of(&ProxyError::backend_connect("refused"), ip), + Attribution::Host + ); + assert_eq!( + Attribution::of(&ProxyError::backend("closed after PROXY header"), ip), + Attribution::Client(ip) + ); + assert_eq!( + Attribution::of( + &ProxyError::backend_session("backend closed before sending a complete line"), + ip + ), + Attribution::Client(ip) + ); + assert_eq!( + Attribution::of( + &ProxyError::backend_session("backend TLS handshake failed"), + ip + ), + Attribution::Client(ip) + ); + assert_eq!( + Attribution::of( + &ProxyError::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset)), + ip + ), + Attribution::Client(ip) + ); + } + + #[test] + fn banned_client_alone_never_opens_the_breaker() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(60)); + for _ in 0..50 { + h.record_failure(client("198.51.100.7"), &lim); + } + assert!(h.gate().is_ok()); + let s = h.snapshot(); + assert_eq!(s.consecutive_failures, 50); + assert_eq!(s.failure_sources, 1); + assert!(!s.down); + } + + #[test] + fn failures_spanning_distinct_clients_open_the_breaker() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(60)); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("198.51.100.7"), &lim); + assert!(h.gate().is_ok()); + + h.record_failure(client("203.0.113.4"), &lim); + assert!(h.gate().is_err()); + assert_eq!(h.snapshot().failure_sources, 2); + } + + #[test] + fn host_level_failure_opens_the_breaker_without_breadth() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(60)); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(Attribution::Host, &lim); + assert!(h.gate().is_err()); + let s = h.snapshot(); + assert!(s.host_level_failure); + assert!(s.down); + } + + #[test] + fn cooldown_expiry_clears_stale_evidence() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(0)); + for _ in 0..3 { + h.record_failure(Attribution::Host, &lim); + } + assert!(h.gate().is_ok()); + let s = h.snapshot(); + assert!(!s.host_level_failure); + assert_eq!(s.failure_sources, 0); + + h.record_failure( + client("198.51.100.7"), + &limits(3, 2, Duration::from_secs(60)), + ); + assert!(h.gate().is_ok()); + } + + #[test] + fn cooldown_expiry_still_re_opens_on_a_failed_host_probe() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(0)); + for _ in 0..3 { + h.record_failure(Attribution::Host, &lim); + } + assert!(h.gate().is_ok()); + + h.record_failure(Attribution::Host, &limits(3, 2, Duration::from_secs(60))); + assert!(h.gate().is_err()); + } + + #[test] + fn success_clears_the_failure_streak() { + let h = DestHealth::default(); + let lim = limits(3, 2, Duration::from_secs(60)); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("203.0.113.4"), &lim); + h.record_success(); + let s = h.snapshot(); + assert_eq!(s.consecutive_failures, 0); + assert_eq!(s.failure_sources, 0); + + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("198.51.100.7"), &lim); + h.record_failure(client("198.51.100.7"), &lim); + assert!(h.gate().is_ok()); + } + + #[test] + fn min_clients_zero_counts_failures_alone() { + let h = DestHealth::default(); + let lim = limits(3, 0, Duration::from_secs(60)); + for _ in 0..3 { + h.record_failure(client("198.51.100.7"), &lim); + } + assert!(h.gate().is_err()); + } + + #[test] + fn tracked_failure_sources_are_bounded() { + let h = DestHealth::default(); + let lim = limits(0, 2, Duration::from_secs(60)); + for i in 0..(MAX_TRACKED_FAILURE_SOURCES + 100) { + let ip = IpAddr::from([10, (i / 256) as u8, (i % 256) as u8, 1]); + h.record_failure(Attribution::Client(ip), &lim); + } + assert_eq!( + h.snapshot().failure_sources as usize, + MAX_TRACKED_FAILURE_SOURCES + ); + } + #[tokio::test] async fn conn_registry_kick_signals_live_sessions() { let reg = ConnRegistry::default(); @@ -912,7 +1146,7 @@ mod tests { fn dest_health_threshold_zero_never_trips() { let h = DestHealth::default(); for _ in 0..100 { - h.record(false, 0, Duration::from_secs(60)); + h.record_failure(Attribution::Host, &limits(0, 2, Duration::from_secs(60))); } assert!(h.gate().is_ok()); } diff --git a/src/proto/imap.rs b/src/proto/imap.rs index 0706a92..830ac8c 100644 --- a/src/proto/imap.rs +++ b/src/proto/imap.rs @@ -89,6 +89,7 @@ pub async fn handle( ctx, &dest_id, stream, + peer, identifier.as_deref(), &residual, started, diff --git a/src/proto/managesieve.rs b/src/proto/managesieve.rs index fe264fe..62a1a8e 100644 --- a/src/proto/managesieve.rs +++ b/src/proto/managesieve.rs @@ -76,6 +76,7 @@ pub async fn handle( ctx, &dest_id, stream, + peer, identifier.as_deref(), &residual, started, diff --git a/src/proto/pop3.rs b/src/proto/pop3.rs index 26033b2..a5bd893 100644 --- a/src/proto/pop3.rs +++ b/src/proto/pop3.rs @@ -75,6 +75,7 @@ pub async fn handle( ctx, &dest_id, stream, + peer, identifier.as_deref(), &residual, started, diff --git a/src/proto/smtp.rs b/src/proto/smtp.rs index 42c7b8a..5048097 100644 --- a/src/proto/smtp.rs +++ b/src/proto/smtp.rs @@ -82,6 +82,7 @@ pub async fn handle_submission( ctx, &dest_id, stream, + peer, identifier.as_deref(), &residual, started, @@ -572,7 +573,7 @@ pub async fn handle_passthrough( let (dest, ep, tls_cfg) = ctx.endpoint(&dest_id, proto)?; let fwd = dest.forwarding_for(proto); forward::guard_loop(&ctx.self_binds, dest, ep, fwd, ctx.config.server.proxy_ttl)?; - let backend = common::establish(ctx, &dest_id, || { + let backend = common::establish(ctx, &dest_id, peer, || { outbound::dial(dest, ep, fwd, &tls_cfg, peer, local) }) .await?; diff --git a/src/tests/admin.rs b/src/tests/admin.rs index 9f294d7..7adba82 100644 --- a/src/tests/admin.rs +++ b/src/tests/admin.rs @@ -473,6 +473,10 @@ async fn admin_mapping_crud_and_reads() { body.contains("legacy") && body.contains("secondary"), "body: {body}" ); + assert!( + body.contains("failure_sources") && body.contains("host_level_failure"), + "body: {body}" + ); let (status, _) = admin_request( addr, diff --git a/src/tests/containers/legacy.rs b/src/tests/containers/legacy.rs index 1c9ef01..191fc42 100644 --- a/src/tests/containers/legacy.rs +++ b/src/tests/containers/legacy.rs @@ -7,10 +7,12 @@ use std::time::Duration; use testcontainers::core::{IntoContainerPort, WaitFor}; -use testcontainers::runners::{AsyncBuilder, AsyncRunner}; -use testcontainers::{ContainerAsync, GenericBuildableImage, GenericImage, ImageExt}; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage, ImageExt}; use tokio::net::TcpStream; +use super::{ensure_image, owner_labels}; + pub const MASTER_USER: &str = "admin"; pub const MASTER_PASSWORD: &str = "MasterPass#2026"; @@ -218,15 +220,17 @@ pub struct Legacy { impl Legacy { pub async fn start() -> Legacy { - let image: GenericImage = GenericBuildableImage::new("proxy-legacymail", "test") - .with_dockerfile_string(DOCKERFILE.to_string()) - .with_data(DOVECOT_CONF.as_bytes().to_vec(), "dovecot.conf") - .with_data(USERS.as_bytes().to_vec(), "users") - .with_data(MASTER_USERS.as_bytes().to_vec(), "master-users") - .with_data(ENTRYPOINT.as_bytes().to_vec(), "entrypoint.sh") - .build_image() - .await - .expect("build legacy image"); + let image = ensure_image( + "proxy-legacymail", + DOCKERFILE, + &[ + ("dovecot.conf", DOVECOT_CONF.as_bytes()), + ("users", USERS.as_bytes()), + ("master-users", MASTER_USERS.as_bytes()), + ("entrypoint.sh", ENTRYPOINT.as_bytes()), + ], + ) + .await; let container = image .with_exposed_port(143u16.tcp()) @@ -239,6 +243,7 @@ impl Legacy { .with_exposed_port(25u16.tcp()) .with_wait_for(WaitFor::message_on_stdout("LEGACY-MAIL READY")) .with_startup_timeout(Duration::from_secs(240)) + .with_labels(owner_labels()) .start() .await .expect("start legacy container"); diff --git a/src/tests/containers/mod.rs b/src/tests/containers/mod.rs index d694326..a43c31e 100644 --- a/src/tests/containers/mod.rs +++ b/src/tests/containers/mod.rs @@ -5,12 +5,16 @@ */ use std::collections::HashMap; +use std::hash::{BuildHasher, Hash, Hasher}; use std::net::{SocketAddr, TcpListener as StdTcpListener}; -use std::sync::Arc; -use std::time::Duration; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Once, OnceLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use testcontainers::runners::AsyncBuilder; +use testcontainers::{GenericBuildableImage, GenericImage}; use tokio::net::TcpStream; -use tokio::sync::watch; +use tokio::sync::{Mutex, watch}; use tokio::task::JoinHandle; use crate::net::acceptor::Listener; @@ -34,11 +38,100 @@ pub mod toolbox_matrix; pub const HOST_GATEWAY: &str = "host.docker.internal"; +pub const OWNER_LABEL: &str = "art.stalw.proxy.test"; + pub fn pick_free_port() -> u16 { let l = StdTcpListener::bind("127.0.0.1:0").unwrap(); l.local_addr().unwrap().port() } +fn run_id() -> &'static str { + static RUN_ID: OnceLock = OnceLock::new(); + RUN_ID.get_or_init(|| { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + format!("{}-{nanos}", std::process::id()) + }) +} + +pub fn owner_labels() -> [(&'static str, &'static str); 1] { + sweep_abandoned_containers(); + [(OWNER_LABEL, run_id())] +} + +fn docker_ids(filter: &str) -> Vec { + Command::new("docker") + .args(["ps", "-aq", "--filter", filter]) + .output() + .map(|out| { + String::from_utf8_lossy(&out.stdout) + .split_whitespace() + .map(String::from) + .collect() + }) + .unwrap_or_default() +} + +fn sweep_abandoned_containers() { + static SWEPT: Once = Once::new(); + SWEPT.call_once(|| { + let mine = docker_ids(&format!("label={OWNER_LABEL}={}", run_id())); + let stale: Vec = docker_ids(&format!("label={OWNER_LABEL}")) + .into_iter() + .filter(|id| !mine.contains(id)) + .collect(); + if stale.is_empty() { + return; + } + eprintln!("removing {} abandoned test container(s)", stale.len()); + let _ = Command::new("docker") + .args(["rm", "-f", "-v"]) + .args(&stale) + .output(); + }); +} + +fn content_tag(dockerfile: &str, data: &[(&str, &[u8])]) -> String { + let mut hasher = ahash::RandomState::with_seeds(0x5354, 0x414c, 0x5741, 0x5254).build_hasher(); + dockerfile.hash(&mut hasher); + for (target, bytes) in data { + target.hash(&mut hasher); + bytes.hash(&mut hasher); + } + format!("{:016x}", hasher.finish()) +} + +async fn image_present(reference: &str) -> bool { + tokio::process::Command::new("docker") + .args(["image", "inspect", reference]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await + .map(|status| status.success()) + .unwrap_or(false) +} + +pub async fn ensure_image(name: &str, dockerfile: &str, data: &[(&str, &[u8])]) -> GenericImage { + static BUILD_LOCK: Mutex<()> = Mutex::const_new(()); + let tag = content_tag(dockerfile, data); + let _guard = BUILD_LOCK.lock().await; + if image_present(&format!("{name}:{tag}")).await { + return GenericImage::new(name.to_owned(), tag); + } + let mut builder = + GenericBuildableImage::new(name, &tag).with_dockerfile_string(dockerfile.to_string()); + for (target, bytes) in data { + builder = builder.with_data(bytes.to_vec(), *target); + } + builder + .build_image() + .await + .unwrap_or_else(|e| panic!("build image {name}:{tag}: {e}")) +} + pub struct Proxy { shutdown: watch::Sender, handles: Vec>, diff --git a/src/tests/containers/stalwart.rs b/src/tests/containers/stalwart.rs index 5be29bd..c3acb01 100644 --- a/src/tests/containers/stalwart.rs +++ b/src/tests/containers/stalwart.rs @@ -12,7 +12,7 @@ use testcontainers::runners::AsyncRunner; use testcontainers::{ContainerAsync, GenericImage, ImageExt}; use super::http::{self, basic_auth}; -use super::pick_free_port; +use super::{owner_labels, pick_free_port}; pub const NEW_IMAGE: &str = "stalwartlabs/stalwart"; pub const NEW_TAG: &str = "latest"; @@ -114,6 +114,7 @@ impl Stalwart { .with_mapped_port(ports.sieve, 4190u16.tcp()) .with_mapped_port(ports.smtp, 25u16.tcp()) .with_startup_timeout(Duration::from_secs(180)) + .with_labels(owner_labels()) .start() .await .expect("start new stalwart"); @@ -196,6 +197,7 @@ impl Stalwart { .with_mapped_port(ports.sieve, 4190u16.tcp()) .with_mapped_port(ports.smtp, 25u16.tcp()) .with_startup_timeout(Duration::from_secs(180)) + .with_labels(owner_labels()) .start() .await .expect("start old stalwart"); diff --git a/src/tests/containers/toolbox.rs b/src/tests/containers/toolbox.rs index 7b01695..b135756 100644 --- a/src/tests/containers/toolbox.rs +++ b/src/tests/containers/toolbox.rs @@ -7,11 +7,11 @@ use std::time::Duration; use testcontainers::core::{ExecCommand, Host, WaitFor}; -use testcontainers::runners::{AsyncBuilder, AsyncRunner}; -use testcontainers::{ContainerAsync, GenericBuildableImage, GenericImage, ImageExt}; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage, ImageExt}; use tokio::io::AsyncReadExt; -use super::HOST_GATEWAY; +use super::{HOST_GATEWAY, ensure_image, owner_labels}; const DOCKERFILE: &str = r#"FROM debian:trixie-slim ENV DEBIAN_FRONTEND=noninteractive @@ -54,16 +54,13 @@ impl Exec { impl Toolbox { pub async fn start() -> Toolbox { - let image: GenericImage = GenericBuildableImage::new("proxy-toolbox", "test") - .with_dockerfile_string(DOCKERFILE.to_string()) - .build_image() - .await - .expect("build toolbox image"); + let image = ensure_image("proxy-toolbox", DOCKERFILE, &[]).await; let container = image .with_wait_for(WaitFor::seconds(1)) .with_startup_timeout(Duration::from_secs(300)) .with_host(HOST_GATEWAY, Host::HostGateway) + .with_labels(owner_labels()) .start() .await .expect("start toolbox container"); diff --git a/src/tests/dest_health.rs b/src/tests/dest_health.rs new file mode 100644 index 0000000..d49829e --- /dev/null +++ b/src/tests/dest_health.rs @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinHandle; + +use super::harness::*; +use crate::proto::common::Ctx; + +fn config(port: u16, mapping_path: &str, threshold: u32, min_clients: u32) -> String { + format!( + r#" +[server] +host_down_threshold = {threshold} +host_down_min_clients = {min_clients} +host_down_cooldown = "60s" +backend_connect_retries = 0 + +[mapping] +source = "file" +normalize = "lowercase" +[mapping.file] +path = "{mapping_path}" + +[capabilities.imap] +allow_plain_auth_without_tls = true + +[routing] +default_destination = "legacy" + +[destination.legacy] +host = "127.0.0.1" +proxy_protocol = false +allow_plaintext_auth = true +[destination.legacy.protocol.imap] +port = {port} +tls = "plain" + +[listener.imap] +protocol = "imap" +bind = ["127.0.0.1:0"] +tls = "plain" +"# + ) +} + +struct Backend { + port: u16, + serving: Arc, + accepted: Arc, + handle: JoinHandle<()>, +} + +async fn banning_backend() -> Backend { + let listener = TcpListener::bind(unused_addr()).await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let serving = Arc::new(AtomicBool::new(false)); + let accepted = Arc::new(AtomicU32::new(0)); + let serving_task = serving.clone(); + let accepted_task = accepted.clone(); + let handle = tokio::spawn(async move { + loop { + let Ok((socket, _)) = listener.accept().await else { + return; + }; + let _ = socket.set_nodelay(true); + accepted_task.fetch_add(1, Ordering::Relaxed); + if serving_task.load(Ordering::Relaxed) { + tokio::spawn(serve_imap_auth(socket)); + } else { + drop(socket); + } + } + }); + Backend { + port, + serving, + accepted, + handle, + } +} + +async fn serve_imap_auth(mut socket: TcpStream) { + let _ = socket + .write_all(b"* OK [CAPABILITY IMAP4rev2] backend ready\r\n") + .await; + + let mut buf = Vec::new(); + let mut scratch = [0u8; 1024]; + let tag = loop { + let Ok(n) = socket.read(&mut scratch).await else { + return; + }; + if n == 0 { + return; + } + buf.extend_from_slice(&scratch[..n]); + if let Some(end) = find(&buf, b"\r\n") { + let line = String::from_utf8_lossy(&buf[..end]).into_owned(); + break line.split(' ').next().unwrap_or("a1").to_string(); + } + }; + let _ = socket + .write_all(format!("{tag} OK [CAPABILITY IMAP4rev2] authenticated\r\n").as_bytes()) + .await; + + loop { + if find(&buf, b"NOOP").is_some() { + break; + } + let Ok(n) = socket.read(&mut scratch).await else { + return; + }; + if n == 0 { + return; + } + buf.extend_from_slice(&scratch[..n]); + } + let _ = socket.write_all(b"a2 OK NOOP done\r\n").await; +} + +async fn imap_login(ctx: &Arc, peer: &str, user: &str) -> Vec { + let listener = ctx.config.listener["imap"].clone(); + let peer: SocketAddr = peer.parse().unwrap(); + let (mut client, server) = tcp_pair().await; + let ctx_task = ctx.clone(); + let handler = tokio::spawn(async move { + let _ = crate::proto::imap::handle( + &ctx_task, + &listener, + Box::new(server), + peer, + "127.0.0.1:143".parse().unwrap(), + ) + .await; + }); + + let _greeting = read_line(&mut client).await; + send( + &mut client, + format!("a1 LOGIN {user} secret\r\na2 NOOP\r\n").as_bytes(), + ) + .await; + let resp = read_to_eof(&mut client).await; + handler.abort(); + resp +} + +#[tokio::test] +async fn one_banned_client_does_not_mark_the_destination_down() { + let backend = banning_backend().await; + let mapping = mapping_file(""); + let toml = config(backend.port, &mapping.path().display().to_string(), 3, 2); + let ctx = make_ctx(&toml).await; + + for i in 0..5 { + let resp = imap_login(&ctx, "198.51.100.7:40000", "banned@example.com").await; + assert!( + find(&resp, b"a1 NO [UNAVAILABLE]").is_some(), + "attempt {i} should report the backend unavailable, got {:?}", + String::from_utf8_lossy(&resp) + ); + } + + let health = ctx.dest_health.get("legacy").unwrap().snapshot(); + assert_eq!(health.consecutive_failures, 5); + assert_eq!(health.failure_sources, 1); + assert!(!health.host_level_failure); + assert!( + !health.down, + "a single client must not open the circuit breaker" + ); + + backend.serving.store(true, Ordering::Relaxed); + let resp = imap_login(&ctx, "203.0.113.4:40000", "alice@example.com").await; + assert!( + find(&resp, b"a1 OK").is_some(), + "an unrelated account must still reach the backend, got {:?}", + String::from_utf8_lossy(&resp) + ); + assert!(!ctx.dest_health.get("legacy").unwrap().snapshot().down); + assert!(backend.accepted.load(Ordering::Relaxed) >= 6); + + backend.handle.abort(); +} + +#[tokio::test] +async fn rejections_spanning_distinct_clients_mark_the_destination_down() { + let backend = banning_backend().await; + let mapping = mapping_file(""); + let toml = config(backend.port, &mapping.path().display().to_string(), 3, 2); + let ctx = make_ctx(&toml).await; + + for peer in [ + "198.51.100.7:40000", + "198.51.100.7:40001", + "203.0.113.4:40000", + ] { + let resp = imap_login(&ctx, peer, "user@example.com").await; + assert!(find(&resp, b"a1 NO [UNAVAILABLE]").is_some()); + } + + let health = ctx.dest_health.get("legacy").unwrap().snapshot(); + assert_eq!(health.failure_sources, 2); + assert!( + health.down, + "failures from distinct clients are evidence the host is unhealthy" + ); + + let accepted_before = backend.accepted.load(Ordering::Relaxed); + backend.serving.store(true, Ordering::Relaxed); + let resp = imap_login(&ctx, "192.0.2.9:40000", "alice@example.com").await; + assert!(find(&resp, b"a1 NO [UNAVAILABLE]").is_some()); + assert_eq!( + backend.accepted.load(Ordering::Relaxed), + accepted_before, + "an open breaker must short-circuit before dialing the backend" + ); + + backend.handle.abort(); +} + +#[tokio::test] +async fn min_clients_zero_restores_counting_by_failures_alone() { + let backend = banning_backend().await; + let mapping = mapping_file(""); + let toml = config(backend.port, &mapping.path().display().to_string(), 3, 0); + let ctx = make_ctx(&toml).await; + + for _ in 0..3 { + let resp = imap_login(&ctx, "198.51.100.7:40000", "banned@example.com").await; + assert!(find(&resp, b"a1 NO [UNAVAILABLE]").is_some()); + } + + let health = ctx.dest_health.get("legacy").unwrap().snapshot(); + assert_eq!(health.failure_sources, 1); + assert!( + health.down, + "without the breadth requirement a single client still opens the breaker" + ); + + backend.handle.abort(); +} + +#[tokio::test] +async fn unreachable_host_marks_the_destination_down_from_one_client() { + let closed_port = { + let l = TcpListener::bind(unused_addr()).await.unwrap(); + l.local_addr().unwrap().port() + }; + let mapping = mapping_file(""); + let toml = config(closed_port, &mapping.path().display().to_string(), 3, 2); + let ctx = make_ctx(&toml).await; + + for _ in 0..3 { + let resp = imap_login(&ctx, "198.51.100.7:40000", "user@example.com").await; + assert!(find(&resp, b"a1 NO [UNAVAILABLE]").is_some()); + } + + let health = ctx.dest_health.get("legacy").unwrap().snapshot(); + assert!(health.host_level_failure); + assert!( + health.down, + "connect-stage failures are host evidence regardless of how many clients saw them" + ); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0b1b114..67dcc39 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -7,6 +7,7 @@ mod admin; mod config_validation; mod containers; +mod dest_health; mod forwarding; mod harness; mod http_proxy;