diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 167595569f..38632bea3b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "istio build-tools", - "image": "gcr.io/istio-testing/build-tools:master-e55cb3f9967290bfb749f97c35db81bcdb04451d", + "image": "registry.istio.io/testing/build-tools:master-b7201a4e3411e85dff202449182d26efd7491b89", "privileged": true, "remoteEnv": { "USE_GKE_GCLOUD_AUTH_PLUGIN": "True", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f5801ff492..a0e0446b46 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,15 +1,45 @@ - version: 2 updates: - # Maintain dependencies for GitHub Actions - package-ecosystem: "cargo" - # Workflow files stored in the default location of `.github/workflows`. (You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.) directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" + target-branch: "master" + groups: + all-dependencies: + patterns: + - "*" + + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "disabled" + target-branch: "release-1.29" + groups: + all-dependencies: + patterns: + - "*" + + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "disabled" + target-branch: "release-1.28" + groups: + all-dependencies: + patterns: + - "*" + + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "disabled" + target-branch: "release-1.27" groups: - all: - applies-to: version-updates - patterns: - - "*" \ No newline at end of file + all-dependencies: + patterns: + - "*" diff --git a/Cargo.lock b/Cargo.lock index 0b3fb882ac..1ca1165a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -29,22 +29,22 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -64,12 +64,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -87,15 +81,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arcstr" @@ -124,7 +118,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -136,7 +130,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", "synstructure", ] @@ -148,18 +142,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -181,18 +164,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -203,15 +186,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" dependencies = [ "aws-lc-sys", "zeroize", @@ -219,11 +202,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.28.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" dependencies = [ - "bindgen 0.69.5", + "bindgen", "cc", "cmake", "dunce", @@ -236,16 +219,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "instant", "rand 0.8.5", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -253,7 +236,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -270,16 +253,14 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -287,26 +268,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.100", - "which", -] - -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags 2.9.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -317,17 +279,17 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "boring" -version = "4.16.0" +version = "4.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd4d65a24a5e58e9b820723e496bfa920dd0afd31676646c81cfc3b6f34e039" +checksum = "4acbe9eda68fc7fbfb395aace52dfc37075928536ec2f149abce54dbd40e38d5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "boring-sys", "foreign-types 0.5.0", "libc", @@ -337,9 +299,8 @@ dependencies = [ [[package]] name = "boring-additions" version = "0.0.1" -source = "git+https://github.com/janrueth/boring-rustls-provider#aa6e1c36f880002ceb56f99a64d19e0503e0bec7" +source = "git+https://github.com/janrueth/boring-rustls-provider#490340afa77e2c08fc45853124f99d49f4f9f8a0" dependencies = [ - "aead", "boring", "boring-sys", "foreign-types 0.5.0", @@ -348,7 +309,7 @@ dependencies = [ [[package]] name = "boring-rustls-provider" version = "0.0.1" -source = "git+https://github.com/janrueth/boring-rustls-provider#aa6e1c36f880002ceb56f99a64d19e0503e0bec7" +source = "git+https://github.com/janrueth/boring-rustls-provider#490340afa77e2c08fc45853124f99d49f4f9f8a0" dependencies = [ "aead", "boring", @@ -356,22 +317,19 @@ dependencies = [ "boring-sys", "boring-sys-additions", "foreign-types 0.5.0", - "lazy_static", - "once_cell", "rustls", "rustls-pki-types", - "rustls-webpki 0.102.8", "spki", ] [[package]] name = "boring-sys" -version = "4.16.0" +version = "4.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9a2a6a85b9cdadd64a1856ac5632afe0816518e20aadd372f4e4172aa94e2a" +checksum = "08c9f08dc17fab4192e5e6b0920e6c06eb9e3e4ab98b9cff00368dfe6d1e30c9" dependencies = [ "autocfg", - "bindgen 0.70.1", + "bindgen", "cmake", "fs_extra", "fslock", @@ -380,16 +338,16 @@ dependencies = [ [[package]] name = "boring-sys-additions" version = "0.0.1" -source = "git+https://github.com/janrueth/boring-rustls-provider#aa6e1c36f880002ceb56f99a64d19e0503e0bec7" +source = "git+https://github.com/janrueth/boring-rustls-provider#490340afa77e2c08fc45853124f99d49f4f9f8a0" dependencies = [ "boring-sys", ] [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -399,9 +357,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -414,10 +372,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -434,9 +393,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -446,11 +405,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -498,18 +456,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.35" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstyle", "clap_lex", @@ -517,9 +475,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -551,16 +509,26 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -574,18 +542,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpp_demangle" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" dependencies = [ "cfg-if", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -679,15 +647,15 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -695,9 +663,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" dependencies = [ "ctor-proc-macro", "dtor", @@ -705,15 +673,15 @@ dependencies = [ [[package]] name = "ctor-proc-macro" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "debugid" @@ -726,9 +694,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", ] @@ -749,9 +717,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -770,7 +738,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -781,18 +749,18 @@ checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" [[package]] name = "dtor" -version = "0.0.5" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" dependencies = [ "dtor-proc-macro", ] [[package]] name = "dtor-proc-macro" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" [[package]] name = "dunce" @@ -809,7 +777,7 @@ dependencies = [ "chrono", "rust_decimal", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "winnow", ] @@ -823,7 +791,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -847,27 +815,27 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -887,7 +855,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -898,12 +866,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -912,6 +880,33 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "findshlibs" version = "0.10.2" @@ -932,9 +927,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -991,7 +986,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -1008,9 +1003,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1021,6 +1016,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fslock" version = "0.2.1" @@ -1087,7 +1091,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -1120,19 +1124,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" -dependencies = [ - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1156,44 +1147,44 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1210,25 +1201,32 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hcn" version = "0.1.0" @@ -1260,21 +1258,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hickory-client" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bbd1b5def7a1b77783366577e86cb51172196f689823b0f8107da9391ba183f" +checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555" dependencies = [ "cfg-if", "data-encoding", @@ -1283,22 +1275,20 @@ dependencies = [ "hickory-proto", "once_cell", "radix_trie", - "rand 0.9.0", - "thiserror 2.0.12", + "rand 0.9.2", + "thiserror 2.0.17", "tokio", "tracing", ] [[package]] name = "hickory-proto" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", - "critical-section", "data-encoding", "enum-as-inner", "futures-channel", @@ -1307,10 +1297,10 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.0", + "rand 0.9.2", "ring", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -1319,9 +1309,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -1330,20 +1320,20 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.0", + "rand 0.9.2", "resolv-conf", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", ] [[package]] name = "hickory-server" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716f516285473ce476dfc996bac9a3c9ef2fee4f380ebec5980b12216fe4f547" +checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585" dependencies = [ "async-trait", "bytes", @@ -1356,7 +1346,7 @@ dependencies = [ "ipnet", "prefix-trie", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-util", @@ -1365,22 +1355,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "hostname" -version = "0.4.1" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "cfg-if", - "libc", - "windows-link", + "windows-sys 0.61.2", ] [[package]] @@ -1431,13 +1410,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1445,6 +1425,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1452,11 +1433,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -1470,29 +1450,35 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.9", + "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1500,7 +1486,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.62.2", ] [[package]] @@ -1514,21 +1500,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1537,104 +1524,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1643,9 +1592,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1653,12 +1602,32 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", ] [[package]] @@ -1676,7 +1645,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.9", + "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", @@ -1693,13 +1662,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1746,9 +1715,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jemalloc_pprof" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a883828bd6a4b957cd9f618886ff19e5f3ebd34e06ba0e855849e049fef32fb" +checksum = "74ff642505c7ce8d31c0d43ec0e235c6fd4585d9b8172d8f9dd04d36590200b5" dependencies = [ "anyhow", "libc", @@ -1763,19 +1732,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1791,31 +1760,56 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "kqueue" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] [[package]] -name = "lazycell" -version = "1.3.0" +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", ] [[package]] @@ -1826,71 +1820,57 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-ip-address" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782" +checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" dependencies = [ "libc", "neli", - "thiserror 1.0.69", + "thiserror 2.0.17", "windows-sys 0.59.0", ] [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] name = "mappings" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce9229c438fbf1c333926e2053c4c091feabbd40a1b590ec62710fea2384af9e" +checksum = "db4d277bb50d4508057e7bddd7fcd19ef4a4cc38051b6a5a36868d75ae2cbeb9" dependencies = [ "anyhow", "libc", @@ -1901,11 +1881,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1916,15 +1896,15 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -1955,48 +1935,60 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", ] [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", + "equivalent", "parking_lot", "portable-atomic", "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "neli" @@ -2068,11 +2060,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -2089,14 +2081,46 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.10.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -2134,9 +2158,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2180,19 +2204,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -2224,11 +2248,11 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -2245,7 +2269,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -2256,9 +2280,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2266,17 +2290,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2284,15 +2302,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -2303,19 +2321,19 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -2344,7 +2362,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -2361,9 +2379,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pingora-pool" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacdd5dbdec690d468856d988b170c8bb4ab62e0edefc0f432ba5e326489f421" +checksum = "996c574f30a6e1ad10b47ac1626a86e0e47d5075953dd049d60df16ba5f7076e" dependencies = [ "crossbeam-queue", "log", @@ -2376,9 +2394,9 @@ dependencies = [ [[package]] name = "pingora-timeout" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685bb8808cc1919c63a06ab14fdac9b84a4887ced49259a5c0adc8bfb2ffe558" +checksum = "548cd21d41611c725827677937e68f2cd008bbfa09f3416d3fbad07e1e42f6d7" dependencies = [ "once_cell", "parking_lot", @@ -2423,9 +2441,18 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -2444,9 +2471,9 @@ dependencies = [ [[package]] name = "pprof" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0" +checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187" dependencies = [ "aligned-vec", "backtrace", @@ -2457,26 +2484,27 @@ dependencies = [ "log", "nix 0.26.4", "once_cell", - "parking_lot", "protobuf", - "protobuf-codegen-pure", + "protobuf-codegen", "smallvec", + "spin", "symbolic-demangle", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] name = "pprof_util" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c568b3f8c1c37886ae07459b1946249e725c315306b03be5632f84c239f781" +checksum = "4429d44e5e2c8a69399fc0070379201eed018e3df61e04eb7432811df073c224" dependencies = [ "anyhow", + "backtrace", "flate2", "num", "paste", - "prost", + "prost 0.13.5", ] [[package]] @@ -2485,14 +2513,14 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] name = "prefix-trie" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5f930995ba4986bd239ba8d8fded67cad82d1db329c4f316f312847cba16aa" +checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20" dependencies = [ "ipnet", "num-traits", @@ -2500,28 +2528,28 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" dependencies = [ "dtoa", "itoa", @@ -2531,13 +2559,13 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -2559,14 +2587,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive 0.14.1", ] [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck", "itertools 0.14.0", @@ -2575,10 +2613,10 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.14.1", "prost-types", "regex", - "syn 2.0.100", + "syn 2.0.110", "tempfile", ] @@ -2592,57 +2630,96 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ - "prost", + "prost 0.14.1", ] [[package]] name = "protobuf" -version = "2.28.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] [[package]] name = "protobuf-codegen" -version = "2.28.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" dependencies = [ + "anyhow", + "once_cell", "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", ] [[package]] -name = "protobuf-codegen-pure" -version = "2.28.0" +name = "protobuf-parse" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" dependencies = [ + "anyhow", + "indexmap", + "log", "protobuf", - "protobuf-codegen", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radix_trie" @@ -2680,13 +2757,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", ] [[package]] @@ -2734,7 +2810,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2743,7 +2819,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", ] [[package]] @@ -2757,9 +2833,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2767,9 +2843,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2777,80 +2853,71 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887a643fa081058097896d87764863994f6c32a1716e76adc479bd283974a825" +checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" dependencies = [ "aws-lc-rs", "pem", "ring", "rustls-pki-types", "time", - "x509-parser", + "x509-parser 0.18.0", "yasna", ] [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] -name = "regex" -version = "1.11.1" +name = "redox_syscall" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "bitflags 2.10.0", ] [[package]] -name = "regex-automata" -version = "0.1.10" +name = "regex" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ - "regex-syntax 0.6.29", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "resolv-conf" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" -dependencies = [ - "hostname", -] +checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "ring" @@ -2860,7 +2927,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -2868,9 +2935,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.37.1" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", "num-traits", @@ -2878,15 +2945,15 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -2912,7 +2979,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2921,38 +2988,38 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2962,9 +3029,9 @@ dependencies = [ [[package]] name = "rustls-openssl" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa58b5fa1750e963ffe57b7248a2b00953fa1f8687339e41e526cf2e6671aa5" +checksum = "346f161084062dd5a443adfb0de9bbfab62699333e2d8424ca91da5f22ec88d1" dependencies = [ "foreign-types 0.3.2", "once_cell", @@ -2985,26 +3052,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -3014,9 +3073,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -3035,19 +3094,13 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -3056,12 +3109,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.0", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3069,9 +3122,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3085,40 +3138,51 @@ checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -3129,7 +3193,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -3162,33 +3226,36 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3196,11 +3263,21 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" -source = "git+https://github.com/keithmattix/socket2.git?branch=add-tcp-retries-to-windows#3b6ea10cc0fde38a8819e05b338f9a3629c03ad6" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", ] [[package]] @@ -3220,9 +3297,9 @@ checksum = "2f2b15926089e5526bb2dd738a2eb0e59034356e06eb71e1cd912358c0e62c4d" [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" @@ -3232,9 +3309,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.15.1" +version = "12.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae323968b25398709a61322678b6da5a8ac0d223e02a700f0c7caffa2161d195" +checksum = "d03f433c9befeea460a01d750e698aa86caf86dcfbd77d552885cd6c89d52f50" dependencies = [ "debugid", "memmap2", @@ -3244,9 +3321,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.15.1" +version = "12.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd59a01210ac9da7d95d925273c8da00272dafac4655f6c9d592d3a5a0ff2ce0" +checksum = "13d359ef6192db1760a34321ec4f089245ede4342c27e59be99642f12a859de8" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -3266,9 +3343,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -3283,13 +3360,34 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3300,15 +3398,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.5", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -3329,7 +3427,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -3340,7 +3438,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", "test-case-core", ] @@ -3365,11 +3463,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -3380,35 +3478,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tikv-jemalloc-ctl" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b" +checksum = "661f1f6a57b3a36dc9174a2c10f19513b4866816e13425d3e418b11cc37bc24c" dependencies = [ "libc", "paste", @@ -3417,9 +3514,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" dependencies = [ "cc", "libc", @@ -3427,9 +3524,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -3437,30 +3534,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -3477,9 +3574,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3497,9 +3594,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3518,44 +3615,43 @@ checksum = "ab41256c16d6fc2b3021545f20bf77a73200b18bd54040ac656dddfca6205bfa" dependencies = [ "futures-util", "pin-project-lite", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", ] [[package]] name = "tokio" -version = "1.44.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", - "mio", + "mio 1.1.0", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.9", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -3574,9 +3670,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -3587,9 +3683,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.13.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85839f0b32fd242bb3209262371d07feda6d780d16ee9d2bc88581b89da1549b" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "base64 0.22.1", @@ -3599,7 +3695,7 @@ dependencies = [ "http-body-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio-stream", "tower-layer", "tower-service", @@ -3608,16 +3704,41 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.13.0" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost 0.14.1", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85f0383fadd15609306383a90e85eaed44169f931a5d2be1b42c76ceff1825e" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "prost-types", "quote", - "syn 2.0.100", + "syn 2.0.110", + "tempfile", + "tonic-build", ] [[package]] @@ -3654,9 +3775,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3677,20 +3798,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3719,14 +3840,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -3746,15 +3867,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unsafe-libyaml" @@ -3770,9 +3891,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3780,12 +3901,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3794,11 +3909,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.16.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3846,50 +3963,37 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3897,31 +4001,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.100", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -3941,9 +4045,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -3963,11 +4067,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4001,15 +4105,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -4020,18 +4124,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -4042,25 +4146,36 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] [[package]] name = "windows-result" @@ -4073,9 +4188,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] @@ -4092,9 +4207,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -4126,6 +4241,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4150,13 +4283,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4169,6 +4319,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4181,6 +4337,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4193,12 +4355,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4211,6 +4385,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4223,6 +4403,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4235,6 +4421,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4247,11 +4439,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -4267,25 +4465,16 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "x509-parser" @@ -4301,7 +4490,25 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.17", "time", ] @@ -4316,11 +4523,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4328,54 +4534,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -4395,21 +4581,32 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4418,13 +4615,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.110", ] [[package]] @@ -4454,7 +4651,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "hashbrown", + "hashbrown 0.15.5", "hcn", "hickory-client", "hickory-proto", @@ -4475,20 +4672,23 @@ dependencies = [ "log", "matches", "netns-rs", - "nix 0.29.0", + "nix 0.30.1", + "notify", + "notify-debouncer-full", + "num_cpus", "oid-registry", "once_cell", "openssl", + "openssl-sys", "pin-project-lite", "pingora-pool", "ppp", "pprof", "prometheus-client", "prometheus-parse", - "prost", - "prost-build", + "prost 0.14.1", "prost-types", - "rand 0.9.0", + "rand 0.9.2", "rcgen", "ring", "rustc_version", @@ -4496,22 +4696,26 @@ dependencies = [ "rustls-native-certs", "rustls-openssl", "rustls-pemfile", + "rustls-webpki", "serde", "serde_json", "serde_yaml", - "socket2 0.6.0", + "socket2 0.6.3", "split-iter", + "tempfile", "test-case", "textnonce", - "thiserror 2.0.12", + "thiserror 2.0.17", "tikv-jemallocator", + "time", "tls-listener", "tokio", "tokio-rustls", "tokio-stream", "tokio-util", "tonic", - "tonic-build", + "tonic-prost", + "tonic-prost-build", "tower", "tracing", "tracing-appender", @@ -4520,6 +4724,6 @@ dependencies = [ "tracing-subscriber", "url", "windows", - "x509-parser", + "x509-parser 0.17.0", "ztunnel", ] diff --git a/Cargo.toml b/Cargo.toml index 2a9fa446a4..a234d08892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ztunnel" version = "0.0.0" edition = "2024" -rust-version = "1.85" +rust-version = "1.90" [features] default = ["tls-aws-lc"] @@ -10,8 +10,8 @@ jemalloc = ["dep:tikv-jemallocator", "dep:jemalloc_pprof"] tls-boring = ["dep:boring", "dep:boring-sys", "boring-rustls-provider/fips-only"] tls-ring = ["dep:ring", "rustls/ring", "tokio-rustls/ring", "hyper-rustls/ring", "dep:rcgen"] tls-aws-lc = ["dep:ring", "rustls/aws_lc_rs", "tokio-rustls/aws_lc_rs", "hyper-rustls/aws-lc-rs", "dep:rcgen", "rcgen/aws_lc_rs"] -tls-openssl = ["dep:rustls-openssl", "dep:openssl" ] -testing = ["dep:rcgen", "rcgen/x509-parser"] # Enables utilities supporting tests. +tls-openssl = ["dep:rustls-openssl", "dep:openssl", "dep:openssl-sys"] +testing = ["dep:rcgen", "rcgen/x509-parser", "dep:tempfile"] # Enables utilities supporting tests. [lib] path = "src/lib.rs" @@ -40,28 +40,29 @@ boring-sys = { version = "4", optional = true } ring = { version = "0.17", optional = true } # Enabled with 'tls-openssl' -rustls-openssl = { version = "0.2", optional = true } +rustls-openssl = { version = "0.3", optional = true, features = ["tls12"]} openssl = { version = "0.10", optional = true } +openssl-sys = { version = "0.9", optional = true } anyhow = "1.0" async-stream = "0.3" async-trait = "0.1" base64 = "0.22" byteorder = "1.5" -bytes = { version = "1.10", features = ["serde"] } +bytes = { version = "1.11", features = ["serde"] } chrono = "0.4" duration-str = "0.17" futures = "0.3" futures-core = "0.3" futures-util = "0.3" -jemalloc_pprof = { version = "0.6.0", optional = true } +jemalloc_pprof = { version = "0.8", optional = true, features = ["symbolize"] } tikv-jemallocator = { version = "0.6.0", features = ["profiling", "unprefixed_malloc_on_supported_platforms"], optional = true } hashbrown = "0.15" hickory-client = "0.25" hickory-proto = "0.25" hickory-resolver = "0.25" hickory-server = { version = "0.25", features = [ "resolver" ]} -http-body = { package = "http-body", version = "1" } +http-body = { version = "1" } http-body-util = "0.1" hyper = { version = "1.6", features = ["full"] } hyper-rustls = { version = "0.27.0", default-features = false, features = ["logging", "http1", "http2"] } @@ -71,38 +72,43 @@ itertools = "0.14" keyed_priority_queue = "0.4" libc = "0.2" log = "0.4" -nix = { version = "0.29", features = ["socket", "sched", "uio", "fs", "ioctl", "user", "net", "mount"] } +nix = { version = "0.30", features = ["socket", "sched", "uio", "fs", "ioctl", "user", "net", "mount", "resource" ] } +notify = "6.1" +notify-debouncer-full = "0.3" once_cell = "1.21" +num_cpus = "1.16" ppp = "2.3" -prometheus-client = { version = "0.23" } +prometheus-client = { version = "0.24" } prometheus-parse = "0.2" -prost = "0.13" -prost-types = "0.13" +prost = { version = "0.14", default-features = false } +prost-types = { version = "0.14", default-features = false } rand = { version = "0.9" , features = ["small_rng"]} -rcgen = { version = "0.13", optional = true, features = ["pem"] } -rustls = { version = "0.23", default-features = false } +rcgen = { version = "0.14", optional = true, features = ["pem"] } +rustls = { version = "0.23", features = ["tls12"], default-features = false } rustls-native-certs = "0.8" rustls-pemfile = "2.2" serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" serde_yaml = "0.9" -socket2 = { git = "https://github.com/keithmattix/socket2.git", branch="add-tcp-retries-to-windows", features = ["all"] } +socket2 = { version = "0.6.3", features = ["all"] } textnonce = { version = "1.0" } thiserror = "2.0" tls-listener = { version = "0.11" } tokio = { version = "1.44", features = ["full", "test-util"] } tokio-rustls = { version = "0.26", default-features = false } tokio-stream = { version = "0.1", features = ["net"] } -tonic = { version = "0.13", default-features = false, features = ["prost", "codegen"] } +tonic = { version = "0.14", default-features = false, features = ["codegen"] } +tonic-prost = { version = "0.14", default-features = false } tower = { version = "0.5", features = ["full"] } tracing = { version = "0.1"} tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "json"] } url = "2.5" x509-parser = { version = "0.17", default-features = false } +rustls-webpki = { version = "0.103", default-features = false, features = ["alloc"] } tracing-log = "0.2" backoff = "0.4" pin-project-lite = "0.2" -pingora-pool = "0.4" +pingora-pool = "0.6" flurry = "0.5" h2 = "0.4" http = "1.3" @@ -112,18 +118,18 @@ tracing-core = "0.1" tracing-appender = "0.2" tokio-util = { version = "0.7", features = ["io-util"] } educe = "0.6" +tempfile = { version = "3.21", optional = true} [target.'cfg(target_os = "linux")'.dependencies] netns-rs = "0.1" -pprof = { version = "0.14", features = ["protobuf", "protobuf-codec", "criterion"] } +pprof = { version = "0.15", features = ["protobuf", "protobuf-codec", "criterion"] } [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58.0", features = ["Win32_System_HostCompute", "Win32_System_HostComputeNetwork", "Win32_System_HostComputeSystem", "Win32_NetworkManagement_IpHelper"] } hcn = { git = "https://github.com/keithmattix/hcn-rs.git" } [build-dependencies] -tonic-build = { version = "0.13", default-features = false, features = ["prost"] } -prost-build = "0.13" +tonic-prost-build = { version = "0.14", default-features = false } anyhow = "1.0" rustc_version = "0.4" cfg-if = "1.0" @@ -157,9 +163,10 @@ local-ip-address = "0.6" matches = "0.1" test-case = "3.3" oid-registry = "0.8" -rcgen = { version = "0.13", features = ["pem", "x509-parser"] } +rcgen = { version = "0.14", features = ["pem", "x509-parser"] } x509-parser = { version = "0.17", default-features = false, features = ["verify"] } -ctor = "0.4" +time = "0.3" +ctor = "0.5" [lints.clippy] # This rule makes code more confusing diff --git a/PROFILING.md b/PROFILING.md index 235063fe6a..5ab0f11798 100644 --- a/PROFILING.md +++ b/PROFILING.md @@ -5,7 +5,7 @@ 1. Port-forward admin port (15000): ```sh -k port-forward -n istio-system ztunnel-qkvdj 15000:15000 +kubectl port-forward -n istio-system ztunnel-qkvdj 15000:15000 ``` 1. Either open `localhost:15000` in a browser for help, or just `curl` the CPU profile: @@ -23,7 +23,7 @@ curl localhost:15000/debug/pprof/profile > profile.prof 1. Port-forward admin port (15000): ```sh -k port-forward -n istio-system ztunnel-qkvdj 15000:15000 +kubectl port-forward -n istio-system ztunnel-qkvdj 15000:15000 ``` 1. Either open `localhost:15000` in a browser for help, or just `curl` the memory profile: @@ -32,13 +32,13 @@ k port-forward -n istio-system ztunnel-qkvdj 15000:15000 curl localhost:15000/debug/pprof/heap > mem.pb.gz ``` -1. If working remotely, copy container binaries to local path for symbol resolution: +1. Copy container binaries to local path for symbol resolution: ```sh # ztunnel main binary -kubectl cp kube-system/ztunnel-qkvdj:/usr/local/bin/ztunnel ../../ztunnel-libs-pprof/ztunnel +kubectl cp istio-system/ztunnel-qkvdj:/usr/local/bin/ztunnel ../../ztunnel-libs-pprof/ztunnel # stdlibs (optional) -kubectl cp kube-system/ztunnel-qkvdj:/usr/lib/$BINARY_COMPILED_ARCH/ ../../ztunnel-libs-pprof/ +kubectl cp istio-system/ztunnel-qkvdj:/usr/lib/$BINARY_COMPILED_ARCH/ ../../ztunnel-libs-pprof/ ``` 1. Observe in your tooling of choice, such as golang's `pprof`: diff --git a/benches/throughput.rs b/benches/throughput.rs index 085c677a9f..52a61dbf20 100644 --- a/benches/throughput.rs +++ b/benches/throughput.rs @@ -99,6 +99,7 @@ fn create_test_policies() -> Vec { scope: ztunnel::rbac::RbacScope::Global, namespace: "default".into(), rules: rules.clone(), + dry_run: false, }); } @@ -470,10 +471,10 @@ fn hbone_connection_config() -> ztunnel::config::ConfigSource { workload: Workload { workload_ips: vec![hbone_connection_ip(i)], protocol: InboundProtocol::HBONE, - uid: strng::format!("cluster1//v1/Pod/default/remote{}", i), - name: strng::format!("workload-{}", i), - namespace: strng::format!("namespace-{}", i), - service_account: strng::format!("service-account-{}", i), + uid: strng::format!("cluster1//v1/Pod/default/remote{i}"), + name: strng::format!("workload-{i}"), + namespace: strng::format!("namespace-{i}"), + service_account: strng::format!("service-account-{i}"), ..test_helpers::test_default_workload() }, services: Default::default(), diff --git a/build.rs b/build.rs index b1ce305ae6..bf2c09f666 100644 --- a/build.rs +++ b/build.rs @@ -21,6 +21,17 @@ fn main() -> Result<(), anyhow::Error> { // Fuzzing uses custom cfg (https://rust-fuzz.github.io/book/cargo-fuzz/guide.html) // Tell cargo to expect this (https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html). println!("cargo::rustc-check-cfg=cfg(fuzzing)"); + + // OpenSSL version detection (https://docs.rs/openssl/latest/openssl/#feature-detection) + println!("cargo:rustc-check-cfg=cfg(ossl350)"); + if let Ok(v) = env::var("DEP_OPENSSL_VERSION_NUMBER") { + let version = u64::from_str_radix(&v, 16).unwrap(); + #[allow(clippy::unusual_byte_groupings)] + if version >= 0x3_05_00_00_0 { + println!("cargo:rustc-cfg=ossl350"); + } + } + let proto_files = [ "proto/xds.proto", "proto/workload.proto", @@ -36,7 +47,7 @@ fn main() -> Result<(), anyhow::Error> { .map(|i| std::env::current_dir().unwrap().join(i)) .collect::>(); let config = { - let mut c = prost_build::Config::new(); + let mut c = tonic_prost_build::Config::new(); c.disable_comments(Some(".")); c.bytes([ ".istio.workload.Workload", @@ -47,9 +58,10 @@ fn main() -> Result<(), anyhow::Error> { ]); c }; - tonic_build::configure() + tonic_prost_build::configure() .build_server(true) - .compile_protos_with_config( + .protoc_arg("--experimental_allow_proto3_optional") + .compile_with_config( config, &proto_files .iter() @@ -106,9 +118,6 @@ fn main() -> Result<(), anyhow::Error> { "cargo:rustc-env=ZTUNNEL_BUILD_RUSTC_VERSION={}", rustc_version::version().unwrap() ); - println!( - "cargo:rustc-env=ZTUNNEL_BUILD_PROFILE_NAME={}", - profile_name - ); + println!("cargo:rustc-env=ZTUNNEL_BUILD_PROFILE_NAME={profile_name}"); Ok(()) } diff --git a/common/.commonfiles.sha b/common/.commonfiles.sha index 0b150bb7b7..be30dc2eb7 100644 --- a/common/.commonfiles.sha +++ b/common/.commonfiles.sha @@ -1 +1 @@ -3ba8e7b5084a695e9380da530ef13e671f3408aa +de36763d9ee5de8aaaea29ec16812bf82ccab59a diff --git a/common/config/.golangci.yml b/common/config/.golangci.yml index a3908b1d12..b9b6f68bf8 100644 --- a/common/config/.golangci.yml +++ b/common/config/.golangci.yml @@ -184,6 +184,13 @@ linters: - linters: - staticcheck text: 'S1007' + # TODO: remove once we have updated package names + - linters: + - revive + text: "var-naming: avoid meaningless package names" + - linters: + - revive + text: "var-naming: avoid package names that conflict with Go standard library package names" paths: - .*\.pb\.go - .*\.gen\.go diff --git a/common/config/.hadolint.yml b/common/config/.hadolint.yml index 599f46259a..f900b9a18d 100644 --- a/common/config/.hadolint.yml +++ b/common/config/.hadolint.yml @@ -13,5 +13,6 @@ trustedRegistries: - gcr.io - docker.io - quay.io + - registry.istio.io - "*.pkg.dev" - "cgr.dev" diff --git a/common/config/license-lint.yml b/common/config/license-lint.yml index ecd3b5914a..7eeec6a304 100644 --- a/common/config/license-lint.yml +++ b/common/config/license-lint.yml @@ -143,3 +143,14 @@ allowlisted_modules: # Simplified BSD License: https://github.com/gomarkdown/markdown/blob/master/LICENSE.txt - github.com/gomarkdown/markdown + +# MPL-2.0 +# https://github.com/cyphar/filepath-securejoin/blob/main/LICENSE.MPL-2.0 +# BSD +# https://github.com/cyphar/filepath-securejoin/blob/main/LICENSE.BSD +- github.com/cyphar/filepath-securejoin + +# The code is MIT: https://github.com/quic-go/quic-go/blob/master/LICENSE +# However, the logo is not https://github.com/quic-go/quic-go/blob/master/assets/LICENSE.md. +# However, we do not consume the logo. +- github.com/quic-go/quic-go \ No newline at end of file diff --git a/common/scripts/kind_provisioner.sh b/common/scripts/kind_provisioner.sh index 9cee3363af..3a414d7d52 100644 --- a/common/scripts/kind_provisioner.sh +++ b/common/scripts/kind_provisioner.sh @@ -32,7 +32,7 @@ set -x #################################################################### # DEFAULT_KIND_IMAGE is used to set the Kubernetes version for KinD unless overridden in params to setup_kind_cluster(s) -DEFAULT_KIND_IMAGE="gcr.io/istio-testing/kind-node:v1.32.0" +DEFAULT_KIND_IMAGE="registry.istio.io/testing/kind-node:v1.35.0" # the default kind cluster should be ipv4 if not otherwise specified KIND_IP_FAMILY="${KIND_IP_FAMILY:-ipv4}" diff --git a/common/scripts/metallb-native.yaml b/common/scripts/metallb-native.yaml index d874bac7ea..9daa3c4579 100644 --- a/common/scripts/metallb-native.yaml +++ b/common/scripts/metallb-native.yaml @@ -1,5 +1,5 @@ # Downloaded from https://github.com/metallb/metallb/raw/v0.13.12/config/manifests/metallb-native.yaml -# With quay.io hub replaced with gcr.io/istio-testing +# With quay.io hub replaced with registry.istio.io/testing # And probes tuned to startup faster apiVersion: v1 kind: Namespace @@ -1533,7 +1533,7 @@ spec: value: memberlist - name: METALLB_DEPLOYMENT value: controller - image: gcr.io/istio-testing/metallb/controller:v0.14.3 + image: registry.istio.io/testing/metallb/controller:v0.14.3 livenessProbe: failureThreshold: 3 httpGet: @@ -1634,7 +1634,7 @@ spec: value: app=metallb,component=speaker - name: METALLB_ML_SECRET_KEY_PATH value: /etc/ml_secret_key - image: gcr.io/istio-testing/metallb/speaker:v0.14.3 + image: registry.istio.io/testing/metallb/speaker:v0.14.3 livenessProbe: failureThreshold: 3 httpGet: diff --git a/common/scripts/report_build_info.sh b/common/scripts/report_build_info.sh index f21e370f14..80f4aa12ed 100755 --- a/common/scripts/report_build_info.sh +++ b/common/scripts/report_build_info.sh @@ -36,7 +36,7 @@ if [[ -z "${IGNORE_DIRTY_TREE}" ]] && ! git diff-index --quiet HEAD --; then fi GIT_DESCRIBE_TAG=$(git describe --tags --always) -HUB=${HUB:-"docker.io/istio"} +HUB=${HUB:-"registry.istio.io/release"} # used by common/scripts/gobuild.sh echo "istio.io/istio/pkg/version.buildVersion=${VERSION:-$BUILD_GIT_REVISION}" diff --git a/common/scripts/setup_env.sh b/common/scripts/setup_env.sh index 461ef8c1f6..924fc8f7d3 100755 --- a/common/scripts/setup_env.sh +++ b/common/scripts/setup_env.sh @@ -49,6 +49,8 @@ elif [[ ${LOCAL_ARCH} == s390x ]]; then TARGET_ARCH=s390x elif [[ ${LOCAL_ARCH} == ppc64le ]]; then TARGET_ARCH=ppc64le +elif [[ ${LOCAL_ARCH} == riscv64 ]]; then + TARGET_ARCH=riscv64 else echo "This system's architecture, ${LOCAL_ARCH}, isn't supported" exit 1 @@ -72,10 +74,10 @@ else fi # Build image to use -TOOLS_REGISTRY_PROVIDER=${TOOLS_REGISTRY_PROVIDER:-gcr.io} -PROJECT_ID=${PROJECT_ID:-istio-testing} +TOOLS_REGISTRY_PROVIDER=${TOOLS_REGISTRY_PROVIDER:-registry.istio.io} +PROJECT_ID=${PROJECT_ID:-testing} if [[ "${IMAGE_VERSION:-}" == "" ]]; then - IMAGE_VERSION=master-e55cb3f9967290bfb749f97c35db81bcdb04451d + IMAGE_VERSION=master-b7201a4e3411e85dff202449182d26efd7491b89 fi if [[ "${IMAGE_NAME:-}" == "" ]]; then IMAGE_NAME=build-tools diff --git a/examples/localhost.yaml b/examples/localhost.yaml index a75800680b..fa92f2f5f2 100644 --- a/examples/localhost.yaml +++ b/examples/localhost.yaml @@ -36,6 +36,7 @@ policies: name: deny-9999 namespace: default scope: Namespace + dryRun: false services: - name: local namespace: default @@ -46,6 +47,7 @@ services: 80: 8080 subjectAltNames: - spiffe://cluster.local/ns/default/sa/local + canonical: true - name: remote namespace: default hostname: example2.com @@ -53,3 +55,4 @@ services: - remote/127.10.0.2 ports: 80: 8080 + canonical: true diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 6659f0c2af..855851e00c 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -315,9 +315,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -535,9 +535,9 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -613,9 +613,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -755,6 +755,27 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "findshlibs" version = "0.10.2" @@ -812,6 +833,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.31" @@ -1239,7 +1269,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -1417,6 +1447,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.13" @@ -1432,7 +1482,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.8", "widestring", "windows-sys 0.48.0", "winreg", @@ -1519,6 +1569,26 @@ dependencies = [ "indexmap", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1533,9 +1603,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libfuzzer-sys" @@ -1557,6 +1627,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.8.0", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1600,9 +1681,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" dependencies = [ "hashbrown", ] @@ -1615,11 +1696,11 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1670,6 +1751,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.3" @@ -1751,9 +1844,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1772,14 +1865,46 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.8.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +dependencies = [ + "crossbeam-channel", + "file-id", + "log", + "notify", + "parking_lot", + "walkdir", +] + [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1794,9 +1919,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -1866,12 +1991,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1961,9 +2080,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pingora-pool" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacdd5dbdec690d468856d988b170c8bb4ab62e0edefc0f432ba5e326489f421" +checksum = "996c574f30a6e1ad10b47ac1626a86e0e47d5075953dd049d60df16ba5f7076e" dependencies = [ "crossbeam-queue", "log", @@ -1976,9 +2095,9 @@ dependencies = [ [[package]] name = "pingora-timeout" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685bb8808cc1919c63a06ab14fdac9b84a4887ced49259a5c0adc8bfb2ffe558" +checksum = "548cd21d41611c725827677937e68f2cd008bbfa09f3416d3fbad07e1e42f6d7" dependencies = [ "once_cell", "parking_lot", @@ -2038,9 +2157,9 @@ dependencies = [ [[package]] name = "pprof" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0" +checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187" dependencies = [ "aligned-vec", "backtrace", @@ -2051,13 +2170,13 @@ dependencies = [ "log", "nix 0.26.4", "once_cell", - "parking_lot", "protobuf", - "protobuf-codegen-pure", + "protobuf-codegen", "smallvec", + "spin", "symbolic-demangle", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.11", ] [[package]] @@ -2100,9 +2219,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" dependencies = [ "dtoa", "itoa", @@ -2112,9 +2231,9 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2", "quote", @@ -2135,9 +2254,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -2145,9 +2264,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ "heck", "itertools 0.14.0", @@ -2165,9 +2284,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools 0.14.0", @@ -2178,36 +2297,62 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ "prost", ] [[package]] name = "protobuf" -version = "2.28.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] [[package]] name = "protobuf-codegen" -version = "2.28.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" dependencies = [ + "anyhow", + "once_cell", "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", ] [[package]] -name = "protobuf-codegen-pure" -version = "2.28.0" +name = "protobuf-parse" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" dependencies = [ + "anyhow", + "indexmap", + "log", "protobuf", - "protobuf-codegen", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", ] [[package]] @@ -2359,9 +2504,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" dependencies = [ "aws-lc-rs", "pem", @@ -2373,9 +2518,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.8.0", ] @@ -2388,17 +2533,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2409,15 +2545,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -2511,7 +2641,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -2555,6 +2685,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -2634,18 +2775,28 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.218" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2726,6 +2877,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + [[package]] name = "split-iter" version = "0.1.0" @@ -2877,30 +3047,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2971,11 +3141,11 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.8", "tokio-macros", "windows-sys 0.52.0", ] @@ -3027,9 +3197,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.13.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85839f0b32fd242bb3209262371d07feda6d780d16ee9d2bc88581b89da1549b" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "base64 0.22.1", @@ -3039,7 +3209,7 @@ dependencies = [ "http-body-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio-stream", "tower-layer", "tower-service", @@ -3048,9 +3218,32 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.13.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85f0383fadd15609306383a90e85eaed44169f931a5d2be1b42c76ceff1825e" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" dependencies = [ "prettyplease", "proc-macro2", @@ -3058,6 +3251,8 @@ dependencies = [ "prost-types", "quote", "syn", + "tempfile", + "tonic-build", ] [[package]] @@ -3094,9 +3289,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3117,9 +3312,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3128,9 +3323,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3159,14 +3354,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -3458,6 +3653,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.2.0" @@ -3504,6 +3705,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3528,13 +3738,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3547,6 +3774,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3559,6 +3792,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3571,12 +3810,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3589,6 +3840,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3601,6 +3858,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3613,6 +3876,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3625,6 +3894,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.6" @@ -3842,7 +4117,10 @@ dependencies = [ "libc", "log", "netns-rs", - "nix 0.29.0", + "nix 0.30.1", + "notify", + "notify-debouncer-full", + "num_cpus", "once_cell", "pin-project-lite", "pingora-pool", @@ -3851,7 +4129,6 @@ dependencies = [ "prometheus-client", "prometheus-parse", "prost", - "prost-build", "prost-types", "rand 0.9.0", "rcgen", @@ -3860,10 +4137,11 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pemfile", + "rustls-webpki 0.103.3", "serde", "serde_json", "serde_yaml", - "socket2", + "socket2 0.6.0", "split-iter", "textnonce", "thiserror 2.0.11", @@ -3873,7 +4151,8 @@ dependencies = [ "tokio-stream", "tokio-util", "tonic", - "tonic-build", + "tonic-prost", + "tonic-prost-build", "tower", "tracing", "tracing-appender", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 64a71b3e49..146fd66096 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] hyper = "1.1" libfuzzer-sys = "0.4" -prost = "0.13" +prost = { version = "0.14", default-features = false } anyhow = "1.0" [dependencies.ztunnel] diff --git a/proto/authorization.proto b/proto/authorization.proto index 94e9767f52..6c4c03c95b 100644 --- a/proto/authorization.proto +++ b/proto/authorization.proto @@ -34,6 +34,10 @@ message Authorization { // take place. // Rules are OR-ed. repeated Rule rules = 5; + + // Whether or not this is a dry run policy. + // Dry run policies are not enforced, but their matches are logged + bool dry_run = 6; } message Rule { diff --git a/proto/workload.proto b/proto/workload.proto index 7cf0693da8..df398d2eae 100644 --- a/proto/workload.proto +++ b/proto/workload.proto @@ -89,6 +89,10 @@ message Service { // Extension provides a mechanism to attach arbitrary additional configuration to an object. repeated Extension extensions = 10; + + // canonical marks this Service as taking priority during hostname lookups, + // when there is not a match in the namespace of the client. + bool canonical = 11; } enum IPFamilies { @@ -143,6 +147,9 @@ message LoadBalancing { // 3. Endpoints matching `[NETWORK]` // 4. Any endpoints FAILOVER = 2; + // In PASSTHROUGH mode, endpoint selection will not be done and traffic passes directly through to the original + // desitnation address. + PASSTHROUGH = 3; } enum HealthPolicy { // Only select healthy endpoints diff --git a/src/admin.rs b/src/admin.rs index 8bf5776483..325ce68104 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -394,20 +394,20 @@ fn change_log_level(reset: bool, level: &str) -> Response> { if !reset && level.is_empty() { return list_loggers(); } - if !level.is_empty() { - if let Err(_e) = validate_log_level(level) { - // Invalid level provided - return plaintext_response( - hyper::StatusCode::BAD_REQUEST, - format!("Invalid level provided: {}\n{}", level, HELP_STRING), - ); - }; - } + if !level.is_empty() + && let Err(_e) = validate_log_level(level) + { + // Invalid level provided + return plaintext_response( + hyper::StatusCode::BAD_REQUEST, + format!("Invalid level provided: {level}\n{HELP_STRING}"), + ); + }; match telemetry::set_level(reset, level) { Ok(_) => list_loggers(), Err(e) => plaintext_response( hyper::StatusCode::BAD_REQUEST, - format!("Failed to set new level: {}\n{}", e, HELP_STRING), + format!("Failed to set new level: {e}\n{HELP_STRING}"), ), } } @@ -568,7 +568,7 @@ mod tests { "certChain": [ { "expirationTime": "2023-03-11T12:57:26Z", - "pem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNYRENDQVVTZ0F3SUJBZ0lVTDVaZ0toTEI1YUt3YXRuZE1sR25CZWZ3Qkxnd0RRWUpLb1pJaHZjTgpBUUVMQlFBd0dERVdNQlFHQTFVRUNnd05ZMngxYzNSbGNpNXNiMk5oYkRBZUZ3MHlNekF6TVRFd05UVTMKTWpaYUZ3MHlNekF6TVRFeE1qVTNNalphTUJneEZqQVVCZ05WQkFvTURXTnNkWE4wWlhJdWJHOWpZV3d3CldUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSYXIyQm1JWUFndkptT3JTcENlRlE3OUpQeQo4Y3c0K3pFRThmcXI1N2svdW1NcDVqWFpFR0JwZWRCSVkrcWZtSlBYRWlyYTlFOTJkU21rZks1QUtNV3gKbzJrd1p6QTFCZ05WSFJFRUxqQXNoaXB6Y0dsbVptVTZMeTkwY25WemRGOWtiMjFoYVc0dmJuTXZibUZ0ClpYTndZV05sTDNOaEwzTmhMVEF3RHdZRFZSMFBBUUgvQkFVREF3ZWdBREFkQmdOVkhTVUVGakFVQmdncgpCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQ2xKZVJpdmpLYVkKdm5TUHhjUXZPNTNxVFpiUUdHWFc5OHI5Qm1FWGUwYm5YeXZlMWJUVlNYcWVNMXZHdE1DalJGai91dE9VCkRwcHphQVJGRlRzenN2QWdJNStwNFhpbVU4U0FwTlhUYVZjWHkwcG04c2dIWUF6U2drMExBcW1wTWJxbwpvNDB6dmFxVk9nQ1F0c2Vobkg5SCtMQXd1WDl1T08vY2J5NnRidjhrSkhrMWZOTmZ6RTlxZVUwUGFhWWQKZjZXQzhkaWliRGJoN0tjR29rSG80NDMvT05Mb0tJZU9aTFJIbXBFdDdyYnprTDl4elNlNnVZaGQ1SlNGCk55dlY2T3Zoc1FXVVpqd1BmanUvUVJUTzFPdWgrUUZYaTAxNFpvUjRVRnRZaDRjcXphcUlpYVQ0MERyMgpNTHk4eEhJUzRmM1ltUXJEei9VN1pUSG9xaWFLaVBZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "pem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNXekNDQVVPZ0F3SUJBZ0lVTDVaZ0toTEI1YUt3YXRuZE1sR25CZWZ3Qkxnd0RRWUpLb1pJaHZjTgpBUUVMQlFBd0dERVdNQlFHQTFVRUNnd05ZMngxYzNSbGNpNXNiMk5oYkRBZUZ3MHlNekF6TVRFd05UVTMKTWpaYUZ3MHlNekF6TVRFeE1qVTNNalphTUJneEZqQVVCZ05WQkFvTURXTnNkWE4wWlhJdWJHOWpZV3d3CldUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSYXIyQm1JWUFndkptT3JTcENlRlE3OUpQeQo4Y3c0K3pFRThmcXI1N2svdW1NcDVqWFpFR0JwZWRCSVkrcWZtSlBYRWlyYTlFOTJkU21rZks1QUtNV3gKbzJnd1pqQTFCZ05WSFJFRUxqQXNoaXB6Y0dsbVptVTZMeTkwY25WemRGOWtiMjFoYVc0dmJuTXZibUZ0ClpYTndZV05sTDNOaEwzTmhMVEF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFsSW4xek1jTXdjbi8KUEFoN1JvRGI2dnFzZUx6T1RyU1NWMW5qNWt6aGNMdUU0YUNMNFNWbk54SytYTnJUVXdoU3dOdGVZbXFuCnVKTG5DUVVzdS9nVjVWZUt3OGRlNDErWjYvUVhjSzMwNHZXMVl5d2NMcVNWZWd5QkcvT0NzUndvRjIzSwpVMkg1ZXdKV1RSQi9YWGl2TERkMEZsOGIwTkNCN2ZtcmRsRDlZMXlaU1g2aXJwTk1QT1Y5L1B1ckllUUkKR2hvK2dsYjlIME96Tjc5Z2JudldGbEw0RzZVaTlLbzNmeGZhUWpVVVRWbFdpMlh4VlE0MGR6VHV2cG11Ci9qRVh4M0pOQ01zRU5hb3dNYnFTZTlqck9zd0UwMy80ejJCZjBTbkRkdGRwalloN0xZZkRqWkxldTIweAp6VzlNTFM3NU1qdG4vYjV4bHlXeGFyMWh5MnAxS1E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", "serialNumber": "271676055104741785552467469040731750696653685944", "validFrom": "2023-03-11T05:57:26Z" }, @@ -588,7 +588,7 @@ mod tests { "certChain": [ { "expirationTime": "2023-03-11T13:57:26Z", - "pem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNYRENDQVVTZ0F3SUJBZ0lVSlVGNVVGbU52OVhYQlFWaDFDbFk0VFNLRng4d0RRWUpLb1pJaHZjTgpBUUVMQlFBd0dERVdNQlFHQTFVRUNnd05ZMngxYzNSbGNpNXNiMk5oYkRBZUZ3MHlNekF6TVRFd05qVTMKTWpaYUZ3MHlNekF6TVRFeE16VTNNalphTUJneEZqQVVCZ05WQkFvTURXTnNkWE4wWlhJdWJHOWpZV3d3CldUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSYXIyQm1JWUFndkptT3JTcENlRlE3OUpQeQo4Y3c0K3pFRThmcXI1N2svdW1NcDVqWFpFR0JwZWRCSVkrcWZtSlBYRWlyYTlFOTJkU21rZks1QUtNV3gKbzJrd1p6QTFCZ05WSFJFRUxqQXNoaXB6Y0dsbVptVTZMeTkwY25WemRGOWtiMjFoYVc0dmJuTXZibUZ0ClpYTndZV05sTDNOaEwzTmhMVEV3RHdZRFZSMFBBUUgvQkFVREF3ZWdBREFkQmdOVkhTVUVGakFVQmdncgpCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSWdscTIvNnJyWlIKa25UUmZqM201SnU0MmFycGlxVVNHR3A2Mks3L09zeDc5RmovZDBwdU1hMzFkMFhwS0w3N0F2QmtvcVk3CjFWejJKOHRzUkZhZEM1ZmFtQlRXdUN4OUE5R0V3WHEzQmllK2l1a2RGWjZqUTRsb2EybHVWWWFZanhUbgpqR3NLQm0xR0hwMHpacFFVNkdENzA2c2RaTjltaGlqWVA4RnpxWGg1TTlzTzQ4UldveElOUmhXd0pKejQKYUlaZWlRTlJWdkRNZm93MGtxdFFtN001TnQzanA2RkJjTzhGQkJvV0p3MXNCSitLME5XN0VuUG82Yyt0CjE5MkZ0Nmx0eXpvV1BSMnVIYUZENi9FRjZVTkowcTN1ejZicjNYRFg1Q3lrRjQxSEMrNHRSMjQ3RWhmZgpGQkpyUVc0dXAxdHAzdnZGYTdHYnl6bkZWUEc4M3dvPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "pem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNXekNDQVVPZ0F3SUJBZ0lVSlVGNVVGbU52OVhYQlFWaDFDbFk0VFNLRng4d0RRWUpLb1pJaHZjTgpBUUVMQlFBd0dERVdNQlFHQTFVRUNnd05ZMngxYzNSbGNpNXNiMk5oYkRBZUZ3MHlNekF6TVRFd05qVTMKTWpaYUZ3MHlNekF6TVRFeE16VTNNalphTUJneEZqQVVCZ05WQkFvTURXTnNkWE4wWlhJdWJHOWpZV3d3CldUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFSYXIyQm1JWUFndkptT3JTcENlRlE3OUpQeQo4Y3c0K3pFRThmcXI1N2svdW1NcDVqWFpFR0JwZWRCSVkrcWZtSlBYRWlyYTlFOTJkU21rZks1QUtNV3gKbzJnd1pqQTFCZ05WSFJFRUxqQXNoaXB6Y0dsbVptVTZMeTkwY25WemRGOWtiMjFoYVc0dmJuTXZibUZ0ClpYTndZV05sTDNOaEwzTmhMVEV3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFtZ2g1WENwMGp6OWEKS3NvTzZBUlBVWmlKbnhDY2xobHlleUJpbkE1cEFkY0F4V2hNN2xMdklxZXNCT3hpRFdhbFR0Z2QzV29OClJGak1VMUNOa0RmQWRoZDhLSTVoaCtpS0Z3eitYK3JIMThSM0c4SDAyQTZWMnpuYVdGald0a1dvc3c4eQpySHlIYjJBaThXakRVV1dwQ21KL0M3ZUJuVEl3OHMrM2ZMZ2o4Rm5rOVZwcjdSNEovc3ppcGVoczZyRHMKQ1pCQzFKVVA0cXovUis1L3VPWHE3cnBHY05SQVlibXVZNllKbXRWVUxKRXl3THFtUjJCckVvKzFZN0VkCkpxRWFPSUdFTEVrdENNazBvZUhkRmZoWWlqZXdmRXJVbVJFSzM2Yy8xY01XMk44MFlkVUMzd1UyWHlZdwpqWUswdkxWeng3U1Q4TmcwL0xlYUdJWGtrQW1PQ3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", "serialNumber": "212692774886610945930036647276614034927450199839", "validFrom": "2023-03-11T06:57:26Z" }, @@ -702,6 +702,7 @@ mod tests { }), // ..Default::default() // intentionally don't default. we want all fields populated ip_families: 0, extensions: Default::default(), + canonical: true, }; let auth = XdsAuthorization { @@ -757,6 +758,7 @@ mod tests { }], }], }], + dry_run: false, // ..Default::default() // intentionally don't default. we want all fields populated }; @@ -819,14 +821,14 @@ mod tests { let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,info\n" + "current log level is hickory_server::server=off,info\n" ); let resp = change_log_level(true, ""); let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,info\n" + "current log level is hickory_server::server=off,info\n" ); let resp = change_log_level(true, "invalid_level"); @@ -840,49 +842,40 @@ mod tests { let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,debug\n" + "current log level is hickory_server::server=off,debug\n" ); let resp = change_log_level(true, "access=debug,info"); let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,access=debug,info\n" + "current log level is hickory_server::server=off,access=debug,info\n" ); let resp = change_log_level(true, "warn"); let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,warn\n" + "current log level is hickory_server::server=off,warn\n" ); let resp = change_log_level(true, "error"); let resp_str = get_response_str(resp).await; assert_eq!( resp_str, - "current log level is hickory_server::server::server_future=off,error\n" + "current log level is hickory_server::server=off,error\n" ); let resp = change_log_level(true, "trace"); let resp_str = get_response_str(resp).await; - assert!( - resp_str - .contains("current log level is hickory_server::server::server_future=off,trace\n") - ); + assert!(resp_str.contains("current log level is hickory_server::server=off,trace\n")); let resp = change_log_level(true, "info"); let resp_str = get_response_str(resp).await; - assert!( - resp_str - .contains("current log level is hickory_server::server::server_future=off,info\n") - ); + assert!(resp_str.contains("current log level is hickory_server::server=off,info\n")); let resp = change_log_level(true, "off"); let resp_str = get_response_str(resp).await; - assert!( - resp_str - .contains("current log level is hickory_server::server::server_future=off,off\n") - ); + assert!(resp_str.contains("current log level is hickory_server::server=off,off\n")); } } diff --git a/src/app.rs b/src/app.rs index 73e1c0a991..aee95d70ed 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,6 +77,7 @@ pub async fn build_with_cert( // Register metrics. let mut registry = Registry::default(); + register_process_metrics(&mut registry); let istio_registry = metrics::sub_registry(&mut registry); let _ = metrics::meta::Metrics::new(istio_registry); let xds_metrics = xds::Metrics::new(istio_registry); @@ -250,6 +251,11 @@ pub async fn build_with_cert( }) } +fn register_process_metrics(registry: &mut Registry) { + #[cfg(unix)] + registry.register_collector(Box::new(metrics::process::ProcessMetrics::new())); +} + struct DataPlaneTask { block_shutdown: bool, fut: Pin> + Send + Sync + 'static>>, @@ -266,7 +272,8 @@ fn new_data_plane_pool(num_worker_threads: usize) -> mpsc::Sender .thread_name_fn(|| { static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); - format!("ztunnel-proxy-{id}") + // Thread name can only be 16 chars so keep it short + format!("ztunnel-{id}") }) .enable_all() .build() diff --git a/src/baggage.rs b/src/baggage.rs index 375405dd0d..33d6c2b554 100644 --- a/src/baggage.rs +++ b/src/baggage.rs @@ -18,7 +18,7 @@ use hyper::{ http::HeaderValue, }; -#[derive(Default)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct Baggage { pub cluster_id: Option, pub namespace: Option, @@ -29,6 +29,43 @@ pub struct Baggage { pub zone: Option, } +pub fn baggage_header_val(baggage: &Baggage, workload_type: &str) -> String { + [ + baggage + .cluster_id + .as_ref() + .map(|cluster| format!("k8s.cluster.name={cluster}")), + baggage + .namespace + .as_ref() + .map(|namespace| format!("k8s.namespace.name={namespace}")), + baggage + .workload_name + .as_ref() + .map(|workload| format!("k8s.{workload_type}.name={workload}")), + baggage + .service_name + .as_ref() + .map(|service| format!("service.name={service}")), + baggage + .revision + .as_ref() + .map(|revision| format!("service.version={revision}")), + baggage + .region + .as_ref() + .map(|region| format!("cloud.region={region}")), + baggage + .zone + .as_ref() + .map(|zone| format!("cloud.availability_zone={zone}")), + ] + .into_iter() + .flatten() + .collect::>() + .join(",") +} + pub fn parse_baggage_header(headers: GetAll) -> Result { let mut baggage = Baggage { ..Default::default() @@ -67,8 +104,9 @@ pub mod tests { use hyper::{HeaderMap, http::HeaderValue}; use crate::proxy::BAGGAGE_HEADER; + use crate::strng::Strng; - use super::parse_baggage_header; + use super::{Baggage, baggage_header_val, parse_baggage_header}; #[test] fn baggage_parser() -> anyhow::Result<()> { @@ -90,8 +128,8 @@ pub mod tests { let mut hm = HeaderMap::new(); let baggage_str = "k8s.cluster.name=,k8s.namespace.name=,k8s.deployment.name=,service.name=,service.version="; let header_value = HeaderValue::from_str(baggage_str)?; - let baggage = parse_baggage_header(hm.get_all(BAGGAGE_HEADER))?; hm.append(BAGGAGE_HEADER, header_value); + let baggage = parse_baggage_header(hm.get_all(BAGGAGE_HEADER))?; assert_eq!(baggage.cluster_id, None); assert_eq!(baggage.namespace, None); assert_eq!(baggage.workload_name, None); @@ -136,4 +174,37 @@ pub mod tests { assert_eq!(baggage.revision, None); Ok(()) } + + #[test] + fn baggage_header_val_can_be_parsed() -> anyhow::Result<()> { + { + let baggage = Baggage { + ..Default::default() + }; + let mut hm = HeaderMap::new(); + hm.append( + BAGGAGE_HEADER, + HeaderValue::from_str(&baggage_header_val(&baggage, "deployment"))?, + ); + let parsed = parse_baggage_header(hm.get_all(BAGGAGE_HEADER))?; + assert_eq!(baggage, parsed); + } + { + let baggage = Baggage { + cluster_id: Some(Strng::from("cluster")), + namespace: Some(Strng::from("default")), + workload_name: Some(Strng::from("workload")), + service_name: Some(Strng::from("service")), + ..Default::default() + }; + let mut hm = HeaderMap::new(); + hm.append( + BAGGAGE_HEADER, + HeaderValue::from_str(&baggage_header_val(&baggage, "deployment"))?, + ); + let parsed = parse_baggage_header(hm.get_all(BAGGAGE_HEADER))?; + assert_eq!(baggage, parsed); + } + Ok(()) + } } diff --git a/src/cert_fetcher.rs b/src/cert_fetcher.rs index df2a53a6c1..b659d57205 100644 --- a/src/cert_fetcher.rs +++ b/src/cert_fetcher.rs @@ -102,10 +102,10 @@ impl CertFetcherImpl { impl CertFetcher for CertFetcherImpl { fn prefetch_cert(&self, w: &Workload) { - if self.should_prefetch_certificate(w) { - if let Err(e) = self.tx.try_send(Request::Fetch(w.identity(), Warmup)) { - info!("couldn't prefetch: {:?}", e) - } + if self.should_prefetch_certificate(w) + && let Err(e) = self.tx.try_send(Request::Fetch(w.identity(), Warmup)) + { + info!("couldn't prefetch: {:?}", e) } } diff --git a/src/config.rs b/src/config.rs index 05235eff9e..8f9f0d5124 100644 --- a/src/config.rs +++ b/src/config.rs @@ -55,11 +55,13 @@ const LOCAL_XDS_PATH: &str = "LOCAL_XDS_PATH"; const LOCAL_XDS: &str = "LOCAL_XDS"; const XDS_ON_DEMAND: &str = "XDS_ON_DEMAND"; const XDS_ADDRESS: &str = "XDS_ADDRESS"; +const PREFERED_SERVICE_NAMESPACE: &str = "PREFERED_SERVICE_NAMESPACE"; const CA_ADDRESS: &str = "CA_ADDRESS"; const SECRET_TTL: &str = "SECRET_TTL"; const FAKE_CA: &str = "FAKE_CA"; const USE_ENV_FOR_DEFAULT_ISTIOD_ADDR: &str = "USE_ENV_FOR_DEFAULT_ISTIOD_ADDR"; const ZTUNNEL_WORKER_THREADS: &str = "ZTUNNEL_WORKER_THREADS"; +const ZTUNNEL_CPU_LIMIT: &str = "ZTUNNEL_CPU_LIMIT"; const POOL_MAX_STREAMS_PER_CONNECTION: &str = "POOL_MAX_STREAMS_PER_CONNECTION"; const POOL_UNUSED_RELEASE_TIMEOUT: &str = "POOL_UNUSED_RELEASE_TIMEOUT"; // CONNECTION_TERMINATION_DEADLINE configures an explicit deadline @@ -78,6 +80,8 @@ const HTTP2_FRAME_SIZE: &str = "HTTP2_FRAME_SIZE"; const UNSTABLE_ENABLE_SOCKS5: &str = "UNSTABLE_ENABLE_SOCKS5"; +const CRL_PATH: &str = "CRL_PATH"; + const DEFAULT_WORKER_THREADS: u16 = 2; const DEFAULT_ADMIN_PORT: u16 = 15000; const DEFAULT_READINESS_PORT: u16 = 15021; @@ -113,6 +117,13 @@ const PROXY_MODE_DEDICATED: &str = "dedicated"; const PROXY_MODE_SHARED: &str = "shared"; const LOCALHOST_APP_TUNNEL: &str = "LOCALHOST_APP_TUNNEL"; +const ENABLE_ENHANCED_BAGGAGE: &str = "ENABLE_RESPONSE_BAGGAGE"; + +/// When true, authorization policy logs are emitted at INFO level instead of DEBUG. +pub static AUTHZ_POLICY_INFO_LOGGING: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| { + env::var("AUTHZ_POLICY_INFO_LOGGING").unwrap_or_default() == "true" + }); #[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)] pub enum RootCert { @@ -243,6 +254,12 @@ pub struct Config { // Allow custom alternative XDS hostname verification pub alt_xds_hostname: Option, + /// Prefered service namespace to use for service resolution. + /// If unset, local namespaces is preferred and other namespaces have equal priority. + /// If set, the local namespace is preferred, then the defined prefered_service_namespace + /// and finally other namespaces at an equal priority. + pub prefered_service_namespace: Option, + /// TTL for CSR requests pub secret_ttl: Duration, /// YAML config for local XDS workloads @@ -303,6 +320,12 @@ pub struct Config { pub ztunnel_identity: Option, pub ztunnel_workload: Option, + + pub ipv6_enabled: bool, + + // path to CRL file; if set, enables CRL checking + pub crl_path: Option, + pub enable_enhanced_baggage: bool, } #[derive(serde::Serialize, Clone, Copy, Debug)] @@ -410,6 +433,60 @@ fn parse_headers(prefix: &str) -> Result { Ok(metadata) } +fn get_cpu_count() -> Result { + // Allow overriding the count with an env var. This can be used to pass the CPU limit on Kubernetes + // from the downward API. + // Note the downward API will return the total thread count ("logical cores") if no limit is set, + // so it is really the same as num_cpus. + // We allow num_cpus for cases its not set (not on Kubernetes, etc). + match parse::(ZTUNNEL_CPU_LIMIT)? { + Some(limit) => Ok(limit), + // This is *logical cores* + None => Ok(num_cpus::get()), + } +} + +/// Parse worker threads configuration, supporting both fixed numbers and percentages +fn parse_worker_threads(default: usize) -> Result { + match parse::(ZTUNNEL_WORKER_THREADS)? { + Some(value) => { + if let Some(percent_str) = value.strip_suffix('%') { + // Parse as percentage + let percent: f64 = percent_str.parse().map_err(|e| { + Error::EnvVar( + ZTUNNEL_WORKER_THREADS.to_string(), + value.clone(), + format!("invalid percentage: {e}"), + ) + })?; + + if percent <= 0.0 || percent > 100.0 { + return Err(Error::EnvVar( + ZTUNNEL_WORKER_THREADS.to_string(), + value, + "percentage must be between 0 and 100".to_string(), + )); + } + + let cpu_count = get_cpu_count()?; + // Round up, minimum of 1 + let threads = ((cpu_count as f64 * percent / 100.0).ceil() as usize).max(1); + Ok(threads) + } else { + // Parse as fixed number + value.parse::().map_err(|e| { + Error::EnvVar( + ZTUNNEL_WORKER_THREADS.to_string(), + value, + format!("invalid number: {e}"), + ) + }) + } + } + None => Ok(default), + } +} + pub fn parse_config() -> Result { let pc = parse_proxy_config()?; construct_config(pc) @@ -459,6 +536,14 @@ pub fn construct_config(pc: ProxyConfig) -> Result { .or_else(|| Some(default_istiod_address.clone())), ))?; + let prefered_service_namespace = match parse::(PREFERED_SERVICE_NAMESPACE) { + Ok(ns) => ns, + Err(e) => { + warn!(err=?e, "failed to parse {PREFERED_SERVICE_NAMESPACE}, continuing with default behavior"); + None + } + }; + let istio_meta_cluster_id = ISTIO_META_PREFIX.to_owned() + CLUSTER_ID; let cluster_id: String = match parse::(&istio_meta_cluster_id)? { Some(id) => id, @@ -558,7 +643,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result { // on a pod-by-pod basis. let dns_proxy_addr: Address = match pc.proxy_metadata.get(DNS_PROXY_ADDR_METADATA) { Some(dns_addr) => Address::new(ipv6_localhost_enabled, dns_addr) - .unwrap_or_else(|_| panic!("failed to parse DNS_PROXY_ADDR: {}", dns_addr)), + .unwrap_or_else(|_| panic!("failed to parse DNS_PROXY_ADDR: {dns_addr}")), None => Address::Localhost(ipv6_localhost_enabled, DEFAULT_DNS_PORT), }; @@ -743,6 +828,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result { xds_address, xds_root_cert, + prefered_service_namespace, ca_address, ca_root_cert, alt_xds_hostname: parse(ALT_XDS_HOSTNAME)?, @@ -756,8 +842,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result { fake_ca, auth, - num_worker_threads: parse_default( - ZTUNNEL_WORKER_THREADS, + num_worker_threads: parse_worker_threads( pc.concurrency.unwrap_or(DEFAULT_WORKER_THREADS).into(), )?, @@ -823,6 +908,13 @@ pub fn construct_config(pc: ProxyConfig) -> Result { localhost_app_tunnel: parse_default(LOCALHOST_APP_TUNNEL, true)?, ztunnel_identity, ztunnel_workload, + ipv6_enabled, + + crl_path: env::var(CRL_PATH) + .ok() + .filter(|s| !s.is_empty()) + .map(PathBuf::from), + enable_enhanced_baggage: parse_default(ENABLE_ENHANCED_BAGGAGE, true)?, }) } @@ -940,10 +1032,10 @@ fn construct_proxy_config(mc_path: &str, pc_env: Option<&str>) -> anyhow::Result } pub fn empty_to_none>(inp: Option) -> Option { - if let Some(inner) = &inp { - if inner.as_ref().is_empty() { - return None; - } + if let Some(inner) = &inp + && inner.as_ref().is_empty() + { + return None; } inp } @@ -1030,11 +1122,14 @@ pub mod tests { #[test] fn config_from_proxyconfig() { + use crate::test_helpers::{MESH_CONFIG_YAML, temp_file_with_content}; + let default_config = construct_config(ProxyConfig::default()) .expect("could not build Config without ProxyConfig"); // mesh config only - let mesh_config_path = "./src/test_helpers/mesh_config.yaml"; + let mesh_config_file = temp_file_with_content(MESH_CONFIG_YAML).unwrap(); + let mesh_config_path = mesh_config_file.path().to_str().unwrap(); let pc = construct_proxy_config(mesh_config_path, None).unwrap(); let cfg = construct_config(pc).unwrap(); assert_eq!(cfg.stats_addr.port(), 15888); @@ -1125,4 +1220,45 @@ pub mod tests { assert!(metadata.vec.contains(&(key, value))); } } + + #[test] + fn test_parse_worker_threads() { + unsafe { + // Test fixed number + env::set_var(ZTUNNEL_WORKER_THREADS, "4"); + assert_eq!(parse_worker_threads(2).unwrap(), 4); + + // Test percentage with CPU limit + env::set_var(ZTUNNEL_CPU_LIMIT, "8"); + env::set_var(ZTUNNEL_WORKER_THREADS, "50%"); + assert_eq!(parse_worker_threads(2).unwrap(), 4); // 50% of 8 CPUs = 4 threads + + // Test percentage with CPU limit + env::set_var(ZTUNNEL_CPU_LIMIT, "16"); + env::set_var(ZTUNNEL_WORKER_THREADS, "30%"); + assert_eq!(parse_worker_threads(2).unwrap(), 5); // Round up to 5 + + // Test low percentage that rounds up to 1 + env::set_var(ZTUNNEL_CPU_LIMIT, "4"); + env::set_var(ZTUNNEL_WORKER_THREADS, "10%"); + assert_eq!(parse_worker_threads(2).unwrap(), 1); // 10% of 4 CPUs = 0.4, rounds up to 1 + + // Test default when no env var is set + env::remove_var(ZTUNNEL_WORKER_THREADS); + assert_eq!(parse_worker_threads(2).unwrap(), 2); + + // Test without CPU limit (should use system CPU count) + env::remove_var(ZTUNNEL_CPU_LIMIT); + let system_cpus = num_cpus::get(); + assert_eq!(get_cpu_count().unwrap(), system_cpus); + + // Test with CPU limit + env::set_var(ZTUNNEL_CPU_LIMIT, "12"); + assert_eq!(get_cpu_count().unwrap(), 12); + + // Clean up + env::remove_var(ZTUNNEL_WORKER_THREADS); + env::remove_var(ZTUNNEL_CPU_LIMIT); + } + } } diff --git a/src/copy.rs b/src/copy.rs index 13e7fe9102..b660512580 100644 --- a/src/copy.rs +++ b/src/copy.rs @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::proxy; -use crate::proxy::ConnectionResult; use crate::proxy::Error::{BackendDisconnected, ClientDisconnected, ReceiveError, SendError}; +use crate::proxy::{self, ConnectionResult}; use bytes::{Buf, Bytes, BytesMut}; use pin_project_lite::pin_project; use std::future::Future; @@ -416,9 +415,9 @@ mod tests { let metrics = std::sync::Arc::new(crate::proxy::Metrics::new( crate::metrics::sub_registry(&mut registry), )); - let source_addr = "127.0.0.1:12345".parse().unwrap(); - let dest_addr = "127.0.0.1:34567".parse().unwrap(); - let cr = ConnectionResult::new( + let source_addr: std::net::SocketAddr = "127.0.0.1:12345".parse().unwrap(); + let dest_addr: std::net::SocketAddr = "127.0.0.1:34567".parse().unwrap(); + let cr = crate::proxy::metrics::ConnectionResultBuilder::new( source_addr, dest_addr, None, @@ -432,7 +431,8 @@ mod tests { destination_service: None, }, metrics.clone(), - ); + ) + .build(); copy_bidirectional(ztunnel_downsteam, ztunnel_upsteam, &cr).await }); const ITERS: usize = 1000; @@ -461,9 +461,9 @@ mod tests { let metrics = std::sync::Arc::new(crate::proxy::Metrics::new( crate::metrics::sub_registry(&mut registry), )); - let source_addr = "127.0.0.1:12345".parse().unwrap(); - let dest_addr = "127.0.0.1:34567".parse().unwrap(); - let cr = ConnectionResult::new( + let source_addr: std::net::SocketAddr = "127.0.0.1:12345".parse().unwrap(); + let dest_addr: std::net::SocketAddr = "127.0.0.1:34567".parse().unwrap(); + let cr = crate::proxy::metrics::ConnectionResultBuilder::new( source_addr, dest_addr, None, @@ -477,7 +477,8 @@ mod tests { destination_service: None, }, metrics.clone(), - ); + ) + .build(); copy_bidirectional(WeirdIO(ztunnel_downsteam), WeirdIO(ztunnel_upsteam), &cr).await }); const WRITES: usize = 2560; diff --git a/src/dns/forwarder.rs b/src/dns/forwarder.rs index 4995c7a988..fcd2e4c5fc 100644 --- a/src/dns/forwarder.rs +++ b/src/dns/forwarder.rs @@ -180,12 +180,12 @@ mod tests { .expect("expected resolve error"); // Expect NoRecordsFound with a NXDomain response code. - if let ResolveErrorKind::Proto(proto) = err.kind() { - if let ProtoErrorKind::NoRecordsFound { response_code, .. } = proto.kind() { - // Respond with the error code. - assert_eq!(&ResponseCode::NXDomain, response_code); - return; - } + if let ResolveErrorKind::Proto(proto) = err.kind() + && let ProtoErrorKind::NoRecordsFound { response_code, .. } = proto.kind() + { + // Respond with the error code. + assert_eq!(&ResponseCode::NXDomain, response_code); + return; } panic!("unexpected error kind {}", err.kind()) } diff --git a/src/dns/handler.rs b/src/dns/handler.rs index e9027c5648..93ccbe0f3c 100644 --- a/src/dns/handler.rs +++ b/src/dns/handler.rs @@ -119,11 +119,11 @@ async fn send_lookup_error( } LookupError::ResponseCode(code) => send_error(request, response_handle, code).await, LookupError::ResolveError(e) => { - if let ResolveErrorKind::Proto(proto) = e.kind() { - if let ProtoErrorKind::NoRecordsFound { response_code, .. } = proto.kind() { - // Respond with the error code. - return send_error(request, response_handle, *response_code).await; - } + if let ResolveErrorKind::Proto(proto) = e.kind() + && let ProtoErrorKind::NoRecordsFound { response_code, .. } = proto.kind() + { + // Respond with the error code. + return send_error(request, response_handle, *response_code).await; } // TODO(nmittler): log? send_error(request, response_handle, ResponseCode::ServFail).await diff --git a/src/dns/metrics.rs b/src/dns/metrics.rs index 03d80ab244..fc454531ac 100644 --- a/src/dns/metrics.rs +++ b/src/dns/metrics.rs @@ -57,7 +57,7 @@ impl Metrics { ); let forwarded_duration = Family::::new_with_constructor(|| { - Histogram::new(vec![0.005f64, 0.001, 0.01, 0.1, 1.0, 5.0]) + Histogram::new(vec![0.0005f64, 0.001, 0.01, 0.1, 1.0, 5.0]) }); registry.register_with_unit( "dns_upstream_request_duration", diff --git a/src/dns/server.rs b/src/dns/server.rs index c11d449beb..a7107e6f1a 100644 --- a/src/dns/server.rs +++ b/src/dns/server.rs @@ -21,7 +21,6 @@ use hickory_resolver::system_conf::read_system_conf; use hickory_server::ServerFuture; use hickory_server::authority::LookupError; use hickory_server::server::Request; -use itertools::Itertools; use once_cell::sync::Lazy; use rand::rng; use rand::seq::SliceRandom; @@ -47,9 +46,10 @@ use crate::drain::{DrainMode, DrainWatcher}; use crate::metrics::{DeferRecorder, IncrementRecorder, Recorder}; use crate::proxy::Error; use crate::state::DemandProxyState; -use crate::state::service::IpFamily; +use crate::state::service::{IpFamily, Service, ServiceMatch}; use crate::state::workload::Workload; use crate::state::workload::address::Address; +use crate::strng::Strng; use crate::{config, dns}; const DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5; @@ -85,6 +85,8 @@ impl Server { drain: DrainWatcher, socket_factory: &(dyn SocketFactory + Send + Sync), local_workload_information: Arc, + prefered_service_namespace: Option, + ipv6_enabled: bool, ) -> Result { // if the address we got from config is supposed to be v6-enabled, // actually check if the local pod context our socketfactory operates in supports V6. @@ -102,6 +104,8 @@ impl Server { forwarder, metrics, local_workload_information, + prefered_service_namespace, + ipv6_enabled, ); let store = Arc::new(store); let handler = dns::handler::Handler::new(store.clone()); @@ -191,6 +195,8 @@ struct Store { svc_domain: Name, metrics: Arc, local_workload: Arc, + prefered_service_namespace: Option, + ipv6_enabled: bool, } impl Store { @@ -200,6 +206,8 @@ impl Store { forwarder: Arc, metrics: Arc, local_workload_information: Arc, + prefered_service_namespace: Option, + ipv6_enabled: bool, ) -> Self { let domain = as_name(domain); let svc_domain = append_name(as_name("svc"), &domain); @@ -211,6 +219,8 @@ impl Store { svc_domain, metrics, local_workload: local_workload_information, + prefered_service_namespace, + ipv6_enabled, } } @@ -248,20 +258,14 @@ impl Store { // Insert an alias for a stripped search domain. add_alias(Alias { name: stripped_name.clone(), - stripped: Some(Stripped { - name: stripped_name.clone(), - search_domain: search_domain.clone(), - }), + stripped: Some(stripped_name.clone()), }); // If the name can be expanded to a k8s FQDN, add that as well. for kube_fqdn in self.to_kube_fqdns(&stripped_name, &namespaced_domain) { add_alias(Alias { name: kube_fqdn, - stripped: Some(Stripped { - name: stripped_name.clone(), - search_domain: search_domain.clone(), - }), + stripped: Some(stripped_name.clone()), }); } } @@ -359,10 +363,10 @@ impl Store { let search_name_str = search_name.to_string().into(); search_name.set_fqdn(true); - let service = state + let services: Vec> = state .services .get_by_host(&search_name_str) - .iter() + .into_iter() .flatten() // Remove things without a VIP, unless they are Kubernetes headless services. // This will trigger us to forward upstream. @@ -382,13 +386,23 @@ impl Store { }) // Get the service matching the client namespace. If no match exists, just // return the first service. - .find_or_first(|service| service.namespace == client.namespace) - .cloned(); + .collect(); + + let preferred_namespace: Strng = self + .prefered_service_namespace + .as_deref() + .unwrap_or("") + .into(); + let service: Option<&Arc> = ServiceMatch::find_best_match( + services.iter(), + Some(&client.namespace), + Some(&preferred_namespace), + ); // First, lookup the host as a service. if let Some(service) = service { return Some(ServerMatch { - server: Address::Service(service), + server: Address::Service(service.clone()), name: search_name, alias, }); @@ -400,6 +414,13 @@ impl Store { None } + fn record_type_enabled(&self, addr: &IpAddr) -> bool { + match addr { + IpAddr::V4(_) => true, // IPv4 always + IpAddr::V6(_) => self.ipv6_enabled, // IPv6 must be not be disabled in config + } + } + /// Gets the list of addresses of the requested record type from the server. fn get_addresses( &self, @@ -412,7 +433,7 @@ impl Store { .workload_ips .iter() .filter_map(|addr| { - if is_record_type(addr, record_type) { + if is_record_type(addr, record_type) && self.record_type_enabled(addr) { Some(*addr) } else { None @@ -431,10 +452,9 @@ impl Store { debug!("failed to fetch workload for {}", ep.workload_uid); return None; }; - wl.workload_ips - .iter() - .copied() - .find(|addr| is_record_type(addr, record_type)) + wl.workload_ips.iter().copied().find(|addr| { + is_record_type(addr, record_type) && self.record_type_enabled(addr) + }) }) .collect() } else { @@ -446,6 +466,7 @@ impl Store { .filter_map(|vip| { if is_record_type(&vip.address, record_type) && client.network == vip.network + && self.record_type_enabled(&vip.address) { Some(vip.address) } else { @@ -616,7 +637,7 @@ impl Resolver for Store { // From this point on, we are the authority for the response. let is_authoritative = true; - if !service_family_allowed(&service_match.server, record_type) { + if !service_family_allowed(&service_match.server, record_type, self.ipv6_enabled) { access_log( request, Some(&client), @@ -645,31 +666,14 @@ impl Resolver for Store { // If the service was found by stripping off one of the search domains, create a // CNAME record to map to the appropriate canonical name. - if let Some(stripped) = service_match.alias.stripped { - if service_match.name.is_wildcard() { - // The match is a wildcard... - - // Create a CNAME record that maps from the wildcard with the search domain to - // the wildcard without it. - let cname_record_name = service_match - .name - .clone() - .append_domain(&stripped.search_domain) - .unwrap(); - let canonical_name = service_match.name; - records.push(cname_record(cname_record_name, canonical_name)); - - // For wildcards, continue using the original requested hostname for IP records. - } else { - // The match is NOT a wildcard... - - // Create a CNAME record to map from the requested name -> stripped name. - let canonical_name = stripped.name; - records.push(cname_record(requested_name.clone(), canonical_name.clone())); - - // Also use the stripped name as the IP record name. - ip_record_name = canonical_name; - } + if let Some(stripped) = service_match.alias.stripped + && !service_match.name.is_wildcard() + { + // Create a CNAME record to map from the requested name -> stripped name. + records.push(cname_record(requested_name.clone(), stripped.clone())); + + // Also use the stripped name as the IP record name. + ip_record_name = stripped; } access_log(request, Some(&client), "success", records.len()); @@ -685,7 +689,13 @@ impl Resolver for Store { /// anyway, so would naturally work. /// Headless services, however, do not have VIPs, and the Pods behind them can have dual stack IPs even with /// the Service being single-stack. In this case, we are NOT supposed to return both IPs. -fn service_family_allowed(server: &Address, record_type: RecordType) -> bool { +/// If IPv6 is globally disabled, AAAA records are not allowed. +fn service_family_allowed(server: &Address, record_type: RecordType, ipv6_enabled: bool) -> bool { + // If IPv6 is globally disabled, don't allow AAAA records + if !ipv6_enabled && record_type == RecordType::AAAA { + return false; + } + match server { Address::Service(service) => match service.ip_families { Some(IpFamily::IPv4) if record_type == RecordType::AAAA => false, @@ -704,7 +714,7 @@ struct Alias { /// If `Some`, indicates that this alias was generated from the requested host that /// was stripped of - stripped: Option, + stripped: Option, } impl Display for Alias { @@ -713,16 +723,6 @@ impl Display for Alias { } } -/// Created for an alias generated by stripping a search domain from the requested host. -#[derive(Debug)] -struct Stripped { - /// The requested hostname with the `search_domain` removed. - name: Name, - - /// The search domain that was removed from the requested host to generate `name`. - search_domain: Name, -} - /// Returned when a server was successfully found for the requested hostname. #[derive(Debug)] struct ServerMatch { @@ -957,6 +957,8 @@ mod tests { const NS1: &str = "ns1"; const NS2: &str = "ns2"; + const NS3: &str = "ns3"; + const PREFERRED: &str = "preferred-ns"; const NW1: Strng = strng::literal!("nw1"); const NW2: Strng = strng::literal!("nw2"); @@ -1064,6 +1066,8 @@ mod tests { forwarder, metrics: test_metrics(), local_workload, + prefered_service_namespace: None, + ipv6_enabled: true, }; let namespaced_domain = n(format!("{}.svc.cluster.local", c.client_namespace)); @@ -1287,16 +1291,10 @@ mod tests { Case { name: "success: wild card with search domain returns A record correctly", host: "foo.svc.mesh.company.net.ns1.svc.cluster.local.", - expect_records: vec![ - cname( - n("*.svc.mesh.company.net.ns1.svc.cluster.local."), - n("*.svc.mesh.company.net."), - ), - a( - n("foo.svc.mesh.company.net.ns1.svc.cluster.local."), - ipv4("10.1.2.3"), - ), - ], + expect_records: vec![a( + n("foo.svc.mesh.company.net.ns1.svc.cluster.local."), + ipv4("10.1.2.3"), + )], ..Default::default() }, Case { @@ -1379,6 +1377,30 @@ mod tests { expect_code: ResponseCode::NXDomain, ..Default::default() }, + Case { + name: "success: preferred namespace is chosen if local namespace is not defined", + host: "preferred.io.", + expect_records: vec![a(n("preferred.io."), ipv4("10.10.10.211"))], + ..Default::default() + }, + Case { + name: "success: external service resolves to local namespace's address", + host: "everywhere.io.", + expect_records: vec![a(n("everywhere.io."), ipv4("10.10.10.112"))], + ..Default::default() + }, + Case { + name: "success: canonical services are preferred when no ns-local hostname is present", + host: "canonical.svc", + expect_records: vec![a(n("canonical.svc."), ipv4("10.10.10.141"))], + ..Default::default() + }, + Case { + name: "success: namespace-local service should be preferred over canonical", + host: "canonical.with.local", + expect_records: vec![a(n("canonical.with.local."), ipv4("10.10.10.150"))], + ..Default::default() + }, ]; // Create and start the proxy. @@ -1396,6 +1418,8 @@ mod tests { drain, &factory, local_workload, + Some(PREFERRED.to_string()), + true, // ipv6_enabled for tests ) .await .unwrap(); @@ -1415,8 +1439,8 @@ mod tests { tasks.push(async move { let name = format!("[{protocol}] {}", c.name); let resp = send_request(&mut client, n(c.host), c.query_type).await; - assert_eq!(c.expect_authoritative, resp.authoritative(), "{}", name); - assert_eq!(c.expect_code, resp.response_code(), "{}", name); + assert_eq!(c.expect_authoritative, resp.authoritative(), "{name}"); + assert_eq!(c.expect_code, resp.response_code(), "{name}"); if c.expect_code == ResponseCode::NoError { let mut actual = resp.answers().to_vec(); @@ -1427,7 +1451,7 @@ mod tests { if c.expect_authoritative { sort_records(&mut actual); } - assert_eq!(c.expect_records, actual, "{}", name); + assert_eq!(c.expect_records, actual, "{name}"); } }); } @@ -1482,6 +1506,8 @@ mod tests { drain, &factory, local_workload, + None, + true, // ipv6_enabled for tests ) .await .unwrap(); @@ -1496,7 +1522,7 @@ mod tests { for (protocol, client) in [("tcp", &mut tcp_client), ("udp", &mut udp_client)] { let name = format!("[{protocol}] {}", c.name); let resp = send_request(client, n(c.host), RecordType::A).await; - assert_eq!(c.expect_code, resp.response_code(), "{}", name); + assert_eq!(c.expect_code, resp.response_code(), "{name}"); if c.expect_code == ResponseCode::NoError { assert!(!resp.answers().is_empty()); } @@ -1531,6 +1557,8 @@ mod tests { }), state.clone(), ), + prefered_service_namespace: None, + ipv6_enabled: true, }; let ip4n6_client_ip = ip("::ffff:202:202"); @@ -1538,7 +1566,7 @@ mod tests { match store.lookup(&req).await { Ok(_) => {} Err(e) => { - panic!("IPv6 encoded IPv4 should work! Error was {:?}", e) + panic!("IPv6 encoded IPv4 should work! Error was {e:?}"); } } } @@ -1564,6 +1592,8 @@ mod tests { drain, &factory, local_workload, + None, + true, // ipv6_enabled for tests ) .await .unwrap(); @@ -1680,6 +1710,34 @@ mod tests { xds_external_service("www.google.com", &[na(NW1, "1.1.1.1")]), xds_service("productpage", NS1, &[na(NW1, "9.9.9.9")]), xds_service("example", NS2, &[na(NW1, "10.10.10.10")]), + // Service with the same name in another namespace + // This should not be used if the preferred service namespace is set + xds_namespaced_external_service("everywhere.io", NS2, &[na(NW1, "10.10.10.110")]), + xds_namespaced_external_service("preferred.io", NS2, &[na(NW1, "10.10.10.210")]), + // Preferred service namespace + xds_namespaced_external_service("everywhere.io", PREFERRED, &[na(NW1, "10.10.10.111")]), + xds_namespaced_external_service("preferred.io", PREFERRED, &[na(NW1, "10.10.10.211")]), + // Service with the same name in the same namespace + // Client in NS1 should use this service + xds_namespaced_external_service("everywhere.io", NS1, &[na(NW1, "10.10.10.112")]), + // Service that is canonical should be preferrred when no ns-local definition + xds_namespaced_external_service("canonical.svc", NS2, &[na(NW1, "10.10.10.140")]), + xds_namespaced_external_canonical_service( + "canonical.svc", + NS3, + &[na(NW1, "10.10.10.141")], + ), + // Client in NS1 should prefer local over canonical + xds_namespaced_external_service( + "canonical.with.local", + NS1, + &[na(NW1, "10.10.10.150")], + ), + xds_namespaced_external_canonical_service( + "canonical.with.local", + NS2, + &[na(NW1, "10.10.10.151")], + ), with_fqdn( "details.ns2.svc.cluster.remote", xds_service( @@ -1786,7 +1844,7 @@ mod tests { .unwrap() .iter() .map(|(_, addr)| *addr) - .collect_vec() + .collect() } fn kube_fqdn, S2: AsRef>(name: S1, ns: S2) -> String { @@ -1805,6 +1863,11 @@ mod tests { svc } + fn with_canonical(canonical: bool, mut svc: XdsService) -> XdsService { + svc.canonical = canonical; + svc + } + fn xds_service, S2: AsRef>( name: S1, ns: S2, @@ -1830,12 +1893,28 @@ mod tests { } fn xds_external_service>(hostname: S, addrs: &[NetworkAddress]) -> XdsService { + xds_namespaced_external_service(hostname, NS1, addrs) + } + + fn xds_namespaced_external_service, S2: AsRef>( + hostname: S1, + ns: S2, + vips: &[NetworkAddress], + ) -> XdsService { with_fqdn( hostname.as_ref(), - xds_service(hostname.as_ref(), NS1, addrs), + xds_service(hostname.as_ref(), ns.as_ref(), vips), ) } + fn xds_namespaced_external_canonical_service, S2: AsRef>( + hostname: S1, + ns: S2, + vips: &[NetworkAddress], + ) -> XdsService { + with_canonical(true, xds_namespaced_external_service(hostname, ns, vips)) + } + fn xds_workload( name: &str, ns: &str, diff --git a/src/hyper_util.rs b/src/hyper_util.rs index f72127be82..4214a80700 100644 --- a/src/hyper_util.rs +++ b/src/hyper_util.rs @@ -102,21 +102,6 @@ impl hyper::rt::Timer for TokioTimer { } } -struct TokioTimeout { - inner: Pin>>, -} - -impl Future for TokioTimeout -where - T: Future, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { - self.inner.as_mut().poll(context) - } -} - // Use TokioSleep to get tokio::time::Sleep to implement Unpin. // see https://docs.rs/tokio/latest/tokio/time/struct.Sleep.html pub(crate) struct TokioSleep { diff --git a/src/identity/auth.rs b/src/identity/auth.rs index af7c70c545..97a4d56fd9 100644 --- a/src/identity/auth.rs +++ b/src/identity/auth.rs @@ -58,10 +58,7 @@ async fn load_token(path: &PathBuf) -> io::Result> { let t = tokio::fs::read(path).await?; if t.is_empty() { - return Err(io::Error::new( - io::ErrorKind::Other, - "token file exists, but was empty", - )); + return Err(io::Error::other("token file exists, but was empty")); } Ok(t) } diff --git a/src/identity/caclient.rs b/src/identity/caclient.rs index 806a2438ed..f71dc7abf4 100644 --- a/src/identity/caclient.rs +++ b/src/identity/caclient.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::collections::BTreeMap; +use std::sync::Arc; use async_trait::async_trait; use prost_types::Struct; @@ -21,15 +22,24 @@ use tonic::IntoRequest; use tonic::metadata::{AsciiMetadataKey, AsciiMetadataValue}; use tracing::{debug, error, instrument, warn}; +use crate::config::RootCert; use crate::identity::Error; use crate::identity::auth::AuthSource; use crate::identity::manager::Identity; -use crate::tls::{self, TlsGrpcChannel}; +use crate::tls::{self, RootCertManager, TlsGrpcChannel, control_plane_client_config}; use crate::xds::istio::ca::IstioCertificateRequest; use crate::xds::istio::ca::istio_certificate_service_client::IstioCertificateServiceClient; pub struct CaClient { - pub client: IstioCertificateServiceClient, + pub client: std::sync::RwLock>, + /// gRPC endpoint of istiod ( retained to be able to "rebuild_channel") + address: String, + /// alternate hostname for TLS SNI / certificate verification + alt_hostname: Option, + /// Token source for authorization header on gRPC requests + auth: AuthSource, + /// Signals when CA root cert file on disk has changed and TLS channel needs to be rebuilt + root_cert_manager: Option>, pub enable_impersonated_identity: bool, pub secret_ttl: i64, ca_headers: Vec<(AsciiMetadataKey, AsciiMetadataValue)>, @@ -39,27 +49,93 @@ impl CaClient { pub async fn new( address: String, alt_hostname: Option, - cert_provider: Box, + root_cert: RootCert, auth: AuthSource, enable_impersonated_identity: bool, secret_ttl: i64, ca_headers: Vec<(AsciiMetadataKey, AsciiMetadataValue)>, ) -> Result { - let svc = - tls::grpc_connector(address, auth, cert_provider.fetch_cert(alt_hostname).await?)?; - let client = IstioCertificateServiceClient::new(svc); + //Build initial TLS ClientConfig from the current root cert + let client_config = control_plane_client_config(&root_cert, alt_hostname.clone()).await?; + let tls_grpc_channel = tls::grpc_connector(address.clone(), auth.clone(), client_config)?; + let initial_client = IstioCertificateServiceClient::new(tls_grpc_channel); + + // Start the file watcher only for file-based certs. + let root_cert_manager = if let RootCert::File(_) = &root_cert { + Some(RootCertManager::new(root_cert.clone())?) + } else { + None + }; + Ok(CaClient { - client, + client: std::sync::RwLock::new(initial_client), + address, + alt_hostname, + auth, + root_cert_manager, enable_impersonated_identity, secret_ttl, ca_headers, }) } + + /// Rebuilds gRPC client using the current root cert from disk. + /// + /// Reads the cert file again and recreates gRPC channel using the new info stored in `CaClient` + /// + /// Returns an error if: + /// - cert file is missing or malformed + /// - gRPC connector cannot be created. + async fn rebuild_channel( + &self, + ) -> Result, Error> { + let root_cert = self + .root_cert_manager + .as_ref() + .expect("rebuild_channel requires root_cert_manager to be Some") + .root_cert(); + + let client_config = + control_plane_client_config(&root_cert, self.alt_hostname.clone()).await?; + let tls_grpc_channel = + tls::grpc_connector(self.address.clone(), self.auth.clone(), client_config)?; + Ok(IstioCertificateServiceClient::new(tls_grpc_channel)) + } } impl CaClient { #[instrument(skip_all)] async fn fetch_certificate(&self, id: &Identity) -> Result { + // Hot reload check + // If the cert has changed since last call, rebuild the channel before making the request. + // If rebuild fails: + // - log a warning + // - rearm the flag to try again on next call attempt + // - fail through the existing channel + if let Some(ref manager) = self.root_cert_manager + && manager.take_dirty() + { + match self.rebuild_channel().await { + Ok(new_client) => { + // Replace current client + let t = std::time::Instant::now(); + *self.client.write().unwrap() = new_client; + debug!( + write_lock_wait_ms = t.elapsed().as_millis(), + "TLS channel rebuild after CA root cert rotation" + ); + } + Err(e) => { + warn!(error = %e, "failed to rebuild TLS channel after root cert rotation; retaining old channel, will retry on next fetch"); + // rearm so the next fetch_certificate call tries again + manager.mark_dirty(); + } + } + } + + // Clone the client *before* releasing the RwLock so it is not carried across awaits + let client = self.client.read().unwrap().clone(); + let cs = tls::csr::CsrOptions { san: id.to_string(), } @@ -93,8 +169,7 @@ impl CaClient { } }); - let resp = self - .client + let resp = client .clone() .create_certificate(req.into_request()) .await @@ -227,13 +302,13 @@ pub mod mock { let not_after = not_before + self.cfg.cert_lifetime; let mut state = self.state.write().await; + state.fetches.push(id.to_owned()); if state.error { return Err(Error::Spiffe("injected test error".into())); } let certs = state .cert_gen .new_certs(&id.to_owned().into(), not_before, not_after); - state.fetches.push(id.to_owned()); Ok(certs) } @@ -257,13 +332,16 @@ pub mod mock { #[cfg(test)] mod tests { - use std::time::Duration; + use std::{io::Write, time::Duration}; use matches::assert_matches; + use tempfile::NamedTempFile; use crate::{ + config::RootCert, identity::{Error, Identity}, - test_helpers, tls, + test_helpers, + tls::{self, RootCertManager, mock::TEST_ROOT}, xds::istio::ca::IstioCertificateResponse, }; @@ -316,4 +394,27 @@ mod tests { .await; assert_matches!(res, Ok(_)); } + + #[test] + fn root_cert_manager_dirty_flag_api() { + let mut root_file = NamedTempFile::new().unwrap(); + root_file.write_all(TEST_ROOT).unwrap(); + + let manager = RootCertManager::new(RootCert::File(root_file.path().to_path_buf())) + .expect("RootCertManager must be created"); + + assert!(!manager.take_dirty(), "new manager should not be dirty"); + + // simulate file watcher trigger + manager.mark_dirty(); + + assert!( + manager.take_dirty(), + "take_dirty must return true when dirty" + ); + assert!( + !manager.take_dirty(), + "take_dirty must return false after reset" + ); + } } diff --git a/src/identity/manager.rs b/src/identity/manager.rs index 4c7a73a262..e643b598b5 100644 --- a/src/identity/manager.rs +++ b/src/identity/manager.rs @@ -345,6 +345,9 @@ impl Worker { } let (state, refresh_at) = match res { Err(err) => { + // Check if we should retain the existing valid certificate + let existing_cert_info = self.get_existing_cert_info(&id).await; + // Use the next backoff to determine when to retry the fetch and default // to the constant value if the backoff has been reset. In the case of // None we'll use the max_interval to retry the fetch. The max_interval @@ -362,6 +365,7 @@ impl Worker { // Note that we are using a backoff-per-unique-identity-request. This is to prevent issues // when a cert cannot be fetched for Pod A, but that should not stall retries for // pods B, C, and D. + let mut keyed_backoff = match pending_backoffs_by_id.remove(&id) { Some(backoff) => { backoff @@ -382,12 +386,24 @@ impl Worker { } } }; - let retry = keyed_backoff.next_backoff().unwrap_or(CERT_REFRESH_FAILURE_RETRY_DELAY_MAX_INTERVAL); + let retry_delay = keyed_backoff.next_backoff().unwrap_or(CERT_REFRESH_FAILURE_RETRY_DELAY_MAX_INTERVAL); // Store the per-key backoff, we're gonna retry. pending_backoffs_by_id.insert(id.clone(), keyed_backoff); - tracing::debug!(%id, "certificate fetch failed ({err}), retrying in {retry:?}"); - let refresh_at = Instant::now() + retry; - (CertState::Unavailable(err), refresh_at) + let refresh_at = Instant::now() + retry_delay; + + match existing_cert_info { + // we do have a valid existing certificate, schedule retry + Some((valid_cert, cert_expiry_instant)) => { + let effective_refresh_at = std::cmp::min(refresh_at, cert_expiry_instant); + tracing::info!(%id, "certificate renewal failed ({err}); retaining existing valid certificate until {:?}; next retry at {:?}", cert_expiry_instant, effective_refresh_at); + (CertState::Available(valid_cert), effective_refresh_at) + }, + // we don't have a valid existing certificate + None => { + tracing::warn!(%id, "certificate fetch failed ({err}) and no valid existing certificate; will retry in {retry_delay:?} (backoff capped at {CERT_REFRESH_FAILURE_RETRY_DELAY_MAX_INTERVAL:?})"); + (CertState::Unavailable(err), refresh_at) + } + } }, Ok(certs) => { tracing::debug!(%id, "certificate fetch succeeded"); @@ -444,6 +460,40 @@ impl Worker { None => false, } } + + /// Returns existing valid certificate and its expiry time, or None if unavailable/expired + async fn get_existing_cert_info( + &self, + id: &Identity, + ) -> Option<(Arc, Instant)> { + if let Some(cert_channel) = self.certs.lock().await.get(id) { + match &*cert_channel.rx.borrow() { + CertState::Available(cert) => { + let now = self + .time_conv + .instant_to_system_time(std::time::Instant::now()); + if let Some(now) = now { + let cert_expiry = cert.cert.expiration().not_after; + + if now < cert_expiry { + if let Some(expiry_instant) = + self.time_conv.system_time_to_instant(cert_expiry) + { + tracing::debug!(%id, "existing certificate valid until {:?}", cert_expiry); + return Some((cert.clone(), expiry_instant.into())); + } + } else { + tracing::debug!(%id, "existing certificate expired at {:?}", cert_expiry); + } + } + } + _ => { + tracing::debug!(%id, "no valid certificate available to retain"); + } + } + } + None + } } // tokio::select evaluates each pattern before checking the (optional) associated condition. Work @@ -503,9 +553,7 @@ impl SecretManager { .clone() .expect("ca_address must be set to use CA"), cfg.alt_ca_hostname.clone(), - Box::new(tls::ControlPlaneAuthentication::RootCert( - cfg.ca_root_cert.clone(), - )), + cfg.ca_root_cert.clone(), cfg.auth.clone(), cfg.proxy_mode == ProxyMode::Shared, cfg.secret_ttl.as_secs().try_into().unwrap_or(60 * 60 * 24), @@ -559,10 +607,10 @@ impl SecretManager { let rx = st.rx.clone(); drop(certs); - if let Some(existing_pri) = init_pri(&rx) { - if pri > existing_pri { - self.post(Request::Fetch(id.clone(), pri)).await; - } + if let Some(existing_pri) = init_pri(&rx) + && pri > existing_pri + { + self.post(Request::Fetch(id.clone(), pri)).await; } Ok(rx) } @@ -1109,6 +1157,46 @@ mod tests { assert!(sm.fetch_certificate(&id).await.is_ok()); } + #[tokio::test(start_paused = true)] + async fn test_get_existing_cert_info_basic() { + let test = setup(1); + let id = identity("basic-test"); + let info = test.secret_manager.worker.get_existing_cert_info(&id).await; + assert!(info.is_none()); + + // cleanup + test.tear_down().await; + } + + #[tokio::test(start_paused = true)] + async fn test_certificate_retention_on_refresh_failure() { + let mut test = setup(1); + let id = identity("retention-test"); + let start = Instant::now(); + + // get initial certificate + let initial_cert = test.secret_manager.fetch_certificate(&id).await.unwrap(); + let initial_serial = initial_cert.cert.serial().clone(); + let initial_fetch_count = test.caclient.fetches().await.len(); + + // simulate ca errors + test.caclient.set_error(true).await; + assert!(test.caclient.fetch_certificate(&id).await.is_err()); + + // wait for background refresh + tokio::time::sleep_until(start + CERT_HALFLIFE + SEC).await; + + // verify background refresh was attempted and valid certs were retained + let post_refresh_fetch_count = test.caclient.fetches().await.len(); + let current_cert = test.secret_manager.fetch_certificate(&id).await.unwrap(); + let current_serial = current_cert.cert.serial().clone(); + + assert!(post_refresh_fetch_count > initial_fetch_count); + assert_eq!(initial_serial, current_serial); + + test.tear_down().await; + } + #[test] fn identity_from_string() { assert_eq!( diff --git a/src/inpod/linux/config.rs b/src/inpod/linux/config.rs index 3d2e27c468..813fc290f6 100644 --- a/src/inpod/linux/config.rs +++ b/src/inpod/linux/config.rs @@ -148,7 +148,7 @@ impl crate::proxy::SocketFactory for InPodSocketPortReuseFactory { })?; if let Err(e) = sock.set_reuseport(true) { - tracing::warn!("setting set_reuseport failed: {} addr: {}", e, addr); + tracing::warn!("setting set_reuseport failed: {e} addr: {addr}"); } sock.bind(addr)?; diff --git a/src/inpod/linux/netns.rs b/src/inpod/linux/netns.rs index 9a105a15f0..b6ab2d2967 100644 --- a/src/inpod/linux/netns.rs +++ b/src/inpod/linux/netns.rs @@ -13,8 +13,7 @@ // limitations under the License. use nix::sched::{CloneFlags, setns}; -use std::os::fd::OwnedFd; -use std::os::unix::io::AsRawFd; +use std::os::fd::{AsFd, OwnedFd}; use std::sync::Arc; #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] @@ -53,7 +52,7 @@ impl InpodNetns { } pub fn new(cur_netns: Arc, workload_netns: OwnedFd) -> std::io::Result { - let res = nix::sys::stat::fstat(workload_netns.as_raw_fd()) + let res = nix::sys::stat::fstat(workload_netns.as_fd()) .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; let inode = res.st_ino; let dev = res.st_dev; @@ -65,7 +64,7 @@ impl InpodNetns { }), }) } - pub fn workload_netns(&self) -> std::os::fd::BorrowedFd { + pub fn workload_netns(&self) -> std::os::fd::BorrowedFd<'_> { use std::os::fd::AsFd; self.inner.netns.as_fd() } @@ -110,6 +109,7 @@ mod tests { use nix::sched::unshare; use nix::unistd::gettid; use std::assert; + use std::os::fd::AsRawFd; use std::os::fd::OwnedFd; use std::process::Command; diff --git a/src/inpod/linux/test_helpers.rs b/src/inpod/linux/test_helpers.rs index 14bb3d1797..0732e5b6d1 100644 --- a/src/inpod/linux/test_helpers.rs +++ b/src/inpod/linux/test_helpers.rs @@ -37,7 +37,7 @@ use std::os::fd::{AsRawFd, OwnedFd}; use tracing::debug; pub fn uid(i: usize) -> crate::inpod::WorkloadUid { - crate::inpod::WorkloadUid::new(format!("uid{}", i)) + crate::inpod::WorkloadUid::new(format!("uid{i}")) } pub struct Fixture { @@ -138,7 +138,7 @@ pub async fn read_msg(s: &mut UnixStream) -> WorkloadResponse { debug!("read {} bytes", read_amount); let ret = WorkloadResponse::decode(&buf[..read_amount]) - .unwrap_or_else(|_| panic!("failed to decode. read amount: {}", read_amount)); + .unwrap_or_else(|_| panic!("failed to decode. read amount: {read_amount}")); debug!("decoded {:?}", ret); ret diff --git a/src/inpod/linux/workloadmanager.rs b/src/inpod/linux/workloadmanager.rs index b2f89f29ef..37449a6ddf 100644 --- a/src/inpod/linux/workloadmanager.rs +++ b/src/inpod/linux/workloadmanager.rs @@ -401,7 +401,7 @@ pub(crate) mod tests { assert!(e.contains("EOF")); } Ok(()) => {} - Err(e) => panic!("expected error due to EOF {:?}", e), + Err(e) => panic!("expected error due to EOF {e:?}"), } } diff --git a/src/lib.rs b/src/lib.rs index 3fcfab46b9..4b6d50d35a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use once_cell::sync::Lazy; +use std::env; + pub mod admin; pub mod app; pub mod assertions; @@ -41,3 +44,11 @@ pub mod xds; #[cfg(any(test, feature = "testing"))] pub mod test_helpers; + +#[allow(dead_code)] +static PQC_ENABLED: Lazy = + Lazy::new(|| env::var("COMPLIANCE_POLICY").unwrap_or_default() == "pqc"); + +#[allow(dead_code)] +static TLS12_ENABLED: Lazy = + Lazy::new(|| env::var("TLS12_ENABLED").unwrap_or_default() == "true"); diff --git a/src/main.rs b/src/main.rs index f11bafeec6..586a88ba05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,10 @@ extern crate core; +#[cfg(unix)] +use nix::sys::resource::{Resource, getrlimit, setrlimit}; use std::sync::Arc; -use tracing::info; +use tracing::{info, warn}; use ztunnel::*; #[cfg(feature = "jemalloc")] @@ -28,6 +30,31 @@ static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; #[unsafe(export_name = "malloc_conf")] pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0"; +// We use this on Unix systems to increase the number of open file descriptors +// if possible. This is useful for high-load scenarios where the default limit +// is too low, which can lead to droopped connections and other issues: +// see: https://github.com/istio/ztunnel/issues/1585 +#[cfg(unix)] +fn increase_open_files_limit() { + if let Ok((soft_limit, hard_limit)) = getrlimit(Resource::RLIMIT_NOFILE) { + if let Err(e) = setrlimit(Resource::RLIMIT_NOFILE, hard_limit, hard_limit) { + warn!("failed to set file descriptor limits: {e}"); + } else { + info!( + "set file descriptor limits from {} to {}", + soft_limit, hard_limit + ); + } + } else { + warn!("failed to get file descriptor limits"); + } +} + +#[cfg(not(unix))] +fn increase_open_files_limit() { + // No-op on non-Unix platforms +} + fn main() -> anyhow::Result<()> { let _log_flush = telemetry::setup_logging(); @@ -74,6 +101,7 @@ fn version() -> anyhow::Result<()> { async fn proxy(cfg: Arc) -> anyhow::Result<()> { info!("version: {}", version::BuildInfo::new()); + increase_open_files_limit(); info!("running with config: {}", serde_yaml::to_string(&cfg)?); app::build(cfg).await?.wait_termination().await } diff --git a/src/metrics.rs b/src/metrics.rs index d528ccd31f..f6d63c6e1f 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -24,6 +24,7 @@ use tracing_core::field::Value; use crate::identity::Identity; pub mod meta; +pub mod process; pub mod server; use crate::strng::{RichStrng, Strng}; diff --git a/src/metrics/process.rs b/src/metrics/process.rs new file mode 100644 index 0000000000..a20e9d2516 --- /dev/null +++ b/src/metrics/process.rs @@ -0,0 +1,110 @@ +// Copyright Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(unix)] +use nix::sys::resource::{Resource, getrlimit}; +use prometheus_client::collector::Collector; +use prometheus_client::encoding::{DescriptorEncoder, EncodeMetric}; +use prometheus_client::metrics; +#[cfg(unix)] +use tracing::error; + +// Track open fds +#[derive(Debug)] +pub struct ProcessMetrics {} + +#[cfg(unix)] +const FD_PATH: &str = "/dev/fd"; + +impl ProcessMetrics { + pub fn new() -> Self { + Self {} + } + + #[cfg(unix)] + fn encode_open_fds(&self, encoder: &mut DescriptorEncoder) -> Result<(), std::fmt::Error> { + // Count open fds by listing /proc/self/fd + let open_fds = match std::fs::read_dir(FD_PATH) { + Ok(entries) => entries.count() as u64, + Err(e) => { + error!("Failed to read {}: {}", FD_PATH, e); + 0 + } + }; + // exclude the fd used to read the directory + let gauge = metrics::gauge::ConstGauge::new(open_fds - 1); + let metric_encoder = encoder.encode_descriptor( + "process_open_fds", + "Number of open file descriptors", + None, + gauge.metric_type(), + )?; + gauge.encode(metric_encoder)?; + Ok(()) + } + + #[cfg(unix)] + fn encode_max_fds(&self, encoder: &mut DescriptorEncoder) -> Result<(), std::fmt::Error> { + let fds = match getrlimit(Resource::RLIMIT_NOFILE) { + Ok((soft_limit, _)) => soft_limit, + Err(e) => { + error!("Failed to get rlimit: {}", e); + return Ok(()); + } + }; + let gauge = metrics::gauge::ConstGauge::new(fds); + let metric_encoder = encoder.encode_descriptor( + "process_max_fds", + "Maximum number of file descriptors", + None, + gauge.metric_type(), + )?; + gauge.encode(metric_encoder)?; + Ok(()) + } +} + +impl Default for ProcessMetrics { + fn default() -> Self { + Self::new() + } +} + +#[cfg(unix)] +impl Collector for ProcessMetrics { + fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { + match self.encode_open_fds(&mut encoder) { + Ok(_) => {} + Err(e) => { + error!("Failed to encode open fds: {}", e); + return Ok(()); + } + } + match self.encode_max_fds(&mut encoder) { + Ok(_) => {} + Err(e) => { + error!("Failed to encode max fds: {}", e); + } + } + Ok(()) + } +} + +#[cfg(not(unix))] +impl Collector for ProcessMetrics { + fn encode(&self, _encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { + // Process metrics not available on non-Unix platforms + Ok(()) + } +} diff --git a/src/metrics/server.rs b/src/metrics/server.rs index 0c19641e92..55f52d2dec 100644 --- a/src/metrics/server.rs +++ b/src/metrics/server.rs @@ -103,18 +103,12 @@ fn content_type(req: &Request) -> &str { req.headers() .get_all(http::header::ACCEPT) .iter() - .find_map(|v| { - match v - .to_str() - .unwrap_or_default() - .to_lowercase() - .split(";") - .collect::>() - .first() - { - Some(&"application/openmetrics-text") => Some(ContentType::OpenMetrics), - _ => None, - } + .flat_map(|entry| entry.to_str().ok()) + // get_all can return multiple in one line still + .flat_map(|entry| entry.split(",").map(|entry| entry.to_lowercase())) + .find_map(|v| match v.split(";").collect::>().first() { + Some(&"application/openmetrics-text") => Some(ContentType::OpenMetrics), + _ => None, }) .unwrap_or_default() .into() @@ -141,6 +135,16 @@ mod test { "application/openmetrics-text;charset=utf-8;version=1.0.0" ); + let mixed_req = http::Request::builder() + .header("X-Custom-Beep", "boop") + .header("Accept", "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.6,application/openmetrics-text;version=1.0.0;escaping=allow-utf-8;q=0.5,application/openmetrics-text;version=0.0.1;q=0.4,text/plain;version=1.0.0;escaping=allow-utf-8;q=0.3,text/plain;version=0.0.4;q=0.2,*/*;q=0.1") + .body("I would like openmetrics") + .unwrap(); + assert_eq!( + super::content_type(&mixed_req), + "application/openmetrics-text;charset=utf-8;version=1.0.0" + ); + let unsupported_req_accept = http::Request::builder() .header("Accept", "application/json") .body("I would like some json") diff --git a/src/proxy.rs b/src/proxy.rs index 338986ee90..da41414408 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -115,11 +115,10 @@ impl DefaultSocketFactory { .with_time(cfg.keepalive_time) .with_retries(cfg.keepalive_retries) .with_interval(cfg.keepalive_interval); - tracing::trace!( - "set keepalive: {:?}", - socket2::SockRef::from(&s).set_tcp_keepalive(&ka) - ); + let res = socket2::SockRef::from(&s).set_tcp_keepalive(&ka); + tracing::trace!("set keepalive: {:?}", res); } + #[cfg(target_os = "linux")] if cfg.user_timeout_enabled { self.set_tcp_user_timeout(s)?; } @@ -275,6 +274,7 @@ pub(super) struct ProxyInputs { resolver: Option>, // If true, inbound connections created with these inputs will not attempt to preserve the original source IP. pub disable_inbound_freebind: bool, + pub(super) crl_manager: Option>, } #[allow(clippy::too_many_arguments)] @@ -288,6 +288,7 @@ impl ProxyInputs { resolver: Option>, local_workload_information: Arc, disable_inbound_freebind: bool, + crl_manager: Option>, ) -> Arc { Arc::new(Self { cfg, @@ -298,6 +299,7 @@ impl ProxyInputs { local_workload_information, resolver, disable_inbound_freebind, + crl_manager, }) } } @@ -319,7 +321,7 @@ impl Proxy { old_cfg.inbound_addr = inbound.address(); let mut new_pi = (*pi).clone(); new_pi.cfg = Arc::new(old_cfg); - std::mem::swap(&mut pi, &mut Arc::new(new_pi)); + pi = Arc::new(new_pi); warn!("TEST FAKE: new address is {:?}", pi.cfg.inbound_addr); } @@ -386,7 +388,7 @@ impl fmt::Display for AuthorizationRejectionError { match self { Self::NoWorkload => write!(fmt, "workload not found"), Self::WorkloadMismatch => write!(fmt, "workload mismatch"), - Self::ExplicitlyDenied(a, b) => write!(fmt, "explicitly denied by: {}/{}", a, b), + Self::ExplicitlyDenied(a, b) => write!(fmt, "explicitly denied by: {a}/{b}"), Self::NotAllowed => write!(fmt, "allow policies exist, but none allowed"), } } @@ -497,6 +499,9 @@ pub enum Error { #[error("requested service {0} found, but has no IP addresses")] NoIPForService(String), + #[error("no service for target address: {0}")] + NoService(SocketAddr), + #[error( "ip addresses were resolved for workload {0}, but valid dns response had no A/AAAA records" )] @@ -568,6 +573,7 @@ pub struct TraceParent { pub const BAGGAGE_HEADER: &str = "baggage"; pub const TRACEPARENT_HEADER: &str = "traceparent"; +pub const X_FORWARDED_NETWORK_HEADER: &str = "x-forwarded-network"; impl TraceParent { pub fn header(&self) -> hyper::header::HeaderValue { @@ -857,8 +863,8 @@ impl HboneAddress { impl std::fmt::Display for HboneAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - HboneAddress::SocketAddr(addr) => write!(f, "{}", addr), - HboneAddress::SvcHostname(host, port) => write!(f, "{}:{}", host, port), + HboneAddress::SocketAddr(addr) => write!(f, "{addr}"), + HboneAddress::SvcHostname(host, port) => write!(f, "{host}:{port}"), } } } diff --git a/src/proxy/connection_manager.rs b/src/proxy/connection_manager.rs index a02cfa0322..99368fe5a6 100644 --- a/src/proxy/connection_manager.rs +++ b/src/proxy/connection_manager.rs @@ -231,12 +231,12 @@ impl ConnectionManager { // uses a counter to determine if there are other tracked connections or not so it may retain the tx/rx channels when necessary pub fn release(&self, c: &InboundConnection) { let mut drains = self.drains.write().expect("mutex"); - if let Some((k, mut v)) = drains.remove_entry(c) { - if v.count > 1 { - // something else is tracking this connection, decrement count but retain - v.count -= 1; - drains.insert(k, v); - } + if let Some((k, mut v)) = drains.remove_entry(c) + && v.count > 1 + { + // something else is tracking this connection, decrement count but retain + v.count -= 1; + drains.insert(k, v); } } @@ -631,6 +631,7 @@ mod tests { scope: Scope::Global as i32, namespace: auth_namespace.into(), rules: vec![], + dry_run: false, }; let mut auth_xds_name = String::with_capacity(1 + auth_namespace.len() + auth_name.len()); auth_xds_name.push_str(auth_namespace); diff --git a/src/proxy/h2.rs b/src/proxy/h2.rs index b86ac4df59..84f079a6af 100644 --- a/src/proxy/h2.rs +++ b/src/proxy/h2.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::copy; -use bytes::{BufMut, Bytes}; +use bytes::Bytes; use futures_core::ready; use h2::Reason; use std::io::Error; @@ -60,7 +60,13 @@ async fn do_ping_pong( // drive_connection() exits first, no need to error again return; } - log::error!("ping error: {e}"); + // If this was a broken pipe error, then the connection is already closed and we + // we can't ping it. This isn't an error, but more of a race condition we cannot + // catch. + if Some(std::io::ErrorKind::BrokenPipe) != e.get_io().map(|io| io.kind()) { + log::error!("ping error: {e}"); + } + let _ = tx.send(()); return; } @@ -85,7 +91,10 @@ pub struct H2StreamWriteHalf { _dropped: Option, } -pub struct TokioH2Stream(H2Stream); +pub struct TokioH2Stream { + stream: H2Stream, + buf: Bytes, +} struct DropCounter { // Whether the other end of this shared counter has already dropped. @@ -144,7 +153,10 @@ impl Drop for DropCounter { // then the specific implementation will conflict with the generic one. impl TokioH2Stream { pub fn new(stream: H2Stream) -> Self { - Self(stream) + Self { + stream, + buf: Bytes::new(), + } } } @@ -154,24 +166,21 @@ impl tokio::io::AsyncRead for TokioH2Stream { cx: &mut Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, ) -> Poll> { - let pinned = std::pin::Pin::new(&mut self.0.read); - copy::ResizeBufRead::poll_bytes(pinned, cx).map(|r| match r { - Ok(bytes) => { - if buf.remaining() < bytes.len() { - Err(Error::new( - std::io::ErrorKind::Other, - format!( - "kould overflow buffer of with {} remaining", - buf.remaining() - ), - )) - } else { - buf.put(bytes); - Ok(()) - } - } - Err(e) => Err(e), - }) + // Just return the bytes we have left over and don't poll the stream because + // its unclear what to do if there are bytes left over from the previous read, and when we + // poll, we get an error. + if self.buf.is_empty() { + // If we have no unread bytes, we can poll the stream + // and fill self.buf with the bytes we read. + let pinned = std::pin::Pin::new(&mut self.stream.read); + let res = ready!(copy::ResizeBufRead::poll_bytes(pinned, cx))?; + self.buf = res; + } + // Copy as many bytes as we can from self.buf. + let cnt = Ord::min(buf.remaining(), self.buf.len()); + buf.put_slice(&self.buf[..cnt]); + self.buf = self.buf.split_off(cnt); + Poll::Ready(Ok(())) } } @@ -181,7 +190,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - let pinned = std::pin::Pin::new(&mut self.0.write); + let pinned = std::pin::Pin::new(&mut self.stream.write); let buf = Bytes::copy_from_slice(buf); copy::AsyncWriteBuf::poll_write_buf(pinned, cx, buf) } @@ -190,7 +199,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream { mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let pinned = std::pin::Pin::new(&mut self.0.write); + let pinned = std::pin::Pin::new(&mut self.stream.write); copy::AsyncWriteBuf::poll_flush(pinned, cx) } @@ -198,7 +207,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream { mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let pinned = std::pin::Pin::new(&mut self.0.write); + let pinned = std::pin::Pin::new(&mut self.stream.write); copy::AsyncWriteBuf::poll_shutdown(pinned, cx) } } @@ -302,6 +311,6 @@ fn h2_to_io_error(e: h2::Error) -> std::io::Error { if e.is_io() { e.into_io().unwrap() } else { - std::io::Error::new(std::io::ErrorKind::Other, e) + std::io::Error::other(e) } } diff --git a/src/proxy/h2/client.rs b/src/proxy/h2/client.rs index 4c768d537e..37471535fe 100644 --- a/src/proxy/h2/client.rs +++ b/src/proxy/h2/client.rs @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::baggage::{Baggage, parse_baggage_header}; use crate::config; use crate::identity::Identity; -use crate::proxy::Error; +use crate::proxy::{BAGGAGE_HEADER, Error}; use bytes::{Buf, Bytes}; use h2::SendStream; use h2::client::{Connection, SendRequest}; @@ -101,10 +102,10 @@ impl H2ConnectClient { pub async fn send_request( &mut self, req: http::Request<()>, - ) -> Result { + ) -> Result<(crate::proxy::h2::H2Stream, Option), Error> { let cur = self.stream_count.fetch_add(1, Ordering::SeqCst); trace!(current_streams = cur, "sending request"); - let (send, recv) = match self.internal_send(req).await { + let (send, recv, baggage) = match self.internal_send(req).await { Ok(r) => r, Err(e) => { // Request failed, so drop the stream now @@ -123,14 +124,14 @@ impl H2ConnectClient { _dropped: dropped2, }; let h2 = crate::proxy::h2::H2Stream { read, write }; - Ok(h2) + Ok((h2, baggage)) } // helper to allow us to handle errors once async fn internal_send( &mut self, req: Request<()>, - ) -> Result<(SendStream, h2::RecvStream), Error> { + ) -> Result<(SendStream, h2::RecvStream, Option), Error> { // "This function must return `Ready` before `send_request` is called" // We should always be ready though, because we make sure we don't go over the max stream limit out of band. futures::future::poll_fn(|cx| self.sender.poll_ready(cx)).await?; @@ -139,7 +140,8 @@ impl H2ConnectClient { if response.status() != 200 { return Err(Error::HttpStatus(response.status())); } - Ok((stream, response.into_body())) + let baggage = parse_baggage_header(response.headers().get_all(BAGGAGE_HEADER)).ok(); + Ok((stream, response.into_body(), baggage)) } } diff --git a/src/proxy/inbound.rs b/src/proxy/inbound.rs index d61f30c185..f5a5748735 100644 --- a/src/proxy/inbound.rs +++ b/src/proxy/inbound.rs @@ -22,15 +22,21 @@ use tokio::sync::watch; use tracing::{Instrument, debug, error, info, info_span, trace_span}; -use super::{ConnectionResult, Error, HboneAddress, LocalWorkloadInformation, ResponseFlags, util}; -use crate::baggage::parse_baggage_header; +use super::{ + ConnectionResult, ConnectionResultBuilder, Error, HboneAddress, LocalWorkloadInformation, + ResponseFlags, util, +}; +use crate::baggage::{baggage_header_val, parse_baggage_header}; use crate::identity::Identity; use crate::config::Config; use crate::drain::DrainWatcher; use crate::proxy::h2::server::{H2Request, RequestParts}; use crate::proxy::metrics::{ConnectionOpen, Reporter}; -use crate::proxy::{BAGGAGE_HEADER, ProxyInputs, TRACEPARENT_HEADER, TraceParent, metrics}; +use crate::proxy::{ + BAGGAGE_HEADER, ProxyInputs, TRACEPARENT_HEADER, TraceParent, X_FORWARDED_NETWORK_HEADER, + metrics, +}; use crate::rbac::Connection; use crate::socket::to_canonical; use crate::state::service::Service; @@ -83,6 +89,7 @@ impl Inbound { let pi = self.pi.clone(); let acceptor = InboundCertProvider { local_workload: self.pi.local_workload_information.clone(), + crl_manager: self.pi.crl_manager.clone(), }; // Safety: we set nodelay directly in tls_server, so it is safe to convert to a normal listener. @@ -108,7 +115,18 @@ impl Inbound { let dst = to_canonical(raw_socket.local_addr().expect("local_addr available")); let network = pi.cfg.network.clone(); let acceptor = crate::tls::InboundAcceptor::new(acceptor.clone()); + + let socket_labels = metrics::SocketLabels { + reporter: Reporter::destination, + }; + pi.metrics.record_socket_open(&socket_labels); + let metrics_for_socket_close = pi.metrics.clone(); + let serve_client = async move { + let _socket_guard = metrics::SocketCloseGuard::new( + metrics_for_socket_close, + Reporter::destination, + ); let tls = match acceptor.accept(raw_socket).await { Ok(tls) => tls, Err(e) => { @@ -157,7 +175,7 @@ impl Inbound { }; // This is small since it only handles the TLS layer -- the HTTP2 layer is boxed // and measured above. - assertions::size_between_ref(1000, 1500, &serve_client); + assertions::size_between_ref(1000, 1600, &serve_client); tokio::task::spawn(serve_client.in_current_span()); } }; @@ -202,7 +220,9 @@ impl Inbound { // At this point in processing, we never built up full context to log a complete access log. // Instead, just log a minimal error line. metrics::log_early_deny(src, dst, Reporter::destination, e); - if let Err(err) = req.send_error(build_response(code)) { + if let Err(err) = + req.send_error(build_response(code, None, pi.cfg.enable_enhanced_baggage)) + { tracing::warn!("failed to send HTTP response: {err}"); } return; @@ -276,7 +296,9 @@ impl Inbound { Ok(res) => res, Err(InboundFlagError(err, flag, code)) => { ri.result_tracker.record_with_flag(Err(err), flag); - if let Err(err) = req.send_error(build_response(code)) { + if let Err(err) = + req.send_error(build_response(code, None, pi.cfg.enable_enhanced_baggage)) + { tracing::warn!("failed to send HTTP response: {err}"); } return; @@ -293,7 +315,11 @@ impl Inbound { // See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt for more information about the // proxy protocol. let send = req - .send_response(build_response(StatusCode::OK)) + .send_response(build_response( + StatusCode::OK, + Some(ri.destination_workload.as_ref()), + pi.cfg.enable_enhanced_baggage, + )) .and_then(|h2_stream| async { if let Some(TunnelRequest { protocol: Protocol::PROXY, @@ -374,19 +400,30 @@ impl Inbound { }; let for_host = parse_forwarded_host(req); - let baggage = - parse_baggage_header(req.headers().get_all(BAGGAGE_HEADER)).unwrap_or_default(); + let baggage = if pi.cfg.enable_enhanced_baggage { + parse_baggage_header(req.headers().get_all(BAGGAGE_HEADER)).unwrap_or_default() + } else { + Default::default() + }; // We assume it is from gateway if it's a hostname request. // We may need a more explicit indicator in the future. // Note: previously this attempted to check that the src identity was equal to the Gateway; // this check is broken as the gateway only forwards an HBONE request, it doesn't initiate it itself. - let from_gateway = matches!(hbone_addr, HboneAddress::SvcHostname(_, _)); + let from_gateway = req + .headers() + .get(X_FORWARDED_NETWORK_HEADER) + .and_then(|h| h.to_str().ok()) + .map(|s| !s.eq_ignore_ascii_case(&pi.cfg.network)) // If the network is different, it's from a gateway + .unwrap_or(false); + if from_gateway { debug!("request from gateway"); } let source = match from_gateway { - true => None, // we cannot lookup source workload since we don't know the network, see https://github.com/istio/ztunnel/issues/515 + // we cannot lookup source workload since we don't know the network, see https://github.com/istio/ztunnel/issues/515. + // Instead, we will use baggage + true => None, false => { let src_network_addr = NetworkAddress { // we can assume source network is our network because we did not traverse a gateway @@ -398,15 +435,22 @@ impl Inbound { } }; - let derived_source = metrics::DerivedWorkload { - identity: rbac_ctx.conn.src_identity.clone(), - cluster_id: baggage.cluster_id, - region: baggage.region, - zone: baggage.zone, - namespace: baggage.namespace, - app: baggage.service_name, - workload_name: baggage.workload_name, - revision: baggage.revision, + let derived_source = if pi.cfg.enable_enhanced_baggage { + metrics::DerivedWorkload { + identity: rbac_ctx.conn.src_identity.clone(), + cluster_id: baggage.cluster_id, + region: baggage.region, + zone: baggage.zone, + namespace: baggage.namespace, + app: baggage.service_name, + workload_name: baggage.workload_name, + revision: baggage.revision, + } + } else { + metrics::DerivedWorkload { + identity: rbac_ctx.conn.src_identity.clone(), + ..Default::default() + } }; let ds = proxy::guess_inbound_service( &rbac_ctx.conn, @@ -414,7 +458,7 @@ impl Inbound { upstream_service, &destination_workload, ); - let result_tracker = Box::new(metrics::ConnectionResult::new( + let connection_result_builder = ConnectionResultBuilder::new( rbac_ctx.conn.src, // For consistency with outbound logs, report the original destination (with 15008 port) // as dst.addr, and the target address as dst.hbone_addr @@ -425,18 +469,21 @@ impl Inbound { reporter: Reporter::destination, source, derived_source: Some(derived_source), - destination: Some(destination_workload), + destination: Some(destination_workload.clone()), connection_security_policy: metrics::SecurityPolicy::mutual_tls, destination_service: ds, }, pi.metrics.clone(), - )); + ); + + let result_tracker = Box::new(connection_result_builder.build()); Ok(InboundRequest { for_host, rbac_ctx, result_tracker, upstream_addr, tunnel_request, + destination_workload, }) } @@ -447,19 +494,10 @@ impl Inbound { local_workload: &Workload, hbone_host: &Strng, ) -> Result, Error> { - // Validate a service exists for the hostname - let services = state.read().find_service_by_hostname(hbone_host)?; - - services - .iter() - .max_by_key(|s| { - let is_local_namespace = s.namespace == local_workload.namespace; - match is_local_namespace { - true => 1, - false => 0, - } - }) - .cloned() + state + .read() + .services + .get_best_by_host(hbone_host, Some(&local_workload.namespace)) .ok_or_else(|| Error::NoHostname(hbone_host.to_string())) } @@ -662,6 +700,7 @@ struct InboundRequest { result_tracker: Box, upstream_addr: SocketAddr, tunnel_request: Option, + destination_workload: Arc, } /// InboundError represents an error with an associated status code. @@ -683,6 +722,7 @@ impl InboundFlagError { #[derive(Clone)] struct InboundCertProvider { local_workload: Arc, + crl_manager: Option>, } #[async_trait::async_trait] @@ -693,7 +733,7 @@ impl crate::tls::ServerCertProvider for InboundCertProvider { "fetching cert" ); let cert = self.local_workload.fetch_certificate().await?; - Ok(Arc::new(cert.server_config()?)) + Ok(Arc::new(cert.server_config(self.crl_manager.clone())?)) } } @@ -704,9 +744,24 @@ pub fn parse_forwarded_host(req: &T) -> Option { .and_then(proxy::parse_forwarded_host) } -fn build_response(status: StatusCode) -> Response<()> { - Response::builder() - .status(status) +// Second argument is local workload and cluster name +fn build_response( + status: StatusCode, + local_wl: Option<&Workload>, + enable_response_baggage: bool, +) -> Response<()> { + let mut builder = Response::builder().status(status); + + if let Some(local_wl) = local_wl + && enable_response_baggage + { + builder = builder.header( + BAGGAGE_HEADER, + baggage_header_val(&local_wl.baggage(), &local_wl.workload_type), + ) + } + + builder .body(()) .expect("builder with known status code should not fail") } @@ -914,6 +969,7 @@ mod tests { None, local_workload, false, + None, )); let inbound_request = Inbound::build_inbound_request(&pi, conn, &request_parts).await; match want { @@ -962,6 +1018,7 @@ mod tests { waypoint: waypoint.service_attached(), load_balancer: None, ip_families: None, + canonical: true, }); let workloads = vec![ @@ -1048,4 +1105,87 @@ mod tests { }) } } + + #[test] + fn test_build_response_baggage_feature_gate() { + use super::build_response; + use crate::proxy::BAGGAGE_HEADER; + use crate::test_helpers; + use http::StatusCode; + + // Create a test workload + let workload = test_helpers::test_default_workload(); + + // Test with baggage enabled + let mut config_enabled = test_helpers::test_config(); + config_enabled.enable_enhanced_baggage = true; + + let response_enabled = build_response( + StatusCode::OK, + Some(&workload), + config_enabled.enable_enhanced_baggage, + ); + assert!(response_enabled.headers().contains_key(BAGGAGE_HEADER)); + + let baggage_header = response_enabled.headers().get(BAGGAGE_HEADER).unwrap(); + let baggage_value = baggage_header.to_str().unwrap(); + // Check that baggage header contains cluster_id from the test workload + assert!(baggage_value.contains("k8s.cluster.name=Kubernetes")); + + // Test with baggage disabled + let mut config_disabled = test_helpers::test_config(); + config_disabled.enable_enhanced_baggage = false; + + let response_disabled = build_response( + StatusCode::OK, + Some(&workload), + config_disabled.enable_enhanced_baggage, + ); + assert!(!response_disabled.headers().contains_key(BAGGAGE_HEADER)); + + // Test with None workload (should not have baggage regardless of config) + let response_no_workload = + build_response(StatusCode::OK, None, config_enabled.enable_enhanced_baggage); + assert!(!response_no_workload.headers().contains_key(BAGGAGE_HEADER)); + } + + #[test] + fn test_incoming_baggage_parsing_feature_gate() { + use crate::baggage::{Baggage, parse_baggage_header}; + use crate::proxy::BAGGAGE_HEADER; + use crate::test_helpers; + use http::{HeaderMap, HeaderValue}; + + // Create mock baggage header + let mut headers = HeaderMap::new(); + headers.insert(BAGGAGE_HEADER, HeaderValue::from_str("k8s.cluster.name=test-cluster,k8s.namespace.name=test-ns,k8s.deployment.name=test-app").unwrap()); + + // Test with baggage enabled + let config_enabled = test_helpers::test_config(); + assert!(config_enabled.enable_enhanced_baggage); // Default should be true + + let baggage_enabled = if config_enabled.enable_enhanced_baggage { + parse_baggage_header(headers.get_all(BAGGAGE_HEADER)).unwrap_or_default() + } else { + Baggage::default() + }; + + assert_eq!(baggage_enabled.cluster_id, Some("test-cluster".into())); + assert_eq!(baggage_enabled.namespace, Some("test-ns".into())); + assert_eq!(baggage_enabled.workload_name, Some("test-app".into())); + + // Test with baggage disabled + let mut config_disabled = test_helpers::test_config(); + config_disabled.enable_enhanced_baggage = false; + + let baggage_disabled = if config_disabled.enable_enhanced_baggage { + parse_baggage_header(headers.get_all(BAGGAGE_HEADER)).unwrap_or_default() + } else { + Baggage::default() + }; + + assert_eq!(baggage_disabled.cluster_id, None); + assert_eq!(baggage_disabled.namespace, None); + assert_eq!(baggage_disabled.workload_name, None); + } } diff --git a/src/proxy/inbound_passthrough.rs b/src/proxy/inbound_passthrough.rs index 5fabdab906..7de178f7ce 100644 --- a/src/proxy/inbound_passthrough.rs +++ b/src/proxy/inbound_passthrough.rs @@ -43,10 +43,11 @@ impl InboundPassthrough { pi: Arc, drain: DrainWatcher, ) -> Result { - let listener = pi + let mut listener = pi .socket_factory .tcp_bind(pi.cfg.inbound_plaintext_addr) .map_err(|e| Error::Bind(pi.cfg.inbound_plaintext_addr, e))?; + listener.set_socket_options(Some(pi.cfg.socket_config)); let enable_orig_src = super::maybe_set_transparent(&pi, &listener)?; @@ -76,7 +77,17 @@ impl InboundPassthrough { let pi = self.pi.clone(); match socket { Ok((stream, remote)) => { + let socket_labels = metrics::SocketLabels { + reporter: Reporter::destination, + }; + pi.metrics.record_socket_open(&socket_labels); + + let metrics_for_socket_close = pi.metrics.clone(); let serve_client = async move { + let _socket_guard = metrics::SocketCloseGuard::new( + metrics_for_socket_close, + Reporter::destination, + ); debug!(component="inbound passthrough", "connection started"); // Since this task is spawned, make sure we are guaranteed to terminate tokio::select! { @@ -178,21 +189,24 @@ impl InboundPassthrough { upstream_services, &upstream_workload, ); - let result_tracker = Box::new(metrics::ConnectionResult::new( - source_addr, - dest_addr, - None, - start, - metrics::ConnectionOpen { - reporter: Reporter::destination, - source: source_workload, - derived_source: Some(derived_source), - destination: Some(upstream_workload), - connection_security_policy: metrics::SecurityPolicy::unknown, - destination_service: ds, - }, - pi.metrics.clone(), - )); + let result_tracker = Box::new( + metrics::ConnectionResultBuilder::new( + source_addr, + dest_addr, + None, + start, + metrics::ConnectionOpen { + reporter: Reporter::destination, + source: source_workload, + derived_source: Some(derived_source), + destination: Some(upstream_workload), + connection_security_policy: metrics::SecurityPolicy::unknown, + destination_service: ds, + }, + pi.metrics.clone(), + ) + .build(), + ); let mut conn_guard = match pi .connection_manager diff --git a/src/proxy/metrics.rs b/src/proxy/metrics.rs index f912c246a9..bb2ed17f4e 100644 --- a/src/proxy/metrics.rs +++ b/src/proxy/metrics.rs @@ -23,6 +23,7 @@ use prometheus_client::encoding::{ }; use prometheus_client::metrics::counter::{Atomic, Counter}; use prometheus_client::metrics::family::Family; +use prometheus_client::metrics::gauge::Gauge; use prometheus_client::registry::Registry; use tracing::event; @@ -40,6 +41,8 @@ use crate::strng::{RichStrng, Strng}; pub struct Metrics { pub connection_opens: Family, pub connection_close: Family, + pub connection_failures: Family, + pub open_sockets: Family, pub received_bytes: Family, pub sent_bytes: Family, @@ -71,6 +74,14 @@ pub enum ResponseFlags { AuthorizationPolicyDenied, // connection denied because we could not establish an upstream connection ConnectionFailure, + // TLS handshake failure + TlsFailure, + // HTTP/2 handshake failure + Http2HandshakeFailure, + // Network policy blocking connection + NetworkPolicyError, + // Identity/certificate error + IdentityError, } impl EncodeLabelValue for ResponseFlags { @@ -79,6 +90,10 @@ impl EncodeLabelValue for ResponseFlags { ResponseFlags::None => writer.write_str("-"), ResponseFlags::AuthorizationPolicyDenied => writer.write_str("DENY"), ResponseFlags::ConnectionFailure => writer.write_str("CONNECT"), + ResponseFlags::TlsFailure => writer.write_str("TLS_FAILURE"), + ResponseFlags::Http2HandshakeFailure => writer.write_str("H2_HANDSHAKE_FAILURE"), + ResponseFlags::NetworkPolicyError => writer.write_str("NETWORK_POLICY"), + ResponseFlags::IdentityError => writer.write_str("IDENTITY_ERROR"), } } } @@ -183,6 +198,26 @@ impl CommonTrafficLabels { self.destination_service_namespace = w.namespace.clone().into(); self } + + fn with_derived_destination(mut self, w: Option<&DerivedWorkload>) -> Self { + let Some(w) = w else { return self }; + self.destination_workload = w.workload_name.clone().into(); + self.destination_canonical_service = w.app.clone().into(); + self.destination_canonical_revision = w.revision.clone().into(); + self.destination_workload_namespace = w.namespace.clone().into(); + self.destination_app = w.workload_name.clone().into(); + self.destination_version = w.revision.clone().into(); + self.destination_cluster = w.cluster_id.clone().into(); + // This is the identity from the TLS handshake; this is the most trustworthy source so use it + self.destination_principal = w.identity.clone().into(); + + let mut local = self.locality.0.unwrap_or_default(); + local.destination_region = w.region.clone().into(); + local.destination_zone = w.zone.clone().into(); + self.locality = OptionallyEncode(Some(local)); + + self + } } impl From for CommonTrafficLabels { @@ -202,6 +237,12 @@ impl From for CommonTrafficLabels { } } +/// Minimal labels for socket metrics (without direction) +#[derive(Clone, Hash, Default, Debug, PartialEq, Eq, EncodeLabelSet)] +pub struct SocketLabels { + pub reporter: Reporter, +} + #[derive(Clone, Hash, Default, Debug, PartialEq, Eq, EncodeLabelSet)] pub struct CommonTrafficLabels { reporter: Reporter, @@ -242,7 +283,7 @@ pub struct CommonTrafficLabels { #[derive(Clone, Hash, Default, Debug, PartialEq, Eq)] struct OptionallyEncode(Option); impl EncodeLabelSet for OptionallyEncode { - fn encode(&self, encoder: LabelSetEncoder) -> Result<(), std::fmt::Error> { + fn encode(&self, encoder: &mut LabelSetEncoder) -> Result<(), std::fmt::Error> { match &self.0 { None => Ok(()), Some(ll) => ll.encode(encoder), @@ -330,43 +371,92 @@ impl Metrics { on_demand_dns.clone(), ); + let connection_failures = Family::default(); + registry.register( + "tcp_connections_failed", + "The total number of TCP connections that failed to establish (unstable)", + connection_failures.clone(), + ); + + let open_sockets = Family::default(); + registry.register( + "tcp_sockets_open", + "The current number of open TCP sockets (unstable)", + open_sockets.clone(), + ); + Self { connection_opens, connection_close, received_bytes, sent_bytes, on_demand_dns, + connection_failures, + open_sockets, } } + + pub fn record_socket_open(&self, labels: &SocketLabels) { + self.open_sockets.get_or_create(labels).inc(); + } + + pub fn record_socket_close(&self, labels: &SocketLabels) { + self.open_sockets.get_or_create(labels).dec(); + } +} + +/// Guard to ensure socket close is recorded even if task is cancelled +/// This should be created at the start of an async block that handles a socket +/// Stores only the minimal information needed to reconstruct labels, avoiding +/// cloning the large CommonTrafficLabels struct +pub struct SocketCloseGuard { + metrics: Arc, + reporter: Reporter, +} + +impl Drop for SocketCloseGuard { + fn drop(&mut self) { + let labels = SocketLabels { + reporter: self.reporter, + }; + self.metrics.record_socket_close(&labels); + } +} + +impl SocketCloseGuard { + /// Create a new socket close guard + pub fn new(metrics: Arc, reporter: Reporter) -> Self { + Self { metrics, reporter } + } } #[derive(Debug)] /// ConnectionResult abstracts recording a metric and emitting an access log upon a connection completion pub struct ConnectionResult { // Src address and name - src: (SocketAddr, Option), + pub(crate) src: (SocketAddr, Option), // Dst address and name - dst: (SocketAddr, Option), - hbone_target: Option, - start: Instant, + pub(crate) dst: (SocketAddr, Option), + pub(crate) hbone_target: Option, + pub(crate) start: Instant, // TODO: storing CommonTrafficLabels adds ~600 bytes retained throughout a connection life time. // We can pre-fetch the metrics we need at initialization instead of storing this, then keep a more // efficient representation for the fields we need to log. Ideally, this would even be optional // in case logs were disabled. - tl: CommonTrafficLabels, - metrics: Arc, + pub(crate) tl: CommonTrafficLabels, + pub(crate) metrics: Arc, // sent records the number of bytes sent on this connection - sent: AtomicU64, + pub(crate) sent: AtomicU64, // sent_metric records the number of bytes sent on this connection to the aggregated metric counter - sent_metric: Counter, + pub(crate) sent_metric: Counter, // recv records the number of bytes received on this connection - recv: AtomicU64, + pub(crate) recv: AtomicU64, // recv_metric records the number of bytes received on this connection to the aggregated metric counter - recv_metric: Counter, + pub(crate) recv_metric: Counter, // Have we recorded yet? - recorded: bool, + pub(crate) recorded: bool, } // log_early_deny allows logging a connection is denied before we have enough information to emit proper @@ -391,12 +481,25 @@ pub fn log_early_deny( "inbound" }, - error = format!("{}", err), + error = format!("{err}"), "connection failed" ); } +/// Logs a message at INFO level if AUTHZ_POLICY_INFO_LOGGING is set to "true", +/// otherwise logs at DEBUG level. +#[macro_export] +macro_rules! authpol_log { + ($($arg:tt)+) => { + if *$crate::config::AUTHZ_POLICY_INFO_LOGGING { + tracing::info!($($arg)+); + } else { + tracing::debug!($($arg)+); + } + }; +} + macro_rules! access_log { ($res:expr, $($fields:tt)*) => { let err = $res.as_ref().err().map(|e| e.to_string()); @@ -423,7 +526,17 @@ macro_rules! access_log { } }; } -impl ConnectionResult { + +pub struct ConnectionResultBuilder { + src: (SocketAddr, Option), + dst: (SocketAddr, Option), + hbone_target: Option, + start: Instant, + tl: CommonTrafficLabels, + metrics: Arc, +} + +impl ConnectionResultBuilder { pub fn new( src: SocketAddr, dst: SocketAddr, @@ -441,30 +554,61 @@ impl ConnectionResult { conn.destination.as_ref().map(|wl| wl.name.clone().into()), ); let tl = CommonTrafficLabels::from(conn); - metrics.connection_opens.get_or_create(&tl).inc(); - - let mtls = tl.connection_security_policy == SecurityPolicy::mutual_tls; src.1 = src.1.or(tl.source_canonical_service.clone().inner()); dst.1 = dst.1.or(tl.destination_canonical_service.clone().inner()); + Self { + src, + dst, + hbone_target, + start, + tl, + metrics, + } + } + + pub fn with_derived_source(mut self, w: &DerivedWorkload) -> Self { + self.tl = self.tl.with_derived_source(Some(w)); + self.src.1 = w.workload_name.clone().map(RichStrng::from); + self + } + + pub fn with_derived_destination(mut self, w: &DerivedWorkload) -> Self { + self.tl = self.tl.with_derived_destination(Some(w)); + self.dst.1 = w.workload_name.clone().map(RichStrng::from); + self + } + + pub fn build(self) -> ConnectionResult { + // Grab the metrics with our labels now, so we don't need to fetch them each time. + // The inner metric is an Arc so clone is fine/cheap. + // With the raw Counter, we increment is a simple atomic add operation (~1ns). + // Fetching the metric itself is ~300ns; fast, but we call it on each read/write so it would + // add up. + let sent_metric = self.metrics.sent_bytes.get_or_create(&self.tl).clone(); + let recv_metric = self.metrics.received_bytes.get_or_create(&self.tl).clone(); + let sent = atomic::AtomicU64::new(0); + let recv = atomic::AtomicU64::new(0); + + let mtls = self.tl.connection_security_policy == SecurityPolicy::mutual_tls; event!( target: "access", parent: None, tracing::Level::DEBUG, - src.addr = %src.0, - src.workload = src.1.as_deref().map(to_value), - src.namespace = tl.source_workload_namespace.to_value(), - src.identity = tl.source_principal.as_ref().filter(|_| mtls).map(to_value_owned), + src.addr = %self.src.0, + src.workload = self.src.1.as_deref().map(to_value), + src.namespace = self.tl.source_workload_namespace.to_value(), + src.identity = self.tl.source_principal.as_ref().filter(|_| mtls).map(to_value_owned), - dst.addr = %dst.0, - dst.hbone_addr = hbone_target.as_ref().map(display), - dst.service = tl.destination_service.to_value(), - dst.workload = dst.1.as_deref().map(to_value), - dst.namespace = tl.destination_workload_namespace.to_value(), - dst.identity = tl.destination_principal.as_ref().filter(|_| mtls).map(to_value_owned), + dst.addr = %self.dst.0, + dst.hbone_addr = self.hbone_target.as_ref().map(display), + dst.service = self.tl.destination_service.to_value(), + dst.workload = self.dst.1.as_deref().map(to_value), + dst.namespace = self.tl.destination_workload_namespace.to_value(), + dst.identity = self.tl.destination_principal.as_ref().filter(|_| mtls).map(to_value_owned), - direction = if tl.reporter == Reporter::source { + direction = if self.tl.reporter == Reporter::source { "outbound" } else { "inbound" @@ -472,23 +616,15 @@ impl ConnectionResult { "connection opened" ); - // Grab the metrics with our labels now, so we don't need to fetch them each time. - // The inner metric is an Arc so clone is fine/cheap. - // With the raw Counter, we increment is a simple atomic add operation (~1ns). - // Fetching the metric itself is ~300ns; fast, but we call it on each read/write so it would - // add up. - let sent_metric = metrics.sent_bytes.get_or_create(&tl).clone(); - let recv_metric = metrics.received_bytes.get_or_create(&tl).clone(); - let sent = atomic::AtomicU64::new(0); - let recv = atomic::AtomicU64::new(0); - Self { - src, - dst, - hbone_target, - start, - tl, - metrics, + self.metrics.connection_opens.get_or_create(&self.tl).inc(); + ConnectionResult { + src: self.src, + dst: self.dst, + hbone_target: self.hbone_target, + start: self.start, + tl: self.tl, + metrics: self.metrics, sent, sent_metric, recv, @@ -496,7 +632,9 @@ impl ConnectionResult { recorded: false, } } +} +impl ConnectionResult { pub fn increment_send(&self, res: u64) { self.sent.inc_by(res); self.sent_metric.inc_by(res); @@ -508,7 +646,7 @@ impl ConnectionResult { } // Record our final result, with more details as a response flag. - pub fn record_with_flag( + pub fn record_with_flag( mut self, res: Result<(), E>, flag: ResponseFlags, @@ -518,12 +656,44 @@ impl ConnectionResult { } // Record our final result. - pub fn record(mut self, res: Result<(), E>) { + pub fn record(mut self, res: Result<(), E>) { + // If no specific flag was set and we have an error, try to infer the failure reason + if self.tl.response_flags == ResponseFlags::None + && let Err(ref err) = res + { + self.tl.response_flags = Self::extract_failure_reason(err); + } self.record_internal(res) } + // Extract failure reason from error type using downcasting + fn extract_failure_reason(err: &E) -> ResponseFlags { + use std::any::Any; + + // Try to downcast the error itself to proxy::Error + if let Some(proxy_err) = (err as &dyn Any).downcast_ref::() { + return match proxy_err { + proxy::Error::Tls(_) => ResponseFlags::TlsFailure, + proxy::Error::Http2Handshake(_) | proxy::Error::H2(_) => { + ResponseFlags::Http2HandshakeFailure + } + proxy::Error::MaybeHBONENetworkPolicyError(_) => ResponseFlags::NetworkPolicyError, + proxy::Error::Identity(_) => ResponseFlags::IdentityError, + proxy::Error::AuthorizationPolicyRejection(_) + | proxy::Error::AuthorizationPolicyLateRejection => { + ResponseFlags::AuthorizationPolicyDenied + } + proxy::Error::ConnectionFailed(_) => ResponseFlags::ConnectionFailure, + _ => ResponseFlags::ConnectionFailure, + }; + } + + // Default to generic connection failure if we can't identify the error type + ResponseFlags::ConnectionFailure + } + // Internal-only function that takes `&mut` to facilitate Drop. Public consumers must use consuming functions. - fn record_internal(&mut self, res: Result<(), E>) { + fn record_internal(&mut self, res: Result<(), E>) { debug_assert!(!self.recorded, "record called multiple times"); if self.recorded { return; @@ -534,6 +704,18 @@ impl ConnectionResult { // Unconditionally record the connection was closed self.metrics.connection_close.get_or_create(tl).inc(); + if matches!( + tl.response_flags, + ResponseFlags::ConnectionFailure + | ResponseFlags::AuthorizationPolicyDenied + | ResponseFlags::TlsFailure + | ResponseFlags::Http2HandshakeFailure + | ResponseFlags::NetworkPolicyError + | ResponseFlags::IdentityError + ) { + self.metrics.connection_failures.get_or_create(tl).inc(); + } + // Unconditionally write out an access log let mtls = tl.connection_security_policy == SecurityPolicy::mutual_tls; let bytes = ( diff --git a/src/proxy/outbound.rs b/src/proxy/outbound.rs index b093c34b79..31745820e5 100644 --- a/src/proxy/outbound.rs +++ b/src/proxy/outbound.rs @@ -25,20 +25,24 @@ use tokio::sync::watch; use tracing::{Instrument, debug, error, info, info_span, trace_span}; use crate::identity::Identity; +use crate::strng::Strng; use crate::proxy::metrics::Reporter; use crate::proxy::{ - BAGGAGE_HEADER, Error, HboneAddress, ProxyInputs, TRACEPARENT_HEADER, TraceParent, util, + BAGGAGE_HEADER, Error, HboneAddress, ProxyInputs, TRACEPARENT_HEADER, TraceParent, + X_FORWARDED_NETWORK_HEADER, util, }; -use crate::proxy::{ConnectionOpen, ConnectionResult, DerivedWorkload, metrics}; +use crate::proxy::{ConnectionOpen, ConnectionResultBuilder, DerivedWorkload, metrics}; +use crate::baggage::{self, Baggage}; use crate::drain::DrainWatcher; use crate::drain::run_with_drain; use crate::proxy::h2::{H2Stream, client::WorkloadKey}; -use crate::state::ServiceResolutionMode; -use crate::state::service::ServiceDescription; +use crate::state::service::{LoadBalancerMode, Service, ServiceDescription}; use crate::state::workload::OutboundProtocol; use crate::state::workload::{InboundProtocol, NetworkAddress, Workload, address::Address}; +use crate::state::{ServiceResolutionMode, Upstream}; +use crate::tls::identity_from_connection; use crate::{assertions, copy, proxy, socket}; use super::h2::TokioH2Stream; @@ -51,11 +55,12 @@ pub struct Outbound { impl Outbound { pub(super) async fn new(pi: Arc, drain: DrainWatcher) -> Result { - let listener = pi + let mut listener = pi .socket_factory .tcp_bind(pi.cfg.outbound_addr) .map_err(|e| Error::Bind(pi.cfg.outbound_addr, e))?; let transparent = super::maybe_set_transparent(&pi, &listener)?; + listener.set_socket_options(Some(pi.cfg.socket_config)); info!( address=%listener.local_addr(), @@ -90,6 +95,11 @@ impl Outbound { let mut force_shutdown = force_shutdown.clone(); match socket { Ok((stream, _remote)) => { + let socket_labels = metrics::SocketLabels { + reporter: Reporter::source, + }; + self.pi.metrics.record_socket_open(&socket_labels); + let mut oc = OutboundConnection { pi: self.pi.clone(), id: TraceParent::new(), @@ -97,7 +107,12 @@ impl Outbound { hbone_port: self.pi.cfg.inbound_addr.port(), }; let span = info_span!("outbound", id=%oc.id); + let metrics_for_socket_close = self.pi.metrics.clone(); let serve_outbound_connection = async move { + let _socket_guard = metrics::SocketCloseGuard::new( + metrics_for_socket_close, + Reporter::source, + ); debug!(component="outbound", "connection started"); // Since this task is spawned, make sure we are guaranteed to terminate tokio::select! { @@ -143,8 +158,18 @@ pub(super) struct OutboundConnection { impl OutboundConnection { async fn proxy(&mut self, source_stream: TcpStream) { - let source_addr = - socket::to_canonical(source_stream.peer_addr().expect("must receive peer addr")); + let peer = match source_stream.peer_addr() { + Ok(addr) => addr, + Err(e) => { + debug!( + component = "outbound", + "failed to get peer address, dropping connection: {}", e + ); + return; + } + }; + + let source_addr = socket::to_canonical(peer); let dst_addr = socket::orig_dst_addr_or_default(&source_stream); self.proxy_to(source_stream, source_addr, dst_addr).await; } @@ -187,7 +212,7 @@ impl OutboundConnection { let metrics = self.pi.metrics.clone(); let hbone_target = req.hbone_target_destination.clone(); - let result_tracker = Box::new(ConnectionResult::new( + let connection_result_builder = Box::new(ConnectionResultBuilder::new( source_addr, req.actual_destination, hbone_target, @@ -196,27 +221,26 @@ impl OutboundConnection { metrics, )); - let res = match req.protocol { + match req.protocol { OutboundProtocol::DOUBLEHBONE => { // We box this since its not a common path and it would make the future really big. Box::pin(self.proxy_to_double_hbone( source_stream, source_addr, &req, - &result_tracker, + connection_result_builder, )) .await } OutboundProtocol::HBONE => { - self.proxy_to_hbone(source_stream, source_addr, &req, &result_tracker) + self.proxy_to_hbone(source_stream, source_addr, &req, connection_result_builder) .await } OutboundProtocol::TCP => { - self.proxy_to_tcp(source_stream, &req, &result_tracker) + self.proxy_to_tcp(source_stream, &req, connection_result_builder) .await } }; - result_tracker.record(res) } async fn proxy_to_double_hbone( @@ -224,51 +248,87 @@ impl OutboundConnection { stream: TcpStream, remote_addr: SocketAddr, req: &Request, - connection_stats: &ConnectionResult, - ) -> Result<(), Error> { - // Create the outer HBONE stream - let upgraded = Box::pin(self.send_hbone_request(remote_addr, req)).await?; - // Wrap upgraded to implement tokio's Async{Write,Read} - let upgraded = TokioH2Stream::new(upgraded); - - // For the inner one, we do it manually to avoid connection pooling. - // Otherwise, we would only ever reach one workload in the remote cluster. - // We also need to abort tasks the right way to get graceful terminations. - let wl_key = WorkloadKey { - src_id: req.source.identity(), - dst_id: req.final_sans.clone(), - src: remote_addr.ip(), - dst: req.actual_destination, - }; - - // Fetch certs and establish inner TLS connection. - let cert = self - .pi - .local_workload_information - .fetch_certificate() - .await?; - let connector = cert.outbound_connector(wl_key.dst_id.clone())?; - let tls_stream = connector.connect(upgraded).await?; + mut connection_stats_builder: Box, + ) { + // async move block allows use of ? operator + let res = (async move { + // Create the outer HBONE stream + let (upgraded, _) = Box::pin(self.send_hbone_request(remote_addr, req)).await?; + // Wrap upgraded to implement tokio's Async{Write,Read} + let upgraded = TokioH2Stream::new(upgraded); + + // For the inner one, we do it manually to avoid connection pooling. + // Otherwise, we would only ever reach one workload in the remote cluster. + // We also need to abort tasks the right way to get graceful terminations. + let wl_key = WorkloadKey { + src_id: req.source.identity(), + dst_id: req.final_sans.clone(), + src: remote_addr.ip(), + dst: req.actual_destination, + }; - // Spawn inner CONNECT tunnel - let (drain_tx, drain_rx) = tokio::sync::watch::channel(false); - let mut sender = - super::h2::client::spawn_connection(self.pi.cfg.clone(), tls_stream, drain_rx, wl_key) + // Fetch certs and establish inner TLS connection. + let cert = self + .pi + .local_workload_information + .fetch_certificate() .await?; - let http_request = self.create_hbone_request(remote_addr, req); - let inner_upgraded = sender.send_request(http_request).await?; - - // Proxy - let res = copy::copy_bidirectional( - copy::TcpStreamSplitter(stream), - inner_upgraded, - connection_stats, - ) + let connector = cert.outbound_connector(wl_key.dst_id.clone())?; + let tls_stream = connector.connect(upgraded).await?; + let (_, ssl) = tls_stream.get_ref(); + let peer_identity = identity_from_connection(ssl); + + // Spawn inner CONNECT tunnel + let (drain_tx, drain_rx) = tokio::sync::watch::channel(false); + let mut sender = super::h2::client::spawn_connection( + self.pi.cfg.clone(), + tls_stream, + drain_rx, + wl_key, + ) + .await?; + let origin_network = &self.pi.cfg.network; + let http_request = self.create_hbone_request(remote_addr, req, Some(origin_network)); + let (inner_upgraded, baggage) = sender.send_request(http_request).await?; + + // Proxy + let derived_workload = baggage.map(|baggage| DerivedWorkload { + workload_name: baggage.workload_name, + app: baggage.service_name, + namespace: baggage.namespace, + identity: peer_identity, + cluster_id: baggage.cluster_id, + region: baggage.region, + zone: baggage.zone, + revision: baggage.revision, + }); + Result::<_, Error>::Ok((derived_workload, drain_tx, inner_upgraded)) + }) .await; - let _ = drain_tx.send(true); + match res { + Err(e) => { + let connection_stats = connection_stats_builder.build(); + connection_stats.record(Err(e)); + } + Ok((derived_workload, drain_tx, inner_upgraded)) => { + if let Some(derived_workload) = derived_workload { + *connection_stats_builder = + connection_stats_builder.with_derived_destination(&derived_workload); + } + + let connection_stats = connection_stats_builder.build(); + let res = copy::copy_bidirectional( + copy::TcpStreamSplitter(stream), + inner_upgraded, + &connection_stats, + ) + .await; + let _ = drain_tx.send(true); - res + connection_stats.record(res); + } + } } async fn proxy_to_hbone( @@ -276,18 +336,25 @@ impl OutboundConnection { stream: TcpStream, remote_addr: SocketAddr, req: &Request, - connection_stats: &ConnectionResult, - ) -> Result<(), Error> { - let upgraded: H2Stream = Box::pin(self.send_hbone_request(remote_addr, req)).await?; - copy::copy_bidirectional(copy::TcpStreamSplitter(stream), upgraded, connection_stats).await + connection_stats_builder: Box, + ) { + let connection_stats = Box::new(connection_stats_builder.build()); + let res = (async { + let (upgraded, _) = Box::pin(self.send_hbone_request(remote_addr, req)).await?; + copy::copy_bidirectional(copy::TcpStreamSplitter(stream), upgraded, &connection_stats) + .await + }) + .await; + connection_stats.record(res); } fn create_hbone_request( - &mut self, + &self, remote_addr: SocketAddr, req: &Request, + origin_network: Option<&Strng>, ) -> http::Request<()> { - http::Request::builder() + let mut builder = http::Request::builder() .uri( req.hbone_target_destination .as_ref() @@ -296,22 +363,34 @@ impl OutboundConnection { ) .method(hyper::Method::CONNECT) .version(hyper::Version::HTTP_2) - .header(BAGGAGE_HEADER, baggage(req, self.pi.cfg.cluster_id.clone())) + .header(BAGGAGE_HEADER, baggage(req)) .header( FORWARDED, build_forwarded(remote_addr, &req.intended_destination_service), ) - .header(TRACEPARENT_HEADER, self.id.header()) + .header(TRACEPARENT_HEADER, self.id.header()); + + // Add x-istio-origin-network header for inner CONNECT requests in double HBONE + if let Some(network) = origin_network { + builder = builder.header(X_FORWARDED_NETWORK_HEADER, network.as_str()); + } + + builder .body(()) .expect("builder with known status code should not fail") } + /// returns upgraded stream and peer's baggage async fn send_hbone_request( &mut self, remote_addr: SocketAddr, req: &Request, - ) -> Result { - let request = self.create_hbone_request(remote_addr, req); + ) -> Result<(H2Stream, Option), Error> { + // This is the single cluster/single-HBONE codepath (and also the outer tunnel + // for double HBONE). We don't need the x-istio-origin-network header here because: + // - For single HBONE: both source and destination are in the same network + // - For double HBONE outer: the gateway doesn't need origin network info + let request = self.create_hbone_request(remote_addr, req, None); let pool_key = Box::new(WorkloadKey { src_id: req.source.identity(), // Clone here shouldn't be needed ideally, we could just take ownership of Request. @@ -319,32 +398,38 @@ impl OutboundConnection { src: remote_addr.ip(), dst: req.actual_destination, }); - let upgraded = Box::pin(self.pool.send_request_pooled(&pool_key, request)) + let (upgraded, baggage) = Box::pin(self.pool.send_request_pooled(&pool_key, request)) .instrument(trace_span!("outbound connect")) .await?; - Ok(upgraded) + Ok((upgraded, baggage)) } async fn proxy_to_tcp( &mut self, stream: TcpStream, req: &Request, - connection_stats: &ConnectionResult, - ) -> Result<(), Error> { - let outbound = super::freebind_connect( - None, // No need to spoof source IP on outbound - req.actual_destination, - self.pi.socket_factory.as_ref(), - ) - .await?; + connection_stats_builder: Box, + ) { + let connection_stats = Box::new(connection_stats_builder.build()); - // Proxying data between downstream and upstream - copy::copy_bidirectional( - copy::TcpStreamSplitter(stream), - copy::TcpStreamSplitter(outbound), - connection_stats, - ) - .await + let res = (async { + let outbound = super::freebind_connect( + None, // No need to spoof source IP on outbound + req.actual_destination, + self.pi.socket_factory.as_ref(), + ) + .await?; + + // Proxying data between downstream and upstream + copy::copy_bidirectional( + copy::TcpStreamSplitter(stream), + copy::TcpStreamSplitter(outbound), + &connection_stats, + ) + .await + }) + .await; + connection_stats.record(res); } fn conn_metrics_from_request(req: &Request) -> ConnectionOpen { @@ -369,6 +454,83 @@ impl OutboundConnection { } } + // This function is called when the select next hop is on a different network, + // so we expect the upstream workload to have a network gatewy configured. + // + // When we use a gateway to reach to a workload on a remote network we have to + // use double HBONE (HBONE incapsulated inside HBONE). The gateway will + // terminate the outer HBONE tunnel and forward the inner HBONE to the actual + // destination as a opaque stream of bytes and the actual destination will + // interpret it as an HBONE connection. + // + // If the upstream workload does not have an E/W gateway this function returns + // an error indicating that it could not find a valid destination. + // + // A note about double HBONE, in double HBONE both inner and outer HBONE use + // destination service name as HBONE target URI. + // + // Having target URI in the outer HBONE tunnel allows E/W gateway to figure out + // where to route the data next witout the need to terminate inner HBONE tunnel. + // In other words, it could forward inner HBONE as if it's an opaque stream of + // bytes without trying to interpret it. + // + // NOTE: when connecting through an E/W gateway, regardless of whether there is + // a waypoint or not, we always use service hostname and the service port. It's + // somewhat different from how regular HBONE works, so I'm calling it out here. + async fn build_request_through_gateway( + &self, + source: Arc, + // next hop on the remote network that we picked as our destination. + // It may be a local view of a Waypoint workload on remote network or + // a local view of the service workload (when waypoint is not + // configured). + upstream: Upstream, + // This is a target service we wanted to reach in the first place. + // + // NOTE: Crossing network boundaries is only supported for services + // at the moment, so we should always have a service we could use. + service: &Service, + target: SocketAddr, + ) -> Result { + if let Some(gateway) = &upstream.workload.network_gateway { + let gateway_upstream = self + .pi + .state + .fetch_network_gateway(gateway, &source, target) + .await?; + let hbone_target_destination = Some(HboneAddress::SvcHostname( + service.hostname.clone(), + target.port(), + )); + + debug!("built request to a destination on another network through an E/W gateway"); + Ok(Request { + protocol: OutboundProtocol::DOUBLEHBONE, + source, + hbone_target_destination, + actual_destination_workload: Some(gateway_upstream.workload.clone()), + intended_destination_service: Some(ServiceDescription::from(service)), + actual_destination: gateway_upstream.workload_socket_addr().ok_or( + Error::NoValidDestination(Box::new((*gateway_upstream.workload).clone())), + )?, + // The outer tunnel of double HBONE is terminated by the E/W + // gateway and so for the credentials of the next hop + // (upstream_sans) we use gateway credentials. + upstream_sans: gateway_upstream.workload_and_services_san(), + // The inner HBONE tunnel is terminated by either the server + // we want to reach or a Waypoint in front of it, depending on + // the configuration. So for the final destination credentials + // (final_sans) we use the upstream workload credentials. + final_sans: upstream.service_sans(), + }) + } else { + // Do not try to send cross-network traffic without network gateway. + Err(Error::NoValidDestination(Box::new( + (*upstream.workload).clone(), + ))) + } + } + // build_request computes all information about the request we should send // TODO: Do we want a single lock for source and upstream...? async fn build_request( @@ -381,7 +543,7 @@ impl OutboundConnection { // If this is to-service traffic check for a service waypoint // Capture result of whether this is svc addressed - let svc_addressed = if let Some(Address::Service(target_service)) = state + let service = if let Some(Address::Service(target_service)) = state .fetch_address(&NetworkAddress { network: self.pi.cfg.network.clone(), address: target.ip(), @@ -393,6 +555,18 @@ impl OutboundConnection { .fetch_service_waypoint(&target_service, &source_workload, target) .await? { + if waypoint.workload.network != source_workload.network { + debug!("picked a waypoint on remote network"); + return self + .build_request_through_gateway( + source_workload.clone(), + waypoint, + &target_service, + target, + ) + .await; + } + let upstream_sans = waypoint.workload_and_services_san(); let actual_destination = waypoint @@ -413,10 +587,10 @@ impl OutboundConnection { }); } // this was service addressed but we did not find a waypoint - true + Some(target_service) } else { // this wasn't service addressed - false + None }; let Some(us) = state @@ -428,7 +602,15 @@ impl OutboundConnection { ) .await? else { - if svc_addressed { + if let Some(service) = service + && service. + load_balancer. + as_ref(). + // If we are not a passthrough service, we should have an upstream + map(|lb| lb.mode != LoadBalancerMode::Passthrough). + // If the service had no lb, we should have an upstream + unwrap_or(true) + { return Err(Error::NoHealthyUpstream(target)); } debug!("built request as passthrough; no upstream found"); @@ -446,37 +628,26 @@ impl OutboundConnection { // Check whether we are using an E/W gateway and sending cross network traffic if us.workload.network != source_workload.network { - if let Some(ew_gtw) = &us.workload.network_gateway { - let gtw_us = { - self.pi - .state - .fetch_network_gateway(ew_gtw, &source_workload, target) - .await? - }; - - let svc = us - .destination_service - .as_ref() - .expect("Workloads with network gateways must be service addressed."); - let hbone_target_destination = - Some(HboneAddress::SvcHostname(svc.hostname.clone(), us.port)); - - return Ok(Request { - protocol: OutboundProtocol::DOUBLEHBONE, - source: source_workload, - hbone_target_destination, - actual_destination_workload: Some(gtw_us.workload.clone()), - intended_destination_service: us.destination_service.clone(), - actual_destination: gtw_us.workload_socket_addr().ok_or( - Error::NoValidDestination(Box::new((*gtw_us.workload).clone())), - )?, - upstream_sans: gtw_us.workload_and_services_san(), - final_sans: us.service_sans(), - }); - } else { - // Do not try to send cross-network traffic without network gateway. - return Err(Error::NoValidDestination(Box::new((*us.workload).clone()))); - } + // Workloads on remote network must be service addressed, so if we got here + // and we don't have a service for the original target address then it's a + // bug either in ztunnel itself or in istiod. + // + // For a double HBONE protocol implementation we have to know the + // destination service and if there is no service for the target it's a bug. + // + // This situation "should never happen" because for workloads fetch_upstream + // above only checks the workloads on the same network as this ztunnel + // instance and therefore it should not be able to find a workload on a + // different network. + debug_assert!( + service.is_some(), + "workload on remote network is not service addressed" + ); + debug!("picked a workload on remote network"); + let service = service.as_ref().ok_or(Error::NoService(target))?; + return self + .build_request_through_gateway(source_workload.clone(), us, service, target) + .await; } // We are not using a network gateway and there is no workload address. @@ -491,7 +662,7 @@ impl OutboundConnection { // Check if we need to go through a workload addressed waypoint. // Don't traverse waypoint twice if the source is sandwich-outbound. // Don't traverse waypoint if traffic was addressed to a service (handled before) - if !from_waypoint && !svc_addressed { + if !from_waypoint && service.is_none() { // For case upstream server has enabled waypoint let waypoint = state .fetch_workload_waypoint(&us.workload, &source_workload, target) @@ -567,17 +738,8 @@ fn build_forwarded(remote_addr: SocketAddr, server: &Option) } } -fn baggage(r: &Request, cluster: String) -> String { - format!( - "k8s.cluster.name={cluster},k8s.namespace.name={namespace},k8s.{workload_type}.name={workload_name},service.name={name},service.version={version},cloud.region={region},cloud.availability_zone={zone}", - namespace = r.source.namespace, - workload_type = r.source.workload_type, - workload_name = r.source.workload_name, - name = r.source.canonical_name, - version = r.source.canonical_revision, - region = r.source.locality.region, - zone = r.source.locality.zone, - ) +fn baggage(r: &Request) -> String { + baggage::baggage_header_val(&r.source.baggage(), &r.source.workload_type) } #[derive(Debug)] @@ -624,10 +786,10 @@ mod tests { use crate::state::WorkloadInfo; use crate::test_helpers::helpers::{initialize_telemetry, test_proxy_metrics}; use crate::test_helpers::new_proxy_state; - use crate::xds::istio::workload::TunnelProtocol as XdsProtocol; use crate::xds::istio::workload::Workload as XdsWorkload; use crate::xds::istio::workload::address::Type as XdsAddressType; use crate::xds::istio::workload::{IpFamilies, Port}; + use crate::xds::istio::workload::{LoadBalancing, TunnelProtocol as XdsProtocol}; use crate::xds::istio::workload::{ NamespacedHostname as XdsNamespacedHostname, NetworkAddress as XdsNetworkAddress, PortList, }; @@ -717,6 +879,7 @@ mod tests { connection_manager: ConnectionManager::default(), resolver: None, disable_inbound_freebind: false, + crl_manager: None, }), id: TraceParent::new(), pool: WorkloadHBONEPool::new( @@ -816,6 +979,8 @@ mod tests { #[tokio::test] async fn build_request_double_hbone() { + // example.com service has a workload on remote network. + // E/W gateway is addressed by an IP. run_build_request_multi( "127.0.0.1", "127.0.0.3:80", @@ -867,11 +1032,13 @@ mod tests { ], Some(ExpectedRequest { protocol: OutboundProtocol::DOUBLEHBONE, - hbone_destination: "example.com:8080", + hbone_destination: "example.com:80", destination: "10.22.1.1:15009", }), ) .await; + // example.com service has a workload on remote network. + // E/W gateway is addressed by a hostname. run_build_request_multi( "127.0.0.1", "127.0.0.3:80", @@ -944,11 +1111,218 @@ mod tests { ], Some(ExpectedRequest { protocol: OutboundProtocol::DOUBLEHBONE, - hbone_destination: "example.com:8080", + hbone_destination: "example.com:80", destination: "127.0.0.5:15008", }), ) .await; + // example.com service has a waypoint and waypoint workload is on remote network. + // E/W gateway is addressed by an IP. + run_build_request_multi( + "127.0.0.1", + "127.0.0.3:80", + vec![ + XdsAddressType::Service(XdsService { + hostname: "example.com".to_string(), + addresses: vec![XdsNetworkAddress { + network: "".to_string(), + address: vec![127, 0, 0, 3], + }], + ports: vec![Port { + service_port: 80, + target_port: 8080, + }], + waypoint: Some(xds::istio::workload::GatewayAddress { + destination: Some( + xds::istio::workload::gateway_address::Destination::Hostname( + XdsNamespacedHostname { + namespace: Default::default(), + hostname: "waypoint.com".into(), + }, + ), + ), + hbone_mtls_port: 15008, + }), + ..Default::default() + }), + XdsAddressType::Service(XdsService { + hostname: "waypoint.com".to_string(), + addresses: vec![XdsNetworkAddress { + network: "".to_string(), + address: vec![127, 0, 0, 4], + }], + ports: vec![Port { + service_port: 15008, + target_port: 15008, + }], + ..Default::default() + }), + XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/remote-waypoint-pod".to_string(), + addresses: vec![], + network: "remote".to_string(), + network_gateway: Some(xds::istio::workload::GatewayAddress { + destination: Some( + xds::istio::workload::gateway_address::Destination::Address( + XdsNetworkAddress { + network: "remote".to_string(), + address: vec![10, 22, 1, 1], + }, + ), + ), + hbone_mtls_port: 15009, + }), + services: std::collections::HashMap::from([( + "/waypoint.com".to_string(), + PortList { + ports: vec![Port { + service_port: 15008, + target_port: 15008, + }], + }, + )]), + ..Default::default() + }), + XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/remote-ew-gtw".to_string(), + addresses: vec![Bytes::copy_from_slice(&[10, 22, 1, 1])], + network: "remote".to_string(), + ..Default::default() + }), + ], + Some(ExpectedRequest { + protocol: OutboundProtocol::DOUBLEHBONE, + hbone_destination: "example.com:80", + destination: "10.22.1.1:15009", + }), + ) + .await; + } + + #[tokio::test] + async fn build_request_failover_to_remote() { + // Similar to the double HBONE test that we already have, but it sets up a scenario when + // load balancing logic will pick a workload on a remote cluster when local workloads are + // unhealthy, thus showing the expected failover behavior. + let service = XdsAddressType::Service(XdsService { + hostname: "example.com".to_string(), + addresses: vec![XdsNetworkAddress { + network: "".to_string(), + address: vec![127, 0, 0, 3], + }], + ports: vec![Port { + service_port: 80, + target_port: 8080, + }], + // Prefer routing to workloads on the same network, but when nothing is healthy locally + // allow failing over to remote networks. + load_balancing: Some(xds::istio::workload::LoadBalancing { + routing_preference: vec![ + xds::istio::workload::load_balancing::Scope::Network.into(), + ], + mode: xds::istio::workload::load_balancing::Mode::Failover.into(), + ..Default::default() + }), + ..Default::default() + }); + let ew_gateway = XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/remote-ew-gtw".to_string(), + addresses: vec![Bytes::copy_from_slice(&[10, 22, 1, 1])], + network: "remote".to_string(), + ..Default::default() + }); + let remote_workload = XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/remote-example.com-pod".to_string(), + addresses: vec![], + network: "remote".to_string(), + network_gateway: Some(xds::istio::workload::GatewayAddress { + destination: Some(xds::istio::workload::gateway_address::Destination::Address( + XdsNetworkAddress { + network: "remote".to_string(), + address: vec![10, 22, 1, 1], + }, + )), + hbone_mtls_port: 15009, + }), + services: std::collections::HashMap::from([( + "/example.com".to_string(), + PortList { + ports: vec![Port { + service_port: 80, + target_port: 8080, + }], + }, + )]), + ..Default::default() + }); + let healthy_local_workload = XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/local-example.com-pod".to_string(), + addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 2])], + network: "".to_string(), + tunnel_protocol: xds::istio::workload::TunnelProtocol::Hbone.into(), + services: std::collections::HashMap::from([( + "/example.com".to_string(), + PortList { + ports: vec![Port { + service_port: 80, + target_port: 8080, + }], + }, + )]), + status: xds::istio::workload::WorkloadStatus::Healthy.into(), + ..Default::default() + }); + let unhealthy_local_workload = XdsAddressType::Workload(XdsWorkload { + uid: "Kubernetes//Pod/default/local-example.com-pod".to_string(), + addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 2])], + network: "".to_string(), + tunnel_protocol: xds::istio::workload::TunnelProtocol::Hbone.into(), + services: std::collections::HashMap::from([( + "/example.com".to_string(), + PortList { + ports: vec![Port { + service_port: 80, + target_port: 8080, + }], + }, + )]), + status: xds::istio::workload::WorkloadStatus::Unhealthy.into(), + ..Default::default() + }); + + run_build_request_multi( + "127.0.0.1", + "127.0.0.3:80", + vec![ + service.clone(), + ew_gateway.clone(), + remote_workload.clone(), + healthy_local_workload.clone(), + ], + Some(ExpectedRequest { + protocol: OutboundProtocol::HBONE, + hbone_destination: "127.0.0.2:8080", + destination: "127.0.0.2:15008", + }), + ) + .await; + + run_build_request_multi( + "127.0.0.1", + "127.0.0.3:80", + vec![ + service.clone(), + ew_gateway.clone(), + remote_workload.clone(), + unhealthy_local_workload.clone(), + ], + Some(ExpectedRequest { + protocol: OutboundProtocol::DOUBLEHBONE, + hbone_destination: "example.com:80", + destination: "10.22.1.1:15009", + }), + ) + .await; } #[tokio::test] @@ -1462,6 +1836,88 @@ mod tests { .await; } + #[tokio::test] + async fn build_request_passthrough_svc() { + run_build_request( + "127.0.0.1", + "1.2.3.4:80", + XdsAddressType::Service(XdsService { + hostname: "example.com".to_string(), + waypoint: None, + load_balancing: Some(LoadBalancing { + mode: xds::istio::workload::load_balancing::Mode::Passthrough.into(), + ..Default::default() + }), + addresses: vec![ + XdsNetworkAddress { + network: "".to_string(), + address: vec![1, 2, 3, 4], + }, + XdsNetworkAddress { + network: "".to_string(), + address: vec![1, 5, 6, 7], + }, + ], + ports: vec![Port { + service_port: 80, + target_port: 80, + }], + ..Default::default() + }), + Some(ExpectedRequest { + protocol: OutboundProtocol::TCP, + hbone_destination: "", + destination: "1.2.3.4:80", + }), + ) + .await; + } + + #[tokio::test] + async fn build_request_passthrough_svc_with_waypoint() { + run_build_request( + "127.0.0.1", + "1.2.3.4:80", + XdsAddressType::Service(XdsService { + hostname: "example.com".to_string(), + waypoint: Some(xds::istio::workload::GatewayAddress { + destination: Some(xds::istio::workload::gateway_address::Destination::Address( + XdsNetworkAddress { + network: "".to_string(), + address: [127, 0, 0, 10].to_vec(), + }, + )), + hbone_mtls_port: 15008, + }), + load_balancing: Some(LoadBalancing { + mode: xds::istio::workload::load_balancing::Mode::Passthrough.into(), + ..Default::default() + }), + addresses: vec![ + XdsNetworkAddress { + network: "".to_string(), + address: vec![1, 2, 3, 4], + }, + XdsNetworkAddress { + network: "".to_string(), + address: vec![1, 5, 6, 7], + }, + ], + ports: vec![Port { + service_port: 80, + target_port: 80, + }], + ..Default::default() + }), + Some(ExpectedRequest { + protocol: OutboundProtocol::HBONE, + destination: "127.0.0.10:15008", + hbone_destination: "1.2.3.4:80", + }), + ) + .await; + } + #[test] fn build_forwarded() { assert_eq!( @@ -1485,6 +1941,110 @@ mod tests { ); } + #[tokio::test] + async fn test_x_forwarded_network_header() { + initialize_telemetry(); + + // Create a test config with a specific network + let cfg = Arc::new(Config { + network: "test-network".into(), + local_node: Some("local-node".to_string()), + ..crate::config::parse_config().unwrap() + }); + + // Create a source workload and add it to state + let source = XdsWorkload { + uid: "cluster1//v1/Pod/ns/source-workload".to_string(), + name: "source-workload".to_string(), + namespace: "ns".to_string(), + addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 1])], + node: "local-node".to_string(), + ..Default::default() + }; + + let state = new_proxy_state(&[source], &[], &[]); + let sock_fact = Arc::new(crate::proxy::DefaultSocketFactory::default()); + + let wi = WorkloadInfo { + name: "source-workload".to_string(), + namespace: "ns".to_string(), + service_account: "default".to_string(), + }; + let local_workload_information = Arc::new(LocalWorkloadInformation::new( + Arc::new(wi.clone()), + state.clone(), + identity::mock::new_secret_manager(Duration::from_secs(10)), + )); + + let outbound = OutboundConnection { + pi: Arc::new(ProxyInputs { + state: state.clone(), + cfg: cfg.clone(), + metrics: test_proxy_metrics(), + socket_factory: sock_fact.clone(), + local_workload_information: local_workload_information.clone(), + connection_manager: ConnectionManager::default(), + resolver: None, + disable_inbound_freebind: false, + crl_manager: None, + }), + id: TraceParent::new(), + pool: WorkloadHBONEPool::new( + cfg.clone(), + sock_fact, + local_workload_information.clone(), + ), + hbone_port: cfg.inbound_addr.port(), + }; + + // Get the source workload from state + let source_workload = outbound + .pi + .local_workload_information + .get_workload() + .await + .unwrap(); + + // Create a minimal test request with required fields + let req = Request { + protocol: OutboundProtocol::HBONE, + source: source_workload, + hbone_target_destination: Some(HboneAddress::SocketAddr( + "10.0.0.1:8080".parse().unwrap(), + )), + actual_destination_workload: None, + intended_destination_service: None, + actual_destination: "10.0.0.1:8080".parse().unwrap(), + upstream_sans: vec![], + final_sans: vec![], + }; + + let remote_addr = "127.0.0.1:12345".parse().unwrap(); + + // Test the single HBONE case - header should NOT be added when origin_network is None + let http_request_no_header = outbound.create_hbone_request(remote_addr, &req, None); + assert!( + http_request_no_header + .headers() + .get(X_FORWARDED_NETWORK_HEADER) + .is_none(), + "x-istio-origin-network header should not be present when origin_network is None (single HBONE)" + ); + + // Test the double HBONE inner request case - header should be added when network is specified + let network = crate::strng::Strng::from("test-network"); + let http_request_with_header = + outbound.create_hbone_request(remote_addr, &req, Some(&network)); + assert_eq!( + http_request_with_header + .headers() + .get(X_FORWARDED_NETWORK_HEADER) + .unwrap(), + "test-network", + "x-istio-origin-network header should contain the network name for double HBONE inner request" + ); + } + #[derive(PartialEq, Debug)] struct ExpectedRequest<'a> { protocol: OutboundProtocol, diff --git a/src/proxy/pool.rs b/src/proxy/pool.rs index 460d13946e..6ed5bf23c4 100644 --- a/src/proxy/pool.rs +++ b/src/proxy/pool.rs @@ -29,6 +29,7 @@ use tokio::sync::watch; use tokio::sync::Mutex; use tracing::{Instrument, debug, trace}; +use crate::baggage::Baggage; use crate::config; use flurry; @@ -370,7 +371,7 @@ impl WorkloadHBONEPool { &mut self, workload_key: &WorkloadKey, request: http::Request<()>, - ) -> Result { + ) -> Result<(H2Stream, Option), Error> { let mut connection = self.connect(workload_key).await?; connection.send_request(request).await @@ -599,9 +600,10 @@ mod test { } /// This is really a test for TokioH2Stream, but its nicer here because we have access to - /// streams + /// streams. + /// Most important, we make sure there are no panics. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn small_reads() { + async fn read_buffering() { let (mut pool, srv) = setup_test(3).await; let key = key(&srv, 2); @@ -614,16 +616,31 @@ mod test { .unwrap() }; - let c = pool.send_request_pooled(&key.clone(), req()).await.unwrap(); + let (c, _baggage) = pool.send_request_pooled(&key.clone(), req()).await.unwrap(); let mut c = TokioH2Stream::new(c); c.write_all(b"abcde").await.unwrap(); - let mut b = [0u8; 0]; - // Crucially, this should error rather than panic. - if let Err(e) = c.read(&mut b).await { - assert_eq!(e.kind(), io::ErrorKind::Other); - } else { - panic!("Should have errored"); - } + let mut b = [0u8; 100]; + // Properly buffer reads and don't error + assert_eq!(c.read(&mut b).await.unwrap(), 8); + assert_eq!(&b[..8], b"poolsrv\n"); // this is added by itself + assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1); + assert_eq!(&b[..1], b"a"); + assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1); + assert_eq!(&b[..1], b"b"); + assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1); + assert_eq!(&b[..1], b"c"); + assert_eq!(c.read(&mut b).await.unwrap(), 2); // there are only two bytes left + assert_eq!(&b[..2], b"de"); + + // Once we drop the pool, we should still retained the buffered data, + // but then we should error. + c.write_all(b"abcde").await.unwrap(); + assert_eq!(c.read(&mut b[..3]).await.unwrap(), 3); + assert_eq!(&b[..3], b"abc"); + drop(pool); + assert_eq!(c.read(&mut b[..2]).await.unwrap(), 2); + assert_eq!(&b[..2], b"de"); + assert!(c.read(&mut b).await.is_err()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/proxy/socks5.rs b/src/proxy/socks5.rs index 870f02b612..3fe3ba72fb 100644 --- a/src/proxy/socks5.rs +++ b/src/proxy/socks5.rs @@ -86,6 +86,11 @@ impl Socks5 { let mut force_shutdown = force_shutdown.clone(); match socket { Ok((stream, _remote)) => { + let socket_labels = crate::proxy::metrics::SocketLabels { + reporter: crate::proxy::metrics::Reporter::source, + }; + self.pi.metrics.record_socket_open(&socket_labels); + let oc = OutboundConnection { pi: self.pi.clone(), id: TraceParent::new(), @@ -93,7 +98,12 @@ impl Socks5 { hbone_port: self.pi.cfg.inbound_addr.port(), }; let span = info_span!("socks5", id=%oc.id); + let metrics_for_socket_close = self.pi.metrics.clone(); let serve = (async move { + let _socket_guard = crate::proxy::metrics::SocketCloseGuard::new( + metrics_for_socket_close, + crate::proxy::metrics::Reporter::source, + ); debug!(component="socks5", "connection started"); // Since this task is spawned, make sure we are guaranteed to terminate tokio::select! { @@ -140,8 +150,17 @@ async fn handle_socks_connection(mut oc: OutboundConnection, mut stream: TcpStre warn!("failed to send socks success response: {err}"); return; } - let remote_addr = - socket::to_canonical(stream.peer_addr().expect("must receive peer addr")); + let peer = match stream.peer_addr() { + Ok(addr) => addr, + Err(e) => { + debug!( + component = "socks5", + "failed to get peer address, dropping connection: {}", e + ); + return; + } + }; + let remote_addr = socket::to_canonical(peer); oc.proxy_to(stream, remote_addr, target).await } Err(e) => { @@ -160,7 +179,7 @@ async fn negotiate_socks_connection( pi: &ProxyInputs, stream: &mut TcpStream, ) -> Result { - let remote_addr = socket::to_canonical(stream.peer_addr().expect("must receive peer addr")); + let remote_addr = socket::to_canonical(stream.peer_addr()?); // Version(5), Number of auth methods let mut version = [0u8; 2]; @@ -203,8 +222,7 @@ async fn negotiate_socks_connection( if version != 0x05 { return Err(SocksError::invalid_protocol(format!( - "unsupported version {}", - version + "unsupported version {version}", ))); } diff --git a/src/proxyfactory.rs b/src/proxyfactory.rs index 74625e3652..0bdb883b11 100644 --- a/src/proxyfactory.rs +++ b/src/proxyfactory.rs @@ -15,6 +15,7 @@ use crate::config; use crate::identity::SecretManager; use crate::state::{DemandProxyState, WorkloadInfo}; +use crate::tls; use std::sync::Arc; use tracing::error; @@ -34,6 +35,7 @@ pub struct ProxyFactory { proxy_metrics: Arc, dns_metrics: Option>, drain: DrainWatcher, + crl_manager: Option>, } impl ProxyFactory { @@ -55,6 +57,36 @@ impl ProxyFactory { } }; + // Initialize CRL manager if crl_path is set + let crl_manager = if let Some(crl_path) = &config.crl_path { + match tls::crl::CrlManager::new(crl_path.clone()) { + Ok(manager) => { + let manager_arc = Arc::new(manager); + + if let Err(e) = manager_arc.start_file_watcher() { + tracing::warn!( + "crl file watcher could not be started: {}. \ + crl validation will continue with current file, but \ + crl updates will require restarting ztunnel.", + e + ); + } + + Some(manager_arc) + } + Err(e) => { + tracing::warn!( + path = ?crl_path, + error = %e, + "failed to initialize crl manager" + ); + None + } + } + } else { + None + }; + Ok(ProxyFactory { config, state, @@ -62,6 +94,7 @@ impl ProxyFactory { proxy_metrics, dns_metrics, drain, + crl_manager, }) } @@ -112,6 +145,8 @@ impl ProxyFactory { drain.clone(), socket_factory.as_ref(), local_workload_information.as_fetcher(), + self.config.prefered_service_namespace.clone(), + self.config.ipv6_enabled, ) .await?; resolver = Some(server.resolver()); @@ -130,6 +165,7 @@ impl ProxyFactory { resolver, local_workload_information, false, + self.crl_manager.clone(), ); result.connection_manager = Some(cm); result.proxy = Some(Proxy::from_inputs(pi, drain).await?); @@ -175,6 +211,7 @@ impl ProxyFactory { None, local_workload_information, true, + self.crl_manager.clone(), ); let inbound = Inbound::new(pi, self.drain.clone()).await?; diff --git a/src/rbac.rs b/src/rbac.rs index e8495ef38b..265996c0b8 100644 --- a/src/rbac.rs +++ b/src/rbac.rs @@ -39,6 +39,7 @@ pub struct Authorization { pub scope: RbacScope, pub action: RbacAction, pub rules: Vec>>, + pub dry_run: bool, } #[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Serialize)] @@ -360,6 +361,7 @@ impl TryFrom for Authorization { scope: RbacScope::from(xds::istio::security::Scope::try_from(resource.scope)?), action: RbacAction::from(xds::istio::security::Action::try_from(resource.action)?), rules, + dry_run: resource.dry_run, }) } } @@ -483,6 +485,7 @@ mod tests { scope: RbacScope::Global, action: RbacAction::Allow, rules, + dry_run: false, } } diff --git a/src/socket.rs b/src/socket.rs index 8464bb88a8..cd226a865e 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -19,15 +19,11 @@ use tokio::io; use tokio::net::TcpSocket; use tokio::net::{TcpListener, TcpStream}; use std::io::Error; -use socket2::SockRef; - +use socket2::{SockRef, TcpKeepalive}; +use crate::config::SocketConfig; #[cfg(target_os = "linux")] -use { - socket2::Domain, - std::io::ErrorKind, - tracing::warn, -}; +use {socket2::Domain, std::io::ErrorKind, tracing::warn}; #[cfg(target_os = "linux")] pub fn set_freebind_and_transparent(socket: &TcpSocket) -> io::Result<()> { @@ -35,11 +31,11 @@ pub fn set_freebind_and_transparent(socket: &TcpSocket) -> io::Result<()> { match socket.domain()? { Domain::IPV4 => { socket.set_ip_transparent_v4(true)?; - socket.set_freebind(true)?; + socket.set_freebind_v4(true)?; } Domain::IPV6 => { linux::set_ipv6_transparent(&socket)?; - socket.set_freebind_ipv6(true)? + socket.set_freebind_v6(true)? } _ => return Err(Error::new(ErrorKind::Unsupported, "unsupported domain")), }; @@ -71,8 +67,8 @@ fn orig_dst_addr(stream: &tokio::net::TcpStream) -> io::Result { if !sock.ip_transparent_v4().unwrap_or(false) && !linux::ipv6_transparent(&sock).unwrap_or(false) { // In TPROXY mode, this is normal, so don't bother logging warn!( - peer=?stream.peer_addr().unwrap(), - local=?stream.local_addr().unwrap(), + peer=%stream.peer_addr().map(|a| a.to_string()).unwrap_or_else(|_| "N.A.".to_string()), + local=%stream.local_addr().map(|a| a.to_string()).unwrap_or_else(|_| "N.A.".to_string()), "failed to read SO_ORIGINAL_DST: {e4:?}, {e6:?}" ); } @@ -170,11 +166,11 @@ mod linux { } pub fn original_dst(sock: &SockRef) -> io::Result { - sock.original_dst() + sock.original_dst_v4() } pub fn original_dst_ipv6(sock: &SockRef) -> io::Result { - sock.original_dst_ipv6() + sock.original_dst_v6() } } #[cfg(target_os = "windows")] @@ -184,29 +180,55 @@ mod windows { use tokio::io; pub fn original_dst(sock: &SockRef) -> io::Result { - sock.original_dst() + sock.original_dst_v4() } pub fn original_dst_ipv6(sock: &SockRef) -> io::Result { - sock.original_dst_ipv6() + sock.original_dst_v6() } } /// Listener is a wrapper For TCPListener with sane defaults. Notably, setting NODELAY -pub struct Listener(TcpListener); +/// You can also pass it additional socket options to set on accepted connections. +pub struct Listener { + listener: TcpListener, + cfg: Option, +} impl Listener { pub fn new(l: TcpListener) -> Self { - Self(l) + Listener { + listener: l, + cfg: None, + } } pub fn local_addr(&self) -> SocketAddr { - self.0.local_addr().expect("local_addr is available") + self.listener.local_addr().expect("local_addr is available") } pub fn inner(self) -> TcpListener { - self.0 + self.listener + } + pub fn set_socket_options(&mut self, cfg: Option) { + self.cfg = cfg; } pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { - let (stream, remote) = self.0.accept().await?; + let (stream, remote) = self.listener.accept().await?; stream.set_nodelay(true)?; + if let Some(cfg) = self.cfg { + if cfg.keepalive_enabled { + let ka = TcpKeepalive::new() + .with_time(cfg.keepalive_time) + .with_retries(cfg.keepalive_retries) + .with_interval(cfg.keepalive_interval); + let res = SockRef::from(&stream).set_tcp_keepalive(&ka); + tracing::trace!("set keepalive: {:?}", res); + } + #[cfg(target_os = "linux")] + if cfg.user_timeout_enabled { + let ut = cfg.keepalive_time + cfg.keepalive_retries * cfg.keepalive_interval; + let res = SockRef::from(&stream).set_tcp_user_timeout(Some(ut)); + tracing::trace!("set user timeout: {:?}", res); + } + } Ok((stream, remote)) } } @@ -216,7 +238,7 @@ impl Listener { #[cfg(target_os = "linux")] impl Listener { pub fn set_transparent(&self) -> io::Result<()> { - let socket = SockRef::from(&self.0); + let socket = SockRef::from(&self.listener); match socket.domain()? { Domain::IPV4 => { socket.set_ip_transparent_v4(true)?; @@ -240,3 +262,41 @@ impl Listener { )) } } + +#[cfg(test)] +pub mod socket_tests { + use tokio::net::TcpStream; + + use crate::{config::SocketConfig, socket::Listener}; + + #[tokio::test] + async fn test_keep_alive_options() { + let cfg = SocketConfig { + keepalive_enabled: true, + keepalive_time: std::time::Duration::from_secs(10), + keepalive_retries: 5, + keepalive_interval: std::time::Duration::from_secs(2), + user_timeout_enabled: true, + }; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + + TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + + let mut socklistener = Listener::new(listener); + socklistener.set_socket_options(Some(cfg)); + + let (stream, _) = socklistener.accept().await.unwrap(); + // Verify keepalive options + let sock = socket2::SockRef::from(&stream); + assert_eq!(sock.tcp_keepalive_time().unwrap(), cfg.keepalive_time); + assert_eq!(sock.tcp_keepalive_retries().unwrap(), cfg.keepalive_retries); + assert_eq!( + sock.tcp_keepalive_interval().unwrap(), + cfg.keepalive_interval + ); + let ut = cfg.keepalive_time + cfg.keepalive_retries * cfg.keepalive_interval; + assert_eq!(sock.tcp_user_timeout().unwrap(), Some(ut)); + } +} diff --git a/src/state.rs b/src/state.rs index c7efe0418c..b0ea11e1bb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::authpol_log; use crate::identity::{Identity, SecretManager}; use crate::proxy::{Error, OnDemandDnsLabels}; use crate::rbac::Authorization; @@ -73,6 +74,12 @@ pub struct Upstream { pub destination_service: Option, } +#[derive(Clone, Debug, Eq, PartialEq)] +enum UpstreamDestination { + UpstreamParts(Arc, u16, Option>), + OriginalDestination, +} + impl Upstream { pub fn workload_socket_addr(&self) -> Option { self.selected_workload_ip @@ -275,10 +282,14 @@ impl ProxyState { } /// Find services by hostname. - pub fn find_service_by_hostname(&self, hostname: &Strng) -> Result>, Error> { + pub fn find_service_by_hostname( + &self, + hostname: &Strng, + namespace: &Strng, + ) -> Result, Error> { // Hostnames for services are more common, so lookup service first and fallback to workload. self.services - .get_by_host(hostname) + .get_best_by_host(hostname, Some(namespace)) .ok_or_else(|| Error::NoHostname(hostname.to_string())) } @@ -288,11 +299,16 @@ impl ProxyState { source_workload: &Workload, addr: SocketAddr, resolution_mode: ServiceResolutionMode, - ) -> Option<(Arc, u16, Option>)> { + ) -> Option { if let Some(svc) = self .services .get_by_vip(&network_addr(network.clone(), addr.ip())) { + if let Some(lb) = &svc.load_balancer + && lb.mode == LoadBalancerMode::Passthrough + { + return Some(UpstreamDestination::OriginalDestination); + } return self.find_upstream_from_service( source_workload, addr.port(), @@ -304,7 +320,7 @@ impl ProxyState { .workloads .find_address(&network_addr(network, addr.ip())) { - return Some((wl, addr.port(), None)); + return Some(UpstreamDestination::UpstreamParts(wl, addr.port(), None)); } None } @@ -315,7 +331,7 @@ impl ProxyState { svc_port: u16, resolution_mode: ServiceResolutionMode, svc: Arc, - ) -> Option<(Arc, u16, Option>)> { + ) -> Option { // Randomly pick an upstream // TODO: do this more efficiently, and not just randomly let Some((ep, wl)) = self.load_balance(source_workload, &svc, svc_port, resolution_mode) @@ -343,7 +359,11 @@ impl ProxyState { return None; }; - Some((wl, target_port, Some(svc))) + Some(UpstreamDestination::UpstreamParts( + wl, + target_port, + Some(svc), + )) } fn load_balance<'a>( @@ -366,6 +386,19 @@ impl ProxyState { debug!("failed to fetch workload for {}", ep.workload_uid); return None; }; + + let in_network = wl.network == src.network; + let has_network_gateway = wl.network_gateway.is_some(); + let has_address = !wl.workload_ips.is_empty() || !wl.hostname.is_empty(); + if !has_address { + // Workload has no IP. We can only reach it via a network gateway + // WDS is client-agnostic, so we will get a network gateway for a workload + // even if it's in the same network; we should never use it. + if in_network || !has_network_gateway { + return None; + } + } + match resolution_mode { ServiceResolutionMode::Standard => { if target_port.unwrap_or_default() == 0 && !ep.port.contains_key(&svc_port) { @@ -519,7 +552,7 @@ impl DemandProxyState { let workload = wl.authorization_policies.iter(); // Aggregate all of them based on type - let (allow, deny): (Vec<_>, Vec<_>) = ns + let (all_allow, all_deny): (Vec<_>, Vec<_>) = ns .iter() .chain(global.iter()) .chain(workload) @@ -534,6 +567,11 @@ impl DemandProxyState { }) .partition(|p| p.action == rbac::RbacAction::Allow); + let (deny, deny_dry_run): (Vec<&Authorization>, Vec<&Authorization>) = + all_deny.iter().partition(|p| !p.dry_run); + let (allow, allow_dry_run): (Vec<&Authorization>, Vec<&Authorization>) = + all_allow.iter().partition(|p| !p.dry_run); + trace!( allow = allow.len(), deny = deny.len(), @@ -542,10 +580,15 @@ impl DemandProxyState { // Allow and deny logic follows https://istio.io/latest/docs/reference/config/security/authorization-policy/ + for pol in deny_dry_run.iter() { + if pol.matches(conn) { + authpol_log!(policy = pol.to_key().as_str(), "dry-run: deny policy match"); + } + } // "If there are any DENY policies that match the request, deny the request." for pol in deny.iter() { if pol.matches(conn) { - debug!(policy = pol.to_key().as_str(), "deny policy match"); + authpol_log!(policy = pol.to_key().as_str(), "deny policy match"); return Err(proxy::AuthorizationRejectionError::ExplicitlyDenied( pol.namespace.to_owned(), pol.name.to_owned(), @@ -554,15 +597,30 @@ impl DemandProxyState { trace!(policy = pol.to_key().as_str(), "deny policy does not match"); } } + let mut dry_run_allow_matched = false; + for pol in allow_dry_run.iter() { + if pol.matches(conn) { + dry_run_allow_matched = true; + authpol_log!( + policy = pol.to_key().as_str(), + "dry-run: allow policy match" + ); + } + } + if allow.is_empty() && !allow_dry_run.is_empty() && !dry_run_allow_matched { + // this is going to be an allow, but the conn would be denied if dry-run policies + // became enforced because none matched + authpol_log!("dry-run: no allow policies match"); + } // "If there are no ALLOW policies for the workload, allow the request." if allow.is_empty() { - debug!("no allow policies, allow"); + authpol_log!("no allow policies, allow"); return Ok(()); } // "If any of the ALLOW policies match the request, allow the request." for pol in allow.iter() { if pol.matches(conn) { - debug!(policy = pol.to_key().as_str(), "allow policy match"); + authpol_log!(policy = pol.to_key().as_str(), "allow policy match"); return Ok(()); } else { trace!( @@ -572,7 +630,7 @@ impl DemandProxyState { } } // "Deny the request." - debug!("no allow policies matched"); + authpol_log!("no allow policies matched"); Err(proxy::AuthorizationRejectionError::NotAllowed) } @@ -776,10 +834,11 @@ impl DemandProxyState { &self, source_workload: &Workload, original_target_address: SocketAddr, - upstream: Option<(Arc, u16, Option>)>, + upstream: Option, ) -> Result, Error> { - let Some((wl, port, svc)) = upstream else { - return Ok(None); + let (wl, port, svc) = match upstream { + Some(UpstreamDestination::UpstreamParts(wl, port, svc)) => (wl, port, svc), + None | Some(UpstreamDestination::OriginalDestination) => return Ok(None), }; let svc_desc = svc.clone().map(|s| ServiceDescription::from(s.as_ref())); let ip_family_restriction = svc.as_ref().and_then(|s| s.ip_families); @@ -842,7 +901,11 @@ impl DemandProxyState { (us, original_destination_address) } Some(Address::Workload(w)) => { - let us = Some((w, gw_address.hbone_mtls_port, None)); + let us = Some(UpstreamDestination::UpstreamParts( + w, + gw_address.hbone_mtls_port, + None, + )); (us, original_destination_address) } None => { @@ -857,7 +920,7 @@ impl DemandProxyState { self.finalize_upstream(source_workload, target_address, res) .await? .ok_or_else(|| { - Error::UnknownNetworkGateway(format!("network gateway {:?} not found", gw_address)) + Error::UnknownNetworkGateway(format!("network gateway {gw_address:?} not found")) }) } @@ -899,7 +962,11 @@ impl DemandProxyState { (us, original_destination_address) } Some(Address::Workload(w)) => { - let us = Some((w, gw_address.hbone_mtls_port, None)); + let us = Some(UpstreamDestination::UpstreamParts( + w, + gw_address.hbone_mtls_port, + None, + )); (us, original_destination_address) } None => { @@ -913,7 +980,7 @@ impl DemandProxyState { }; self.finalize_upstream(source_workload, target_address, res) .await? - .ok_or_else(|| Error::UnknownWaypoint(format!("waypoint {:?} not found", gw_address))) + .ok_or_else(|| Error::UnknownWaypoint(format!("waypoint {gw_address:?} not found"))) } pub async fn fetch_service_waypoint( @@ -1356,26 +1423,28 @@ mod tests { _ => ServiceResolutionMode::Standard, }; - let (_, port, _) = state - .find_upstream("".into(), &wl, "10.0.0.1:80".parse().unwrap(), mode) - .expect("upstream to be found"); + let port = match state.find_upstream("".into(), &wl, "10.0.0.1:80".parse().unwrap(), mode) { + Some(UpstreamDestination::UpstreamParts(_, port, _)) => port, + _ => panic!("upstream to be found"), + }; + assert_eq!(port, tc.expected_port()); } fn create_workload(dest_uid: u8) -> Workload { Workload { name: "test".into(), - namespace: format!("ns{}", dest_uid).into(), + namespace: format!("ns{dest_uid}").into(), trust_domain: "cluster.local".into(), service_account: "defaultacct".into(), workload_ips: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 0, dest_uid))], - uid: format!("{}", dest_uid).into(), + uid: format!("{dest_uid}").into(), ..test_helpers::test_default_workload() } } fn get_workload(state: &DemandProxyState, dest_uid: u8) -> Arc { - let key: Strng = format!("{}", dest_uid).into(); + let key: Strng = format!("{dest_uid}").into(); state.read().workloads.by_uid[&key].clone() } @@ -1384,7 +1453,7 @@ mod tests { dest_uid: u8, src_svc_acct: &str, ) -> crate::state::ProxyRbacContext { - let key: Strng = format!("{}", dest_uid).into(); + let key: Strng = format!("{dest_uid}").into(); let workload = &state.read().workloads.by_uid[&key]; crate::state::ProxyRbacContext { conn: rbac::Connection { @@ -1415,6 +1484,17 @@ mod tests { ) } + fn create_dry_run_wildcard_rbac_policy(action: rbac::RbacAction) -> rbac::Authorization { + rbac::Authorization { + action, + namespace: "ns1".into(), + name: "wildcard".into(), + rules: vec![vec![]], + scope: rbac::RbacScope::Namespace, + dry_run: true, + } + } + // test that we confirm with https://istio.io/latest/docs/reference/config/security/authorization-policy/. // We don't test #1 as ztunnel doesn't support custom policies. // 1. If there are any CUSTOM policies that match the request, evaluate and deny the request if the evaluation result is deny. @@ -1427,6 +1507,15 @@ mod tests { let mut state = ProxyState::new(None); state.workloads.insert(Arc::new(create_workload(1))); state.workloads.insert(Arc::new(create_workload(2))); + // Dry run policies should have no effect. + state.policies.insert( + "wildcard-allow".into(), + create_dry_run_wildcard_rbac_policy(rbac::RbacAction::Allow), + ); + state.policies.insert( + "wildcard-deny".into(), + create_dry_run_wildcard_rbac_policy(rbac::RbacAction::Deny), + ); state.policies.insert( "allow".into(), rbac::Authorization { @@ -1446,6 +1535,7 @@ mod tests { ], ], scope: rbac::RbacScope::Namespace, + dry_run: false, }, ); state.policies.insert( @@ -1467,6 +1557,7 @@ mod tests { ], ], scope: rbac::RbacScope::Namespace, + dry_run: false, }, ); @@ -1528,6 +1619,109 @@ mod tests { assert!(mock_proxy_state.assert_rbac(&ctx).await.is_ok()); } + #[tokio::test] + async fn assert_rbac_dry_run_with_real_policies() { + initialize_telemetry(); + crate::telemetry::set_level(true, "debug").ok(); + + let mut state = ProxyState::new(None); + state.workloads.insert(Arc::new(create_workload(1))); + + // Real deny policy that matches denyacct + state.policies.insert( + "real-deny".into(), + rbac::Authorization { + action: rbac::RbacAction::Deny, + namespace: "ns1".into(), + name: "real-deny".into(), + rules: vec![vec![vec![rbac::RbacMatch { + principals: vec![StringMatch::Exact( + "cluster.local/ns/default/sa/denyacct".into(), + )], + ..Default::default() + }]]], + scope: rbac::RbacScope::Namespace, + dry_run: false, + }, + ); + + // Dry-run deny policy that matches both defaultacct and denyacct + state.policies.insert( + "dry-run-deny".into(), + rbac::Authorization { + action: rbac::RbacAction::Deny, + namespace: "ns1".into(), + name: "dry-run-deny".into(), + rules: vec![ + vec![vec![rbac::RbacMatch { + principals: vec![StringMatch::Exact( + "cluster.local/ns/default/sa/defaultacct".into(), + )], + ..Default::default() + }]], + vec![vec![rbac::RbacMatch { + principals: vec![StringMatch::Exact( + "cluster.local/ns/default/sa/denyacct".into(), + )], + ..Default::default() + }]], + ], + scope: rbac::RbacScope::Namespace, + dry_run: true, + }, + ); + + // Real allow policy that matches defaultacct + state.policies.insert( + "real-allow".into(), + rbac::Authorization { + action: rbac::RbacAction::Allow, + namespace: "ns1".into(), + name: "real-allow".into(), + rules: vec![vec![vec![rbac::RbacMatch { + principals: vec![StringMatch::Exact( + "cluster.local/ns/default/sa/defaultacct".into(), + )], + ..Default::default() + }]]], + scope: rbac::RbacScope::Namespace, + dry_run: false, + }, + ); + + // Dry-run allow policy that matches defaultacct + state.policies.insert( + "dry-run-allow".into(), + rbac::Authorization { + action: rbac::RbacAction::Allow, + namespace: "ns1".into(), + name: "dry-run-allow".into(), + rules: vec![vec![vec![rbac::RbacMatch { + principals: vec![StringMatch::Exact( + "cluster.local/ns/default/sa/defaultacct".into(), + )], + ..Default::default() + }]]], + scope: rbac::RbacScope::Namespace, + dry_run: true, + }, + ); + + let mock_proxy_state = create_state(state); + + let ctx = get_rbac_context(&mock_proxy_state, 1, "defaultacct"); + assert!(mock_proxy_state.assert_rbac(&ctx).await.is_ok()); + + crate::telemetry::testing::assert_contains(std::collections::HashMap::from([ + ("policy", "ns1/dry-run-deny"), + ("message", "dry-run: deny policy match"), + ])); + crate::telemetry::testing::assert_contains(std::collections::HashMap::from([ + ("policy", "ns1/dry-run-allow"), + ("message", "dry-run: allow policy match"), + ])); + } + #[tokio::test] async fn test_load_balance() { initialize_telemetry(); @@ -1571,6 +1765,22 @@ mod tests { }, ..test_helpers::test_default_workload() }; + let wl_empty_ip = Workload { + uid: "cluster1//v1/Pod/default/wl_empty_ip".into(), + name: "wl_empty_ip".into(), + namespace: "default".into(), + trust_domain: "cluster.local".into(), + service_account: "default".into(), + workload_ips: vec![], // none! + network: "network".into(), + locality: Locality { + region: "reg".into(), + zone: "zone".into(), + subzone: "".into(), + }, + ..test_helpers::test_default_workload() + }; + let _ep_almost = Workload { uid: "cluster1//v1/Pod/default/ep_almost".into(), name: "wl_almost".into(), @@ -1617,6 +1827,11 @@ mod tests { port: HashMap::from([(80u16, 80u16)]), status: HealthStatus::Healthy, }, + Endpoint { + workload_uid: "cluster1//v1/Pod/default/wl_empty_ip".into(), + port: HashMap::from([(80u16, 80u16)]), + status: HealthStatus::Healthy, + }, ]); let strict_svc = Service { endpoints: endpoints.clone(), @@ -1649,6 +1864,7 @@ mod tests { state.workloads.insert(Arc::new(wl_no_locality.clone())); state.workloads.insert(Arc::new(wl_match.clone())); state.workloads.insert(Arc::new(wl_almost.clone())); + state.workloads.insert(Arc::new(wl_empty_ip.clone())); state.services.insert(strict_svc.clone()); state.services.insert(failover_svc.clone()); @@ -1663,6 +1879,15 @@ mod tests { assert!(want.contains(&got.unwrap()), "{}", desc); } }; + let assert_not_endpoint = + |src: &Workload, svc: &Service, uid: &str, tries: usize, desc: &str| { + for _ in 0..tries { + let got = state + .load_balance(src, svc, 80, ServiceResolutionMode::Standard) + .map(|(ep, _)| ep.workload_uid.as_str()); + assert!(got != Some(uid), "{}", desc); + } + }; assert_endpoint( &wl_no_locality, @@ -1708,5 +1933,12 @@ mod tests { vec!["cluster1//v1/Pod/default/wl_match"], "failover full match selects closest match", ); + assert_not_endpoint( + &wl_no_locality, + &failover_svc, + "cluster1//v1/Pod/default/wl_empty_ip", + 10, + "failover no match can select any endpoint", + ); } } diff --git a/src/state/policy.rs b/src/state/policy.rs index a759ffa9e4..d1c1383796 100644 --- a/src/state/policy.rs +++ b/src/state/policy.rs @@ -84,12 +84,11 @@ impl PolicyStore { RbacScope::Global => Some(strng::EMPTY), RbacScope::Namespace => Some(rbac.namespace), RbacScope::WorkloadSelector => None, - } { - if let Some(pl) = self.by_namespace.get_mut(&key) { - pl.remove(&xds_name); - if pl.is_empty() { - self.by_namespace.remove(&key); - } + } && let Some(pl) = self.by_namespace.get_mut(&key) + { + pl.remove(&xds_name); + if pl.is_empty() { + self.by_namespace.remove(&key); } } } @@ -128,6 +127,7 @@ mod tests { namespaces: vec![StringMatch::Exact("whatever".into())], ..Default::default() }]]], + dry_run: false, }; let policy_key = policy.to_key(); // insert this namespace-scoped policy into policystore then assert it is diff --git a/src/state/service.rs b/src/state/service.rs index a4fb8e9b01..8838fe59fb 100644 --- a/src/state/service.rs +++ b/src/state/service.rs @@ -55,6 +55,9 @@ pub struct Service { #[serde(default, skip_serializing_if = "is_default")] pub ip_families: Option, + + #[serde(default)] + pub canonical: bool, } /// EndpointSet is an abstraction over a set of endpoints. @@ -123,6 +126,9 @@ pub enum LoadBalancerMode { Strict, // Prefer select endpoints matching all LoadBalancerScopes when picking endpoints but allow mismatches Failover, + // In PASSTHROUGH mode, endpoint selection will not be done and traffic passes directly through to the original + // desitnation address. + Passthrough, } impl From for LoadBalancerMode { @@ -133,6 +139,9 @@ impl From for LoadBalancerMode { xds::istio::workload::load_balancing::Mode::UnspecifiedMode => { LoadBalancerMode::Standard } + xds::istio::workload::load_balancing::Mode::Passthrough => { + LoadBalancerMode::Passthrough + } } } } @@ -322,6 +331,7 @@ impl TryFrom<&XdsService> for Service { waypoint, load_balancer: lb, ip_families, + canonical: s.canonical, }; Ok(svc) } @@ -361,6 +371,14 @@ impl ServiceStore { self.by_host.get(hostname).map(|v| v.to_vec()) } + // Returns the "best" [Srevice] matching the given hostname. + // If a namespace is provided, a Service from that namespace is preferred. + // Next, a Service marked `canonical` is prerferred. + pub fn get_best_by_host(&self, hostname: &Strng, ns: Option<&Strng>) -> Option> { + let services = self.get_by_host(hostname)?; + Some(ServiceMatch::find_best_match(services.iter(), ns, None)?.clone()) + } + pub fn get_by_workload(&self, workload: &Workload) -> Vec> { workload .services @@ -574,3 +592,64 @@ impl ServiceStore { self.staged_services.len() } } + +/// Represents the reason a service was matched during lookup. +/// Used with fold_while to implement priority-based service selection +/// with short-circuit on best match (namespace + primary hostname). +/// +/// Priority order (lower is better): Namespace > Canonical > First +pub enum ServiceMatch<'a> { + Canonical(&'a Arc), + Namespace(&'a Arc), + PreferredNamespace(&'a Arc), + First(&'a Arc), + None, +} + +impl<'a> From> for Option<&'a Arc> { + fn from(value: ServiceMatch<'a>) -> Option<&'a Arc> { + match value { + ServiceMatch::Canonical(s) + | ServiceMatch::First(s) + | ServiceMatch::Namespace(s) + | ServiceMatch::PreferredNamespace(s) => Some(s), + ServiceMatch::None => None, + } + } +} + +impl<'a> ServiceMatch<'a> { + /// Finds the best matching service from an iterator using fold_while. + /// Short-circuits on Namespace match - the best possible result. + pub fn find_best_match( + mut services: impl Iterator>, + client_ns: Option<&Strng>, + preferred_namespace: Option<&Strng>, + ) -> Option<&'a Arc> { + services + .fold_while(ServiceMatch::None, |r, s| { + if Some(&s.namespace) == client_ns { + itertools::FoldWhile::Done(ServiceMatch::Namespace(s)) + } else if s.canonical { + itertools::FoldWhile::Continue(ServiceMatch::Canonical(s)) + } else { + // TODO: deprecate preferred_service_namespace + // https://github.com/istio/ztunnel/issues/1709 + if let Some(preferred_namespace) = preferred_namespace + && preferred_namespace == &s.namespace + && !matches!(r, ServiceMatch::Canonical(_)) + { + return itertools::FoldWhile::Continue(ServiceMatch::PreferredNamespace(s)); + } + match r { + ServiceMatch::None => { + itertools::FoldWhile::Continue(ServiceMatch::First(s)) + } + _ => itertools::FoldWhile::Continue(r), + } + } + }) + .into_inner() + .into() + } +} diff --git a/src/state/workload.rs b/src/state/workload.rs index 3be9ca120f..a2aaad86dc 100644 --- a/src/state/workload.rs +++ b/src/state/workload.rs @@ -14,6 +14,7 @@ use crate::identity::Identity; +use crate::baggage::Baggage; use crate::state::WorkloadInfo; use crate::strng::Strng; use crate::xds::istio::workload::{Port, PortList}; @@ -36,7 +37,7 @@ use std::sync::Arc; use std::{fmt, net}; use thiserror::Error; use tokio::sync::watch::{Receiver, Sender}; -use tracing::{error, trace}; +use tracing::trace; use xds::istio::workload::ApplicationTunnel as XdsApplicationTunnel; use xds::istio::workload::GatewayAddress as XdsGatewayAddress; use xds::istio::workload::Workload as XdsWorkload; @@ -301,6 +302,19 @@ impl Workload { service_account: self.service_account.clone(), } } + + pub fn baggage(&self) -> Baggage { + Baggage { + cluster_id: (!self.cluster_id.is_empty()).then_some(self.cluster_id.clone()), + namespace: (!self.namespace.is_empty()).then_some(self.namespace.clone()), + workload_name: (!self.workload_name.is_empty()).then_some(self.workload_name.clone()), + service_name: (!self.canonical_name.is_empty()).then_some(self.canonical_name.clone()), + revision: (!self.canonical_revision.is_empty()) + .then_some(self.canonical_revision.clone()), + region: (!self.locality.region.is_empty()).then_some(self.locality.region.clone()), + zone: (!self.locality.zone.is_empty()).then_some(self.locality.zone.clone()), + } + } } impl fmt::Display for Workload { @@ -825,10 +839,9 @@ impl WorkloadStore { for wip in prev.workload_ips.iter() { if let Entry::Occupied(mut o) = self.by_addr.entry(network_addr(prev.network.clone(), *wip)) + && o.get_mut().remove_uid(prev.uid.clone()) { - if o.get_mut().remove_uid(prev.uid.clone()) { - o.remove(); - } + o.remove(); } } } @@ -906,8 +919,9 @@ pub enum WorkloadError { mod tests { use super::*; use crate::config::ConfigSource; - use crate::state::{DemandProxyState, ProxyState, ServiceResolutionMode}; + use crate::state::{DemandProxyState, ProxyState, ServiceResolutionMode, UpstreamDestination}; use crate::test_helpers::helpers::initialize_telemetry; + use crate::test_helpers::{LOCALHOST_YAML, temp_file_with_content}; use crate::xds::istio::workload::PortList as XdsPortList; use crate::xds::istio::workload::Service as XdsService; use crate::xds::istio::workload::WorkloadStatus as XdsStatus; @@ -1028,8 +1042,8 @@ mod tests { }, )]); - let uid1 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip1); - let uid2 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip2); + let uid1 = format!("cluster1//v1/Pod/default/my-pod/{ip1:?}"); + let uid2 = format!("cluster1//v1/Pod/default/my-pod/{ip2:?}"); updater .insert_workload( @@ -1124,6 +1138,7 @@ mod tests { load_balancing: None, ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1158,6 +1173,7 @@ mod tests { load_balancing: None, ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1215,6 +1231,7 @@ mod tests { load_balancing: None, ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1526,6 +1543,7 @@ mod tests { load_balancing: None, ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1553,6 +1571,7 @@ mod tests { }), ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1604,6 +1623,7 @@ mod tests { }), ip_families: 0, extensions: Default::default(), + canonical: true, }; updater .insert_service( @@ -1625,6 +1645,7 @@ mod tests { load_balancing: None, ip_families: 0, extensions: Default::default(), + canonical: true, }, ) .unwrap(); @@ -1734,7 +1755,7 @@ mod tests { let xds_ip1 = Bytes::copy_from_slice(&[127, 0, 0, 1]); let ip1 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - let uid1 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip1); + let uid1 = format!("cluster1//v1/Pod/default/my-pod/{ip1:?}"); let services = HashMap::from([( "ns/svc1.ns.svc.cluster.local".to_string(), @@ -1828,12 +1849,14 @@ mod tests { .try_into() .unwrap(); for _ in 0..1000 { - if let Some((workload, _, _)) = state.state.read().unwrap().find_upstream( - strng::EMPTY, - &wl, - "127.0.1.1:80".parse().unwrap(), - ServiceResolutionMode::Standard, - ) { + if let Some(UpstreamDestination::UpstreamParts(workload, _, _)) = + state.state.read().unwrap().find_upstream( + strng::EMPTY, + &wl, + "127.0.1.1:80".parse().unwrap(), + ServiceResolutionMode::Standard, + ) + { let n = &workload.name; // borrow name instead of cloning found.insert(n.to_string()); // insert an owned copy of the borrowed n wants.remove(&n.to_string()); // remove using the borrow @@ -1849,11 +1872,8 @@ mod tests { #[tokio::test] async fn local_client() { - let cfg = ConfigSource::File( - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("examples") - .join("localhost.yaml"), - ); + let config_file = temp_file_with_content(LOCALHOST_YAML).unwrap(); + let cfg = ConfigSource::File(config_file.path().to_path_buf()); let (state, demand, _) = setup_test(); let local_client = LocalClient { cfg, @@ -1871,17 +1891,16 @@ mod tests { // Make sure we get a valid workload assert!(wl.is_some()); assert_eq!(wl.as_ref().unwrap().service_account, "default"); - let (_, port, svc) = demand - .state - .read() - .unwrap() - .find_upstream( - strng::EMPTY, - wl.as_ref().unwrap(), - "127.10.0.1:80".parse().unwrap(), - ServiceResolutionMode::Standard, - ) - .expect("should get"); + + let (port, svc) = match demand.state.read().unwrap().find_upstream( + strng::EMPTY, + wl.as_ref().unwrap(), + "127.10.0.1:80".parse().unwrap(), + ServiceResolutionMode::Standard, + ) { + Some(UpstreamDestination::UpstreamParts(_, port, svc)) => (port, svc), + _ => panic!("should get"), + }; // Make sure we get a valid VIP assert_eq!(port, 8080); assert_eq!( @@ -1890,17 +1909,15 @@ mod tests { ); // test that we can have a service in another network than workloads it selects - let (_, port, _) = demand - .state - .read() - .unwrap() - .find_upstream( - "remote".into(), - wl.as_ref().unwrap(), - "127.10.0.2:80".parse().unwrap(), - ServiceResolutionMode::Standard, - ) - .expect("should get"); + let port = match demand.state.read().unwrap().find_upstream( + "remote".into(), + wl.as_ref().unwrap(), + "127.10.0.2:80".parse().unwrap(), + ServiceResolutionMode::Standard, + ) { + Some(UpstreamDestination::UpstreamParts(_, port, _)) => port, + _ => panic!("should get"), + }; // Make sure we get a valid VIP assert_eq!(port, 8080); } diff --git a/src/telemetry.rs b/src/telemetry.rs index 228a9584ab..93c3b00788 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -24,7 +24,7 @@ use serde::Serializer; use serde::ser::SerializeMap; use thiserror::Error; -use tracing::{Event, Subscriber, error, field, info, warn}; +use tracing::{Event, Subscriber, field, info, warn}; use tracing_appender::non_blocking::NonBlocking; use tracing_core::Field; use tracing_core::field::Visit; @@ -88,8 +88,8 @@ fn default_filter() -> filter::Targets { // Read from env var, but prefix with setting DNS logs to warn as they are noisy; they can be explicitly overriden let var: String = env::var("RUST_LOG") .map_err(|_| ()) - .map(|v| "hickory_server::server::server_future=off,".to_string() + v.as_str()) - .unwrap_or("hickory_server::server::server_future=off,info".to_string()); + .map(|v| "hickory_server::server=off,".to_string() + v.as_str()) + .unwrap_or("hickory_server::server=off,info".to_string()); filter::Targets::from_str(&var).expect("static filter should build") } @@ -170,7 +170,7 @@ impl Visitor<'_> { } else { " " }; - write!(self.writer, "{}{:?}", padding, value) + write!(self.writer, "{padding}{value:?}") } } @@ -188,9 +188,9 @@ impl field::Visit for Visitor<'_> { // Skip fields that are actually log metadata that have already been handled name if name.starts_with("log.") => Ok(()), // For the message, write out the message and a tab to separate the future fields - "message" => write!(self.writer, "{:?}\t", val), + "message" => write!(self.writer, "{val:?}\t"), // For the rest, k=v. - _ => self.write_padded(&format_args!("{}={:?}", field.name(), val)), + _ => self.write_padded(&format_args!("{}={val:?}", field.name())), } } } @@ -234,17 +234,17 @@ where let target = meta.target(); // No need to prefix everything let target = target.strip_prefix("ztunnel::").unwrap_or(target); - write!(writer, "{}", target)?; + write!(writer, "{target}")?; // Write out span fields. Istio logging outside of Rust doesn't really have this concept if let Some(scope) = ctx.event_scope() { for span in scope.from_root() { write!(writer, ":{}", span.metadata().name())?; let ext = span.extensions(); - if let Some(fields) = &ext.get::>() { - if !fields.is_empty() { - write!(writer, "{{{}}}", fields)?; - } + if let Some(fields) = &ext.get::>() + && !fields.is_empty() + { + write!(writer, "{{{fields}}}")?; } } }; @@ -285,7 +285,7 @@ impl Visit for JsonVisitory { if self.state.is_ok() { self.state = self .serializer - .serialize_entry(field.name(), &format_args!("{:?}", value)) + .serialize_entry(field.name(), &format_args!("{value:?}")) } } @@ -326,9 +326,7 @@ impl io::Write for WriteAdaptor<'_> { let s = std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - self.fmt_write - .write_str(s) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + self.fmt_write.write_str(s).map_err(io::Error::other)?; Ok(s.len()) } @@ -507,7 +505,7 @@ pub mod testing { .map(|h| { h.iter() .sorted_by_key(|(k, _)| *k) - .map(|(k, err)| format!("{}:{}", k, err)) + .map(|(k, err)| format!("{k}:{err}")) .join("\n") }) .join("\n\n"); diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 2a052373eb..f3a07418db 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -30,6 +30,8 @@ use crate::xds::{Handler, LocalConfig, LocalWorkload, ProxyStateUpdater, XdsReso use anyhow::anyhow; use bytes::{BufMut, Bytes}; use hickory_resolver::config::*; +use std::io::Write; +use tempfile::NamedTempFile; use crate::{state, strng}; use http_body_util::{BodyExt, Full}; @@ -169,6 +171,11 @@ pub const TEST_SERVICE_HOST: &str = "local-vip.default.svc.cluster.local"; pub const TEST_SERVICE_DNS_HBONE_NAME: &str = "local-vip-async-dns"; pub const TEST_SERVICE_DNS_HBONE_HOST: &str = "local-vip-async-dns.default.svc.cluster.local"; +// Embedded test data - available when running binary outside source tree +pub const FAKE_JWT: &str = include_str!("test_helpers/fake-jwt"); +pub const MESH_CONFIG_YAML: &str = include_str!("test_helpers/mesh_config.yaml"); +pub const LOCALHOST_YAML: &str = include_str!("../examples/localhost.yaml"); + pub fn localhost_error_message() -> String { let addrs = &[ TEST_WORKLOAD_SOURCE, @@ -177,10 +184,9 @@ pub fn localhost_error_message() -> String { TEST_VIP, ]; format!( - "These tests use the following loopback addresses: {:?}. \ + "These tests use the following loopback addresses: {addrs:?}. \ Your OS may require an explicit alias for each. If so, you'll need to manually \ configure your system for each IP (e.g. `sudo ifconfig lo0 alias 127.0.0.2 up`).", - addrs ) } @@ -204,6 +210,7 @@ pub fn mock_default_service() -> Service { waypoint: None, load_balancer: None, ip_families: None, + canonical: true, } } @@ -247,7 +254,7 @@ fn test_custom_workload( hostname_only: bool, ) -> anyhow::Result { let host = match hostname_only { - true => format!("{}.reflect.internal.", ip_str), + true => format!("{ip_str}.reflect.internal."), false => "".to_string(), }; let wips = match hostname_only { @@ -258,7 +265,7 @@ fn test_custom_workload( workload_ips: wips, hostname: host.into(), protocol, - uid: format!("cluster1//v1/Pod/default/{}", name).into(), + uid: format!("cluster1//v1/Pod/default/{name}").into(), name: name.into(), namespace: "default".into(), service_account: "default".into(), @@ -290,7 +297,7 @@ fn test_custom_svc( }], ports: HashMap::from([(80u16, echo_port)]), endpoints: EndpointSet::from_list([Endpoint { - workload_uid: format!("cluster1//v1/Pod/default/{}", workload_name).into(), + workload_uid: format!("cluster1//v1/Pod/default/{workload_name}").into(), port: HashMap::from([(80u16, echo_port)]), status: HealthStatus::Healthy, }]), @@ -298,6 +305,7 @@ fn test_custom_svc( waypoint: None, load_balancer: None, ip_families: None, + canonical: true, }) } @@ -554,3 +562,12 @@ pub fn mpsc_ack(buffer: usize) -> (MpscAckSender, MpscAckReceiver) { let (ack_tx, ack_rx) = tokio::sync::mpsc::channel::<()>(1); (MpscAckSender { tx, ack_rx }, MpscAckReceiver { rx, ack_tx }) } + +/// Creates a temporary file with the given content and returns the path. +/// The file is automatically deleted when the returned NamedTempFile is dropped +pub fn temp_file_with_content(content: &str) -> std::io::Result { + let mut file = NamedTempFile::new()?; + file.write_all(content.as_bytes())?; + file.flush()?; + Ok(file) +} diff --git a/src/test_helpers/app.rs b/src/test_helpers/app.rs index c7ca0988cb..f63dd028f2 100644 --- a/src/test_helpers/app.rs +++ b/src/test_helpers/app.rs @@ -107,7 +107,7 @@ impl TestApp { let get_resp = move || async move { let req = Request::builder() .method(Method::GET) - .uri(format!("http://localhost:{}/{path}", port)) + .uri(format!("http://localhost:{port}/{path}")) .header("content-type", "application/json") .body(Empty::::new()) .unwrap(); @@ -125,11 +125,8 @@ impl TestApp { } None => get_resp().await, } - #[cfg(not(target_os = "linux"))] - { - get_resp().await - } + get_resp().await } pub async fn admin_request_body(&self, path: &str) -> anyhow::Result { let port = self.admin_address.port(); @@ -138,7 +135,7 @@ impl TestApp { let get_resp = move || async move { let req = Request::builder() .method(Method::GET) - .uri(format!("http://localhost:{}/{path}", port)) + .uri(format!("http://localhost:{port}/{path}")) .header("content-type", "application/json") .body(Empty::::new()) .unwrap(); @@ -152,11 +149,8 @@ impl TestApp { Some(ref ns) => ns.clone().run(get_resp)?.join().unwrap(), None => get_resp().await, } - #[cfg(not(target_os = "linux"))] - { - get_resp().await - } + get_resp().await } pub async fn metrics(&self) -> anyhow::Result { diff --git a/src/test_helpers/ca.rs b/src/test_helpers/ca.rs index e6ed041a04..2ab5be1529 100644 --- a/src/test_helpers/ca.rs +++ b/src/test_helpers/ca.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::PathBuf; use std::time::Duration; use async_trait::async_trait; @@ -27,6 +26,7 @@ use tracing::error; use crate::config::RootCert; use crate::identity::{AuthSource, CaClient}; +use crate::test_helpers::FAKE_JWT; use crate::test_helpers::hyper_tower; use crate::xds::istio::ca::istio_certificate_service_server::{ IstioCertificateService, IstioCertificateServiceServer, @@ -76,14 +76,12 @@ impl CaServer { } } }); + let client = CaClient::new( "https://".to_string() + &server_addr.to_string(), None, - Box::new(tls::ControlPlaneAuthentication::RootCert(root_cert)), - AuthSource::Token( - PathBuf::from(r"src/test_helpers/fake-jwt"), - "Kubernetes".to_string(), - ), + root_cert, + AuthSource::StaticToken(FAKE_JWT.to_string(), "Kubernetes".to_string()), true, 60 * 60 * 24, Vec::new(), diff --git a/src/test_helpers/dns.rs b/src/test_helpers/dns.rs index f7f5f22b0b..875ebe3800 100644 --- a/src/test_helpers/dns.rs +++ b/src/test_helpers/dns.rs @@ -298,6 +298,8 @@ pub async fn run_dns(responses: HashMap>) -> anyhow::Result TestWorkloadBuilder { + pub fn workload_builder(&mut self, name: &str, node: &str) -> TestWorkloadBuilder<'_> { TestWorkloadBuilder::new(name, self) .on_node(node) .identity(identity::Identity::Spiffe { @@ -352,7 +352,7 @@ impl WorkloadManager { } /// service_builder allows creating a new service - pub fn service_builder(&mut self, name: &str) -> TestServiceBuilder { + pub fn service_builder(&mut self, name: &str) -> TestServiceBuilder<'_> { TestServiceBuilder::new(name, self) } @@ -406,6 +406,7 @@ impl<'a> TestServiceBuilder<'a> { waypoint: None, load_balancer: None, ip_families: None, + canonical: true, }, manager, } diff --git a/src/test_helpers/tcp.rs b/src/test_helpers/tcp.rs index 77f7cfc2e2..6af6f3a155 100644 --- a/src/test_helpers/tcp.rs +++ b/src/test_helpers/tcp.rs @@ -31,7 +31,10 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::time::Instant; use tracing::{debug, error, info, trace}; +use crate::baggage::{Baggage, baggage_header_val}; use crate::hyper_util::TokioExecutor; +use crate::proxy::BAGGAGE_HEADER; +use crate::strng::Strng; use crate::{identity, tls}; #[derive(Copy, Clone, Debug)] @@ -247,7 +250,7 @@ impl HboneTestServer { &identity::Identity::Spiffe { trust_domain: "cluster.local".into(), namespace: "default".into(), - service_account: self.name.into(), + service_account: self.name.clone().into(), } .into(), Duration::from_secs(0), @@ -256,13 +259,16 @@ impl HboneTestServer { let acceptor = tls::mock::MockServerCertProvider::new(certs); let mut tls_stream = crate::hyper_util::tls_server(acceptor, self.listener); let mode = self.mode; + let name = self.name.clone(); while let Some(socket) = tls_stream.next().await { let waypoint_message = self.waypoint_message.clone(); + let name = name.clone(); if let Err(err) = http2::Builder::new(TokioExecutor) .serve_connection( TokioIo::new(socket), service_fn(move |req| { let waypoint_message = waypoint_message.clone(); + let name = name.clone(); async move { info!("waypoint: received request"); tokio::task::spawn(async move { @@ -277,7 +283,23 @@ impl HboneTestServer { Err(e) => error!("No upgrade {e}"), } }); - Ok::<_, Infallible>(Response::new(Full::::from("streaming..."))) + let mut resp = Response::new(Full::::from("streaming...")); + let baggage = Baggage { + cluster_id: Some(Strng::from("Kubernetes")), + namespace: Some(Strng::from("default")), + workload_name: Some(Strng::from(&name)), + service_name: Some(Strng::from(&name)), + revision: Some(Strng::from("v1")), + region: Some(Strng::from("r1")), + zone: Some(Strng::from("z1")), + }; + resp.headers_mut().insert( + BAGGAGE_HEADER, + baggage_header_val(&baggage, "deployment") + .parse() + .expect("valid baggage header"), + ); + Ok::<_, Infallible>(resp) } }), ) diff --git a/src/tls.rs b/src/tls.rs index 4228748e8b..1eebcadf01 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -14,6 +14,7 @@ mod certificate; mod control; +pub mod crl; pub mod csr; mod lib; #[cfg(any(test, feature = "testing"))] diff --git a/src/tls/certificate.rs b/src/tls/certificate.rs index 551058ea2c..24749309ed 100644 --- a/src/tls/certificate.rs +++ b/src/tls/certificate.rs @@ -23,7 +23,7 @@ use rustls::client::Resumption; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use rustls::server::WebPkiClientVerifier; -use rustls::{ClientConfig, RootCertStore, ServerConfig, server}; +use rustls::{ClientConfig, CommonState, RootCertStore, ServerConfig}; use rustls_pemfile::Item; use std::io::Cursor; use std::str::FromStr; @@ -36,8 +36,8 @@ use x509_parser::certificate::X509Certificate; #[derive(Clone, Debug)] pub struct Certificate { - pub(in crate::tls) expiry: Expiration, - der: CertificateDer<'static>, + pub expiry: Expiration, + pub der: CertificateDer<'static>, } #[derive(Clone, Debug)] @@ -52,7 +52,7 @@ pub struct WorkloadCertificate { pub cert: Certificate, /// chain is the entire trust chain, excluding the leaf and root pub chain: Vec, - pub(in crate::tls) private_key: PrivateKeyDer<'static>, + pub private_key: PrivateKeyDer<'static>, /// precomputed roots. This is used for verification root_store: Arc, @@ -60,7 +60,7 @@ pub struct WorkloadCertificate { pub roots: Vec, } -pub fn identity_from_connection(conn: &server::ServerConnection) -> Option { +pub fn identity_from_connection(conn: &CommonState) -> Option { use x509_parser::prelude::*; conn.peer_certificates() .and_then(|certs| certs.first()) @@ -110,7 +110,7 @@ pub fn identities(cert: X509Certificate) -> Result, Error> { impl Certificate { // TODO: I would love to parse this once, but ran into lifetime issues. - fn parsed(&self) -> X509Certificate { + fn parsed(&self) -> X509Certificate<'_> { x509_parser::parse_x509_certificate(&self.der) .expect("certificate was already parsed successfully before") .1 @@ -298,20 +298,35 @@ impl WorkloadCertificate { .collect() } - pub fn server_config(&self) -> Result { + pub fn server_config( + &self, + crl_manager: Option>, + ) -> Result { let td = self.cert.identity().map(|i| match i { Identity::Spiffe { trust_domain, .. } => trust_domain, }); - let raw_client_cert_verifier = WebPkiClientVerifier::builder_with_provider( + + // build the base client cert verifier with optional CRL support + let mut builder = WebPkiClientVerifier::builder_with_provider( self.root_store.clone(), crate::tls::lib::provider(), - ) - .build()?; + ); + + // add CRLs if available + if let Some(ref mgr) = crl_manager { + let crls = mgr.get_crl_ders(); + if !crls.is_empty() { + builder = builder.with_crls(crls).allow_unknown_revocation_status(); // fail-open for unknown status + } + } + + // TODO: check if our own certificate is revoked in the CRL and log warning + let raw_client_cert_verifier = builder.build()?; let client_cert_verifier = crate::tls::workload::TrustDomainVerifier::new(raw_client_cert_verifier, td); let mut sc = ServerConfig::builder_with_provider(crate::tls::lib::provider()) - .with_protocol_versions(tls::TLS_VERSIONS) + .with_protocol_versions(tls::tls_versions()) .expect("server config must be valid") .with_client_cert_verifier(client_cert_verifier) .with_single_cert( @@ -322,11 +337,13 @@ impl WorkloadCertificate { Ok(sc) } - pub fn outbound_connector(&self, identity: Vec) -> Result { + // TODO: add CRL support for outbound connections (client verifying server certs) + // this requires a separate design due to complexity - deferred for follow-up + pub fn client_config(&self, identity: Vec) -> Result { let roots = self.root_store.clone(); let verifier = IdentityVerifier { roots, identity }; let mut cc = ClientConfig::builder_with_provider(crate::tls::lib::provider()) - .with_protocol_versions(tls::TLS_VERSIONS) + .with_protocol_versions(tls::tls_versions()) .expect("client config must be valid") .dangerous() // Customer verifier is requires "dangerous" opt-in .with_custom_certificate_verifier(Arc::new(verifier)) @@ -337,6 +354,11 @@ impl WorkloadCertificate { cc.alpn_protocols = vec![b"h2".into()]; cc.resumption = Resumption::disabled(); cc.enable_sni = false; + Ok(cc) + } + + pub fn outbound_connector(&self, identity: Vec) -> Result { + let cc = self.client_config(identity)?; Ok(OutboundConnector { client_config: Arc::new(cc), }) @@ -428,7 +450,6 @@ mod test { SystemTime::now() + Duration::from_secs(60), None, TEST_ROOT_KEY, - TEST_ROOT, ); let cert1 = WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap(); @@ -440,13 +461,12 @@ mod test { SystemTime::now() + Duration::from_secs(60), None, TEST_ROOT2_KEY, - TEST_ROOT2, ); let cert2 = WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap(); // Do a simple handshake between them; we should be able to accept the trusted root - let server = cert1.server_config().unwrap(); + let server = cert1.server_config(None).unwrap(); let tls = TlsAcceptor::from(Arc::new(server)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); diff --git a/src/tls/control.rs b/src/tls/control.rs index 490676dbaa..ec8ed3bc48 100644 --- a/src/tls/control.rs +++ b/src/tls/control.rs @@ -21,13 +21,17 @@ use hyper::body::Incoming; use hyper_rustls::HttpsConnector; use hyper_util::client::legacy::connect::HttpConnector; use itertools::Itertools; +use notify::{RecommendedWatcher, Watcher}; +use notify_debouncer_full::{DebounceEventResult, Debouncer, FileIdMap, new_debouncer}; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme}; use std::future::Future; use std::io::Cursor; +use std::path::Path; use std::pin::Pin; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; use std::task::{Context, Poll}; use std::time::Duration; use tonic::body::Body; @@ -176,13 +180,13 @@ impl ServerCertVerifier for AltHostnameVerifier { } } -async fn control_plane_client_config( +pub(crate) async fn control_plane_client_config( root_cert: &RootCert, alt_hostname: Option, ) -> Result { let roots = root_to_store(root_cert).await?; let c = ClientConfig::builder_with_provider(provider()) - .with_protocol_versions(crate::tls::TLS_VERSIONS)?; + .with_protocol_versions(crate::tls::tls_versions())?; if let Some(alt_hostname) = alt_hostname { debug!("using alternate hostname {alt_hostname} for TLS verification"); Ok(c.dangerous() @@ -275,3 +279,185 @@ impl tower::Service> for TlsGrpcChannel { }) } } + +/// Internal state of [`RootCertManager`] +/// +/// Separated into its own struct to protect it behind a `RwLock` +/// and be mutated after the outer `Arc` is created +struct RootCertManagerInner { + /// Original cert source. Retained to be able to read it again on reload + root_cert: RootCert, + + /// OS watcher + _debouncer: Option>, +} + +/// Watches the CA root certificate file for changes and signals it with a flag. +/// +/// Note: watcher is set on CA root cet's parent directory due to Kubernetes handle of ConfigMaps +/// Whenever a ConfigMap changes Kubernetes creates a new timestamped folder with the content +/// and atomically hot-swap a symlink to point to the new folder. +/// If we setup a watcher on the file itself we will miss this event +pub(crate) struct RootCertManager { + inner: RwLock, + + /// Set to `true` by OS watcher when cert directory changes => ConfigMap changed. + /// Reset by CaClient after a successful channel rebuild + dirty: AtomicBool, +} + +impl RootCertManager { + /// Creates a new manager for `root_cert` + /// + /// If `root_cert` is a `RootCert::File` it starts a folder watcher on the cert's parent dir + /// + /// Returns an error if cert's parent dir cannot be watched (e.g. it does not exist). + /// A missing cert file is not detect here, it will be detected at channel rebuild by [`CaClient`] + pub(crate) fn new(root_cert: RootCert) -> Result, Error> { + let manager = Arc::new(Self { + inner: RwLock::new(RootCertManagerInner { + root_cert: root_cert.clone(), + _debouncer: None, + }), + dirty: AtomicBool::new(false), + }); + + if let RootCert::File(path) = &root_cert { + let debouncer = manager.start_watcher(path)?; + + manager.inner.write().unwrap()._debouncer = Some(debouncer); + } + + Ok(manager) + } + + /// Atomically reads and clears the dirty flag + pub(crate) fn take_dirty(&self) -> bool { + self.dirty.swap(false, Ordering::AcqRel) + } + + /// Re-arms dirty flag + pub(crate) fn mark_dirty(&self) { + self.dirty.store(true, Ordering::Release); + } + + /// Returns the original cert source + pub(crate) fn root_cert(&self) -> RootCert { + self.inner.read().unwrap().root_cert.clone() + } + + /// Register an OS dir watcher on `path`'s parent dir. + /// + /// Any filesystem event in that dir will set the `dirty` flag after a (2 seconds) debounce + fn start_watcher( + self: &Arc, + path: &Path, + ) -> Result, Error> { + let watch_dir = path.parent().ok_or_else(|| { + Error::InvalidRootCert("root cert path must have a parent directory".to_string()) + })?; + + let manager = Arc::clone(self); + + let mut debouncer = new_debouncer( + Duration::from_secs(2), + None, + move |result: DebounceEventResult| match result { + Ok(events) => { + debug!( + event_count = events.len(), + "root cert directory changed; scheduling TLS channel rebuild" + ); + if !events.is_empty() { + manager.dirty.store(true, Ordering::Release); + } + } + Err(errors) => { + for e in errors { + debug!(error = ?e, "root cert watcher error"); + } + } + }, + ) + .map_err(|e| Error::InvalidRootCert(format!("failed to create root cert watcher: {e}")))?; + + debouncer + .watcher() + .watch(watch_dir, notify::RecursiveMode::NonRecursive) + .map_err(|e| { + Error::InvalidRootCert(format!( + "failed to watch root cert directory {watch_dir:?}: {e}" + )) + })?; + + debug!(path = ?watch_dir, "root cert file watcher started"); + Ok(debouncer) + } +} + +#[cfg(test)] +mod tests { + + use std::io::Write; + + use bytes::Bytes; + use tempfile::NamedTempFile; + + use crate::tls::mock::TEST_ROOT; + + use super::*; + + #[test] + fn static_cert_is_never_dirty() { + let manager = RootCertManager::new(RootCert::Static(Bytes::from_static(TEST_ROOT))) + .expect("static cert manager must not fail"); + + assert!(!manager.take_dirty(), "static cert must never be dirty"); + assert!( + manager.inner.read().unwrap()._debouncer.is_none(), + "no debouncer for static certs" + ); + } + + #[test] + fn file_cert_starts_clean() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(TEST_ROOT).unwrap(); + + let manager = RootCertManager::new(RootCert::File(file.path().to_path_buf())) + .expect("file cert manager must not fail"); + + assert!(!manager.take_dirty(), "new manager must not start dirty"); + assert!( + manager.inner.read().unwrap()._debouncer.is_some(), + "file cert must have a debouncer" + ); + } + + #[test] + fn take_dirty_and_make_dirty_work() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(TEST_ROOT).unwrap(); + + let manager = RootCertManager::new(RootCert::File(file.path().to_path_buf())) + .expect("file cert manager must not fail"); + + assert!(!manager.take_dirty(), "new manager must not start dirty"); + + manager.dirty.store(true, Ordering::Release); + assert!( + manager.take_dirty(), + "take_dirty should return true when dirty" + ); + assert!( + !manager.take_dirty(), + "take_dirty should return false after clearing" + ); + + manager.mark_dirty(); + assert!( + manager.take_dirty(), + "take_dirty should return true after rearm" + ); + } +} diff --git a/src/tls/crl.rs b/src/tls/crl.rs new file mode 100644 index 0000000000..25c9dab55b --- /dev/null +++ b/src/tls/crl.rs @@ -0,0 +1,350 @@ +// Copyright Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use notify::RecommendedWatcher; +use notify_debouncer_full::{ + DebounceEventResult, Debouncer, FileIdMap, new_debouncer, + notify::{RecursiveMode, Watcher}, +}; +use rustls::pki_types::CertificateRevocationListDer; +use rustls_pemfile::Item; +use std::io::Cursor; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use tracing::{debug, warn}; + +#[derive(Debug, thiserror::Error)] +pub enum CrlError { + #[error("failed to read CRL file: {0}")] + IoError(#[from] std::io::Error), + + #[error("failed to parse CRL: {0}")] + ParseError(String), + + #[error("CRL error: {0}")] + WebPkiError(String), +} + +#[derive(Clone)] +/// NOTE: CRL updates take effect when new ServerConfigs are created, which happens +/// on certificate refresh (~12hrs). For immediate CRL enforcement, a custom +/// ClientCertVerifier wrapper would be needed, but rustls doesn't provide a +/// built-in mechanism like `with_cert_resolver` for dynamic CRL updates. +pub struct CrlManager { + inner: Arc>, +} + +impl std::fmt::Debug for CrlManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CrlManager").finish_non_exhaustive() + } +} + +struct CrlManagerInner { + crl_ders: Option>>, // None = not loaded, Some = loaded (may be empty) + crl_path: PathBuf, + _debouncer: Option>, +} + +impl CrlManager { + /// creates a new CRL manager + pub fn new(crl_path: PathBuf) -> Result { + debug!(path = ?crl_path, "initializing crl manager"); + + let manager = Self { + inner: Arc::new(RwLock::new(CrlManagerInner { + crl_ders: None, + crl_path: crl_path.clone(), + _debouncer: None, + })), + }; + + // try to load the CRL, but don't fail if the file doesn't exist yet + // (it might be mounted later via ConfigMap) + if let Err(e) = manager.load_crl() { + match e { + CrlError::IoError(ref io_err) if io_err.kind() == std::io::ErrorKind::NotFound => { + warn!( + path = ?crl_path, + "crl file not found, will retry on first validation" + ); + } + _ => { + debug!(error = %e, "failed to initialize crl manager"); + return Err(e); + } + } + } + + Ok(manager) + } + + pub fn load_crl(&self) -> Result<(), CrlError> { + let mut inner = self.inner.write().unwrap(); + + let data = std::fs::read(&inner.crl_path)?; + + // empty file means no revocations - this is valid + if data.is_empty() { + debug!(path = ?inner.crl_path, "crl file is empty, treating as no revocations"); + inner.crl_ders = Some(Vec::new()); + return Ok(()); + } + + // parse all CRL blocks (handles concatenated CRLs) + let is_pem = data.starts_with(b"-----BEGIN"); + let der_crls = if is_pem { + Self::parse_pem_crls(&data)? + } else { + vec![data] + }; + + // empty PEM file (no CRL blocks) means no revocations + if der_crls.is_empty() { + debug!(path = ?inner.crl_path, "no crl blocks found, treating as no revocations"); + inner.crl_ders = Some(Vec::new()); + return Ok(()); + } + + let mut validated_ders = Vec::new(); + + for (idx, der_data) in der_crls.into_iter().enumerate() { + // validate with webpki to catch parse errors early + // rustls will use the raw DER bytes directly + webpki::OwnedCertRevocationList::from_der(&der_data).map_err(|e| { + CrlError::WebPkiError(format!("failed to parse crl {}: {:?}", idx + 1, e)) + })?; + + validated_ders.push(der_data); + } + + // store validated DER bytes + inner.crl_ders = Some(validated_ders); + + debug!( + path = ?inner.crl_path, + format = if is_pem { "PEM" } else { "DER" }, + count = inner.crl_ders.as_ref().map(|v| v.len()).unwrap_or(0), + "crl loaded successfully" + ); + Ok(()) + } + + /// parses PEM-encoded CRL data that may contain multiple CRL blocks + /// returns a Vec of DER-encoded CRLs (empty vec if no blocks found) + fn parse_pem_crls(pem_data: &[u8]) -> Result>, CrlError> { + let mut reader = std::io::BufReader::new(Cursor::new(pem_data)); + + rustls_pemfile::read_all(&mut reader) + .filter_map(|result| match result { + Ok(Item::Crl(crl)) => Some(Ok(crl.to_vec())), + Ok(_) => None, // skip non-CRL items + Err(e) => Some(Err(CrlError::ParseError(format!( + "failed to parse PEM: {}", + e + )))), + }) + .collect() + } + + /// returns CRLs as DER bytes for rustls's with_crls(). + /// if no CRLs are loaded, attempts to load them first. + pub fn get_crl_ders(&self) -> Vec> { + let inner = self.inner.read().unwrap(); + if let Some(ref crl_ders) = inner.crl_ders { + // already loaded, use existing lock directly + crl_ders + .iter() + .map(|der| CertificateRevocationListDer::from(der.clone())) + .collect() + } else { + // not loaded yet, drop lock to call load_crl() + drop(inner); + debug!("crl not loaded, attempting to load now"); + if let Err(e) = self.load_crl() { + debug!(error = %e, "failed to load crl"); + return Vec::new(); + } + // re-acquire after loading + let inner = self.inner.read().unwrap(); + inner + .crl_ders + .as_ref() + .map(|ders| { + ders.iter() + .map(|der| CertificateRevocationListDer::from(der.clone())) + .collect() + }) + .unwrap_or_default() + } + } + + /// starts watching the CRL file for changes. + /// uses debouncer to handle all file update patterns + pub fn start_file_watcher(self: &Arc) -> Result<(), CrlError> { + let crl_path = { + let inner = self.inner.read().unwrap(); + inner.crl_path.clone() + }; + + // watch the parent directory to catch ConfigMap updates via symlinks + let watch_path = crl_path + .parent() + .ok_or_else(|| CrlError::ParseError("crl path has no parent directory".to_string()))?; + + debug!( + path = ?watch_path, + debounce_secs = 2, + "starting crl file watcher" + ); + + let manager = Arc::clone(self); + + // create debouncer with 2-second timeout + // this collapses multiple events (CREATE/CHMOD/RENAME/REMOVE) into a single reload + let mut debouncer = new_debouncer( + Duration::from_secs(2), + None, + move |result: DebounceEventResult| { + match result { + Ok(events) => { + if !events.is_empty() { + debug!(event_count = events.len(), "crl directory events detected"); + + // reload CRL for any changes in the watched directory + // this handles Kubernetes ConfigMap updates (..data symlink changes) + // as well as direct file writes and text editor saves + debug!("crl directory changed, reloading"); + match manager.load_crl() { + Ok(()) => { + debug!("crl reloaded successfully after file change"); + } + Err(e) => debug!(error = %e, "failed to reload crl"), + } + } + } + Err(errors) => { + for error in errors { + debug!(error = ?error, "crl watcher error"); + } + } + } + }, + ) + .map_err(|e| CrlError::ParseError(format!("failed to create debouncer: {}", e)))?; + + // start watching the directory + debouncer + .watcher() + .watch(watch_path, RecursiveMode::NonRecursive) + .map_err(|e| CrlError::ParseError(format!("failed to watch directory: {}", e)))?; + + // store debouncer to keep it alive + { + let mut inner = self.inner.write().unwrap(); + inner._debouncer = Some(debouncer); + } + + debug!("crl file watcher started successfully"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_crl_manager_missing_file() { + let result = CrlManager::new(PathBuf::from("/nonexistent/path/crl.pem")); + assert!(result.is_ok(), "should handle missing CRL file gracefully"); + } + + #[test] + fn test_crl_manager_invalid_file() { + let mut file = NamedTempFile::new().expect("failed to create temporary test file"); + file.write_all(b"not a valid CRL") + .expect("failed to write test data to temporary file"); + file.flush().expect("failed to flush temporary test file"); + + let result = CrlManager::new(file.path().to_path_buf()); + assert!(result.is_err(), "should fail on invalid CRL data"); + } + + #[test] + fn test_crl_manager_empty_file() { + let file = NamedTempFile::new().expect("failed to create temporary test file"); + // file is empty by default + + let result = CrlManager::new(file.path().to_path_buf()); + assert!(result.is_ok(), "should handle empty CRL file gracefully"); + } + + #[test] + fn test_crl_manager_valid_crl() { + use rcgen::{ + CertificateParams, CertificateRevocationListParams, Issuer, KeyIdMethod, KeyPair, + RevocationReason, RevokedCertParams, SerialNumber, + }; + + // generate a CA key pair + let ca_key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256) + .expect("failed to generate CA key pair"); + + // create CA certificate params + let mut ca_params = CertificateParams::default(); + ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + ca_params.key_usages = vec![ + rcgen::KeyUsagePurpose::KeyCertSign, + rcgen::KeyUsagePurpose::CrlSign, + ]; + + // create issuer from CA params and key + let issuer = Issuer::from_params(&ca_params, &ca_key_pair); + + // create CRL with one revoked certificate + let crl_params = CertificateRevocationListParams { + this_update: time::OffsetDateTime::now_utc(), + next_update: time::OffsetDateTime::now_utc() + time::Duration::days(30), + crl_number: SerialNumber::from(1u64), + issuing_distribution_point: None, + revoked_certs: vec![RevokedCertParams { + serial_number: SerialNumber::from(12345u64), + revocation_time: time::OffsetDateTime::now_utc(), + reason_code: Some(RevocationReason::KeyCompromise), + invalidity_date: None, + }], + key_identifier_method: KeyIdMethod::Sha256, + }; + + let crl = crl_params.signed_by(&issuer).expect("failed to sign CRL"); + let crl_pem = crl.pem().expect("failed to encode CRL as PEM"); + + // write CRL to temp file + let mut file = NamedTempFile::new().expect("failed to create temporary test file"); + file.write_all(crl_pem.as_bytes()) + .expect("failed to write CRL to temporary file"); + file.flush().expect("failed to flush temporary test file"); + + // test that CrlManager can load it + let manager = CrlManager::new(file.path().to_path_buf()) + .expect("should successfully parse valid CRL"); + + let ders = manager.get_crl_ders(); + assert_eq!(ders.len(), 1, "should have loaded one CRL"); + } +} diff --git a/src/tls/lib.rs b/src/tls/lib.rs index b0703c83da..40d0697a01 100644 --- a/src/tls/lib.rs +++ b/src/tls/lib.rs @@ -14,6 +14,9 @@ use super::Error; +#[allow(unused_imports)] +use crate::PQC_ENABLED; +use crate::TLS12_ENABLED; use crate::identity::{self, Identity}; use std::fmt::Debug; @@ -26,8 +29,6 @@ use rustls::crypto::CryptoProvider; use rustls::ClientConfig; use rustls::ServerConfig; -use tracing::error; - #[async_trait::async_trait] pub trait ControlPlaneClientCertProvider: Send + Sync { async fn fetch_cert(&self, alt_hostname: Option) -> Result; @@ -38,7 +39,17 @@ pub trait ServerCertProvider: Send + Sync + Clone { async fn fetch_cert(&mut self) -> Result, TlsError>; } -pub(super) static TLS_VERSIONS: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13]; +static TLS_VERSIONS_13_ONLY: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13]; +static TLS_VERSIONS_12_AND_13: &[&rustls::SupportedProtocolVersion] = + &[&rustls::version::TLS13, &rustls::version::TLS12]; + +pub fn tls_versions() -> &'static [&'static rustls::SupportedProtocolVersion] { + if *TLS12_ENABLED { + TLS_VERSIONS_12_AND_13 + } else { + TLS_VERSIONS_13_ONLY + } +} #[cfg(feature = "tls-aws-lc")] pub static CRYPTO_PROVIDER: &str = "tls-aws-lc"; @@ -59,42 +70,98 @@ pub static CRYPTO_PROVIDER: &str = "tls-openssl"; pub(super) fn provider() -> Arc { // Due to 'fips-only' feature on the boring provider, this will use only AES_256_GCM_SHA384 // and AES_128_GCM_SHA256 - // In later code we select to only use TLS 1.3 Arc::new(boring_rustls_provider::provider()) } #[cfg(feature = "tls-ring")] pub(super) fn provider() -> Arc { + let mut cipher_suites = vec![ + rustls::crypto::ring::cipher_suite::TLS13_AES_256_GCM_SHA384, + rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256, + ]; + if *TLS12_ENABLED { + // Add TLS 1.2 FIPS-compatible cipher suites + cipher_suites.extend([ + rustls::crypto::ring::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + rustls::crypto::ring::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + rustls::crypto::ring::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + rustls::crypto::ring::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ]); + } Arc::new(CryptoProvider { - // Limit to only the subset of ciphers that are FIPS compatible - cipher_suites: vec![ - rustls::crypto::ring::cipher_suite::TLS13_AES_256_GCM_SHA384, - rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256, - ], + cipher_suites, ..rustls::crypto::ring::default_provider() }) } #[cfg(feature = "tls-aws-lc")] pub(super) fn provider() -> Arc { - Arc::new(CryptoProvider { - // Limit to only the subset of ciphers that are FIPS compatible - cipher_suites: vec![ - rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384, - rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256, - ], + let mut cipher_suites = vec![ + rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384, + rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256, + ]; + if *TLS12_ENABLED { + // Add TLS 1.2 FIPS-compatible cipher suites + cipher_suites.extend([ + rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + rustls::crypto::aws_lc_rs::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ]); + } + let mut provider = CryptoProvider { + cipher_suites, ..rustls::crypto::aws_lc_rs::default_provider() - }) + }; + + if *PQC_ENABLED { + provider.kx_groups = vec![rustls::crypto::aws_lc_rs::kx_group::X25519MLKEM768] + } + + Arc::new(provider) } #[cfg(feature = "tls-openssl")] pub(super) fn provider() -> Arc { + let mut cipher_suites = vec![ + rustls_openssl::cipher_suite::TLS13_AES_256_GCM_SHA384, + rustls_openssl::cipher_suite::TLS13_AES_128_GCM_SHA256, + ]; + if *TLS12_ENABLED { + // Add TLS 1.2 FIPS-compatible cipher suites + cipher_suites.extend([ + rustls_openssl::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + rustls_openssl::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + rustls_openssl::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + rustls_openssl::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ]); + } + + let kx_groups: Vec<&'static dyn rustls::crypto::SupportedKxGroup> = if *PQC_ENABLED { + // To use PQC with OpenSSL provider the binary needs to be + // both compiled and used with OpenSSL >= 3.5.0. + #[cfg(ossl350)] + { + if openssl::version::number() >= 0x30500000 { + vec![rustls_openssl::kx_group::X25519MLKEM768] + } else { + panic!("COMPLIANCE_POLICY=pqc requires OpenSSL >=3.5.0"); + } + } + #[cfg(not(ossl350))] + { + panic!("COMPLIANCE_POLICY=pqc requires compilation with OpenSSL >=3.5.0"); + } + } else { + vec![ + rustls_openssl::kx_group::SECP256R1, + rustls_openssl::kx_group::SECP384R1, + ] + }; + Arc::new(CryptoProvider { - // Limit to only the subset of ciphers that are FIPS compatible - cipher_suites: vec![ - rustls_openssl::cipher_suite::TLS13_AES_256_GCM_SHA384, - rustls_openssl::cipher_suite::TLS13_AES_128_GCM_SHA256, - ], + cipher_suites, + kx_groups, ..rustls_openssl::default_provider() }) } @@ -208,4 +275,39 @@ pub mod tests { assert!(!future_certs.is_expired()); assert_eq!(future_certs.get_duration_until_refresh(), zero_dur); } + + #[test] + #[cfg(feature = "tls-openssl")] + fn test_openssl_provider_created_successfully() { + // Test that provider can be created without panicking + let provider = super::provider(); + assert!( + !provider.kx_groups.is_empty(), + "kx_groups should not be empty" + ); + } + + #[test] + #[cfg(feature = "tls-openssl")] + fn test_openssl_provider_kx_groups_valid() { + // Provider must have valid key exchange groups regardless of PQC state + let provider = super::provider(); + let expected_len = if *crate::PQC_ENABLED { 1 } else { 2 }; + assert_eq!( + provider.kx_groups.len(), + expected_len, + "PQC={} should have {} kx groups", + *crate::PQC_ENABLED, + expected_len + ); + } + + #[test] + #[cfg(all(feature = "tls-openssl", not(ossl350)))] + fn test_pqc_panic_expected_without_ossl350() { + // Without ossl350 cfg, PQC cannot be enabled (would panic in provider()) + if *crate::PQC_ENABLED { + panic!("PQC_ENABLED=true without ossl350 cfg - provider() will panic"); + } + } } diff --git a/src/tls/mock.rs b/src/tls/mock.rs index b5c6112164..f52a0e704e 100644 --- a/src/tls/mock.rs +++ b/src/tls/mock.rs @@ -18,12 +18,11 @@ use std::fmt::{Display, Formatter}; use rand::RngCore; use rand::SeedableRng; use rand::rngs::SmallRng; -use rcgen::{Certificate, CertificateParams, KeyPair}; use std::net::IpAddr; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use crate::tls::TLS_VERSIONS; +use crate::tls::tls_versions; use rustls::ServerConfig; use super::{ServerCertProvider, TlsError, WorkloadCertificate}; @@ -105,8 +104,7 @@ pub fn generate_test_certs_at( not_after: SystemTime, rng: Option<&mut dyn rand::RngCore>, ) -> WorkloadCertificate { - let (key, cert) = - generate_test_certs_with_root(id, not_before, not_after, rng, TEST_ROOT_KEY, TEST_ROOT); + let (key, cert) = generate_test_certs_with_root(id, not_before, not_after, rng, TEST_ROOT_KEY); let mut workload = WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![TEST_ROOT]).unwrap(); // Certificates do not allow sub-millisecond, but we need this for tests. @@ -121,7 +119,6 @@ pub fn generate_test_certs_with_root( not_after: SystemTime, rng: Option<&mut dyn rand::RngCore>, ca_key: &[u8], - ca_cert: &[u8], ) -> (String, String) { use rcgen::*; let serial_number = { @@ -150,15 +147,17 @@ pub fn generate_test_certs_with_root( ExtendedKeyUsagePurpose::ClientAuth, ]; p.subject_alt_names = vec![match id { - TestIdentity::Identity(i) => SanType::URI(Ia5String::try_from(i.to_string()).unwrap()), + TestIdentity::Identity(i) => { + SanType::URI(string::Ia5String::try_from(i.to_string()).unwrap()) + } TestIdentity::Ip(i) => SanType::IpAddress(*i), }]; let kp = KeyPair::from_pem(std::str::from_utf8(TEST_PKEY).unwrap()).unwrap(); let ca_kp = KeyPair::from_pem(std::str::from_utf8(ca_key).unwrap()).unwrap(); let key = kp.serialize_pem(); - let ca = test_ca(ca_key, ca_cert); - let cert = p.signed_by(&kp, &ca, &ca_kp).unwrap(); + let issuer = Issuer::from_params(&p, &ca_kp); + let cert = p.signed_by(&kp, &issuer).unwrap(); let cert = cert.pem(); (key, cert) } @@ -172,12 +171,6 @@ pub fn generate_test_certs( generate_test_certs_at(id, not_before, not_before + duration_until_expiry, None) } -fn test_ca(key: &[u8], cert: &[u8]) -> Certificate { - let key = KeyPair::from_pem(std::str::from_utf8(key).unwrap()).unwrap(); - let ca_param = CertificateParams::from_ca_cert_pem(std::str::from_utf8(cert).unwrap()).unwrap(); - ca_param.self_signed(&key).unwrap() -} - #[derive(Debug, Clone)] pub struct MockServerCertProvider(Arc); @@ -191,7 +184,7 @@ impl MockServerCertProvider { impl ServerCertProvider for MockServerCertProvider { async fn fetch_cert(&mut self) -> Result, TlsError> { let mut sc = ServerConfig::builder_with_provider(crate::tls::lib::provider()) - .with_protocol_versions(TLS_VERSIONS) + .with_protocol_versions(tls_versions()) .expect("server config must be valid") .with_no_client_auth() .with_single_cert( diff --git a/src/version.rs b/src/version.rs index abf85db26c..f4e08c60e7 100644 --- a/src/version.rs +++ b/src/version.rs @@ -22,7 +22,6 @@ use crate::tls::CRYPTO_PROVIDER; const BUILD_VERSION: &str = env!("ZTUNNEL_BUILD_buildVersion"); const BUILD_GIT_REVISION: &str = env!("ZTUNNEL_BUILD_buildGitRevision"); const BUILD_STATUS: &str = env!("ZTUNNEL_BUILD_buildStatus"); -const BUILD_TAG: &str = env!("ZTUNNEL_BUILD_buildTag"); const BUILD_RUST_VERSION: &str = env!("ZTUNNEL_BUILD_RUSTC_VERSION"); const BUILD_RUST_PROFILE: &str = env!("ZTUNNEL_BUILD_PROFILE_NAME"); @@ -34,7 +33,6 @@ pub struct BuildInfo { rust_version: String, build_profile: String, build_status: String, - git_tag: String, pub istio_version: String, crypto_provider: String, } @@ -47,7 +45,6 @@ impl BuildInfo { rust_version: BUILD_RUST_VERSION.to_string(), build_profile: BUILD_RUST_PROFILE.to_string(), build_status: BUILD_STATUS.to_string(), - git_tag: BUILD_TAG.to_string(), istio_version: env::var("ISTIO_META_ISTIO_VERSION") .unwrap_or_else(|_| "unknown".to_string()), crypto_provider: CRYPTO_PROVIDER.to_string(), @@ -59,13 +56,12 @@ impl Display for BuildInfo { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, - "version.BuildInfo{{Version:\"{}\", GitRevision:\"{}\", RustVersion:\"{}\", BuildProfile:\"{}\", BuildStatus:\"{}\", GitTag:\"{}\", IstioVersion:\"{}\", CryptoProvider:\"{}\"}}", + "version.BuildInfo{{Version:\"{}\", GitRevision:\"{}\", RustVersion:\"{}\", BuildProfile:\"{}\", BuildStatus:\"{}\", IstioVersion:\"{}\", CryptoProvider:\"{}\"}}", self.version, self.git_revision, self.rust_version, self.build_profile, self.build_status, - self.git_tag, self.istio_version, self.crypto_provider, ) diff --git a/src/xds.rs b/src/xds.rs index fc5fc1cb49..fe04c24940 100644 --- a/src/xds.rs +++ b/src/xds.rs @@ -21,7 +21,9 @@ use std::sync::{Arc, RwLock}; use tracing::Level; use tokio::sync::mpsc; -use tracing::{debug, error, info, instrument, trace, warn}; +#[cfg(any(test, feature = "testing"))] +use tracing::error; +use tracing::{debug, info, instrument, trace, warn}; pub use client::*; pub use metrics::*; @@ -62,10 +64,10 @@ impl fmt::Display for DisplayStatus<'_> { " (hint: check the control plane logs for more information)" )?; } - if !s.details().is_empty() { - if let Ok(st) = std::str::from_utf8(s.details()) { - write!(f, ", details: {st}")?; - } + if !s.details().is_empty() + && let Ok(st) = std::str::from_utf8(s.details()) + { + write!(f, ", details: {st}")?; } if let Some(src) = s.source().and_then(|s| s.source()) { write!(f, ", source: {src}")?; diff --git a/src/xds/client.rs b/src/xds/client.rs index adb39f7eac..47888d233f 100644 --- a/src/xds/client.rs +++ b/src/xds/client.rs @@ -131,7 +131,7 @@ struct HandlerWrapper { h: Box>, } -impl RawHandler for HandlerWrapper { +impl RawHandler for HandlerWrapper { fn handle( &self, state: &mut State, @@ -270,7 +270,7 @@ impl Config { pub fn with_watched_handler(self, type_url: Strng, f: impl Handler) -> Config where - F: 'static + prost::Message + Default, + F: 'static + fmt::Debug + prost::Message + Default, { let no_on_demand = f.no_on_demand(); self.with_handler(type_url.clone(), f) @@ -279,7 +279,7 @@ impl Config { fn with_handler(mut self, type_url: Strng, f: impl Handler) -> Config where - F: 'static + prost::Message + Default, + F: 'static + fmt::Debug + prost::Message + Default, { let h = HandlerWrapper { h: Box::new(f) }; self.handlers.insert(type_url, Box::new(h)); @@ -667,14 +667,13 @@ impl AdsClient { if !self.types_to_expect.is_empty() { received_type = Some(msg.type_url.clone()) } - if let XdsSignal::Ack = self.handle_stream_event(msg, &discovery_req_tx).await? { - if let Some(received_type) = received_type { + if let XdsSignal::Ack = self.handle_stream_event(msg, &discovery_req_tx).await? + && let Some(received_type) = received_type { self.types_to_expect.remove(&received_type); if self.types_to_expect.is_empty() { mem::drop(mem::take(&mut self.block_ready)); } - } - }; + }; } } } @@ -688,7 +687,7 @@ impl AdsClient { let type_url = response.type_url.clone(); let nonce = response.nonce.clone(); self.metrics.record(&response, ()); - info!( + debug!( type_url = type_url, // this is a borrow, it's OK size = response.resources.len(), removes = response.removed_resources.len(), @@ -877,7 +876,7 @@ mod tests { fn get_auth(i: usize) -> ProtoResource { let addr = XdsAuthorization { - name: format!("foo{}", i), + name: format!("foo{i}"), namespace: "default".to_string(), scope: crate::xds::istio::security::Scope::Global as i32, action: crate::xds::istio::security::Action::Deny as i32, @@ -889,9 +888,10 @@ mod tests { }], }], }], + dry_run: false, }; ProtoResource { - name: format!("foo{}", i), + name: format!("foo{i}"), aliases: vec![], version: "0.0.1".to_string(), resource: Some(Any { @@ -909,8 +909,8 @@ mod tests { }; let addr = XdsAddress { r#type: Some(XdsType::Workload(XdsWorkload { - name: format!("foo{}", i), - uid: format!("default/foo{}", i), + name: format!("foo{i}"), + uid: format!("default/foo{i}"), namespace: "default".to_string(), addresses: vec![octets.into()], tunnel_protocol: 0, @@ -925,7 +925,7 @@ mod tests { }; ProtoResource { - name: format!("foo{}", i), + name: format!("foo{i}"), aliases: vec![], version: "0.0.1".to_string(), resource: Some(Any { diff --git a/tests/direct.rs b/tests/direct.rs index b510895015..e1b7af3fdc 100644 --- a/tests/direct.rs +++ b/tests/direct.rs @@ -198,8 +198,36 @@ async fn test_quit_lifecycle() { .expect("app exits without error"); } +fn process_metrics_assertions(metrics: &ParsedMetrics) { + for metric in ["process_open_fds", "process_max_fds"] { + let metric = &(metric); + let m = metrics.query(metric, &Default::default()); + assert!(m.is_some(), "expected metric {metric}"); + assert!( + m.to_owned().unwrap().len() == 1, + "expected metric {metric} to have len(1)" + ); + let value = m.unwrap()[0].value.clone(); + match value { + prometheus_parse::Value::Gauge(v) => { + assert!( + v > 0.0, + "expected metric {metric} to be positive, was {value:?}", + ); + } + _ => { + panic!("unexpected metric type"); + } + } + } +} + +fn base_metrics_assertions(metrics: ParsedMetrics) { + process_metrics_assertions(&metrics); +} + async fn run_request_test(target: &str, node: &str) { - run_requests_test(target, node, 1, None, false).await + run_requests_test(target, node, 1, Some(base_metrics_assertions), false).await } async fn run_requests_test( @@ -264,28 +292,25 @@ async fn test_vip_request() { } fn on_demand_dns_assertions(metrics: ParsedMetrics) { - { - let metric = &("istio_on_demand_dns_total"); - let m = metrics.query(metric, &Default::default()); - assert!(m.is_some(), "expected metric {metric}"); - // expecting one cache hit and one cache miss - assert!( - m.to_owned().unwrap().len() == 1, - "expected metric {metric} to have len(1)" - ); - let value = m.unwrap()[0].value.clone(); - let expected = match *metric { - "istio_on_demand_dns_total" => prometheus_parse::Value::Untyped(2.0), - &_ => { - panic!("dev error; unexpected metric"); - } - }; - assert!( - value == expected, - "expected metric {metric} to be 1, was {:?}", - value - ); - } + let metric = &("istio_on_demand_dns_total"); + let m = metrics.query(metric, &Default::default()); + assert!(m.is_some(), "expected metric {metric}"); + // expecting one cache hit and one cache miss + assert!( + m.to_owned().unwrap().len() == 1, + "expected metric {metric} to have len(1)" + ); + let value = m.unwrap()[0].value.clone(); + let expected = match *metric { + "istio_on_demand_dns_total" => prometheus_parse::Value::Untyped(2.0), + &_ => { + panic!("dev error; unexpected metric"); + } + }; + assert!( + value == expected, + "expected metric {metric} to be 1, was {value:?}", + ); } #[tokio::test] @@ -357,13 +382,15 @@ async fn test_stats_exist() { "istio_tcp_connections_closed", "istio_tcp_received_bytes", "istio_tcp_sent_bytes", + "process_max_fds", + "process_open_fds", ]); { for (name, doc) in metric_info { if stable_metrics.contains(&*name) { - assert!(!doc.contains("unstable"), "{}: {}", name, doc); + assert!(!doc.contains("unstable"), "{name}: {doc}"); } else { - assert!(doc.contains("unstable"), "{}: {}", name, doc); + assert!(doc.contains("unstable"), "{name}: {doc}"); } } } diff --git a/tests/namespaced.rs b/tests/namespaced.rs index 04f5a13d1a..43375e00d7 100644 --- a/tests/namespaced.rs +++ b/tests/namespaced.rs @@ -296,7 +296,7 @@ mod namespaced { let want = HashMap::from([ ("scope", "access"), ("src.workload", "client"), - ("dst.workload", "actual-ew-gtw"), + ("dst.workload", "echo"), ("dst.hbone_addr", "remote.default.svc.cluster.local:8080"), ("dst.addr", &dst_addr), ("bytes_sent", &sent), @@ -307,10 +307,7 @@ mod namespaced { "src.identity", "spiffe://cluster.local/ns/default/sa/client", ), - ( - "dst.identity", - "spiffe://cluster.local/ns/default/sa/actual-ew-gtw", - ), + ("dst.identity", "spiffe://cluster.local/ns/default/sa/echo"), ]); telemetry::testing::assert_contains(want); Ok(()) @@ -997,6 +994,7 @@ mod namespaced { )], ..Default::default() }]]], + dry_run: false, }) .await?; let _ = manager @@ -1430,7 +1428,7 @@ mod namespaced { // Use the actual metrics address ztunnel is listening on (e.g., [::]:15020) // but combine it with the node IP for the client to target. let target_metrics_addr = SocketAddr::new(ztunnel_node_ip, zt.metrics_address.port()); - let target_metrics_url = format!("http://{}/metrics", target_metrics_addr); + let target_metrics_url = format!("http://{target_metrics_addr}/metrics"); // Deploy a client workload (simulating Prometheus) let client = manager @@ -1469,8 +1467,7 @@ mod namespaced { assert!( response_str.contains("# TYPE"), - "Expected Prometheus metrics (# TYPE) in response, got:\n{}", - response_str + "Expected Prometheus metrics (# TYPE) in response, got:\n{response_str}", ); info!("Successfully verified metrics response body"); @@ -1487,8 +1484,8 @@ mod namespaced { verify_metrics(&zt, &metrics, &destination_labels()).await; // Verify INBOUND telemetry log for the metrics connection - let dst_addr_log = format!("{}:15008", ztunnel_node_ip); - let dst_hbone_addr_log = format!("{}", target_metrics_addr); + let dst_addr_log = format!("{ztunnel_node_ip}:15008"); + let dst_hbone_addr_log = format!("{target_metrics_addr}"); // We don't know exact byte counts, so omit them from the check for now let want = HashMap::from([