diff --git a/Cargo.lock b/Cargo.lock index 1269d9ad275..73c192236bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytes", "futures-core", "futures-sink", @@ -31,7 +31,7 @@ dependencies = [ "actix-utils", "ahash", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.8.0", "brotli", "bytes", "bytestring", @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -182,7 +182,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -299,11 +299,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -321,7 +322,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "bcder", - "bitflags 2.6.0", + "bitflags 2.8.0", "bytes", "chrono", "clap", @@ -358,7 +359,7 @@ dependencies = [ "reqwest 0.11.27", "ring", "scroll", - "semver 1.0.24", + "semver 1.0.25", "serde", "serde_json", "serde_yaml", @@ -440,7 +441,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "synstructure 0.13.1", ] @@ -463,7 +464,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -508,7 +509,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -540,7 +541,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -557,7 +558,7 @@ checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -583,9 +584,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.13" +version = "1.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a50b30228d3af8865ce83376b4e99e1ffa34728220fe2860e4df0bb5278d6" +checksum = "9f40e82e858e02445402906e454a73e244c7f501fcae198977585946c48e8697" dependencies = [ "aws-credential-types", "aws-runtime", @@ -625,9 +626,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.12.0" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" +checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca" dependencies = [ "aws-lc-sys", "paste", @@ -636,9 +637,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" +checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" dependencies = [ "bindgen", "cc", @@ -650,9 +651,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16d1aa50accc11a4b4d5c50f7fb81cc0cf60328259c587d0e6b0f11385bde46" +checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -675,9 +676,9 @@ dependencies = [ [[package]] name = "aws-sdk-sqs" -version = "1.53.0" +version = "1.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6493ce2b27a2687b0d8a2453bf6ad2499012e9720c3367cb1206496ede475443" +checksum = "2db64ffe78706b344b7c9b620f96c3c0655745e006b87bad20f424562656a0dd" dependencies = [ "aws-credential-types", "aws-runtime", @@ -697,9 +698,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.53.0" +version = "1.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1605dc0bf9f0a4b05b451441a17fcb0bda229db384f23bf5cead3adbab0664ac" +checksum = "33993c0b054f4251ff2946941b56c26b582677303eeca34087594eb901ece022" dependencies = [ "aws-credential-types", "aws-runtime", @@ -719,9 +720,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.54.0" +version = "1.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f3f73466ff24f6ad109095e0f3f2c830bfb4cd6c8b12f744c8e61ebf4d3ba1" +checksum = "3bd3ceba74a584337a8f3839c818f14f1a2288bfd24235120ff22d7e17a0dd54" dependencies = [ "aws-credential-types", "aws-runtime", @@ -741,9 +742,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.54.0" +version = "1.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249b2acaa8e02fd4718705a9494e3eb633637139aa4bb09d70965b0448e865db" +checksum = "07835598e52dd354368429cb2abf447ce523ea446d0a533a63cb42cd0d2d9280" dependencies = [ "aws-credential-types", "aws-runtime", @@ -764,9 +765,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3820e0c08d0737872ff3c7c1f21ebbb6693d832312d6152bf18ef50a5471c2" +checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -787,9 +788,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427cb637d15d63d6f9aae26358e1c9a9c09d5aa490d64b09354c8217cfef0f28" +checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" dependencies = [ "futures-util", "pin-project-lite", @@ -798,9 +799,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.11" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" +checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -818,9 +819,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e69cc50921eb913c6b662f8d909131bb3e6ad6cb6090d3a39b66fc5c52095" +checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" dependencies = [ "aws-smithy-types", ] @@ -837,9 +838,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.6" +version = "1.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05dd41a70fc74051758ee75b5c4db2c0ca070ed9229c3df50e9475cda1cb985" +checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -881,9 +882,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.11" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ddc9bd6c28aeb303477170ddd183760a956a03e083b3902a990238a7e3792d" +checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97" dependencies = [ "base64-simd", "bytes", @@ -916,9 +917,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -936,6 +937,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "base64 0.22.1", "bytes", "futures-util", @@ -986,6 +988,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "backoff" version = "0.4.0" @@ -1057,9 +1070,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bcder" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627747a6774aab38beb35990d88309481378558875a41da1a4b2e373c906ef0" +checksum = "89ffdaa8c6398acd07176317eb6c1f9082869dd1cc3fee7c72c6354866b928cc" dependencies = [ "bytes", "smallvec", @@ -1096,7 +1109,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -1109,7 +1122,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.95", + "syn 2.0.96", "which 4.4.2", ] @@ -1136,9 +1149,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -1218,7 +1231,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tower-service", @@ -1250,9 +1263,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1380,9 +1393,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.7" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "jobserver", "libc", @@ -1470,9 +1483,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.24" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -1480,9 +1493,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.24" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -1492,9 +1505,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.41" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942dc5991a34d8cf58937ec33201856feba9cbceeeab5adf04116ec7c763bff1" +checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" dependencies = [ "clap", ] @@ -1508,7 +1521,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1582,9 +1595,9 @@ dependencies = [ [[package]] name = "const_panic" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53857514f72ee4a2b583de67401e3ff63a5472ca4acf289d09a9ea7636dfec17" +checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" [[package]] name = "containerd-client" @@ -1708,9 +1721,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1789,7 +1802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1821,7 +1834,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1845,7 +1858,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1856,7 +1869,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1875,9 +1888,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "der" @@ -1932,7 +1945,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1942,7 +1955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -1955,7 +1968,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2054,7 +2067,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2116,7 +2129,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2168,7 +2181,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2188,7 +2201,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2200,7 +2213,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2302,9 +2315,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -2569,9 +2582,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "futures-core", "pin-project-lite", @@ -2585,7 +2598,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2663,14 +2676,14 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +checksum = "eded738faa0e88d3abc9d1a13cb11adc2073c400969eeb8793cf7132589959fc" dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -2704,7 +2717,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "ignore", "walkdir", ] @@ -2743,7 +2756,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.0", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -2762,7 +2775,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.7.0", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -2862,7 +2875,7 @@ dependencies = [ "once_cell", "rand", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "tinyvec", "tokio", "tracing", @@ -2886,7 +2899,7 @@ dependencies = [ "resolv-conf", "serde", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -3135,7 +3148,7 @@ dependencies = [ "hyper 1.5.2", "hyper-util", "log", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -3344,7 +3357,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -3409,9 +3422,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3470,9 +3483,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "ipnetwork" @@ -3495,13 +3508,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3608,9 +3621,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -3743,7 +3756,7 @@ dependencies = [ "kube-core", "pem", "rand", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pemfile 2.2.0", "secrecy", "serde", @@ -3784,7 +3797,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -3860,16 +3873,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "listen_ports" @@ -3922,9 +3935,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "loom" @@ -3988,8 +4001,8 @@ dependencies = [ "clap", "glob", "rand", - "syn 2.0.95", - "thiserror 2.0.9", + "syn 2.0.96", + "thiserror 2.0.11", "tracing", "tracing-subscriber", ] @@ -4020,9 +4033,9 @@ dependencies = [ [[package]] name = "mid" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82da46e09851e48c020d3460f6f9bf4349153f4bc500ed357fa7e8a2193a16bb" +checksum = "231c7a3a36cd2b353dad91ce88e85f3a2c5079f18b6221495d44de4a8477181e" dependencies = [ "hex", "hmac-sha256", @@ -4056,7 +4069,7 @@ checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -4093,9 +4106,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -4151,14 +4164,14 @@ dependencies = [ "regex", "reqwest 0.12.12", "rstest", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pemfile 2.2.0", - "semver 1.0.24", + "semver 1.0.25", "serde", "serde_json", "socket2", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-rustls 0.26.1", "tokio-stream", @@ -4174,6 +4187,7 @@ version = "3.130.0" dependencies = [ "actix-codec", "async-trait", + "axum", "bollard", "bytes", "clap", @@ -4198,19 +4212,21 @@ dependencies = [ "nix 0.29.0", "oci-spec", "pnet", - "procfs", + "procfs 0.17.0", + "prometheus", "rand", "rawsocket", "rcgen", + "reqwest 0.12.12", "rstest", - "rustls 0.23.20", - "semver 1.0.24", + "rustls 0.23.21", + "semver 1.0.25", "serde", "serde_json", "socket2", "streammap-ext", "test_bin", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-rustls 0.26.1", "tokio-stream", @@ -4251,7 +4267,7 @@ dependencies = [ "reqwest 0.12.12", "serde", "serde_yaml", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "whoami", @@ -4264,7 +4280,7 @@ version = "3.130.0" dependencies = [ "base64 0.22.1", "bimap", - "bitflags 2.6.0", + "bitflags 2.8.0", "fancy-regex", "ipnet", "k8s-openapi", @@ -4277,7 +4293,7 @@ dependencies = [ "serde_json", "serde_yaml", "tera", - "thiserror 2.0.9", + "thiserror 2.0.11", "toml 0.8.19", "tracing", ] @@ -4289,7 +4305,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -4301,7 +4317,7 @@ dependencies = [ "log", "miette", "mirrord-intproxy-protocol", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-util", "tracing", @@ -4325,11 +4341,11 @@ dependencies = [ "mirrord-protocol", "rand", "rstest", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pemfile 2.2.0", - "semver 1.0.24", + "semver 1.0.25", "serde", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-rustls 0.26.1", "tokio-stream", @@ -4342,7 +4358,7 @@ version = "3.130.0" dependencies = [ "bincode", "mirrord-protocol", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", ] @@ -4364,7 +4380,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-retry", "tracing", @@ -4407,7 +4423,7 @@ dependencies = [ "tempfile", "test-cdylib", "tests", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "tracing-subscriber", @@ -4419,7 +4435,7 @@ version = "3.130.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -4428,8 +4444,8 @@ version = "3.130.0" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", - "semver 1.0.24", - "syn 2.0.95", + "semver 1.0.25", + "syn 2.0.96", ] [[package]] @@ -4455,11 +4471,11 @@ dependencies = [ "rand", "rstest", "schemars", - "semver 1.0.24", + "semver 1.0.25", "serde", "serde_json", "serde_yaml", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-tungstenite", "tracing", @@ -4477,7 +4493,7 @@ dependencies = [ [[package]] name = "mirrord-protocol" -version = "1.15.0" +version = "1.15.1" dependencies = [ "actix-codec", "bincode", @@ -4492,10 +4508,10 @@ dependencies = [ "libc", "mirrord-macros", "nix 0.29.0", - "semver 1.0.24", + "semver 1.0.25", "serde", "socket2", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", ] @@ -4507,7 +4523,7 @@ dependencies = [ "object 0.36.7", "once_cell", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tracing", "which 7.0.1", ] @@ -4522,9 +4538,9 @@ dependencies = [ "kube", "mirrord-protocol", "pnet_packet", - "semver 1.0.24", + "semver 1.0.25", "serde_yaml", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", "tun2", @@ -4553,7 +4569,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -4583,9 +4599,9 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "neli" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43" +checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" dependencies = [ "byteorder", "libc", @@ -4595,9 +4611,9 @@ dependencies = [ [[package]] name = "neli-proc-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" +checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" dependencies = [ "either", "proc-macro2", @@ -4625,7 +4641,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "libc", ] @@ -4636,7 +4652,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", @@ -4726,7 +4742,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -4783,7 +4799,7 @@ dependencies = [ "crc32fast", "flate2", "hashbrown 0.14.5", - "indexmap 2.7.0", + "indexmap 2.7.1", "memchr", "ruzstd 0.5.0", ] @@ -4812,7 +4828,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -4866,9 +4882,9 @@ version = "3.130.0" [[package]] name = "outref" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "overload" @@ -4974,7 +4990,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5009,7 +5025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.9", + "thiserror 2.0.11", "ucd-trie", ] @@ -5033,7 +5049,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5054,7 +5070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.7.0", + "indexmap 2.7.1", ] [[package]] @@ -5112,7 +5128,7 @@ checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5178,7 +5194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.7.0", + "indexmap 2.7.1", "quick-xml", "serde", "time", @@ -5229,7 +5245,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5324,12 +5340,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5383,14 +5399,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -5403,36 +5419,76 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "version_check", "yansi", ] +[[package]] +name = "procfs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +dependencies = [ + "bitflags 2.8.0", + "hex", + "lazy_static", + "procfs-core 0.16.0", + "rustix", +] + [[package]] name = "procfs" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "chrono", "flate2", "hex", - "procfs-core", + "procfs-core 0.17.0", "rustix", ] +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.8.0", + "hex", +] + [[package]] name = "procfs-core" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "chrono", "hex", ] +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot", + "procfs 0.16.0", + "protobuf", + "thiserror 1.0.69", +] + [[package]] name = "prost" version = "0.13.4" @@ -5459,7 +5515,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.95", + "syn 2.0.96", "tempfile", ] @@ -5473,7 +5529,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -5485,6 +5541,12 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "quick-error" version = "1.2.3" @@ -5511,9 +5573,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.0", - "rustls 0.23.20", + "rustls 0.23.21", "socket2", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -5529,10 +5591,10 @@ dependencies = [ "rand", "ring", "rustc-hash 2.1.0", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-pki-types", "slab", - "thiserror 2.0.9", + "thiserror 2.0.11", "tinyvec", "tracing", "web-time", @@ -5707,7 +5769,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -5845,7 +5907,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.20", + "rustls 0.23.21", "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -5917,7 +5979,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.95", + "syn 2.0.96", "unicode-ident", ] @@ -5983,7 +6045,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.24", + "semver 1.0.25", ] [[package]] @@ -5997,11 +6059,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno 0.3.10", "libc", "linux-raw-sys", @@ -6036,9 +6098,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "aws-lc-rs", "log", @@ -6215,7 +6277,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6247,7 +6309,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6289,7 +6351,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6302,7 +6364,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -6331,9 +6393,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -6371,7 +6433,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6382,14 +6444,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -6415,7 +6477,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6449,7 +6511,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_derive", "serde_json", @@ -6462,7 +6524,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -6690,7 +6752,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6733,9 +6795,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -6777,7 +6839,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -6950,7 +7012,7 @@ dependencies = [ "regex", "reqwest 0.12.12", "rstest", - "rustls 0.23.20", + "rustls 0.23.21", "serde", "serde_json", "tempfile", @@ -6980,11 +7042,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -6995,18 +7057,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -7086,9 +7148,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -7104,13 +7166,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -7140,7 +7202,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.21", "tokio", ] @@ -7230,7 +7292,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -7278,7 +7340,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -7325,7 +7387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.8.0", "bytes", "http 1.2.0", "http-body 1.0.1", @@ -7368,7 +7430,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -7693,18 +7755,18 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" @@ -7763,34 +7825,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -7801,9 +7864,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7811,28 +7874,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -7981,7 +8047,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -7992,7 +8058,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -8175,9 +8241,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.22" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -8200,16 +8266,16 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wintun-bindings" -version = "0.7.27" +version = "0.7.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e35d3911efde5ee25586385204127ff6a3f251477dcdd3b222775aaa4d95977" +checksum = "67a02981bed4592bcd271f9bfe154228ddbd2fd69e37a7d358da5d3a1251d696" dependencies = [ "blocking", "c2rust-bitfields", "futures", "libloading", "log", - "thiserror 2.0.9", + "thiserror 2.0.11", "windows-sys 0.59.0", ] @@ -8318,9 +8384,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" [[package]] name = "xmlparser" @@ -8381,7 +8447,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "synstructure 0.13.1", ] @@ -8403,7 +8469,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -8423,7 +8489,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", "synstructure 0.13.1", ] @@ -8444,7 +8510,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] @@ -8466,7 +8532,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.96", ] [[package]] diff --git a/changelog.d/2975.added.md b/changelog.d/2975.added.md new file mode 100644 index 00000000000..17c59da1f7b --- /dev/null +++ b/changelog.d/2975.added.md @@ -0,0 +1 @@ +Add prometheus metrics to the mirrord-agent. diff --git a/mirrord-schema.json b/mirrord-schema.json index 5beadff3c58..19577b952af 100644 --- a/mirrord-schema.json +++ b/mirrord-schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "LayerFileConfig", - "description": "mirrord allows for a high degree of customization when it comes to which features you want to enable, and how they should function.\n\nAll of the configuration fields have a default value, so a minimal configuration would be no configuration at all.\n\nThe configuration supports templating using the [Tera](https://keats.github.io/tera/docs/) template engine. Currently we don't provide additional values to the context, if you have anything you want us to provide please let us know.\n\nTo use a configuration file in the CLI, use the `-f ` flag. Or if using VSCode Extension or JetBrains plugin, simply create a `.mirrord/mirrord.json` file or use the UI.\n\nTo help you get started, here are examples of a basic configuration file, and a complete configuration file containing all fields.\n\n### Basic `config.json` {#root-basic}\n\n```json { \"target\": \"pod/bear-pod\", \"feature\": { \"env\": true, \"fs\": \"read\", \"network\": true } } ```\n\n### Basic `config.json` with templating {#root-basic-templating}\n\n```json { \"target\": \"{{ get_env(name=\"TARGET\", default=\"pod/fallback\") }}\", \"feature\": { \"env\": true, \"fs\": \"read\", \"network\": true } } ```\n\n### Complete `config.json` {#root-complete}\n\nDon't use this example as a starting point, it's just here to show you all the available options. ```json { \"accept_invalid_certificates\": false, \"skip_processes\": \"ide-debugger\", \"target\": { \"path\": \"pod/bear-pod\", \"namespace\": \"default\" }, \"connect_tcp\": null, \"agent\": { \"log_level\": \"info\", \"json_log\": false, \"labels\": { \"user\": \"meow\" }, \"annotations\": { \"cats.io/inject\": \"enabled\" }, \"namespace\": \"default\", \"image\": \"ghcr.io/metalbear-co/mirrord:latest\", \"image_pull_policy\": \"IfNotPresent\", \"image_pull_secrets\": [ { \"secret-key\": \"secret\" } ], \"ttl\": 30, \"ephemeral\": false, \"communication_timeout\": 30, \"startup_timeout\": 360, \"network_interface\": \"eth0\", \"flush_connections\": true }, \"feature\": { \"env\": { \"include\": \"DATABASE_USER;PUBLIC_ENV\", \"exclude\": \"DATABASE_PASSWORD;SECRET_ENV\", \"override\": { \"DATABASE_CONNECTION\": \"db://localhost:7777/my-db\", \"LOCAL_BEAR\": \"panda\" }, \"mapping\": { \".+_TIMEOUT\": \"1000\" } }, \"fs\": { \"mode\": \"write\", \"read_write\": \".+\\\\.json\" , \"read_only\": [ \".+\\\\.yaml\", \".+important-file\\\\.txt\" ], \"local\": [ \".+\\\\.js\", \".+\\\\.mjs\" ] }, \"network\": { \"incoming\": { \"mode\": \"steal\", \"http_filter\": { \"header_filter\": \"host: api\\\\..+\" }, \"port_mapping\": [[ 7777, 8888 ]], \"ignore_localhost\": false, \"ignore_ports\": [9999, 10000] }, \"outgoing\": { \"tcp\": true, \"udp\": true, \"filter\": { \"local\": [\"tcp://1.1.1.0/24:1337\", \"1.1.5.0/24\", \"google.com\", \":53\"] }, \"ignore_localhost\": false, \"unix_streams\": \"bear.+\" }, \"dns\": { \"enabled\": true, \"filter\": { \"local\": [\"1.1.1.0/24:1337\", \"1.1.5.0/24\", \"google.com\"] } } }, \"copy_target\": { \"scale_down\": false } }, \"operator\": true, \"kubeconfig\": \"~/.kube/config\", \"sip_binaries\": \"bash\", \"telemetry\": true, \"kube_context\": \"my-cluster\" } ```\n\n# Options {#root-options}", + "description": "mirrord allows for a high degree of customization when it comes to which features you want to enable, and how they should function.\n\nAll of the configuration fields have a default value, so a minimal configuration would be no configuration at all.\n\nThe configuration supports templating using the [Tera](https://keats.github.io/tera/docs/) template engine. Currently we don't provide additional values to the context, if you have anything you want us to provide please let us know.\n\nTo use a configuration file in the CLI, use the `-f ` flag. Or if using VSCode Extension or JetBrains plugin, simply create a `.mirrord/mirrord.json` file or use the UI.\n\nTo help you get started, here are examples of a basic configuration file, and a complete configuration file containing all fields.\n\n### Basic `config.json` {#root-basic}\n\n```json { \"target\": \"pod/bear-pod\", \"feature\": { \"env\": true, \"fs\": \"read\", \"network\": true } } ```\n\n### Basic `config.json` with templating {#root-basic-templating}\n\n```json { \"target\": \"{{ get_env(name=\"TARGET\", default=\"pod/fallback\") }}\", \"feature\": { \"env\": true, \"fs\": \"read\", \"network\": true } } ```\n\n### Complete `config.json` {#root-complete}\n\nDon't use this example as a starting point, it's just here to show you all the available options. ```json { \"accept_invalid_certificates\": false, \"skip_processes\": \"ide-debugger\", \"target\": { \"path\": \"pod/bear-pod\", \"namespace\": \"default\" }, \"connect_tcp\": null, \"agent\": { \"log_level\": \"info\", \"json_log\": false, \"labels\": { \"user\": \"meow\" }, \"annotations\": { \"cats.io/inject\": \"enabled\" }, \"namespace\": \"default\", \"image\": \"ghcr.io/metalbear-co/mirrord:latest\", \"image_pull_policy\": \"IfNotPresent\", \"image_pull_secrets\": [ { \"secret-key\": \"secret\" } ], \"ttl\": 30, \"ephemeral\": false, \"communication_timeout\": 30, \"startup_timeout\": 360, \"network_interface\": \"eth0\", \"flush_connections\": true, \"metrics\": \"0.0.0.0:9000\", }, \"feature\": { \"env\": { \"include\": \"DATABASE_USER;PUBLIC_ENV\", \"exclude\": \"DATABASE_PASSWORD;SECRET_ENV\", \"override\": { \"DATABASE_CONNECTION\": \"db://localhost:7777/my-db\", \"LOCAL_BEAR\": \"panda\" }, \"mapping\": { \".+_TIMEOUT\": \"1000\" } }, \"fs\": { \"mode\": \"write\", \"read_write\": \".+\\\\.json\" , \"read_only\": [ \".+\\\\.yaml\", \".+important-file\\\\.txt\" ], \"local\": [ \".+\\\\.js\", \".+\\\\.mjs\" ] }, \"network\": { \"incoming\": { \"mode\": \"steal\", \"http_filter\": { \"header_filter\": \"host: api\\\\..+\" }, \"port_mapping\": [[ 7777, 8888 ]], \"ignore_localhost\": false, \"ignore_ports\": [9999, 10000] }, \"outgoing\": { \"tcp\": true, \"udp\": true, \"filter\": { \"local\": [\"tcp://1.1.1.0/24:1337\", \"1.1.5.0/24\", \"google.com\", \":53\"] }, \"ignore_localhost\": false, \"unix_streams\": \"bear.+\" }, \"dns\": { \"enabled\": true, \"filter\": { \"local\": [\"1.1.1.0/24:1337\", \"1.1.5.0/24\", \"google.com\"] } } }, \"copy_target\": { \"scale_down\": false } }, \"operator\": true, \"kubeconfig\": \"~/.kube/config\", \"sip_binaries\": \"bash\", \"telemetry\": true, \"kube_context\": \"my-cluster\" } ```\n\n# Options {#root-options}", "type": "object", "properties": { "accept_invalid_certificates": { @@ -255,7 +255,7 @@ "properties": { "annotations": { "title": "agent.annotations {#agent-annotations}", - "description": "Allows setting up custom annotations for the agent Job and Pod.\n\n```json { \"annotations\": { \"cats.io/inject\": \"enabled\" } } ```", + "description": "Allows setting up custom annotations for the agent Job and Pod.\n\n```json { \"annotations\": { \"cats.io/inject\": \"enabled\" \"prometheus.io/scrape\": \"true\", \"prometheus.io/port\": \"9000\" } } ```", "type": [ "object", "null" @@ -378,6 +378,14 @@ "null" ] }, + "metrics": { + "title": "agent.metrics {#agent-metrics}", + "description": "Enables prometheus metrics for the agent pod.\n\nYou might need to add annotations to the agent pod depending on how prometheus is configured to scrape for metrics.\n\n```json { \"metrics\": \"0.0.0.0:9000\" } ```", + "type": [ + "string", + "null" + ] + }, "namespace": { "title": "agent.namespace {#agent-namespace}", "description": "Namespace where the agent shall live. Note: Doesn't work with ephemeral containers. Defaults to the current kubernetes namespace.", diff --git a/mirrord/agent/Cargo.toml b/mirrord/agent/Cargo.toml index cdba788acf3..07757b7900e 100644 --- a/mirrord/agent/Cargo.toml +++ b/mirrord/agent/Cargo.toml @@ -69,6 +69,8 @@ x509-parser = "0.16" rustls.workspace = true envy = "0.4" socket2.workspace = true +prometheus = { version = "0.13", features = ["process"] } +axum = { version = "0.7", features = ["macros"] } iptables = { git = "https://github.com/metalbear-co/rust-iptables.git", rev = "e66c7332e361df3c61a194f08eefe3f40763d624" } rawsocket = { git = "https://github.com/metalbear-co/rawsocket.git" } procfs = "0.17.0" @@ -78,3 +80,4 @@ rstest.workspace = true mockall = "0.13" test_bin = "0.4" rcgen.workspace = true +reqwest.workspace = true diff --git a/mirrord/agent/README.md b/mirrord/agent/README.md index bf077b5fdcf..d7456ead64c 100644 --- a/mirrord/agent/README.md +++ b/mirrord/agent/README.md @@ -6,3 +6,198 @@ Agent part of [mirrord](https://github.com/metalbear-co/mirrord) responsible for mirrord-agent is written in Rust for safety, low memory consumption and performance. mirrord-agent is distributed as a container image (currently only x86) that is published on [GitHub Packages publicly](https://github.com/metalbear-co/mirrord-agent/pkgs/container/mirrord-agent). + +## Enabling prometheus metrics + +To start the metrics server, you'll need to add this config to your `mirrord.json`: + +```json +{ + "agent": { + "metrics": "0.0.0.0:9000", + "annotations": { + "prometheus.io/scrape": "true", + "prometheus.io/port": "9000" + } +} +``` + +Remember to change the `port` in both `metrics` and `annotations`, they have to match, +otherwise prometheus will try to scrape on `port: 80` or other commonly used ports. + +### Installing prometheus + +Run `kubectl apply -f {file-name}.yaml` on these sequences of `yaml` files and you should +get prometheus running in your cluster. You can access the dashboard from your browser at +`http://{cluster-ip}:30909`, if you're using minikube it might be +`http://192.168.49.2:30909`. + +You'll get prometheus running under the `monitoring` namespace, but it'll be able to look +into resources from all namespaces. The config in `configmap.yaml` sets prometheus to look +at pods only, if you want to use it to scrape other stuff, check +[this example](https://github.com/prometheus/prometheus/blob/main/documentation/examples/prometheus-kubernetes.yml). + +1. `create-namespace.yaml` + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring +``` + +2. `cluster-role.yaml` + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus +rules: +- apiGroups: [""] + resources: + - nodes + - services + - endpoints + - pods + verbs: ["get", "list", "watch"] +- apiGroups: + - extensions + resources: + - ingresses + verbs: ["get", "list", "watch"] +``` + +3. `service-account.yaml` + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus + namespace: monitoring +``` + +4. `cluster-role-binding.yaml` + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: prometheus +subjects: +- kind: ServiceAccount + name: prometheus + namespace: monitoring +``` + +5. `configmap.yaml` + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: monitoring +data: + prometheus.yml: | + global: + keep_dropped_targets: 100 + + scrape_configs: + - job_name: "kubernetes-pods" + + kubernetes_sd_configs: + - role: pod + + relabel_configs: + - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] + action: replace + regex: ([^:]+)(?::\d+)?;(\d+) + replacement: $1:$2 + target_label: __address__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: [__meta_kubernetes_namespace] + action: replace + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + action: replace + target_label: pod +``` + +- If you make any changes to the 5-configmap.yaml file, remember to `kubectl apply` it + **before** restarting the `prometheus` deployment. + +6. `deployment.yaml` + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +spec: + replicas: 1 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9090" + spec: + serviceAccountName: prometheus + containers: + - name: prometheus + image: prom/prometheus + args: + - '--config.file=/etc/prometheus/prometheus.yml' + ports: + - name: web + containerPort: 9090 + volumeMounts: + - name: prometheus-config-volume + mountPath: /etc/prometheus + restartPolicy: Always + volumes: + - name: prometheus-config-volume + configMap: + defaultMode: 420 + name: prometheus-config +``` + +7. `service.yaml` + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: prometheus-service + namespace: monitoring + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '9090' +spec: + selector: + app: prometheus + type: NodePort + ports: + - port: 8080 + targetPort: 9090 + nodePort: 30909 +``` diff --git a/mirrord/agent/src/cli.rs b/mirrord/agent/src/cli.rs index 07c8aa97503..bbcf23f1816 100644 --- a/mirrord/agent/src/cli.rs +++ b/mirrord/agent/src/cli.rs @@ -1,8 +1,11 @@ #![deny(missing_docs)] +use std::net::SocketAddr; + use clap::{Parser, Subcommand}; use mirrord_protocol::{ - MeshVendor, AGENT_IPV6_ENV, AGENT_NETWORK_INTERFACE_ENV, AGENT_OPERATOR_CERT_ENV, + MeshVendor, AGENT_IPV6_ENV, AGENT_METRICS_ENV, AGENT_NETWORK_INTERFACE_ENV, + AGENT_OPERATOR_CERT_ENV, }; const DEFAULT_RUNTIME: &str = "containerd"; @@ -28,6 +31,10 @@ pub struct Args { #[arg(short = 'i', long, env = AGENT_NETWORK_INTERFACE_ENV)] pub network_interface: Option, + /// Controls whether metrics are enabled, and the address to set up the metrics server. + #[arg(long, env = AGENT_METRICS_ENV)] + pub metrics: Option, + /// Return an error after accepting the first client connection, in order to test agent error /// cleanup. /// diff --git a/mirrord/agent/src/client_connection.rs b/mirrord/agent/src/client_connection.rs index 8181e4baabd..7b484cc25da 100644 --- a/mirrord/agent/src/client_connection.rs +++ b/mirrord/agent/src/client_connection.rs @@ -208,7 +208,7 @@ enum ConnectionFramed { #[cfg(test)] mod test { - use std::sync::Arc; + use std::sync::{Arc, Once}; use futures::StreamExt; use mirrord_protocol::ClientCodec; @@ -220,10 +220,19 @@ mod test { use super::*; + static CRYPTO_PROVIDER: Once = Once::new(); + /// Verifies that [`AgentTlsConnector`] correctly accepts a /// connection from a server using the provided certificate. #[tokio::test] async fn agent_tls_connector_valid_cert() { + CRYPTO_PROVIDER.call_once(|| { + rustls::crypto::CryptoProvider::install_default( + rustls::crypto::aws_lc_rs::default_provider(), + ) + .expect("Failed to install crypto provider") + }); + let cert = rcgen::generate_simple_self_signed(vec!["operator".to_string()]).unwrap(); let cert_bytes = cert.cert.der(); let key_bytes = cert.key_pair.serialize_der(); @@ -269,6 +278,13 @@ mod test { /// connection from a server using some other certificate. #[tokio::test] async fn agent_tls_connector_invalid_cert() { + CRYPTO_PROVIDER.call_once(|| { + rustls::crypto::CryptoProvider::install_default( + rustls::crypto::aws_lc_rs::default_provider(), + ) + .expect("Failed to install crypto provider") + }); + let server_cert = rcgen::generate_simple_self_signed(vec!["operator".to_string()]).unwrap(); let cert_bytes = server_cert.cert.der(); let key_bytes = server_cert.key_pair.serialize_der(); diff --git a/mirrord/agent/src/container_handle.rs b/mirrord/agent/src/container_handle.rs index 6e8ba78173d..dd6755e766d 100644 --- a/mirrord/agent/src/container_handle.rs +++ b/mirrord/agent/src/container_handle.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::{ - error::Result, + error::AgentResult, runtime::{Container, ContainerInfo, ContainerRuntime}, }; @@ -22,7 +22,7 @@ pub(crate) struct ContainerHandle(Arc); impl ContainerHandle { /// Retrieve info about the container and initialize this struct. #[tracing::instrument(level = "trace")] - pub(crate) async fn new(container: Container) -> Result { + pub(crate) async fn new(container: Container) -> AgentResult { let ContainerInfo { pid, env: raw_env } = container.get_info().await?; let inner = Inner { pid, raw_env }; diff --git a/mirrord/agent/src/dns.rs b/mirrord/agent/src/dns.rs index 3240856275a..b92487594e0 100644 --- a/mirrord/agent/src/dns.rs +++ b/mirrord/agent/src/dns.rs @@ -16,10 +16,7 @@ use tokio::{ use tokio_util::sync::CancellationToken; use tracing::Level; -use crate::{ - error::{AgentError, Result}, - watched_task::TaskStatus, -}; +use crate::{error::AgentResult, metrics::DNS_REQUEST_COUNT, watched_task::TaskStatus}; #[derive(Debug)] pub(crate) enum ClientGetAddrInfoRequest { @@ -167,6 +164,9 @@ impl DnsWorker { let etc_path = self.etc_path.clone(); let timeout = self.timeout; let attempts = self.attempts; + + DNS_REQUEST_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let support_ipv6 = self.support_ipv6; let lookup_future = async move { let result = Self::do_lookup( @@ -181,15 +181,13 @@ impl DnsWorker { if let Err(result) = message.response_tx.send(result) { tracing::error!(?result, "Failed to send query response"); } + DNS_REQUEST_COUNT.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); }; tokio::spawn(lookup_future); } - pub(crate) async fn run( - mut self, - cancellation_token: CancellationToken, - ) -> Result<(), AgentError> { + pub(crate) async fn run(mut self, cancellation_token: CancellationToken) -> AgentResult<()> { loop { tokio::select! { _ = cancellation_token.cancelled() => break Ok(()), @@ -225,7 +223,7 @@ impl DnsApi { pub(crate) async fn make_request( &mut self, request: ClientGetAddrInfoRequest, - ) -> Result<(), AgentError> { + ) -> AgentResult<()> { let (response_tx, response_rx) = oneshot::channel(); let command = DnsCommand { @@ -244,7 +242,7 @@ impl DnsApi { /// Returns the result of the oldest outstanding DNS request issued with this struct (see /// [`Self::make_request`]). #[tracing::instrument(level = Level::TRACE, skip(self), ret, err)] - pub(crate) async fn recv(&mut self) -> Result { + pub(crate) async fn recv(&mut self) -> AgentResult { let Some(response) = self.responses.next().await else { return future::pending().await; }; diff --git a/mirrord/agent/src/entrypoint.rs b/mirrord/agent/src/entrypoint.rs index 407bf27c33f..ac9157897a0 100644 --- a/mirrord/agent/src/entrypoint.rs +++ b/mirrord/agent/src/entrypoint.rs @@ -12,6 +12,7 @@ use std::{ use client_connection::AgentTlsConnector; use dns::{ClientGetAddrInfoRequest, DnsCommand, DnsWorker}; use futures::TryFutureExt; +use metrics::{start_metrics, CLIENT_COUNT}; use mirrord_protocol::{ClientMessage, DaemonMessage, GetEnvVarsRequest, LogMessage}; use sniffer::tcp_capture::RawSocketTcpCapture; use tokio::{ @@ -32,7 +33,7 @@ use crate::{ client_connection::ClientConnection, container_handle::ContainerHandle, dns::DnsApi, - error::{AgentError, Result}, + error::{AgentError, AgentResult}, file::FileManager, outgoing::{TcpOutgoingApi, UdpOutgoingApi}, runtime::get_container, @@ -72,7 +73,7 @@ struct State { impl State { /// Return [`Err`] if container runtime operations failed. - pub async fn new(args: &Args) -> Result { + pub async fn new(args: &Args) -> AgentResult { let tls_connector = args .operator_tls_cert_pem .clone() @@ -205,6 +206,12 @@ struct ClientConnectionHandler { ready_for_logs: bool, } +impl Drop for ClientConnectionHandler { + fn drop(&mut self) { + CLIENT_COUNT.fetch_sub(1, Ordering::Relaxed); + } +} + impl ClientConnectionHandler { /// Initializes [`ClientConnectionHandler`]. pub async fn new( @@ -212,7 +219,7 @@ impl ClientConnectionHandler { mut connection: ClientConnection, bg_tasks: BackgroundTasks, state: State, - ) -> Result { + ) -> AgentResult { let pid = state.container_pid(); let file_manager = FileManager::new(pid.or_else(|| state.ephemeral.then_some(1))); @@ -238,6 +245,8 @@ impl ClientConnectionHandler { ready_for_logs: false, }; + CLIENT_COUNT.fetch_add(1, Ordering::Relaxed); + Ok(client_handler) } @@ -273,7 +282,7 @@ impl ClientConnectionHandler { id: ClientId, task: BackgroundTask, connection: &mut ClientConnection, - ) -> Result> { + ) -> AgentResult> { if let BackgroundTask::Running(stealer_status, stealer_sender) = task { match TcpStealerApi::new( id, @@ -313,7 +322,7 @@ impl ClientConnectionHandler { /// /// Breaks upon receiver/sender drop. #[tracing::instrument(level = "trace", skip(self))] - async fn start(mut self, cancellation_token: CancellationToken) -> Result<()> { + async fn start(mut self, cancellation_token: CancellationToken) -> AgentResult<()> { let error = loop { select! { message = self.connection.receive() => { @@ -364,7 +373,7 @@ impl ClientConnectionHandler { Ok(message) => self.respond(DaemonMessage::TcpOutgoing(message)).await?, Err(e) => break e, }, - message = self.udp_outgoing_api.daemon_message() => match message { + message = self.udp_outgoing_api.recv_from_task() => match message { Ok(message) => self.respond(DaemonMessage::UdpOutgoing(message)).await?, Err(e) => break e, }, @@ -389,7 +398,7 @@ impl ClientConnectionHandler { /// Sends a [`DaemonMessage`] response to the connected client (`mirrord-layer`). #[tracing::instrument(level = "trace", skip(self))] - async fn respond(&mut self, response: DaemonMessage) -> Result<()> { + async fn respond(&mut self, response: DaemonMessage) -> AgentResult<()> { self.connection.send(response).await.map_err(Into::into) } @@ -397,7 +406,7 @@ impl ClientConnectionHandler { /// /// Returns `false` if the client disconnected. #[tracing::instrument(level = Level::TRACE, skip(self), ret, err(level = Level::DEBUG))] - async fn handle_client_message(&mut self, message: ClientMessage) -> Result { + async fn handle_client_message(&mut self, message: ClientMessage) -> AgentResult { match message { ClientMessage::FileRequest(req) => { if let Some(response) = self.file_manager.handle_message(req)? { @@ -415,7 +424,7 @@ impl ClientConnectionHandler { self.tcp_outgoing_api.send_to_task(layer_message).await? } ClientMessage::UdpOutgoing(layer_message) => { - self.udp_outgoing_api.layer_message(layer_message).await? + self.udp_outgoing_api.send_to_task(layer_message).await? } ClientMessage::GetEnvVarsRequest(GetEnvVarsRequest { env_vars_filter, @@ -495,8 +504,8 @@ impl ClientConnectionHandler { } /// Initializes the agent's [`State`], channels, threads, and runs [`ClientConnectionHandler`]s. -#[tracing::instrument(level = "trace", ret)] -async fn start_agent(args: Args) -> Result<()> { +#[tracing::instrument(level = Level::TRACE, ret, err)] +async fn start_agent(args: Args) -> AgentResult<()> { trace!("start_agent -> Starting agent with args: {args:?}"); // listen for client connections @@ -534,6 +543,18 @@ async fn start_agent(args: Args) -> Result<()> { // To make sure that background tasks are cancelled when we exit early from this function. let cancel_guard = cancellation_token.clone().drop_guard(); + if let Some(metrics_address) = args.metrics { + let cancellation_token = cancellation_token.clone(); + tokio::spawn(async move { + start_metrics(metrics_address, cancellation_token.clone()) + .await + .inspect_err(|fail| { + tracing::error!(?fail, "Failed starting metrics server!"); + cancellation_token.cancel(); + }) + }); + } + let (sniffer_command_tx, sniffer_command_rx) = mpsc::channel::(1000); let (stealer_command_tx, stealer_command_rx) = mpsc::channel::(1000); let (dns_command_tx, dns_command_rx) = mpsc::channel::(1000); @@ -755,7 +776,7 @@ async fn start_agent(args: Args) -> Result<()> { Ok(()) } -async fn clear_iptable_chain() -> Result<()> { +async fn clear_iptable_chain() -> AgentResult<()> { let ipt = new_iptables(); SafeIpTables::load(IPTablesWrapper::from(ipt), false) @@ -766,7 +787,7 @@ async fn clear_iptable_chain() -> Result<()> { Ok(()) } -async fn run_child_agent() -> Result<()> { +async fn run_child_agent() -> AgentResult<()> { let command_args = std::env::args().collect::>(); let (command, args) = command_args .split_first() @@ -790,7 +811,7 @@ async fn run_child_agent() -> Result<()> { /// /// Captures SIGTERM signals sent by Kubernetes when the pod is gracefully deleted. /// When a signal is captured, the child process is killed and the iptables are cleaned. -async fn start_iptable_guard(args: Args) -> Result<()> { +async fn start_iptable_guard(args: Args) -> AgentResult<()> { debug!("start_iptable_guard -> Initializing iptable-guard."); let state = State::new(&args).await?; @@ -827,7 +848,18 @@ async fn start_iptable_guard(args: Args) -> Result<()> { result } -pub async fn main() -> Result<()> { +/// The agent is somewhat started twice, first with [`start_iptable_guard`], and then the +/// proper agent with [`start_agent`]. +/// +/// ## Things to keep in mind due to the double initialization +/// +/// Since the _second_ agent gets spawned as a child of the _first_, they share resources, +/// like the `namespace`, which means: +/// +/// 1. If you try to `bind` a socket to some address before [`start_agent`], it'll actually be bound +/// **twice**, which incurs an error (address already in use). You could get around this by +/// `bind`ing on `0.0.0.0:0`, but this is most likely **not** what you want. +pub async fn main() -> AgentResult<()> { rustls::crypto::CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider()) .expect("Failed to install crypto provider"); diff --git a/mirrord/agent/src/env.rs b/mirrord/agent/src/env.rs index 26fa4681431..5a349709f2d 100644 --- a/mirrord/agent/src/env.rs +++ b/mirrord/agent/src/env.rs @@ -7,7 +7,7 @@ use mirrord_protocol::RemoteResult; use tokio::io::AsyncReadExt; use wildmatch::WildMatch; -use crate::error::Result; +use crate::error::AgentResult; struct EnvFilter { include: Vec, @@ -97,7 +97,7 @@ pub(crate) fn parse_raw_env<'a, S: AsRef + 'a + ?Sized, T: IntoIterator>() } -pub(crate) async fn get_proc_environ(path: PathBuf) -> Result> { +pub(crate) async fn get_proc_environ(path: PathBuf) -> AgentResult> { let mut environ_file = tokio::fs::File::open(path).await?; let mut raw_env_vars = String::with_capacity(8192); diff --git a/mirrord/agent/src/error.rs b/mirrord/agent/src/error.rs index d9ae7cb8b9d..88b811e590b 100644 --- a/mirrord/agent/src/error.rs +++ b/mirrord/agent/src/error.rs @@ -96,4 +96,4 @@ impl From> for AgentError { } } -pub(crate) type Result = std::result::Result; +pub(crate) type AgentResult = std::result::Result; diff --git a/mirrord/agent/src/file.rs b/mirrord/agent/src/file.rs index 0bc30afb151..0fa945fbd85 100644 --- a/mirrord/agent/src/file.rs +++ b/mirrord/agent/src/file.rs @@ -18,7 +18,7 @@ use mirrord_protocol::{file::*, FileRequest, FileResponse, RemoteResult, Respons use nix::unistd::UnlinkatFlags; use tracing::{error, trace, Level}; -use crate::error::Result; +use crate::{error::AgentResult, metrics::OPEN_FD_COUNT}; #[derive(Debug)] pub enum RemoteFile { @@ -76,15 +76,11 @@ pub(crate) struct FileManager { fds_iter: RangeInclusive, } -impl Default for FileManager { - fn default() -> Self { - Self { - root_path: Default::default(), - open_files: Default::default(), - dir_streams: Default::default(), - getdents_streams: Default::default(), - fds_iter: (0..=u64::MAX), - } +impl Drop for FileManager { + fn drop(&mut self) { + let descriptors = + self.open_files.len() + self.dir_streams.len() + self.getdents_streams.len(); + OPEN_FD_COUNT.fetch_sub(descriptors as i64, std::sync::atomic::Ordering::Relaxed); } } @@ -152,7 +148,10 @@ pub fn resolve_path + std::fmt::Debug, R: AsRef + std::fmt: impl FileManager { /// Executes the request and returns the response. #[tracing::instrument(level = Level::TRACE, skip(self), ret, err(level = Level::DEBUG))] - pub fn handle_message(&mut self, request: FileRequest) -> Result> { + pub(crate) fn handle_message( + &mut self, + request: FileRequest, + ) -> AgentResult> { Ok(match request { FileRequest::Open(OpenFileRequest { path, open_options }) => { // TODO: maybe not agent error on this? @@ -206,10 +205,7 @@ impl FileManager { let write_result = self.write_limited(remote_fd, start_from, write_bytes); Some(FileResponse::WriteLimited(write_result)) } - FileRequest::Close(CloseFileRequest { fd }) => { - self.close(fd); - None - } + FileRequest::Close(CloseFileRequest { fd }) => self.close(fd), FileRequest::Access(AccessFileRequest { pathname, mode }) => { let pathname = pathname .strip_prefix("/") @@ -244,10 +240,7 @@ impl FileManager { let read_dir_result = self.read_dir_batch(remote_fd, amount); Some(FileResponse::ReadDirBatch(read_dir_result)) } - FileRequest::CloseDir(CloseDirRequest { remote_fd }) => { - self.close_dir(remote_fd); - None - } + FileRequest::CloseDir(CloseDirRequest { remote_fd }) => self.close_dir(remote_fd), FileRequest::GetDEnts64(GetDEnts64Request { remote_fd, buffer_size, @@ -280,10 +273,13 @@ impl FileManager { pub fn new(pid: Option) -> Self { let root_path = get_root_path_from_optional_pid(pid); trace!("Agent root path >> {root_path:?}"); + Self { - open_files: HashMap::new(), root_path, - ..Default::default() + open_files: Default::default(), + dir_streams: Default::default(), + getdents_streams: Default::default(), + fds_iter: (0..=u64::MAX), } } @@ -309,7 +305,9 @@ impl FileManager { RemoteFile::File(file) }; - self.open_files.insert(fd, remote_file); + if self.open_files.insert(fd, remote_file).is_none() { + OPEN_FD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } Ok(OpenFileResponse { fd }) } @@ -343,7 +341,9 @@ impl FileManager { RemoteFile::File(file) }; - self.open_files.insert(fd, remote_file); + if self.open_files.insert(fd, remote_file).is_none() { + OPEN_FD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } Ok(OpenFileResponse { fd }) } else { @@ -636,20 +636,36 @@ impl FileManager { }) } - pub(crate) fn close(&mut self, fd: u64) { - trace!("FileManager::close -> fd {:#?}", fd,); - + /// Always returns `None`, since we don't return any [`FileResponse`] back to mirrord + /// on `close` of an fd. + #[tracing::instrument(level = Level::TRACE, skip(self))] + pub(crate) fn close(&mut self, fd: u64) -> Option { if self.open_files.remove(&fd).is_none() { - error!("FileManager::close -> fd {:#?} not found", fd); + error!(fd, "fd not found!"); + } else { + OPEN_FD_COUNT.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); } - } - pub(crate) fn close_dir(&mut self, fd: u64) { - trace!("FileManager::close_dir -> fd {:#?}", fd,); + None + } - if self.dir_streams.remove(&fd).is_none() && self.getdents_streams.remove(&fd).is_none() { + /// Always returns `None`, since we don't return any [`FileResponse`] back to mirrord + /// on `close_dir` of an fd. + #[tracing::instrument(level = Level::TRACE, skip(self))] + pub(crate) fn close_dir(&mut self, fd: u64) -> Option { + let closed_dir_stream = self.dir_streams.remove(&fd); + let closed_getdents_stream = self.getdents_streams.remove(&fd); + + if closed_dir_stream.is_some() && closed_getdents_stream.is_some() { + // Closed `dirstream` and `dentsstream` + OPEN_FD_COUNT.fetch_sub(2, std::sync::atomic::Ordering::Relaxed); + } else if closed_dir_stream.is_some() || closed_getdents_stream.is_some() { + OPEN_FD_COUNT.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + } else { error!("FileManager::close_dir -> fd {:#?} not found", fd); } + + None } pub(crate) fn access( @@ -753,7 +769,7 @@ impl FileManager { }) } - #[tracing::instrument(level = "trace", skip(self))] + #[tracing::instrument(level = Level::TRACE, skip(self), err(level = Level::DEBUG))] pub(crate) fn fdopen_dir(&mut self, fd: u64) -> RemoteResult { let path = match self .open_files @@ -770,7 +786,10 @@ impl FileManager { .ok_or_else(|| ResponseError::IdsExhausted("fdopen_dir".to_string()))?; let dir_stream = path.read_dir()?.enumerate(); - self.dir_streams.insert(fd, dir_stream); + + if self.dir_streams.insert(fd, dir_stream).is_none() { + OPEN_FD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } Ok(OpenDirResponse { fd }) } @@ -819,7 +838,7 @@ impl FileManager { /// The possible remote errors are: /// [`ResponseError::NotFound`] if there is not such fd here. /// [`ResponseError::NotDirectory`] if the fd points to a file with a non-directory file type. - #[tracing::instrument(level = "trace", skip(self))] + #[tracing::instrument(level = Level::TRACE, skip(self))] pub(crate) fn get_or_create_getdents64_stream( &mut self, fd: u64, @@ -832,6 +851,7 @@ impl FileManager { let current_and_parent = Self::get_current_and_parent_entries(dir); let stream = GetDEnts64Stream::new(dir.read_dir()?, current_and_parent).peekable(); + OPEN_FD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Ok(e.insert(stream)) } }, diff --git a/mirrord/agent/src/main.rs b/mirrord/agent/src/main.rs index 305ec50e0ed..e9f3e107907 100644 --- a/mirrord/agent/src/main.rs +++ b/mirrord/agent/src/main.rs @@ -22,6 +22,7 @@ mod env; mod error; mod file; mod http; +mod metrics; mod namespace; mod outgoing; mod runtime; @@ -31,7 +32,8 @@ mod util; mod vpn; mod watched_task; +#[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] -async fn main() -> crate::error::Result<()> { +async fn main() -> crate::error::AgentResult<()> { crate::entrypoint::main().await } diff --git a/mirrord/agent/src/metrics.rs b/mirrord/agent/src/metrics.rs new file mode 100644 index 00000000000..59fefbc2305 --- /dev/null +++ b/mirrord/agent/src/metrics.rs @@ -0,0 +1,366 @@ +use std::{ + net::SocketAddr, + sync::{atomic::AtomicI64, Arc}, +}; + +use axum::{extract::State, routing::get, Router}; +use http::StatusCode; +use prometheus::{proto::MetricFamily, IntGauge, Registry}; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; +use tracing::Level; + +use crate::error::AgentError; + +/// Incremented whenever we get a new client in `ClientConnectionHandler`, and decremented +/// when this client is dropped. +pub(crate) static CLIENT_COUNT: AtomicI64 = AtomicI64::new(0); + +/// Incremented whenever we handle a new `DnsCommand`, and decremented after the result of +/// `do_lookup` has been sent back through the response channel. +pub(crate) static DNS_REQUEST_COUNT: AtomicI64 = AtomicI64::new(0); + +/// Incremented and decremented in _open-ish_/_close-ish_ file operations in `FileManager`, +/// Also gets decremented when `FileManager` is dropped. +pub(crate) static OPEN_FD_COUNT: AtomicI64 = AtomicI64::new(0); + +/// Follows the amount of subscribed ports in `update_packet_filter`. We don't really +/// increment/decrement this one, and mostly `set` it to the latest amount of ports, zeroing it when +/// the `TcpConnectionSniffer` gets dropped. +pub(crate) static MIRROR_PORT_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static MIRROR_CONNECTION_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static STEAL_FILTERED_PORT_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static STEAL_UNFILTERED_PORT_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static STEAL_FILTERED_CONNECTION_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static HTTP_REQUEST_IN_PROGRESS_COUNT: AtomicI64 = AtomicI64::new(0); + +pub(crate) static TCP_OUTGOING_CONNECTION: AtomicI64 = AtomicI64::new(0); + +pub(crate) static UDP_OUTGOING_CONNECTION: AtomicI64 = AtomicI64::new(0); + +/// The state with all the metrics [`IntGauge`]s and the prometheus [`Registry`] where we keep them. +/// +/// **Do not** modify the gauges directly! +/// +/// Instead rely on [`Metrics::gather_metrics`], as we actually use a bunch of [`AtomicI64`]s to +/// keep track of the values, they are the ones being (de|in)cremented. These gauges are just set +/// when it's time to send them via [`get_metrics`]. +#[derive(Debug)] +struct Metrics { + registry: Registry, + client_count: IntGauge, + dns_request_count: IntGauge, + open_fd_count: IntGauge, + mirror_port_subscription: IntGauge, + mirror_connection_subscription: IntGauge, + steal_filtered_port_subscription: IntGauge, + steal_unfiltered_port_subscription: IntGauge, + steal_filtered_connection_subscription: IntGauge, + steal_unfiltered_connection_subscription: IntGauge, + http_request_in_progress_count: IntGauge, + tcp_outgoing_connection: IntGauge, + udp_outgoing_connection: IntGauge, +} + +impl Metrics { + /// Creates a [`Registry`] to ... register our [`IntGauge`]s. + fn new() -> Self { + use prometheus::Opts; + + let registry = Registry::new(); + + let client_count = { + let opts = Opts::new( + "mirrord_agent_client_count", + "amount of connected clients to this mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let dns_request_count = { + let opts = Opts::new( + "mirrord_agent_dns_request_count", + "amount of in-progress dns requests in the mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let open_fd_count = { + let opts = Opts::new( + "mirrord_agent_open_fd_count", + "amount of open file descriptors in mirrord-agent file manager", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let mirror_port_subscription = { + let opts = Opts::new( + "mirrord_agent_mirror_port_subscription_count", + "amount of mirror port subscriptions in mirror-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let mirror_connection_subscription = { + let opts = Opts::new( + "mirrord_agent_mirror_connection_subscription_count", + "amount of connections in mirror mode in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let steal_filtered_port_subscription = { + let opts = Opts::new( + "mirrord_agent_steal_filtered_port_subscription_count", + "amount of filtered steal port subscriptions in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let steal_unfiltered_port_subscription = { + let opts = Opts::new( + "mirrord_agent_steal_unfiltered_port_subscription_count", + "amount of unfiltered steal port subscriptions in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let steal_filtered_connection_subscription = { + let opts = Opts::new( + "mirrord_agent_steal_connection_subscription_count", + "amount of filtered connections in steal mode in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let steal_unfiltered_connection_subscription = { + let opts = Opts::new( + "mirrord_agent_steal_unfiltered_connection_subscription_count", + "amount of unfiltered connections in steal mode in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let http_request_in_progress_count = { + let opts = Opts::new( + "mirrord_agent_http_request_in_progress_count", + "amount of in-progress http requests in the mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let tcp_outgoing_connection = { + let opts = Opts::new( + "mirrord_agent_tcp_outgoing_connection_count", + "amount of tcp outgoing connections in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + let udp_outgoing_connection = { + let opts = Opts::new( + "mirrord_agent_udp_outgoing_connection_count", + "amount of udp outgoing connections in mirrord-agent", + ); + IntGauge::with_opts(opts).expect("Valid at initialization!") + }; + + registry + .register(Box::new(client_count.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(dns_request_count.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(open_fd_count.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(mirror_port_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(mirror_connection_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(steal_filtered_port_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(steal_unfiltered_port_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(steal_filtered_connection_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(steal_unfiltered_connection_subscription.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(http_request_in_progress_count.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(tcp_outgoing_connection.clone())) + .expect("Register must be valid at initialization!"); + registry + .register(Box::new(udp_outgoing_connection.clone())) + .expect("Register must be valid at initialization!"); + + Self { + registry, + client_count, + dns_request_count, + open_fd_count, + mirror_port_subscription, + mirror_connection_subscription, + steal_filtered_port_subscription, + steal_unfiltered_port_subscription, + steal_filtered_connection_subscription, + steal_unfiltered_connection_subscription, + http_request_in_progress_count, + tcp_outgoing_connection, + udp_outgoing_connection, + } + } + + /// Calls [`IntGauge::set`] on every [`IntGauge`] of `Self`, setting it to the value of + /// the corresponding [`AtomicI64`] global (the uppercase named version of the gauge). + /// + /// Returns the list of [`MetricFamily`] registered in our [`Metrics::registry`], ready to be + /// encoded and sent to prometheus. + fn gather_metrics(&self) -> Vec { + use std::sync::atomic::Ordering; + + let Self { + registry, + client_count, + dns_request_count, + open_fd_count, + mirror_port_subscription, + mirror_connection_subscription, + steal_filtered_port_subscription, + steal_unfiltered_port_subscription, + steal_filtered_connection_subscription, + steal_unfiltered_connection_subscription, + http_request_in_progress_count, + tcp_outgoing_connection, + udp_outgoing_connection, + } = self; + + client_count.set(CLIENT_COUNT.load(Ordering::Relaxed)); + dns_request_count.set(DNS_REQUEST_COUNT.load(Ordering::Relaxed)); + open_fd_count.set(OPEN_FD_COUNT.load(Ordering::Relaxed)); + mirror_port_subscription.set(MIRROR_PORT_SUBSCRIPTION.load(Ordering::Relaxed)); + mirror_connection_subscription.set(MIRROR_CONNECTION_SUBSCRIPTION.load(Ordering::Relaxed)); + steal_filtered_port_subscription + .set(STEAL_FILTERED_PORT_SUBSCRIPTION.load(Ordering::Relaxed)); + steal_unfiltered_port_subscription + .set(STEAL_UNFILTERED_PORT_SUBSCRIPTION.load(Ordering::Relaxed)); + steal_filtered_connection_subscription + .set(STEAL_FILTERED_CONNECTION_SUBSCRIPTION.load(Ordering::Relaxed)); + steal_unfiltered_connection_subscription + .set(STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.load(Ordering::Relaxed)); + http_request_in_progress_count.set(HTTP_REQUEST_IN_PROGRESS_COUNT.load(Ordering::Relaxed)); + tcp_outgoing_connection.set(TCP_OUTGOING_CONNECTION.load(Ordering::Relaxed)); + udp_outgoing_connection.set(UDP_OUTGOING_CONNECTION.load(Ordering::Relaxed)); + + registry.gather() + } +} + +/// `GET /metrics` +/// +/// Prepares all the metrics with [`Metrics::gather_metrics`], and responds to the prometheus +/// request. +#[tracing::instrument(level = Level::TRACE, ret)] +async fn get_metrics(State(state): State>) -> (StatusCode, String) { + use prometheus::TextEncoder; + + let metric_families = state.gather_metrics(); + match TextEncoder.encode_to_string(&metric_families) { + Ok(response) => (StatusCode::OK, response), + Err(fail) => { + tracing::error!(?fail, "Failed GET /metrics"); + (StatusCode::INTERNAL_SERVER_ERROR, fail.to_string()) + } + } +} + +/// Starts the mirrord-agent prometheus metrics service. +/// +/// You can get the metrics from `GET address/metrics`. +/// +/// - `address`: comes from a mirrord-agent config. +#[tracing::instrument(level = Level::TRACE, skip_all, ret ,err)] +pub(crate) async fn start_metrics( + address: SocketAddr, + cancellation_token: CancellationToken, +) -> Result<(), axum::BoxError> { + let metrics_state = Arc::new(Metrics::new()); + + let app = Router::new() + .route("/metrics", get(get_metrics)) + .with_state(metrics_state); + + let listener = TcpListener::bind(address) + .await + .map_err(AgentError::from) + .inspect_err(|fail| { + tracing::error!(?fail, "Failed to bind TCP socket for metrics server") + })?; + + let cancel_on_error = cancellation_token.clone(); + axum::serve(listener, app) + .with_graceful_shutdown(async move { cancellation_token.cancelled().await }) + .await + .inspect_err(|fail| { + tracing::error!(%fail, "Could not start agent metrics server!"); + cancel_on_error.cancel(); + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{sync::atomic::Ordering, time::Duration}; + + use tokio_util::sync::CancellationToken; + + use super::OPEN_FD_COUNT; + use crate::metrics::start_metrics; + + #[tokio::test] + async fn test_metrics() { + let metrics_address = "127.0.0.1:9000".parse().unwrap(); + let cancellation_token = CancellationToken::new(); + + let metrics_cancellation = cancellation_token.child_token(); + tokio::spawn(async move { + start_metrics(metrics_address, metrics_cancellation) + .await + .unwrap() + }); + + OPEN_FD_COUNT.fetch_add(1, Ordering::Relaxed); + + // Give the server some time to start. + tokio::time::sleep(Duration::from_secs(1)).await; + + let get_all_metrics = reqwest::get("http://127.0.0.1:9000/metrics") + .await + .unwrap() + .error_for_status() + .unwrap() + .text() + .await + .unwrap(); + + assert!(get_all_metrics.contains("mirrord_agent_open_fd_count 1")); + + cancellation_token.drop_guard(); + } +} diff --git a/mirrord/agent/src/outgoing.rs b/mirrord/agent/src/outgoing.rs index 13e3a9e1e06..96a063d7a05 100644 --- a/mirrord/agent/src/outgoing.rs +++ b/mirrord/agent/src/outgoing.rs @@ -18,7 +18,8 @@ use tokio_util::io::ReaderStream; use tracing::Level; use crate::{ - error::Result, + error::AgentResult, + metrics::TCP_OUTGOING_CONNECTION, util::run_thread_in_namespace, watched_task::{TaskStatus, WatchedTask}, }; @@ -81,7 +82,7 @@ impl TcpOutgoingApi { /// Sends the [`LayerTcpOutgoing`] message to the background task. #[tracing::instrument(level = Level::TRACE, skip(self), err)] - pub(crate) async fn send_to_task(&mut self, message: LayerTcpOutgoing) -> Result<()> { + pub(crate) async fn send_to_task(&mut self, message: LayerTcpOutgoing) -> AgentResult<()> { if self.layer_tx.send(message).await.is_ok() { Ok(()) } else { @@ -91,7 +92,7 @@ impl TcpOutgoingApi { /// Receives a [`DaemonTcpOutgoing`] message from the background task. #[tracing::instrument(level = Level::TRACE, skip(self), err)] - pub(crate) async fn recv_from_task(&mut self) -> Result { + pub(crate) async fn recv_from_task(&mut self) -> AgentResult { match self.daemon_rx.recv().await { Some(msg) => Ok(msg), None => Err(self.task_status.unwrap_err().await), @@ -112,6 +113,13 @@ struct TcpOutgoingTask { daemon_tx: Sender, } +impl Drop for TcpOutgoingTask { + fn drop(&mut self) { + let connections = self.readers.keys().chain(self.writers.keys()).count(); + TCP_OUTGOING_CONNECTION.fetch_sub(connections as i64, std::sync::atomic::Ordering::Relaxed); + } +} + impl fmt::Debug for TcpOutgoingTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TcpOutgoingTask") @@ -152,7 +160,7 @@ impl TcpOutgoingTask { /// Runs this task as long as the channels connecting it with [`TcpOutgoingApi`] are open. /// This routine never fails and returns [`Result`] only due to [`WatchedTask`] constraints. #[tracing::instrument(level = Level::TRACE, skip(self))] - async fn run(mut self) -> Result<()> { + async fn run(mut self) -> AgentResult<()> { loop { let channel_closed = select! { biased; @@ -216,6 +224,7 @@ impl TcpOutgoingTask { self.readers.remove(&connection_id); self.writers.remove(&connection_id); + TCP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); let daemon_message = DaemonTcpOutgoing::Close(connection_id); self.daemon_tx.send(daemon_message).await?; @@ -246,6 +255,8 @@ impl TcpOutgoingTask { "Layer connection is shut down as well, sending close message.", ); + TCP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + self.daemon_tx .send(DaemonTcpOutgoing::Close(connection_id)) .await?; @@ -287,6 +298,7 @@ impl TcpOutgoingTask { connection_id, ReaderStream::with_capacity(read_half, Self::READ_BUFFER_SIZE), ); + TCP_OUTGOING_CONNECTION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Ok(DaemonConnect { connection_id, @@ -299,9 +311,12 @@ impl TcpOutgoingTask { result = ?daemon_connect, "Connection attempt finished.", ); + self.daemon_tx .send(DaemonTcpOutgoing::Connect(daemon_connect)) - .await + .await?; + + Ok(()) } // This message handles two cases: @@ -341,9 +356,14 @@ impl TcpOutgoingTask { connection_id, "Peer connection is shut down as well, sending close message to the client.", ); + TCP_OUTGOING_CONNECTION + .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + self.daemon_tx .send(DaemonTcpOutgoing::Close(connection_id)) - .await + .await?; + + Ok(()) } } @@ -352,6 +372,7 @@ impl TcpOutgoingTask { Err(error) => { self.writers.remove(&connection_id); self.readers.remove(&connection_id); + TCP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); tracing::trace!( connection_id, @@ -360,7 +381,9 @@ impl TcpOutgoingTask { ); self.daemon_tx .send(DaemonTcpOutgoing::Close(connection_id)) - .await + .await?; + + Ok(()) } } } @@ -370,6 +393,7 @@ impl TcpOutgoingTask { LayerTcpOutgoing::Close(LayerClose { connection_id }) => { self.writers.remove(&connection_id); self.readers.remove(&connection_id); + TCP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); Ok(()) } diff --git a/mirrord/agent/src/outgoing/udp.rs b/mirrord/agent/src/outgoing/udp.rs index b6baa5e537e..0dab137a92b 100644 --- a/mirrord/agent/src/outgoing/udp.rs +++ b/mirrord/agent/src/outgoing/udp.rs @@ -1,10 +1,11 @@ +use core::fmt; use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, thread, }; -use bytes::BytesMut; +use bytes::{Bytes, BytesMut}; use futures::{ prelude::*, stream::{SplitSink, SplitStream}, @@ -15,21 +16,262 @@ use mirrord_protocol::{ }; use streammap_ext::StreamMap; use tokio::{ + io, net::UdpSocket, select, - sync::mpsc::{self, Receiver, Sender}, + sync::mpsc::{self, error::SendError, Receiver, Sender}, }; use tokio_util::{codec::BytesCodec, udp::UdpFramed}; -use tracing::{debug, trace, warn}; +use tracing::Level; use crate::{ - error::Result, + error::AgentResult, + metrics::UDP_OUTGOING_CONNECTION, util::run_thread_in_namespace, watched_task::{TaskStatus, WatchedTask}, }; -type Layer = LayerUdpOutgoing; -type Daemon = DaemonUdpOutgoing; +/// Task that handles [`LayerUdpOutgoing`] and [`DaemonUdpOutgoing`] messages. +/// +/// We start these tasks from the [`UdpOutgoingApi`] as a [`WatchedTask`]. +struct UdpOutgoingTask { + next_connection_id: ConnectionId, + /// Writing halves of peer connections made on layer's requests. + #[allow(clippy::type_complexity)] + writers: HashMap< + ConnectionId, + ( + SplitSink, (BytesMut, SocketAddr)>, + SocketAddr, + ), + >, + /// Reading halves of peer connections made on layer's requests. + readers: StreamMap>>, + /// Optional pid of agent's target. Used in `SocketStream::connect`. + pid: Option, + layer_rx: Receiver, + daemon_tx: Sender, +} + +impl Drop for UdpOutgoingTask { + fn drop(&mut self) { + let connections = self.readers.keys().chain(self.writers.keys()).count(); + UDP_OUTGOING_CONNECTION.fetch_sub(connections as i64, std::sync::atomic::Ordering::Relaxed); + } +} + +impl fmt::Debug for UdpOutgoingTask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UdpOutgoingTask") + .field("next_connection_id", &self.next_connection_id) + .field("writers", &self.writers.len()) + .field("readers", &self.readers.len()) + .field("pid", &self.pid) + .finish() + } +} + +impl UdpOutgoingTask { + fn new( + pid: Option, + layer_rx: Receiver, + daemon_tx: Sender, + ) -> Self { + Self { + next_connection_id: 0, + writers: Default::default(), + readers: Default::default(), + pid, + layer_rx, + daemon_tx, + } + } + + /// Runs this task as long as the channels connecting it with [`UdpOutgoingApi`] are open. + /// This routine never fails and returns [`AgentResult`] only due to [`WatchedTask`] + /// constraints. + #[tracing::instrument(level = Level::TRACE, skip(self))] + pub(super) async fn run(mut self) -> AgentResult<()> { + loop { + let channel_closed = select! { + biased; + + message = self.layer_rx.recv() => match message { + // We have a message from the layer to be handled. + Some(message) => { + self.handle_layer_msg(message).await.is_err() + }, + // Our channel with the layer is closed, this task is no longer needed. + None => true, + }, + + // We have data coming from one of our peers. + Some((connection_id, remote_read)) = self.readers.next() => { + self.handle_connection_read(connection_id, remote_read.transpose().map(|remote| remote.map(|(read, _)| read.into()))).await.is_err() + }, + }; + + if channel_closed { + tracing::trace!("Client channel closed, exiting"); + break Ok(()); + } + } + } + + /// Returns [`Err`] only when the client has disconnected. + #[tracing::instrument( + level = Level::TRACE, + skip(read), + fields(read = ?read.as_ref().map(|data| data.as_ref().map(Bytes::len).unwrap_or_default())) + err(level = Level::TRACE) + )] + async fn handle_connection_read( + &mut self, + connection_id: ConnectionId, + read: io::Result>, + ) -> Result<(), SendError> { + match read { + Ok(Some(read)) => { + let message = DaemonUdpOutgoing::Read(Ok(DaemonRead { + connection_id, + bytes: read.to_vec(), + })); + + self.daemon_tx.send(message).await? + } + // An error occurred when reading from a peer connection. + // We remove both io halves and inform the layer that the connection is closed. + // We remove the reader, because otherwise the `StreamMap` will produce an extra `None` + // item from the related stream. + Err(error) => { + tracing::trace!( + ?error, + connection_id, + "Reading from peer connection failed, sending close message.", + ); + + self.readers.remove(&connection_id); + self.writers.remove(&connection_id); + UDP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + + let daemon_message = DaemonUdpOutgoing::Close(connection_id); + self.daemon_tx.send(daemon_message).await?; + } + Ok(None) => { + self.writers.remove(&connection_id); + self.readers.remove(&connection_id); + UDP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + + let daemon_message = DaemonUdpOutgoing::Close(connection_id); + self.daemon_tx.send(daemon_message).await?; + } + } + + Ok(()) + } + + /// Returns [`Err`] only when the client has disconnected. + #[allow(clippy::type_complexity)] + #[tracing::instrument(level = Level::TRACE, ret)] + async fn handle_layer_msg( + &mut self, + message: LayerUdpOutgoing, + ) -> Result<(), SendError> { + match message { + // [user] -> [layer] -> [agent] -> [layer] + // `user` is asking us to connect to some remote host. + LayerUdpOutgoing::Connect(LayerConnect { remote_address }) => { + let daemon_connect = + connect(remote_address.clone()) + .await + .and_then(|mirror_socket| { + let connection_id = self.next_connection_id; + self.next_connection_id += 1; + + let peer_address = mirror_socket.peer_addr()?; + let local_address = mirror_socket.local_addr()?; + let local_address = SocketAddress::Ip(local_address); + + let framed = UdpFramed::new(mirror_socket, BytesCodec::new()); + + let (sink, stream): ( + SplitSink, (BytesMut, SocketAddr)>, + SplitStream>, + ) = framed.split(); + + self.writers.insert(connection_id, (sink, peer_address)); + self.readers.insert(connection_id, stream); + UDP_OUTGOING_CONNECTION + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + Ok(DaemonConnect { + connection_id, + remote_address, + local_address, + }) + }); + + tracing::trace!( + result = ?daemon_connect, + "Connection attempt finished.", + ); + + self.daemon_tx + .send(DaemonUdpOutgoing::Connect(daemon_connect)) + .await?; + + Ok(()) + } + // [user] -> [layer] -> [agent] -> [remote] + // `user` wrote some message to the remote host. + LayerUdpOutgoing::Write(LayerWrite { + connection_id, + bytes, + }) => { + let write_result = match self + .writers + .get_mut(&connection_id) + .ok_or(ResponseError::NotFound(connection_id)) + { + Ok((mirror, remote_address)) => mirror + .send((BytesMut::from(bytes.as_slice()), *remote_address)) + .await + .map_err(ResponseError::from), + Err(fail) => Err(fail), + }; + + match write_result { + Ok(()) => Ok(()), + Err(error) => { + self.writers.remove(&connection_id); + self.readers.remove(&connection_id); + UDP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + + tracing::trace!( + connection_id, + ?error, + "Failed to handle layer write, sending close message to the client.", + ); + + let daemon_message = DaemonUdpOutgoing::Close(connection_id); + self.daemon_tx.send(daemon_message).await?; + + Ok(()) + } + } + } + // [layer] -> [agent] + // `layer` closed their interceptor stream. + LayerUdpOutgoing::Close(LayerClose { ref connection_id }) => { + self.writers.remove(connection_id); + self.readers.remove(connection_id); + UDP_OUTGOING_CONNECTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + + Ok(()) + } + } + } +} /// Handles (briefly) the `UdpOutgoingRequest` and `UdpOutgoingResponse` messages, mostly the /// passing of these messages to the `interceptor_task` thread. @@ -41,10 +283,10 @@ pub(crate) struct UdpOutgoingApi { task_status: TaskStatus, /// Sends the `Layer` message to the `interceptor_task`. - layer_tx: Sender, + layer_tx: Sender, /// Reads the `Daemon` message from the `interceptor_task`. - daemon_rx: Receiver, + daemon_rx: Receiver, } /// Performs an [`UdpSocket::connect`] that handles 3 situations: @@ -55,8 +297,9 @@ pub(crate) struct UdpOutgoingApi { /// read access to `/etc/resolv.conf`, otherwise they'll be getting a mismatched connection; /// 3. User is trying to use `sendto` and `recvfrom`, we use the same hack as in DNS to fake a /// connection. -#[tracing::instrument(level = "trace", ret)] -async fn connect(remote_address: SocketAddr) -> Result { +#[tracing::instrument(level = Level::TRACE, ret, err(level = Level::DEBUG))] +async fn connect(remote_address: SocketAddress) -> Result { + let remote_address = remote_address.try_into()?; let mirror_address = match remote_address { std::net::SocketAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), std::net::SocketAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), @@ -75,8 +318,10 @@ impl UdpOutgoingApi { let (layer_tx, layer_rx) = mpsc::channel(1000); let (daemon_tx, daemon_rx) = mpsc::channel(1000); - let watched_task = - WatchedTask::new(Self::TASK_NAME, Self::interceptor_task(layer_rx, daemon_tx)); + let watched_task = WatchedTask::new( + Self::TASK_NAME, + UdpOutgoingTask::new(pid, layer_rx, daemon_tx).run(), + ); let task_status = watched_task.status(); let task = run_thread_in_namespace( @@ -94,150 +339,9 @@ impl UdpOutgoingApi { } } - /// The [`UdpOutgoingApi`] task. - /// - /// Receives [`LayerUdpOutgoing`] messages and replies with [`DaemonUdpOutgoing`]. - #[allow(clippy::type_complexity)] - async fn interceptor_task( - mut layer_rx: Receiver, - daemon_tx: Sender, - ) -> Result<()> { - let mut connection_ids = 0..=ConnectionId::MAX; - - // TODO: Right now we're manually keeping these 2 maps in sync (aviram suggested using - // `Weak` for `writers`). - let mut writers: HashMap< - ConnectionId, - ( - SplitSink, (BytesMut, SocketAddr)>, - SocketAddr, - ), - > = HashMap::default(); - - let mut readers: StreamMap>> = - StreamMap::default(); - - loop { - select! { - biased; - - // [layer] -> [agent] - Some(layer_message) = layer_rx.recv() => { - trace!("udp: interceptor_task -> layer_message {:?}", layer_message); - match layer_message { - // [user] -> [layer] -> [agent] -> [layer] - // `user` is asking us to connect to some remote host. - LayerUdpOutgoing::Connect(LayerConnect { remote_address }) => { - let daemon_connect = connect(remote_address.clone().try_into()?) - .await - .and_then(|mirror_socket| { - let connection_id = connection_ids - .next() - .ok_or_else(|| ResponseError::IdsExhausted("connect".into()))?; - - debug!("interceptor_task -> mirror_socket {:#?}", mirror_socket); - let peer_address = mirror_socket.peer_addr()?; - let local_address = mirror_socket.local_addr()?; - let local_address = SocketAddress::Ip(local_address); - let framed = UdpFramed::new(mirror_socket, BytesCodec::new()); - debug!("interceptor_task -> framed {:#?}", framed); - let (sink, stream): ( - SplitSink, (BytesMut, SocketAddr)>, - SplitStream>, - ) = framed.split(); - - writers.insert(connection_id, (sink, peer_address)); - readers.insert(connection_id, stream); - - Ok(DaemonConnect { - connection_id, - remote_address, - local_address - }) - }); - - let daemon_message = DaemonUdpOutgoing::Connect(daemon_connect); - debug!("interceptor_task -> daemon_message {:#?}", daemon_message); - daemon_tx.send(daemon_message).await? - } - // [user] -> [layer] -> [agent] -> [remote] - // `user` wrote some message to the remote host. - LayerUdpOutgoing::Write(LayerWrite { - connection_id, - bytes, - }) => { - let daemon_write = match writers - .get_mut(&connection_id) - .ok_or(ResponseError::NotFound(connection_id)) - { - Ok((mirror, remote_address)) => mirror - .send((BytesMut::from(bytes.as_slice()), *remote_address)) - .await - .map_err(ResponseError::from), - Err(fail) => Err(fail), - }; - - if let Err(fail) = daemon_write { - warn!("LayerUdpOutgoing::Write -> Failed with {:#?}", fail); - writers.remove(&connection_id); - readers.remove(&connection_id); - - let daemon_message = DaemonUdpOutgoing::Close(connection_id); - daemon_tx.send(daemon_message).await? - } - } - // [layer] -> [agent] - // `layer` closed their interceptor stream. - LayerUdpOutgoing::Close(LayerClose { ref connection_id }) => { - writers.remove(connection_id); - readers.remove(connection_id); - } - } - } - - // [remote] -> [agent] -> [layer] -> [user] - // Read the data from one of the connected remote hosts, and forward the result back - // to the `user`. - Some((connection_id, remote_read)) = readers.next() => { - trace!("interceptor_task -> read connection_id {:#?}", connection_id); - - match remote_read { - Some(read) => { - let daemon_read = read - .map_err(ResponseError::from) - .map(|(bytes, _)| DaemonRead { connection_id, bytes: bytes.to_vec() }); - - let daemon_message = DaemonUdpOutgoing::Read(daemon_read); - daemon_tx.send(daemon_message).await? - } - None => { - trace!("interceptor_task -> close connection {:#?}", connection_id); - writers.remove(&connection_id); - readers.remove(&connection_id); - - let daemon_message = DaemonUdpOutgoing::Close(connection_id); - daemon_tx.send(daemon_message).await? - } - } - } - else => { - // We have no more data coming from any of the remote hosts. - warn!("interceptor_task -> no messages left"); - break; - } - } - } - - Ok(()) - } - /// Sends a `UdpOutgoingRequest` to the `interceptor_task`. - pub(crate) async fn layer_message(&mut self, message: LayerUdpOutgoing) -> Result<()> { - trace!( - "UdpOutgoingApi::layer_message -> layer_message {:#?}", - message - ); - + #[tracing::instrument(level = Level::TRACE, skip(self), err)] + pub(crate) async fn send_to_task(&mut self, message: LayerUdpOutgoing) -> AgentResult<()> { if self.layer_tx.send(message).await.is_ok() { Ok(()) } else { @@ -246,7 +350,7 @@ impl UdpOutgoingApi { } /// Receives a `UdpOutgoingResponse` from the `interceptor_task`. - pub(crate) async fn daemon_message(&mut self) -> Result { + pub(crate) async fn recv_from_task(&mut self) -> AgentResult { match self.daemon_rx.recv().await { Some(msg) => Ok(msg), None => Err(self.task_status.unwrap_err().await), diff --git a/mirrord/agent/src/sniffer.rs b/mirrord/agent/src/sniffer.rs index b1c232eae6d..0d5cccfb584 100644 --- a/mirrord/agent/src/sniffer.rs +++ b/mirrord/agent/src/sniffer.rs @@ -24,8 +24,9 @@ use self::{ tcp_capture::RawSocketTcpCapture, }; use crate::{ - error::AgentError, + error::AgentResult, http::HttpVersion, + metrics::{MIRROR_CONNECTION_SUBSCRIPTION, MIRROR_PORT_SUBSCRIPTION}, util::{ChannelClosedFuture, ClientId, Subscriptions}, }; @@ -141,6 +142,13 @@ pub(crate) struct TcpConnectionSniffer { clients_closed: FuturesUnordered>, } +impl Drop for TcpConnectionSniffer { + fn drop(&mut self) { + MIRROR_PORT_SUBSCRIPTION.store(0, std::sync::atomic::Ordering::Relaxed); + MIRROR_CONNECTION_SUBSCRIPTION.store(0, std::sync::atomic::Ordering::Relaxed); + } +} + impl fmt::Debug for TcpConnectionSniffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TcpConnectionSniffer") @@ -163,7 +171,7 @@ impl TcpConnectionSniffer { command_rx: Receiver, network_interface: Option, is_mesh: bool, - ) -> Result { + ) -> AgentResult { let tcp_capture = RawSocketTcpCapture::new(network_interface, is_mesh).await?; Ok(Self { @@ -190,7 +198,7 @@ where /// Runs the sniffer loop, capturing packets. #[tracing::instrument(level = Level::DEBUG, skip(cancel_token), err)] - pub async fn start(mut self, cancel_token: CancellationToken) -> Result<(), AgentError> { + pub async fn start(mut self, cancel_token: CancellationToken) -> AgentResult<()> { loop { select! { command = self.command_rx.recv() => { @@ -232,7 +240,7 @@ where /// Removes the client with `client_id`, and also unsubscribes its port. /// Adjusts BPF filter if needed. #[tracing::instrument(level = Level::TRACE, err)] - fn handle_client_closed(&mut self, client_id: ClientId) -> Result<(), AgentError> { + fn handle_client_closed(&mut self, client_id: ClientId) -> AgentResult<()> { self.client_txs.remove(&client_id); if self.port_subscriptions.remove_client(client_id) { @@ -245,8 +253,9 @@ where /// Updates BPF filter used by [`Self::tcp_capture`] to match state of /// [`Self::port_subscriptions`]. #[tracing::instrument(level = Level::TRACE, err)] - fn update_packet_filter(&mut self) -> Result<(), AgentError> { + fn update_packet_filter(&mut self) -> AgentResult<()> { let ports = self.port_subscriptions.get_subscribed_topics(); + MIRROR_PORT_SUBSCRIPTION.store(ports.len() as i64, std::sync::atomic::Ordering::Relaxed); let filter = if ports.is_empty() { tracing::trace!("No ports subscribed, setting dummy bpf"); @@ -261,7 +270,7 @@ where } #[tracing::instrument(level = Level::TRACE, err)] - fn handle_command(&mut self, command: SnifferCommand) -> Result<(), AgentError> { + fn handle_command(&mut self, command: SnifferCommand) -> AgentResult<()> { match command { SnifferCommand { client_id, @@ -325,7 +334,7 @@ where &mut self, identifier: TcpSessionIdentifier, tcp_packet: TcpPacketData, - ) -> Result<(), AgentError> { + ) -> AgentResult<()> { let data_tx = match self.sessions.entry(identifier) { Entry::Occupied(e) => e, Entry::Vacant(e) => { @@ -394,6 +403,7 @@ where } } + MIRROR_CONNECTION_SUBSCRIPTION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); e.insert_entry(data_tx) } }; @@ -448,6 +458,7 @@ mod test { async fn get_api(&mut self) -> TcpSnifferApi { let client_id = self.next_client_id; self.next_client_id += 1; + TcpSnifferApi::new(client_id, self.command_tx.clone(), self.task_status.clone()) .await .unwrap() diff --git a/mirrord/agent/src/sniffer/api.rs b/mirrord/agent/src/sniffer/api.rs index 31ec4107f97..08874e93124 100644 --- a/mirrord/agent/src/sniffer/api.rs +++ b/mirrord/agent/src/sniffer/api.rs @@ -14,12 +14,17 @@ use tokio_stream::{ StreamMap, StreamNotifyClose, }; -use super::messages::{SniffedConnection, SnifferCommand, SnifferCommandInner}; +use super::{ + messages::{SniffedConnection, SnifferCommand, SnifferCommandInner}, + AgentResult, +}; use crate::{error::AgentError, util::ClientId, watched_task::TaskStatus}; /// Interface used by clients to interact with the /// [`TcpConnectionSniffer`](super::TcpConnectionSniffer). Multiple instances of this struct operate /// on a single sniffer instance. +/// +/// Enabled by the `mirror` feature for incoming traffic. pub(crate) struct TcpSnifferApi { /// Id of the client using this struct. client_id: ClientId, @@ -55,7 +60,7 @@ impl TcpSnifferApi { client_id: ClientId, sniffer_sender: Sender, mut task_status: TaskStatus, - ) -> Result { + ) -> AgentResult { let (sender, receiver) = mpsc::channel(Self::CONNECTION_CHANNEL_SIZE); let command = SnifferCommand { @@ -79,7 +84,7 @@ impl TcpSnifferApi { /// Send the given command to the connected /// [`TcpConnectionSniffer`](super::TcpConnectionSniffer). - async fn send_command(&mut self, command: SnifferCommandInner) -> Result<(), AgentError> { + async fn send_command(&mut self, command: SnifferCommandInner) -> AgentResult<()> { let command = SnifferCommand { client_id: self.client_id, command, @@ -94,7 +99,7 @@ impl TcpSnifferApi { /// Return the next message from the connected /// [`TcpConnectionSniffer`](super::TcpConnectionSniffer). - pub async fn recv(&mut self) -> Result<(DaemonTcp, Option), AgentError> { + pub async fn recv(&mut self) -> AgentResult<(DaemonTcp, Option)> { tokio::select! { conn = self.receiver.recv() => match conn { Some(conn) => { @@ -158,27 +163,26 @@ impl TcpSnifferApi { } } - /// Tansform the given message into a [`SnifferCommand`] and pass it to the connected + /// Tansforms a [`LayerTcp`] message into a [`SnifferCommand`] and passes it to the connected /// [`TcpConnectionSniffer`](super::TcpConnectionSniffer). - pub async fn handle_client_message(&mut self, message: LayerTcp) -> Result<(), AgentError> { + pub async fn handle_client_message(&mut self, message: LayerTcp) -> AgentResult<()> { match message { LayerTcp::PortSubscribe(port) => { let (tx, rx) = oneshot::channel(); self.send_command(SnifferCommandInner::Subscribe(port, tx)) .await?; self.subscriptions_in_progress.push(rx); - Ok(()) } LayerTcp::PortUnsubscribe(port) => { self.send_command(SnifferCommandInner::UnsubscribePort(port)) - .await + .await?; + Ok(()) } LayerTcp::ConnectionUnsubscribe(connection_id) => { self.connections.remove(&connection_id); - Ok(()) } } diff --git a/mirrord/agent/src/sniffer/tcp_capture.rs b/mirrord/agent/src/sniffer/tcp_capture.rs index 1d8031d08b3..dc8fb2bba04 100644 --- a/mirrord/agent/src/sniffer/tcp_capture.rs +++ b/mirrord/agent/src/sniffer/tcp_capture.rs @@ -12,7 +12,7 @@ use rawsocket::{filter::SocketFilterProgram, RawCapture}; use tokio::net::UdpSocket; use tracing::Level; -use super::{TcpPacketData, TcpSessionIdentifier}; +use super::{AgentResult, TcpPacketData, TcpSessionIdentifier}; use crate::error::AgentError; /// Trait for structs that are able to sniff incoming Ethernet packets and filter TCP packets. @@ -36,7 +36,7 @@ impl RawSocketTcpCapture { /// /// Returned instance initially uses a BPF filter that drops every packet. #[tracing::instrument(level = Level::DEBUG, err)] - pub async fn new(network_interface: Option, is_mesh: bool) -> Result { + pub async fn new(network_interface: Option, is_mesh: bool) -> AgentResult { // Priority is whatever the user set as an option to mirrord, then we check if we're in a // mesh to use `lo` interface, otherwise we try to get the appropriate interface. let interface = match network_interface.or_else(|| is_mesh.then(|| "lo".to_string())) { diff --git a/mirrord/agent/src/steal/api.rs b/mirrord/agent/src/steal/api.rs index 15d2f265ba7..2fc5733f8fa 100644 --- a/mirrord/agent/src/steal/api.rs +++ b/mirrord/agent/src/steal/api.rs @@ -8,11 +8,11 @@ use mirrord_protocol::{ }; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio_stream::wrappers::ReceiverStream; +use tracing::Level; use super::{http::ReceiverStreamBody, *}; use crate::{ - error::{AgentError, Result}, - util::ClientId, + error::AgentResult, metrics::HTTP_REQUEST_IN_PROGRESS_COUNT, util::ClientId, watched_task::TaskStatus, }; @@ -50,17 +50,23 @@ pub(crate) struct TcpStealerApi { response_body_txs: HashMap<(ConnectionId, RequestId), ResponseBodyTx>, } +impl Drop for TcpStealerApi { + fn drop(&mut self) { + HTTP_REQUEST_IN_PROGRESS_COUNT.store(0, std::sync::atomic::Ordering::Relaxed); + } +} + impl TcpStealerApi { /// Initializes a [`TcpStealerApi`] and sends a message to [`TcpConnectionStealer`] signaling /// that we have a new client. - #[tracing::instrument(level = "trace")] + #[tracing::instrument(level = Level::TRACE, err)] pub(crate) async fn new( client_id: ClientId, command_tx: Sender, task_status: TaskStatus, channel_size: usize, protocol_version: semver::Version, - ) -> Result { + ) -> AgentResult { let (daemon_tx, daemon_rx) = mpsc::channel(channel_size); command_tx @@ -80,7 +86,7 @@ impl TcpStealerApi { } /// Send `command` to stealer, with the client id of the client that is using this API instance. - async fn send_command(&mut self, command: Command) -> Result<()> { + async fn send_command(&mut self, command: Command) -> AgentResult<()> { let command = StealerCommand { client_id: self.client_id, command, @@ -98,12 +104,16 @@ impl TcpStealerApi { /// /// Called in the `ClientConnectionHandler`. #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn recv(&mut self) -> Result { + pub(crate) async fn recv(&mut self) -> AgentResult { match self.daemon_rx.recv().await { Some(msg) => { if let DaemonTcp::Close(close) = &msg { self.response_body_txs .retain(|(key_id, _), _| *key_id != close.connection_id); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); } Ok(msg) } @@ -115,7 +125,7 @@ impl TcpStealerApi { /// agent, to an internal stealer command [`Command::PortSubscribe`]. /// /// The actual handling of this message is done in [`TcpConnectionStealer`]. - pub(crate) async fn port_subscribe(&mut self, port_steal: StealType) -> Result<(), AgentError> { + pub(crate) async fn port_subscribe(&mut self, port_steal: StealType) -> AgentResult<()> { self.send_command(Command::PortSubscribe(port_steal)).await } @@ -123,7 +133,7 @@ impl TcpStealerApi { /// agent, to an internal stealer command [`Command::PortUnsubscribe`]. /// /// The actual handling of this message is done in [`TcpConnectionStealer`]. - pub(crate) async fn port_unsubscribe(&mut self, port: Port) -> Result<(), AgentError> { + pub(crate) async fn port_unsubscribe(&mut self, port: Port) -> AgentResult<()> { self.send_command(Command::PortUnsubscribe(port)).await } @@ -134,7 +144,7 @@ impl TcpStealerApi { pub(crate) async fn connection_unsubscribe( &mut self, connection_id: ConnectionId, - ) -> Result<(), AgentError> { + ) -> AgentResult<()> { self.send_command(Command::ConnectionUnsubscribe(connection_id)) .await } @@ -143,7 +153,7 @@ impl TcpStealerApi { /// agent, to an internal stealer command [`Command::ResponseData`]. /// /// The actual handling of this message is done in [`TcpConnectionStealer`]. - pub(crate) async fn client_data(&mut self, tcp_data: TcpData) -> Result<(), AgentError> { + pub(crate) async fn client_data(&mut self, tcp_data: TcpData) -> AgentResult<()> { self.send_command(Command::ResponseData(tcp_data)).await } @@ -154,24 +164,32 @@ impl TcpStealerApi { pub(crate) async fn http_response( &mut self, response: HttpResponseFallback, - ) -> Result<(), AgentError> { + ) -> AgentResult<()> { self.send_command(Command::HttpResponse(response)).await } pub(crate) async fn switch_protocol_version( &mut self, version: semver::Version, - ) -> Result<(), AgentError> { + ) -> AgentResult<()> { self.send_command(Command::SwitchProtocolVersion(version)) .await } - pub(crate) async fn handle_client_message(&mut self, message: LayerTcpSteal) -> Result<()> { + pub(crate) async fn handle_client_message( + &mut self, + message: LayerTcpSteal, + ) -> AgentResult<()> { match message { LayerTcpSteal::PortSubscribe(port_steal) => self.port_subscribe(port_steal).await, LayerTcpSteal::ConnectionUnsubscribe(connection_id) => { self.response_body_txs .retain(|(key_id, _), _| *key_id != connection_id); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); + self.connection_unsubscribe(connection_id).await } LayerTcpSteal::PortUnsubscribe(port) => self.port_unsubscribe(port).await, @@ -202,6 +220,10 @@ impl TcpStealerApi { let key = (response.connection_id, response.request_id); self.response_body_txs.insert(key, tx.clone()); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); self.http_response(HttpResponseFallback::Streamed(http_response)) .await?; @@ -209,6 +231,10 @@ impl TcpStealerApi { for frame in response.internal_response.body { if let Err(err) = tx.send(Ok(frame.into())).await { self.response_body_txs.remove(&key); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); tracing::trace!(?err, "error while sending streaming response frame"); } } @@ -231,12 +257,20 @@ impl TcpStealerApi { } if send_err || body.is_last { self.response_body_txs.remove(key); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); }; Ok(()) } ChunkedResponse::Error(err) => { self.response_body_txs .remove(&(err.connection_id, err.request_id)); + HTTP_REQUEST_IN_PROGRESS_COUNT.store( + self.response_body_txs.len() as i64, + std::sync::atomic::Ordering::Relaxed, + ); tracing::trace!(?err, "ChunkedResponse error received"); Ok(()) } diff --git a/mirrord/agent/src/steal/connection.rs b/mirrord/agent/src/steal/connection.rs index 5e4b6b1219a..6515f2ecfc9 100644 --- a/mirrord/agent/src/steal/connection.rs +++ b/mirrord/agent/src/steal/connection.rs @@ -28,11 +28,12 @@ use tokio::{ sync::mpsc::{Receiver, Sender}, }; use tokio_util::sync::CancellationToken; -use tracing::warn; +use tracing::{warn, Level}; use super::http::HttpResponseFallback; use crate::{ - error::{AgentError, Result}, + error::AgentResult, + metrics::HTTP_REQUEST_IN_PROGRESS_COUNT, steal::{ connections::{ ConnectionMessageIn, ConnectionMessageOut, StolenConnection, StolenConnections, @@ -55,6 +56,22 @@ struct MatchedHttpRequest { } impl MatchedHttpRequest { + fn new( + connection_id: ConnectionId, + port: Port, + request_id: RequestId, + request: Request, + ) -> Self { + HTTP_REQUEST_IN_PROGRESS_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + Self { + connection_id, + port, + request_id, + request, + } + } + async fn into_serializable(self) -> Result, hyper::Error> { let ( Parts { @@ -181,7 +198,7 @@ impl Client { let frames = frames .into_iter() .map(InternalHttpBodyFrame::try_from) - .filter_map(Result::ok) + .filter_map(AgentResult::ok) .collect(); let message = DaemonTcp::HttpRequestChunked(ChunkedRequest::Start(HttpRequest { @@ -210,7 +227,7 @@ impl Client { let frames = frames .into_iter() .map(InternalHttpBodyFrame::try_from) - .filter_map(Result::ok) + .filter_map(AgentResult::ok) .collect(); let message = DaemonTcp::HttpRequestChunked(ChunkedRequest::Body( ChunkedHttpBody { @@ -273,6 +290,8 @@ struct TcpStealerConfig { /// Meant to be run (see [`TcpConnectionStealer::start`]) in a separate thread while the agent /// lives. When handling port subscription requests, this struct manipulates iptables, so it should /// run in the same network namespace as the agent's target. +/// +/// Enabled by the `steal` feature for incoming traffic. pub(crate) struct TcpConnectionStealer { /// For managing active subscriptions and port redirections. port_subscriptions: PortSubscriptions, @@ -299,11 +318,11 @@ impl TcpConnectionStealer { /// Initializes a new [`TcpConnectionStealer`], but doesn't start the actual work. /// You need to call [`TcpConnectionStealer::start`] to do so. - #[tracing::instrument(level = "trace")] + #[tracing::instrument(level = Level::TRACE, err)] pub(crate) async fn new( command_rx: Receiver, support_ipv6: bool, - ) -> Result { + ) -> AgentResult { let config = envy::prefixed("MIRRORD_AGENT_") .from_env::() .unwrap_or_default(); @@ -341,10 +360,7 @@ impl TcpConnectionStealer { /// /// 4. Handling the cancellation of the whole stealer thread (given `cancellation_token`). #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn start( - mut self, - cancellation_token: CancellationToken, - ) -> Result<(), AgentError> { + pub(crate) async fn start(mut self, cancellation_token: CancellationToken) -> AgentResult<()> { loop { tokio::select! { command = self.command_rx.recv() => { @@ -362,7 +378,9 @@ impl TcpConnectionStealer { }, accept = self.port_subscriptions.next_connection() => match accept { - Ok((stream, peer)) => self.incoming_connection(stream, peer).await?, + Ok((stream, peer)) => { + self.incoming_connection(stream, peer).await?; + } Err(error) => { tracing::error!(?error, "Failed to accept a stolen connection"); break Err(error); @@ -380,7 +398,11 @@ impl TcpConnectionStealer { /// Handles a new remote connection that was stolen by [`Self::port_subscriptions`]. #[tracing::instrument(level = "trace", skip(self))] - async fn incoming_connection(&mut self, stream: TcpStream, peer: SocketAddr) -> Result<()> { + async fn incoming_connection( + &mut self, + stream: TcpStream, + peer: SocketAddr, + ) -> AgentResult<()> { let mut real_address = orig_dst::orig_dst_addr(&stream)?; let localhost = if self.support_ipv6 && real_address.is_ipv6() { IpAddr::V6(Ipv6Addr::LOCALHOST) @@ -416,10 +438,7 @@ impl TcpConnectionStealer { /// Handles an update from one of the connections in [`Self::connections`]. #[tracing::instrument(level = "trace", skip(self))] - async fn handle_connection_update( - &mut self, - update: ConnectionMessageOut, - ) -> Result<(), AgentError> { + async fn handle_connection_update(&mut self, update: ConnectionMessageOut) -> AgentResult<()> { match update { ConnectionMessageOut::Closed { connection_id, @@ -526,12 +545,7 @@ impl TcpConnectionStealer { return Ok(()); } - let matched_request = MatchedHttpRequest { - connection_id, - request, - request_id: id, - port, - }; + let matched_request = MatchedHttpRequest::new(connection_id, port, id, request); if !client.send_request_async(matched_request) { self.connections @@ -550,11 +564,18 @@ impl TcpConnectionStealer { Ok(()) } - /// Helper function to handle [`Command::PortSubscribe`] messages. + /// Helper function to handle [`Command::PortSubscribe`] messages for the `TcpStealer`. /// - /// Inserts a subscription into [`Self::port_subscriptions`]. - #[tracing::instrument(level = "trace", skip(self))] - async fn port_subscribe(&mut self, client_id: ClientId, port_steal: StealType) -> Result<()> { + /// Checks if [`StealType`] is a valid [`HttpFilter`], then inserts a subscription into + /// [`Self::port_subscriptions`]. + /// + /// - Returns: `true` if this is an HTTP filtered subscription. + #[tracing::instrument(level = Level::TRACE, skip(self), err)] + async fn port_subscribe( + &mut self, + client_id: ClientId, + port_steal: StealType, + ) -> AgentResult { let spec = match port_steal { StealType::All(port) => Ok((port, None)), StealType::FilteredHttp(port, filter) => Regex::new(&format!("(?i){filter}")) @@ -565,6 +586,11 @@ impl TcpConnectionStealer { .map_err(|err| BadHttpFilterExRegex(filter, err.to_string())), }; + let filtered = spec + .as_ref() + .map(|(_, filter)| filter.is_some()) + .unwrap_or_default(); + let res = match spec { Ok((port, filter)) => self.port_subscriptions.add(client_id, port, filter).await?, Err(e) => Err(e.into()), @@ -573,18 +599,18 @@ impl TcpConnectionStealer { let client = self.clients.get(&client_id).expect("client not found"); let _ = client.tx.send(DaemonTcp::SubscribeResult(res)).await; - Ok(()) + Ok(filtered) } /// Removes the client with `client_id` from our list of clients (layers), and also removes /// their subscriptions from [`Self::port_subscriptions`] and all their open /// connections. #[tracing::instrument(level = "trace", skip(self))] - async fn close_client(&mut self, client_id: ClientId) -> Result<(), AgentError> { + async fn close_client(&mut self, client_id: ClientId) -> AgentResult<()> { self.port_subscriptions.remove_all(client_id).await?; let client = self.clients.remove(&client_id).expect("client not found"); - for connection in client.subscribed_connections.into_iter() { + for connection in client.subscribed_connections { self.connections .send(connection, ConnectionMessageIn::Unsubscribed { client_id }) .await; @@ -612,8 +638,8 @@ impl TcpConnectionStealer { } /// Handles [`Command`]s that were received by [`TcpConnectionStealer::command_rx`]. - #[tracing::instrument(level = "trace", skip(self))] - async fn handle_command(&mut self, command: StealerCommand) -> Result<(), AgentError> { + #[tracing::instrument(level = Level::TRACE, skip(self), err)] + async fn handle_command(&mut self, command: StealerCommand) -> AgentResult<()> { let StealerCommand { client_id, command } = command; match command { @@ -644,7 +670,7 @@ impl TcpConnectionStealer { } Command::PortSubscribe(port_steal) => { - self.port_subscribe(client_id, port_steal).await? + self.port_subscribe(client_id, port_steal).await?; } Command::PortUnsubscribe(port) => { diff --git a/mirrord/agent/src/steal/connections.rs b/mirrord/agent/src/steal/connections.rs index bc47eb80e4b..8e969b0a868 100644 --- a/mirrord/agent/src/steal/connections.rs +++ b/mirrord/agent/src/steal/connections.rs @@ -11,10 +11,14 @@ use tokio::{ sync::mpsc::{self, error::SendError, Receiver, Sender}, task::JoinSet, }; +use tracing::Level; use self::{filtered::DynamicBody, unfiltered::UnfilteredStealTask}; use super::{http::DefaultReversibleStream, subscriptions::PortSubscription}; -use crate::{http::HttpVersion, steal::connections::filtered::FilteredStealTask, util::ClientId}; +use crate::{ + http::HttpVersion, metrics::STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION, + steal::connections::filtered::FilteredStealTask, util::ClientId, +}; mod filtered; mod unfiltered; @@ -287,7 +291,7 @@ impl StolenConnections { /// Adds the given [`StolenConnection`] to this set. Spawns a new [`tokio::task`] that will /// manage it. - #[tracing::instrument(level = "trace", name = "manage_stolen_connection", skip(self))] + #[tracing::instrument(level = Level::TRACE, name = "manage_stolen_connection", skip(self))] pub fn manage(&mut self, connection: StolenConnection) { let connection_id = self.next_connection_id; self.next_connection_id += 1; @@ -458,13 +462,9 @@ impl ConnectionTask { }) .await?; - let task = UnfilteredStealTask { - connection_id: self.connection_id, - client_id, - stream: self.connection.stream, - }; - - task.run(self.tx, &mut self.rx).await + UnfilteredStealTask::new(self.connection_id, client_id, self.connection.stream) + .run(self.tx, &mut self.rx) + .await } PortSubscription::Filtered(filters) => { diff --git a/mirrord/agent/src/steal/connections/filtered.rs b/mirrord/agent/src/steal/connections/filtered.rs index b30e48a5757..ecc9f0064ad 100644 --- a/mirrord/agent/src/steal/connections/filtered.rs +++ b/mirrord/agent/src/steal/connections/filtered.rs @@ -1,5 +1,6 @@ use std::{ - collections::HashMap, future::Future, marker::PhantomData, net::SocketAddr, pin::Pin, sync::Arc, + collections::HashMap, future::Future, marker::PhantomData, net::SocketAddr, ops::Not, pin::Pin, + sync::Arc, }; use bytes::Bytes; @@ -28,9 +29,13 @@ use tokio::{ use tokio_util::sync::{CancellationToken, DropGuard}; use tracing::Level; -use super::{ConnectionMessageIn, ConnectionMessageOut, ConnectionTaskError}; +use super::{ + ConnectionMessageIn, ConnectionMessageOut, ConnectionTaskError, + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION, +}; use crate::{ http::HttpVersion, + metrics::STEAL_FILTERED_CONNECTION_SUBSCRIPTION, steal::{connections::unfiltered::UnfilteredStealTask, http::HttpFilter}, util::ClientId, }; @@ -368,6 +373,18 @@ pub struct FilteredStealTask { /// For safely downcasting the IO stream after an HTTP upgrade. See [`Upgraded::downcast`]. _io_type: PhantomData T>, + + /// Helps us figuring out if we should update some metrics in the `Drop` implementation. + metrics_updated: bool, +} + +impl Drop for FilteredStealTask { + fn drop(&mut self) { + if self.metrics_updated.not() { + STEAL_FILTERED_CONNECTION_SUBSCRIPTION + .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + } + } } impl FilteredStealTask @@ -443,6 +460,8 @@ where } }; + STEAL_FILTERED_CONNECTION_SUBSCRIPTION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + Self { connection_id, original_destination, @@ -453,6 +472,7 @@ where blocked_requests: Default::default(), next_request_id: Default::default(), _io_type: Default::default(), + metrics_updated: false, } } @@ -638,6 +658,8 @@ where queued_raw_data.remove(&client_id); self.subscribed.insert(client_id, false); self.blocked_requests.retain(|key, _| key.0 != client_id); + + STEAL_FILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); }, }, @@ -646,7 +668,10 @@ where // No more requests from the `FilteringService`. // HTTP connection is closed and possibly upgraded. - None => break, + None => { + STEAL_FILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + break + } } } } @@ -788,15 +813,18 @@ where ) -> Result<(), ConnectionTaskError> { let res = self.run_until_http_ends(tx.clone(), rx).await; + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + self.metrics_updated = true; + let res = match res { Ok(data) => self.run_after_http_ends(data, tx.clone(), rx).await, Err(e) => Err(e), }; - for (client_id, subscribed) in self.subscribed { - if subscribed { + for (client_id, subscribed) in self.subscribed.iter() { + if *subscribed { tx.send(ConnectionMessageOut::Closed { - client_id, + client_id: *client_id, connection_id: self.connection_id, }) .await?; diff --git a/mirrord/agent/src/steal/connections/unfiltered.rs b/mirrord/agent/src/steal/connections/unfiltered.rs index 5b6676094c3..ec54691315e 100644 --- a/mirrord/agent/src/steal/connections/unfiltered.rs +++ b/mirrord/agent/src/steal/connections/unfiltered.rs @@ -7,7 +7,10 @@ use tokio::{ sync::mpsc::{Receiver, Sender}, }; -use super::{ConnectionMessageIn, ConnectionMessageOut, ConnectionTaskError}; +use super::{ + ConnectionMessageIn, ConnectionMessageOut, ConnectionTaskError, + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION, +}; use crate::util::ClientId; /// Manages an unfiltered stolen connection. @@ -19,7 +22,23 @@ pub struct UnfilteredStealTask { pub stream: T, } +impl Drop for UnfilteredStealTask { + fn drop(&mut self) { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + } +} + impl UnfilteredStealTask { + pub(crate) fn new(connection_id: ConnectionId, client_id: ClientId, stream: T) -> Self { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + Self { + connection_id, + client_id, + stream, + } + } + /// Runs this task until the managed connection is closed. /// /// # Note @@ -40,6 +59,8 @@ impl UnfilteredStealTask { read = self.stream.read_buf(&mut buf), if !reading_closed => match read { Ok(..) => { if buf.is_empty() { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + tracing::trace!( client_id = self.client_id, connection_id = self.connection_id, @@ -63,6 +84,8 @@ impl UnfilteredStealTask { Err(e) if e.kind() == ErrorKind::WouldBlock => {} Err(e) => { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + tx.send(ConnectionMessageOut::Closed { client_id: self.client_id, connection_id: self.connection_id @@ -85,6 +108,8 @@ impl UnfilteredStealTask { ConnectionMessageIn::Raw { data, .. } => { let res = if data.is_empty() { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + tracing::trace!( client_id = self.client_id, connection_id = self.connection_id, @@ -97,6 +122,8 @@ impl UnfilteredStealTask { }; if let Err(e) = res { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + tx.send(ConnectionMessageOut::Closed { client_id: self.client_id, connection_id: self.connection_id @@ -115,6 +142,8 @@ impl UnfilteredStealTask { }, ConnectionMessageIn::Unsubscribed { .. } => { + STEAL_UNFILTERED_CONNECTION_SUBSCRIPTION.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + return Ok(()); } } diff --git a/mirrord/agent/src/steal/ip_tables.rs b/mirrord/agent/src/steal/ip_tables.rs index 68bddb6a406..9c175220767 100644 --- a/mirrord/agent/src/steal/ip_tables.rs +++ b/mirrord/agent/src/steal/ip_tables.rs @@ -9,7 +9,7 @@ use rand::distributions::{Alphanumeric, DistString}; use tracing::warn; use crate::{ - error::{AgentError, Result}, + error::{AgentError, AgentResult}, steal::ip_tables::{ flush_connections::FlushConnections, mesh::{istio::AmbientRedirect, MeshRedirect, MeshVendorExt}, @@ -84,13 +84,13 @@ pub(crate) trait IPTables { where Self: Sized; - fn create_chain(&self, name: &str) -> Result<()>; - fn remove_chain(&self, name: &str) -> Result<()>; + fn create_chain(&self, name: &str) -> AgentResult<()>; + fn remove_chain(&self, name: &str) -> AgentResult<()>; - fn add_rule(&self, chain: &str, rule: &str) -> Result<()>; - fn insert_rule(&self, chain: &str, rule: &str, index: i32) -> Result<()>; - fn list_rules(&self, chain: &str) -> Result>; - fn remove_rule(&self, chain: &str, rule: &str) -> Result<()>; + fn add_rule(&self, chain: &str, rule: &str) -> AgentResult<()>; + fn insert_rule(&self, chain: &str, rule: &str, index: i32) -> AgentResult<()>; + fn list_rules(&self, chain: &str) -> AgentResult>; + fn remove_rule(&self, chain: &str, rule: &str) -> AgentResult<()>; } #[derive(Clone)] @@ -152,8 +152,13 @@ impl IPTables for IPTablesWrapper { } } - #[tracing::instrument(level = tracing::Level::TRACE, skip(self), ret, fields(table_name=%self.table_name))] - fn create_chain(&self, name: &str) -> Result<()> { + #[tracing::instrument( + level = tracing::Level::TRACE, + skip(self), + ret, + fields(table_name=%self.table_name + ))] + fn create_chain(&self, name: &str) -> AgentResult<()> { self.tables .new_chain(self.table_name, name) .map_err(|e| AgentError::IPTablesError(e.to_string()))?; @@ -165,7 +170,7 @@ impl IPTables for IPTablesWrapper { } #[tracing::instrument(level = "trace")] - fn remove_chain(&self, name: &str) -> Result<()> { + fn remove_chain(&self, name: &str) -> AgentResult<()> { self.tables .flush_chain(self.table_name, name) .map_err(|e| AgentError::IPTablesError(e.to_string()))?; @@ -177,28 +182,28 @@ impl IPTables for IPTablesWrapper { } #[tracing::instrument(level = "trace", ret)] - fn add_rule(&self, chain: &str, rule: &str) -> Result<()> { + fn add_rule(&self, chain: &str, rule: &str) -> AgentResult<()> { self.tables .append(self.table_name, chain, rule) .map_err(|e| AgentError::IPTablesError(e.to_string())) } #[tracing::instrument(level = "trace", ret)] - fn insert_rule(&self, chain: &str, rule: &str, index: i32) -> Result<()> { + fn insert_rule(&self, chain: &str, rule: &str, index: i32) -> AgentResult<()> { self.tables .insert(self.table_name, chain, rule, index) .map_err(|e| AgentError::IPTablesError(e.to_string())) } #[tracing::instrument(level = "trace")] - fn list_rules(&self, chain: &str) -> Result> { + fn list_rules(&self, chain: &str) -> AgentResult> { self.tables .list(self.table_name, chain) .map_err(|e| AgentError::IPTablesError(e.to_string())) } #[tracing::instrument(level = "trace")] - fn remove_rule(&self, chain: &str, rule: &str) -> Result<()> { + fn remove_rule(&self, chain: &str, rule: &str) -> AgentResult<()> { self.tables .delete(self.table_name, chain, rule) .map_err(|e| AgentError::IPTablesError(e.to_string())) @@ -233,7 +238,7 @@ where flush_connections: bool, pod_ips: Option<&str>, ipv6: bool, - ) -> Result { + ) -> AgentResult { let ipt = Arc::new(ipt); let mut redirect = if let Some(vendor) = MeshVendor::detect(ipt.as_ref())? { @@ -265,7 +270,7 @@ where Ok(Self { redirect }) } - pub(crate) async fn load(ipt: IPT, flush_connections: bool) -> Result { + pub(crate) async fn load(ipt: IPT, flush_connections: bool) -> AgentResult { let ipt = Arc::new(ipt); let mut redirect = if let Some(vendor) = MeshVendor::detect(ipt.as_ref())? { @@ -299,7 +304,7 @@ where &self, redirected_port: Port, target_port: Port, - ) -> Result<()> { + ) -> AgentResult<()> { self.redirect .add_redirect(redirected_port, target_port) .await @@ -314,13 +319,13 @@ where &self, redirected_port: Port, target_port: Port, - ) -> Result<()> { + ) -> AgentResult<()> { self.redirect .remove_redirect(redirected_port, target_port) .await } - pub(crate) async fn cleanup(&self) -> Result<()> { + pub(crate) async fn cleanup(&self) -> AgentResult<()> { self.redirect.unmount_entrypoint().await } } diff --git a/mirrord/agent/src/steal/ip_tables/chain.rs b/mirrord/agent/src/steal/ip_tables/chain.rs index c5bc6d65404..c1c34715c85 100644 --- a/mirrord/agent/src/steal/ip_tables/chain.rs +++ b/mirrord/agent/src/steal/ip_tables/chain.rs @@ -4,7 +4,7 @@ use std::sync::{ }; use crate::{ - error::{AgentError, Result}, + error::{AgentError, AgentResult}, steal::ip_tables::IPTables, }; @@ -19,7 +19,7 @@ impl IPTableChain where IPT: IPTables, { - pub fn create(inner: Arc, chain_name: String) -> Result { + pub fn create(inner: Arc, chain_name: String) -> AgentResult { inner.create_chain(&chain_name)?; // Start with 1 because the chain will allways have atleast `-A ` as a rule @@ -32,7 +32,7 @@ where }) } - pub fn load(inner: Arc, chain_name: String) -> Result { + pub fn load(inner: Arc, chain_name: String) -> AgentResult { let existing_rules = inner.list_rules(&chain_name)?.len(); if existing_rules == 0 { @@ -59,7 +59,7 @@ where &self.inner } - pub fn add_rule(&self, rule: &str) -> Result { + pub fn add_rule(&self, rule: &str) -> AgentResult { self.inner .insert_rule( &self.chain_name, @@ -72,7 +72,7 @@ where }) } - pub fn remove_rule(&self, rule: &str) -> Result<()> { + pub fn remove_rule(&self, rule: &str) -> AgentResult<()> { self.inner.remove_rule(&self.chain_name, rule)?; self.chain_size.fetch_sub(1, Ordering::Relaxed); diff --git a/mirrord/agent/src/steal/ip_tables/flush_connections.rs b/mirrord/agent/src/steal/ip_tables/flush_connections.rs index 6675a40651f..c0f19c20b8d 100644 --- a/mirrord/agent/src/steal/ip_tables/flush_connections.rs +++ b/mirrord/agent/src/steal/ip_tables/flush_connections.rs @@ -13,7 +13,7 @@ use tokio::process::Command; use tracing::warn; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{chain::IPTableChain, redirect::Redirect, IPTables, IPTABLE_INPUT}, }; @@ -33,7 +33,7 @@ where const ENTRYPOINT: &'static str = "INPUT"; #[tracing::instrument(level = "trace", skip(ipt, inner))] - pub fn create(ipt: Arc, inner: Box) -> Result { + pub fn create(ipt: Arc, inner: Box) -> AgentResult { let managed = IPTableChain::create(ipt.with_table("filter").into(), IPTABLE_INPUT.to_string())?; @@ -48,7 +48,7 @@ where } #[tracing::instrument(level = "trace", skip(ipt, inner))] - pub fn load(ipt: Arc, inner: Box) -> Result { + pub fn load(ipt: Arc, inner: Box) -> AgentResult { let managed = IPTableChain::load(ipt.with_table("filter").into(), IPTABLE_INPUT.to_string())?; @@ -63,7 +63,7 @@ where T: Redirect + Send + Sync, { #[tracing::instrument(level = "trace", skip(self), ret)] - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { self.inner.mount_entrypoint().await?; self.managed.inner().add_rule( @@ -75,7 +75,7 @@ where } #[tracing::instrument(level = "trace", skip(self), ret)] - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.inner.unmount_entrypoint().await?; self.managed.inner().remove_rule( @@ -87,7 +87,7 @@ where } #[tracing::instrument(level = "trace", skip(self), ret)] - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.inner .add_redirect(redirected_port, target_port) .await?; @@ -115,7 +115,7 @@ where } #[tracing::instrument(level = "trace", skip(self), ret)] - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.inner .remove_redirect(redirected_port, target_port) .await?; diff --git a/mirrord/agent/src/steal/ip_tables/mesh.rs b/mirrord/agent/src/steal/ip_tables/mesh.rs index 88fdff5d0b1..1a3e5acbe62 100644 --- a/mirrord/agent/src/steal/ip_tables/mesh.rs +++ b/mirrord/agent/src/steal/ip_tables/mesh.rs @@ -5,7 +5,7 @@ use fancy_regex::Regex; use mirrord_protocol::{MeshVendor, Port}; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{ output::OutputRedirect, prerouting::PreroutingRedirect, redirect::Redirect, IPTables, IPTABLE_MESH, @@ -29,7 +29,7 @@ impl MeshRedirect where IPT: IPTables, { - pub fn create(ipt: Arc, vendor: MeshVendor, pod_ips: Option<&str>) -> Result { + pub fn create(ipt: Arc, vendor: MeshVendor, pod_ips: Option<&str>) -> AgentResult { let prerouting = PreroutingRedirect::create(ipt.clone())?; for port in Self::get_skip_ports(&ipt, &vendor)? { @@ -45,7 +45,7 @@ where }) } - pub fn load(ipt: Arc, vendor: MeshVendor) -> Result { + pub fn load(ipt: Arc, vendor: MeshVendor) -> AgentResult { let prerouting = PreroutingRedirect::load(ipt.clone())?; let output = OutputRedirect::load(ipt, IPTABLE_MESH.to_string())?; @@ -56,7 +56,7 @@ where }) } - fn get_skip_ports(ipt: &IPT, vendor: &MeshVendor) -> Result> { + fn get_skip_ports(ipt: &IPT, vendor: &MeshVendor) -> AgentResult> { let chain_name = vendor.input_chain(); let lookup_regex = if let Some(regex) = vendor.skip_ports_regex() { regex @@ -86,21 +86,21 @@ impl Redirect for MeshRedirect where IPT: IPTables + Send + Sync, { - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { self.prerouting.mount_entrypoint().await?; self.output.mount_entrypoint().await?; Ok(()) } - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.prerouting.unmount_entrypoint().await?; self.output.unmount_entrypoint().await?; Ok(()) } - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { if self.vendor != MeshVendor::IstioCni { self.prerouting .add_redirect(redirected_port, target_port) @@ -113,7 +113,7 @@ where Ok(()) } - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { if self.vendor != MeshVendor::IstioCni { self.prerouting .remove_redirect(redirected_port, target_port) @@ -129,13 +129,13 @@ where /// Extends the [`MeshVendor`] type with methods that are only relevant for the agent. pub(super) trait MeshVendorExt: Sized { - fn detect(ipt: &IPT) -> Result>; + fn detect(ipt: &IPT) -> AgentResult>; fn input_chain(&self) -> &str; fn skip_ports_regex(&self) -> Option<&Regex>; } impl MeshVendorExt for MeshVendor { - fn detect(ipt: &IPT) -> Result> { + fn detect(ipt: &IPT) -> AgentResult> { if let Ok(val) = std::env::var("MIRRORD_AGENT_ISTIO_CNI") && val.to_lowercase() == "true" { diff --git a/mirrord/agent/src/steal/ip_tables/mesh/istio.rs b/mirrord/agent/src/steal/ip_tables/mesh/istio.rs index cd3d4b06fa9..01e513a6bf9 100644 --- a/mirrord/agent/src/steal/ip_tables/mesh/istio.rs +++ b/mirrord/agent/src/steal/ip_tables/mesh/istio.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use mirrord_protocol::Port; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{ output::OutputRedirect, prerouting::PreroutingRedirect, redirect::Redirect, IPTables, IPTABLE_IPV4_ROUTE_LOCALNET_ORIGINAL, IPTABLE_MESH, @@ -20,14 +20,14 @@ impl AmbientRedirect where IPT: IPTables, { - pub fn create(ipt: Arc, pod_ips: Option<&str>) -> Result { + pub fn create(ipt: Arc, pod_ips: Option<&str>) -> AgentResult { let prerouting = PreroutingRedirect::create(ipt.clone())?; let output = OutputRedirect::create(ipt, IPTABLE_MESH.to_string(), pod_ips)?; Ok(AmbientRedirect { prerouting, output }) } - pub fn load(ipt: Arc) -> Result { + pub fn load(ipt: Arc) -> AgentResult { let prerouting = PreroutingRedirect::load(ipt.clone())?; let output = OutputRedirect::load(ipt, IPTABLE_MESH.to_string())?; @@ -40,7 +40,7 @@ impl Redirect for AmbientRedirect where IPT: IPTables + Send + Sync, { - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { tokio::fs::write("/proc/sys/net/ipv4/conf/all/route_localnet", "1".as_bytes()).await?; self.prerouting.mount_entrypoint().await?; @@ -49,7 +49,7 @@ where Ok(()) } - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.prerouting.unmount_entrypoint().await?; self.output.unmount_entrypoint().await?; @@ -62,7 +62,7 @@ where Ok(()) } - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.prerouting .add_redirect(redirected_port, target_port) .await?; @@ -73,7 +73,7 @@ where Ok(()) } - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.prerouting .remove_redirect(redirected_port, target_port) .await?; diff --git a/mirrord/agent/src/steal/ip_tables/output.rs b/mirrord/agent/src/steal/ip_tables/output.rs index 2286469c00c..9eebad0c9ae 100644 --- a/mirrord/agent/src/steal/ip_tables/output.rs +++ b/mirrord/agent/src/steal/ip_tables/output.rs @@ -6,7 +6,7 @@ use nix::unistd::getgid; use tracing::warn; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{chain::IPTableChain, IPTables, Redirect}, }; @@ -20,8 +20,8 @@ where { const ENTRYPOINT: &'static str = "OUTPUT"; - #[tracing::instrument(skip(ipt), level = tracing::Level::TRACE)] - pub fn create(ipt: Arc, chain_name: String, pod_ips: Option<&str>) -> Result { + #[tracing::instrument(level = tracing::Level::TRACE, skip(ipt), err)] + pub fn create(ipt: Arc, chain_name: String, pod_ips: Option<&str>) -> AgentResult { let managed = IPTableChain::create(ipt, chain_name.clone()).inspect_err( |e| tracing::error!(%e, "Could not create iptables chain \"{chain_name}\"."), )?; @@ -42,7 +42,7 @@ where Ok(OutputRedirect { managed }) } - pub fn load(ipt: Arc, chain_name: String) -> Result { + pub fn load(ipt: Arc, chain_name: String) -> AgentResult { let managed = IPTableChain::load(ipt, chain_name)?; Ok(OutputRedirect { managed }) @@ -56,7 +56,7 @@ impl Redirect for OutputRedirect where IPT: IPTables + Send + Sync, { - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { if USE_INSERT { self.managed.inner().insert_rule( Self::ENTRYPOINT, @@ -73,7 +73,7 @@ where Ok(()) } - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.managed.inner().remove_rule( Self::ENTRYPOINT, &format!("-j {}", self.managed.chain_name()), @@ -82,7 +82,7 @@ where Ok(()) } - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { let redirect_rule = format!( "-o lo -m tcp -p tcp --dport {redirected_port} -j REDIRECT --to-ports {target_port}" ); @@ -92,7 +92,7 @@ where Ok(()) } - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { let redirect_rule = format!( "-o lo -m tcp -p tcp --dport {redirected_port} -j REDIRECT --to-ports {target_port}" ); diff --git a/mirrord/agent/src/steal/ip_tables/prerouting.rs b/mirrord/agent/src/steal/ip_tables/prerouting.rs index 486b0ca1b51..29d5de06103 100644 --- a/mirrord/agent/src/steal/ip_tables/prerouting.rs +++ b/mirrord/agent/src/steal/ip_tables/prerouting.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use mirrord_protocol::Port; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{chain::IPTableChain, IPTables, Redirect, IPTABLE_PREROUTING}, }; @@ -18,13 +18,13 @@ where { const ENTRYPOINT: &'static str = "PREROUTING"; - pub fn create(ipt: Arc) -> Result { + pub fn create(ipt: Arc) -> AgentResult { let managed = IPTableChain::create(ipt, IPTABLE_PREROUTING.to_string())?; Ok(PreroutingRedirect { managed }) } - pub fn load(ipt: Arc) -> Result { + pub fn load(ipt: Arc) -> AgentResult { let managed = IPTableChain::load(ipt, IPTABLE_PREROUTING.to_string())?; Ok(PreroutingRedirect { managed }) @@ -36,7 +36,7 @@ impl Redirect for PreroutingRedirect where IPT: IPTables + Send + Sync, { - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { self.managed.inner().add_rule( Self::ENTRYPOINT, &format!("-j {}", self.managed.chain_name()), @@ -45,7 +45,7 @@ where Ok(()) } - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.managed.inner().remove_rule( Self::ENTRYPOINT, &format!("-j {}", self.managed.chain_name()), @@ -54,7 +54,7 @@ where Ok(()) } - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { let redirect_rule = format!("-m tcp -p tcp --dport {redirected_port} -j REDIRECT --to-ports {target_port}"); @@ -63,7 +63,7 @@ where Ok(()) } - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { let redirect_rule = format!("-m tcp -p tcp --dport {redirected_port} -j REDIRECT --to-ports {target_port}"); diff --git a/mirrord/agent/src/steal/ip_tables/redirect.rs b/mirrord/agent/src/steal/ip_tables/redirect.rs index d18aeb1d7ea..fe52d90fc1e 100644 --- a/mirrord/agent/src/steal/ip_tables/redirect.rs +++ b/mirrord/agent/src/steal/ip_tables/redirect.rs @@ -2,17 +2,17 @@ use async_trait::async_trait; use enum_dispatch::enum_dispatch; use mirrord_protocol::Port; -use crate::error::Result; +use crate::error::AgentResult; #[async_trait] #[enum_dispatch] pub(crate) trait Redirect { - async fn mount_entrypoint(&self) -> Result<()>; + async fn mount_entrypoint(&self) -> AgentResult<()>; - async fn unmount_entrypoint(&self) -> Result<()>; + async fn unmount_entrypoint(&self) -> AgentResult<()>; /// Create port redirection - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()>; + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()>; /// Remove port redirection - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()>; + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()>; } diff --git a/mirrord/agent/src/steal/ip_tables/standard.rs b/mirrord/agent/src/steal/ip_tables/standard.rs index 3302b05c02e..47b9bf0c0af 100644 --- a/mirrord/agent/src/steal/ip_tables/standard.rs +++ b/mirrord/agent/src/steal/ip_tables/standard.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use mirrord_protocol::Port; use crate::{ - error::Result, + error::AgentResult, steal::ip_tables::{ output::OutputRedirect, prerouting::PreroutingRedirect, IPTables, Redirect, IPTABLE_STANDARD, @@ -20,14 +20,14 @@ impl StandardRedirect where IPT: IPTables, { - pub fn create(ipt: Arc, pod_ips: Option<&str>) -> Result { + pub fn create(ipt: Arc, pod_ips: Option<&str>) -> AgentResult { let prerouting = PreroutingRedirect::create(ipt.clone())?; let output = OutputRedirect::create(ipt, IPTABLE_STANDARD.to_string(), pod_ips)?; Ok(StandardRedirect { prerouting, output }) } - pub fn load(ipt: Arc) -> Result { + pub fn load(ipt: Arc) -> AgentResult { let prerouting = PreroutingRedirect::load(ipt.clone())?; let output = OutputRedirect::load(ipt, IPTABLE_STANDARD.to_string())?; @@ -42,21 +42,21 @@ impl Redirect for StandardRedirect where IPT: IPTables + Send + Sync, { - async fn mount_entrypoint(&self) -> Result<()> { + async fn mount_entrypoint(&self) -> AgentResult<()> { self.prerouting.mount_entrypoint().await?; self.output.mount_entrypoint().await?; Ok(()) } - async fn unmount_entrypoint(&self) -> Result<()> { + async fn unmount_entrypoint(&self) -> AgentResult<()> { self.prerouting.unmount_entrypoint().await?; self.output.unmount_entrypoint().await?; Ok(()) } - async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn add_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.prerouting .add_redirect(redirected_port, target_port) .await?; @@ -67,7 +67,7 @@ where Ok(()) } - async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> Result<()> { + async fn remove_redirect(&self, redirected_port: Port, target_port: Port) -> AgentResult<()> { self.prerouting .remove_redirect(redirected_port, target_port) .await?; diff --git a/mirrord/agent/src/steal/subscriptions.rs b/mirrord/agent/src/steal/subscriptions.rs index 0468719bc9c..901ecd725ef 100644 --- a/mirrord/agent/src/steal/subscriptions.rs +++ b/mirrord/agent/src/steal/subscriptions.rs @@ -16,7 +16,11 @@ use super::{ http::HttpFilter, ip_tables::{new_ip6tables, new_iptables, IPTablesWrapper, SafeIpTables}, }; -use crate::{error::AgentError, util::ClientId}; +use crate::{ + error::{AgentError, AgentResult}, + metrics::{STEAL_FILTERED_PORT_SUBSCRIPTION, STEAL_UNFILTERED_PORT_SUBSCRIPTION}, + util::ClientId, +}; /// For stealing incoming TCP connections. #[async_trait::async_trait] @@ -149,7 +153,7 @@ impl IpTablesRedirector { flush_connections: bool, pod_ips: Option, support_ipv6: bool, - ) -> Result { + ) -> AgentResult { let (pod_ips4, pod_ips6) = pod_ips.map_or_else( || (None, None), |ips| { @@ -310,6 +314,13 @@ pub struct PortSubscriptions { subscriptions: HashMap, } +impl Drop for PortSubscriptions { + fn drop(&mut self) { + STEAL_FILTERED_PORT_SUBSCRIPTION.store(0, std::sync::atomic::Ordering::Relaxed); + STEAL_UNFILTERED_PORT_SUBSCRIPTION.store(0, std::sync::atomic::Ordering::Relaxed); + } +} + impl PortSubscriptions { /// Create an empty instance of this struct. /// @@ -351,7 +362,16 @@ impl PortSubscriptions { ) -> Result, R::Error> { let add_redirect = match self.subscriptions.entry(port) { Entry::Occupied(mut e) => { + let filtered = filter.is_some(); if e.get_mut().try_extend(client_id, filter) { + if filtered { + STEAL_FILTERED_PORT_SUBSCRIPTION + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } else { + STEAL_UNFILTERED_PORT_SUBSCRIPTION + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + Ok(false) } else { Err(ResponseError::PortAlreadyStolen(port)) @@ -359,6 +379,14 @@ impl PortSubscriptions { } Entry::Vacant(e) => { + if filter.is_some() { + STEAL_FILTERED_PORT_SUBSCRIPTION + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } else { + STEAL_UNFILTERED_PORT_SUBSCRIPTION + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + e.insert(PortSubscription::new(client_id, filter)); Ok(true) } @@ -395,11 +423,17 @@ impl PortSubscriptions { let remove_redirect = match e.get_mut() { PortSubscription::Unfiltered(subscribed_client) if *subscribed_client == client_id => { e.remove(); + STEAL_UNFILTERED_PORT_SUBSCRIPTION + .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + true } PortSubscription::Unfiltered(..) => false, PortSubscription::Filtered(filters) => { - filters.remove(&client_id); + if filters.remove(&client_id).is_some() { + STEAL_FILTERED_PORT_SUBSCRIPTION + .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + } if filters.is_empty() { e.remove(); diff --git a/mirrord/agent/src/util.rs b/mirrord/agent/src/util.rs index 9dcbc6cd892..0c72cddd82f 100644 --- a/mirrord/agent/src/util.rs +++ b/mirrord/agent/src/util.rs @@ -12,7 +12,7 @@ use tokio::sync::mpsc; use tracing::error; use crate::{ - error::AgentError, + error::AgentResult, namespace::{set_namespace, NamespaceType}, }; @@ -151,7 +151,7 @@ where /// Many of the agent's TCP/UDP connections require that they're made from the `pid`'s namespace to /// work. #[tracing::instrument(level = "trace")] -pub(crate) fn enter_namespace(pid: Option, namespace: &str) -> Result<(), AgentError> { +pub(crate) fn enter_namespace(pid: Option, namespace: &str) -> AgentResult<()> { if let Some(pid) = pid { Ok(set_namespace(pid, NamespaceType::Net).inspect_err(|fail| { error!("Failed setting pid {pid:#?} namespace {namespace:#?} with {fail:#?}") diff --git a/mirrord/agent/src/vpn.rs b/mirrord/agent/src/vpn.rs index dd8c3a5133f..d7d30d5ca6f 100644 --- a/mirrord/agent/src/vpn.rs +++ b/mirrord/agent/src/vpn.rs @@ -17,7 +17,7 @@ use tokio::{ }; use crate::{ - error::{AgentError, Result}, + error::{AgentError, AgentResult}, util::run_thread_in_namespace, watched_task::{TaskStatus, WatchedTask}, }; @@ -75,7 +75,7 @@ impl VpnApi { /// Sends the [`ClientVpn`] message to the background task. #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn layer_message(&mut self, message: ClientVpn) -> Result<()> { + pub(crate) async fn layer_message(&mut self, message: ClientVpn) -> AgentResult<()> { if self.layer_tx.send(message).await.is_ok() { Ok(()) } else { @@ -84,7 +84,7 @@ impl VpnApi { } /// Receives a [`ServerVpn`] message from the background task. - pub(crate) async fn daemon_message(&mut self) -> Result { + pub(crate) async fn daemon_message(&mut self) -> AgentResult { match self.daemon_rx.recv().await { Some(msg) => Ok(msg), None => Err(self.task_status.unwrap_err().await), @@ -121,7 +121,7 @@ impl AsyncRawSocket { } } -async fn create_raw_socket() -> Result { +async fn create_raw_socket() -> AgentResult { let index = nix::net::if_::if_nametoindex("eth0") .map_err(|err| AgentError::VpnError(err.to_string()))?; @@ -139,7 +139,7 @@ async fn create_raw_socket() -> Result { } #[tracing::instrument(level = "debug", ret)] -async fn resolve_interface() -> Result<(IpAddr, IpAddr, IpAddr)> { +async fn resolve_interface() -> AgentResult<(IpAddr, IpAddr, IpAddr)> { // Connect to a remote address so we can later get the default network interface. let temporary_socket = UdpSocket::bind("0.0.0.0:0").await?; temporary_socket.connect("8.8.8.8:53").await?; @@ -209,7 +209,7 @@ impl fmt::Debug for VpnTask { } } -fn interface_index_to_sock_addr(index: i32) -> Result { +fn interface_index_to_sock_addr(index: i32) -> AgentResult { let mut addr_storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() }; let len = std::mem::size_of::() as libc::socklen_t; let macs = procfs::net::arp().map_err(|err| AgentError::VpnError(err.to_string()))?; @@ -245,7 +245,7 @@ impl VpnTask { } #[allow(clippy::indexing_slicing)] - async fn run(mut self) -> Result<()> { + async fn run(mut self) -> AgentResult<()> { // so host won't respond with RST to our packets. // TODO: need to do it for UDP as well to avoid ICMP unreachable. let output = std::process::Command::new("iptables") @@ -318,7 +318,7 @@ impl VpnTask { &mut self, message: ClientVpn, network_configuration: &NetworkConfiguration, - ) -> Result<()> { + ) -> AgentResult<()> { match message { // We make connection to the requested address, split the stream into halves with // `io::split`, and put them into respective maps. diff --git a/mirrord/agent/src/watched_task.rs b/mirrord/agent/src/watched_task.rs index 0212f279163..ad06bb238ee 100644 --- a/mirrord/agent/src/watched_task.rs +++ b/mirrord/agent/src/watched_task.rs @@ -2,7 +2,7 @@ use std::future::Future; use tokio::sync::watch::{self, Receiver, Sender}; -use crate::error::AgentError; +use crate::error::{AgentError, AgentResult}; /// A shared clonable view on a background task's status. #[derive(Debug, Clone)] @@ -83,7 +83,7 @@ impl WatchedTask { impl WatchedTask where - T: Future>, + T: Future>, { /// Execute the wrapped task. /// Store its result in the inner [`TaskStatus`]. diff --git a/mirrord/config/configuration.md b/mirrord/config/configuration.md index 8e8b9ea6aee..0d1af19401d 100644 --- a/mirrord/config/configuration.md +++ b/mirrord/config/configuration.md @@ -68,7 +68,8 @@ configuration file containing all fields. "communication_timeout": 30, "startup_timeout": 360, "network_interface": "eth0", - "flush_connections": true + "flush_connections": true, + "metrics": "0.0.0.0:9000", }, "feature": { "env": { @@ -166,7 +167,11 @@ Allows setting up custom annotations for the agent Job and Pod. ```json { - "annotations": { "cats.io/inject": "enabled" } + "annotations": { + "cats.io/inject": "enabled" + "prometheus.io/scrape": "true", + "prometheus.io/port": "9000" + } } ``` @@ -299,6 +304,19 @@ with `RUST_LOG`. } ``` +### agent.metrics {#agent-metrics} + +Enables prometheus metrics for the agent pod. + +You might need to add annotations to the agent pod depending on how prometheus is +configured to scrape for metrics. + +```json +{ + "metrics": "0.0.0.0:9000" +} +``` + ### agent.namespace {#agent-namespace} Namespace where the agent shall live. diff --git a/mirrord/config/src/agent.rs b/mirrord/config/src/agent.rs index 3dff5adfefc..9600edfcd4d 100644 --- a/mirrord/config/src/agent.rs +++ b/mirrord/config/src/agent.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt, path::Path}; +use std::{collections::HashMap, fmt, net::SocketAddr, path::Path}; use k8s_openapi::api::core::v1::{ResourceRequirements, Toleration}; use mirrord_analytics::CollectAnalytics; @@ -322,7 +322,11 @@ pub struct AgentConfig { /// /// ```json /// { - /// "annotations": { "cats.io/inject": "enabled" } + /// "annotations": { + /// "cats.io/inject": "enabled" + /// "prometheus.io/scrape": "true", + /// "prometheus.io/port": "9000" + /// } /// } /// ``` pub annotations: Option>, @@ -350,6 +354,20 @@ pub struct AgentConfig { /// ``` pub service_account: Option, + /// ### agent.metrics {#agent-metrics} + /// + /// Enables prometheus metrics for the agent pod. + /// + /// You might need to add annotations to the agent pod depending on how prometheus is + /// configured to scrape for metrics. + /// + /// ```json + /// { + /// "metrics": "0.0.0.0:9000" + /// } + /// ``` + pub metrics: Option, + /// /// Create an agent that returns an error after accepting the first client. For testing /// purposes. Only supported with job agents (not with ephemeral agents). diff --git a/mirrord/config/src/lib.rs b/mirrord/config/src/lib.rs index d3f8fae7bc6..7ca3e00dc39 100644 --- a/mirrord/config/src/lib.rs +++ b/mirrord/config/src/lib.rs @@ -119,7 +119,8 @@ pub static MIRRORD_RESOLVED_CONFIG_ENV: &str = "MIRRORD_RESOLVED_CONFIG"; /// "communication_timeout": 30, /// "startup_timeout": 360, /// "network_interface": "eth0", -/// "flush_connections": true +/// "flush_connections": true, +/// "metrics": "0.0.0.0:9000", /// }, /// "feature": { /// "env": { diff --git a/mirrord/kube/src/api/container/util.rs b/mirrord/kube/src/api/container/util.rs index 23fd752181b..d40949d268c 100644 --- a/mirrord/kube/src/api/container/util.rs +++ b/mirrord/kube/src/api/container/util.rs @@ -4,7 +4,9 @@ use futures::{AsyncBufReadExt, TryStreamExt}; use k8s_openapi::api::core::v1::{EnvVar, Pod, Toleration}; use kube::{api::LogParams, Api}; use mirrord_config::agent::{AgentConfig, LinuxCapability}; -use mirrord_protocol::{AGENT_IPV6_ENV, AGENT_NETWORK_INTERFACE_ENV, AGENT_OPERATOR_CERT_ENV}; +use mirrord_protocol::{ + AGENT_IPV6_ENV, AGENT_METRICS_ENV, AGENT_NETWORK_INTERFACE_ENV, AGENT_OPERATOR_CERT_ENV, +}; use regex::Regex; use tracing::warn; @@ -59,7 +61,9 @@ pub(super) fn agent_env(agent: &AgentConfig, params: &&ContainerParams) -> Vec Vec = #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum ClientMessage { Close, + /// TCP sniffer message. + /// + /// These are the messages used by the `mirror` feature, and handled by the + /// `TcpSnifferApi` in the agent. Tcp(LayerTcp), + + /// TCP stealer message. + /// + /// These are the messages used by the `steal` feature, and handled by the `TcpStealerApi` in + /// the agent. TcpSteal(LayerTcpSteal), + /// TCP outgoing message. + /// + /// These are the messages used by the `outgoing` feature (tcp), and handled by the + /// `TcpOutgoingApi` in the agent. TcpOutgoing(LayerTcpOutgoing), + + /// UDP outgoing message. + /// + /// These are the messages used by the `outgoing` feature (udp), and handled by the + /// `UdpOutgoingApi` in the agent. UdpOutgoing(LayerUdpOutgoing), FileRequest(FileRequest), GetEnvVarsRequest(GetEnvVarsRequest), diff --git a/mirrord/protocol/src/error.rs b/mirrord/protocol/src/error.rs index efb7ff08198..67197d76843 100644 --- a/mirrord/protocol/src/error.rs +++ b/mirrord/protocol/src/error.rs @@ -44,7 +44,7 @@ pub enum ResponseError { #[error("Remote operation expected fd `{0}` to be a file, but it's a directory!")] NotFile(u64), - #[error("IO failed for remote operation with `{0}!")] + #[error("IO failed for remote operation: `{0}!")] RemoteIO(RemoteIOError), #[error(transparent)] diff --git a/mirrord/protocol/src/lib.rs b/mirrord/protocol/src/lib.rs index 983fcd3536b..f1a3cc1e5cc 100644 --- a/mirrord/protocol/src/lib.rs +++ b/mirrord/protocol/src/lib.rs @@ -112,4 +112,6 @@ pub const AGENT_OPERATOR_CERT_ENV: &str = "MIRRORD_AGENT_OPERATOR_CERT"; pub const AGENT_NETWORK_INTERFACE_ENV: &str = "MIRRORD_AGENT_INTERFACE"; +pub const AGENT_METRICS_ENV: &str = "MIRRORD_AGENT_METRICS"; + pub const AGENT_IPV6_ENV: &str = "MIRRORD_AGENT_SUPPORT_IPV6"; diff --git a/mirrord/protocol/src/outgoing/tcp.rs b/mirrord/protocol/src/outgoing/tcp.rs index e38fa0c44d0..877e0d2f6c0 100644 --- a/mirrord/protocol/src/outgoing/tcp.rs +++ b/mirrord/protocol/src/outgoing/tcp.rs @@ -3,14 +3,43 @@ use crate::RemoteResult; #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum LayerTcpOutgoing { + /// User is interested in connecting via tcp to some remote address, specified in + /// [`LayerConnect`]. + /// + /// The layer will get a mirrord managed address that it'll `connect` to, meanwhile + /// in the agent we `connect` to the actual remote address. Connect(LayerConnect), + + /// Write data to the remote address the agent is `connect`ed to. + /// + /// There's no `Read` message, as we're calling `read` in the agent, and we send + /// a [`DaemonTcpOutgoing::Read`] message in case we get some data from this connection. Write(LayerWrite), + + /// The layer closed the connection, this message syncs up the agent, closing it + /// over there as well. + /// + /// Connections in the agent may be closed in other ways, such as when an error happens + /// when reading or writing. Which means that this message is not the only way of + /// closing outgoing tcp connections. Close(LayerClose), } #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum DaemonTcpOutgoing { + /// The agent attempted a connection to the remote address specified by + /// [`LayerTcpOutgoing::Connect`], and it might've been successful or not. Connect(RemoteResult), + + /// Read data from the connection. + /// + /// There's no `Write` message, as `write`s come from the user (layer). The agent sending + /// a `write` to the layer like this would make no sense, since it could just `write` it + /// to the remote connection itself. Read(RemoteResult), + + /// Tell the layer that this connection has been `close`d, either by a request from + /// the user with [`LayerTcpOutgoing::Close`], or from some error in the agent when + /// writing or reading from the connection. Close(ConnectionId), } diff --git a/mirrord/protocol/src/outgoing/udp.rs b/mirrord/protocol/src/outgoing/udp.rs index 02b4d97f830..f58378beeea 100644 --- a/mirrord/protocol/src/outgoing/udp.rs +++ b/mirrord/protocol/src/outgoing/udp.rs @@ -3,14 +3,50 @@ use crate::RemoteResult; #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum LayerUdpOutgoing { + /// User is interested in connecting via udp to some remote address, specified in + /// [`LayerConnect`]. + /// + /// The layer will get a mirrord managed address that it'll `connect` to, meanwhile + /// in the agent we `connect` to the actual remote address. + /// + /// Saying that we have an _udp connection_ is a bit weird, considering it's a + /// _connectionless_ protocol, but in mirrord we use a _fakeish_ connection mechanism + /// when dealing with outgoing udp traffic. Connect(LayerConnect), + + /// Write data to the remote address the agent is `connect`ed to. + /// + /// There's no `Read` message, as we're calling `read` in the agent, and we send + /// a [`DaemonUdpOutgoing::Read`] message in case we get some data from this connection. Write(LayerWrite), + + /// The layer closed the connection, this message syncs up the agent, closing it + /// over there as well. + /// + /// Connections in the agent may be closed in other ways, such as when an error happens + /// when reading or writing. Which means that this message is not the only way of + /// closing outgoing udp connections. Close(LayerClose), } #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum DaemonUdpOutgoing { + /// The agent attempted a connection to the remote address specified by + /// [`LayerUdpOutgoing::Connect`], and it might've been successful or not. + /// + /// See the docs for [`LayerUdpOutgoing::Connect`] for a bit more information on the + /// weird idea of `connect` and udp in mirrord. Connect(RemoteResult), + + /// Read data from the connection. + /// + /// There's no `Write` message, as `write`s come from the user (layer). The agent sending + /// a `write` to the layer like this would make no sense, since it could just `write` it + /// to the remote connection itself. Read(RemoteResult), + + /// Tell the layer that this connection has been `close`d, either by a request from + /// the user with [`LayerUdpOutgoing::Close`], or from some error in the agent when + /// writing or reading from the connection. Close(ConnectionId), } diff --git a/mirrord/protocol/src/tcp.rs b/mirrord/protocol/src/tcp.rs index acf3d734121..e98077a62ec 100644 --- a/mirrord/protocol/src/tcp.rs +++ b/mirrord/protocol/src/tcp.rs @@ -52,14 +52,31 @@ pub struct TcpClose { } /// Messages related to Tcp handler from client. +/// +/// Part of the `mirror` feature. #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum LayerTcp { + /// User is interested in mirroring traffic on this `Port`, so add it to the list of + /// ports that the sniffer is filtering. PortSubscribe(Port), + + /// User is not interested in the connection with `ConnectionId` anymore. + /// + /// This means that their app has closed the connection they were `listen`ning on. + /// + /// There is no `ConnectionSubscribe` counter-part of this variant, the subscription + /// happens when the sniffer receives an (agent) internal `SniffedConnection`. ConnectionUnsubscribe(ConnectionId), + + /// Removes this `Port` from the sniffer's filter, the traffic won't be cloned to mirrord + /// anymore. PortUnsubscribe(Port), } /// Messages related to Tcp handler from server. +/// +/// They are the same for both `steal` and `mirror` modes, even though their layer +/// counterparts ([`LayerTcpSteal`] and [`LayerTcp`]) are different. #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum DaemonTcp { NewConnection(NewTcpConnection), @@ -214,10 +231,38 @@ impl StealType { } /// Messages related to Steal Tcp handler from client. +/// +/// `PortSubscribe`, `PortUnsubscribe`, and `ConnectionUnsubscribe` variants are similar +/// to what you'll find in the [`LayerTcp`], but they're handled by different tasks in +/// the agent. +/// +/// Stolen traffic might have an additional overhead when compared to mirrored traffic, as +/// we have an intermmediate HTTP server to handle filtering (based on HTTP headers, etc). #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] pub enum LayerTcpSteal { + /// User is interested in stealing traffic on this `Port`, so add it to the list of + /// ports that the stealer is filtering. + /// + /// The `TcpConnectionStealer` supports an [`HttpFilter`] granting the ability to steal + /// only traffic that matches the user configured filter. It's also possible to just steal + /// all traffic (which we refer as `Unfiltered`). For more info see [`StealType`]. + /// + /// This variant is somewhat related to [`LayerTcpSteal::ConnectionUnsubscribe`], since + /// we don't have a `ConnectionSubscribe` message anywhere, instead what we do is: when + /// a new connection comes in one of the ports we are subscribed to, we consider it a + /// connection subscription (so this mechanism represents the **non-existing** + /// `ConnectionSubscribe` variant). PortSubscribe(StealType), + + /// User has stopped stealing from this connection with [`ConnectionId`]. + /// + /// We do **not** have a `ConnectionSubscribe` variant/message. What happens instead is that we + /// call a _connection subscription_ the act of `accept`ing a new connection on one of the + /// ports we are subscribed to. See the [`LayerTcpSteal::PortSubscribe`] for more info. ConnectionUnsubscribe(ConnectionId), + + /// Removes this `Port` from the stealers's filter, the traffic won't be stolen by mirrord + /// anymore. PortUnsubscribe(Port), Data(TcpData), HttpResponse(HttpResponse>),