diff --git a/src/Cargo.lock b/src/Cargo.lock index b9f7edb8b..ec9335dd2 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -135,13 +135,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -194,7 +194,7 @@ checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" dependencies = [ "libc", "log", - "nix", + "nix 0.26.4", "serde", "serde_bytes", "serde_cbor", @@ -312,7 +312,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.100", + "syn 2.0.87", "which", ] @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", "cfg_aliases", @@ -361,15 +361,16 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", + "syn_derive", ] [[package]] @@ -386,9 +387,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" @@ -466,9 +467,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.51" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] @@ -490,15 +491,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -579,9 +580,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -589,27 +590,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -706,14 +707,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "dunce" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "ecdsa" @@ -741,9 +742,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" @@ -811,7 +812,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -853,9 +854,9 @@ dependencies = [ [[package]] name = "flagset" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" +checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" [[package]] name = "fnv" @@ -886,9 +887,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -901,9 +902,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -911,15 +912,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -928,38 +929,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1009,9 +1010,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -1105,12 +1106,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - [[package]] name = "heapless" version = "0.8.0" @@ -1243,9 +1238,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.1" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1255,9 +1250,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", @@ -1414,7 +1409,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -1457,12 +1452,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.14.5", "serde", ] @@ -1481,7 +1476,7 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", "borsh", - "nix", + "nix 0.29.0", "qos_client", "qos_core", "qos_crypto", @@ -1495,6 +1490,7 @@ dependencies = [ "rustls", "serde", "tokio", + "tokio-rustls", "ureq", "webpki-roots", ] @@ -1522,18 +1518,18 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1570,12 +1566,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1608,9 +1604,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "loom" @@ -1655,6 +1651,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1728,10 +1733,23 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.7.1", "pin-utils", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nom" version = "7.1.3" @@ -1768,9 +1786,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ "num-integer", "num-traits", @@ -1864,9 +1882,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -2031,7 +2049,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -2110,12 +2128,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" @@ -2124,7 +2139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -2171,9 +2186,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2207,8 +2222,9 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", "borsh", + "futures", "libc", - "nix", + "nix 0.29.0", "qos_crypto", "qos_hex", "qos_nsm", @@ -2217,6 +2233,8 @@ dependencies = [ "rustls", "serde", "serde_bytes", + "tokio", + "tokio-vsock", "vsss-rs", "webpki-roots", ] @@ -2259,6 +2277,7 @@ version = "0.1.0" dependencies = [ "borsh", "chunked_transfer", + "futures", "hickory-resolver", "httparse", "qos_core", @@ -2268,6 +2287,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-rustls", "webpki-roots", ] @@ -2308,6 +2328,7 @@ dependencies = [ name = "qos_test_primitives" version = "0.1.0" dependencies = [ + "nix 0.29.0", "rand 0.8.5", ] @@ -2322,9 +2343,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -2379,7 +2400,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.10", ] [[package]] @@ -2393,18 +2414,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2463,7 +2484,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.14", + "getrandom 0.2.10", "libc", "untrusted", "windows-sys 0.52.0", @@ -2556,9 +2577,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "aws-lc-rs", "log", @@ -2659,9 +2680,9 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -2687,20 +2708,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "itoa", "memchr", @@ -2726,20 +2747,20 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "serde_with" -version = "3.11.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -2749,14 +2770,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -2816,6 +2837,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "1.6.4" @@ -2931,15 +2961,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -2966,7 +3008,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3007,7 +3049,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3018,7 +3060,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3073,9 +3115,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -3098,6 +3140,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -3111,14 +3154,37 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-vsock" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1824fc0300433f400df6b6264a9ab00ba93f39d38c3157fb5f05183476c4af10" +dependencies = [ + "bytes", + "futures", + "libc", + "tokio", + "vsock", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" @@ -3126,7 +3192,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -3148,15 +3214,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" @@ -3227,9 +3293,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" @@ -3249,9 +3315,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.1" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ "base64 0.22.1", "log", @@ -3286,11 +3352,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.13.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.2.10", ] [[package]] @@ -3301,9 +3367,19 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" -version = "0.9.5" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vsock" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" +dependencies = [ + "libc", + "nix 0.29.0", +] [[package]] name = "vsss-rs" @@ -3350,35 +3426,34 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", - "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3386,22 +3461,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "webpki" @@ -3415,9 +3490,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.10" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -3519,7 +3594,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3530,7 +3605,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3814,7 +3889,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "synstructure 0.13.1", ] @@ -3858,7 +3933,6 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] @@ -3870,7 +3944,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3890,7 +3964,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "synstructure 0.13.1", ] @@ -3911,7 +3985,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -3933,5 +4007,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] diff --git a/src/init/Cargo.lock b/src/init/Cargo.lock index 482645c04..65a4b2b7e 100644 --- a/src/init/Cargo.lock +++ b/src/init/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aead" version = "0.5.2" @@ -97,12 +112,27 @@ checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" dependencies = [ "libc", "log", - "nix", + "nix 0.26.4", "serde", "serde_bytes", "serde_cbor", ] +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -133,6 +163,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "bitvec" version = "1.0.1" @@ -156,9 +192,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", "cfg_aliases", @@ -166,15 +202,16 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", + "syn_derive", ] [[package]] @@ -189,6 +226,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.16" @@ -247,9 +290,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -300,9 +343,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -310,27 +353,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -485,9 +528,9 @@ dependencies = [ [[package]] name = "flagset" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" +checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" [[package]] name = "fnv" @@ -501,6 +544,95 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -523,9 +655,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -542,6 +674,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "group" version = "0.12.1" @@ -689,6 +827,7 @@ dependencies = [ "qos_core", "qos_nsm", "qos_system", + "tokio", ] [[package]] @@ -708,9 +847,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -742,9 +881,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -761,6 +900,35 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "multiexp" version = "0.4.0" @@ -780,13 +948,26 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.7.1", "pin-utils", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "num" version = "0.4.3" @@ -868,11 +1049,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -913,6 +1103,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pin-utils" version = "0.1.0" @@ -959,12 +1155,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "primeorder" @@ -1010,9 +1203,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1031,14 +1224,17 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", "borsh", + "futures", "libc", - "nix", + "nix 0.29.0", "qos_crypto", "qos_hex", "qos_nsm", "qos_p256", "serde", "serde_bytes", + "tokio", + "tokio-vsock", "vsss-rs", ] @@ -1167,6 +1363,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustversion" version = "1.0.19" @@ -1215,9 +1417,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -1243,23 +1445,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", - "memchr", "ryu", "serde", ] @@ -1272,14 +1473,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] name = "serde_with" -version = "3.11.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ "base64", "chrono", @@ -1295,14 +1496,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -1342,6 +1543,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.0.0" @@ -1352,6 +1562,25 @@ dependencies = [ "rand_core", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.9.8" @@ -1422,15 +1651,27 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "tap" version = "1.0.1" @@ -1454,7 +1695,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -1488,11 +1729,52 @@ dependencies = [ "time-core", ] +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tokio-vsock" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1824fc0300433f400df6b6264a9ab00ba93f39d38c3157fb5f05183476c4af10" +dependencies = [ + "bytes", + "futures", + "libc", + "tokio", + "vsock", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" @@ -1535,9 +1817,19 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "version_check" -version = "0.9.5" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vsock" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" +dependencies = [ + "libc", + "nix 0.29.0", +] [[package]] name = "vsss-rs" @@ -1566,35 +1858,34 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", - "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1602,22 +1893,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "webpki" @@ -1649,9 +1940,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1665,51 +1956,51 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -1747,7 +2038,6 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] @@ -1759,7 +2049,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] [[package]] @@ -1779,5 +2069,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.87", ] diff --git a/src/init/Cargo.toml b/src/init/Cargo.toml index ac1ecf97e..17ae57483 100644 --- a/src/init/Cargo.toml +++ b/src/init/Cargo.toml @@ -10,6 +10,7 @@ qos_aws = { path = "../qos_aws"} qos_system = { path = "../qos_system"} qos_core = { path = "../qos_core", features = ["vm"], default-features = false } qos_nsm = { path = "../qos_nsm", default-features = false } +tokio = { version = "1.38.0", features = ["io-util", "macros", "net", "rt-multi-thread", "time", "signal"], default-features = false } [[bin]] name = "init" diff --git a/src/init/init.rs b/src/init/init.rs index 39f0a6cc8..4834ec74d 100644 --- a/src/init/init.rs +++ b/src/init/init.rs @@ -1,18 +1,14 @@ -use qos_system::{dmesg, get_local_cid, freopen, mount, reboot}; use qos_core::{ - handles::Handles, - io::{SocketAddress, VMADDR_NO_FLAGS}, - reaper::Reaper, - EPHEMERAL_KEY_FILE, - MANIFEST_FILE, - PIVOT_FILE, - QUORUM_FILE, - SEC_APP_SOCK, + handles::Handles, + io::{AsyncStreamPool, SocketAddress, VMADDR_NO_FLAGS}, + reaper::Reaper, + EPHEMERAL_KEY_FILE, MANIFEST_FILE, PIVOT_FILE, QUORUM_FILE, SEC_APP_SOCK, }; use qos_nsm::Nsm; +use qos_system::{dmesg, freopen, get_local_cid, mount, reboot}; //TODO: Feature flag -use qos_aws::{init_platform}; +use qos_aws::init_platform; // Mount common filesystems with conservative permissions fn init_rootfs() { @@ -58,26 +54,35 @@ fn boot() { init_platform(); } -fn main() { +#[tokio::main] +async fn main() { boot(); - dmesg("QuorumOS Booted".to_string()); + dmesg("QuorumOS Booted in Async mode".to_string()); let cid = get_local_cid().unwrap(); dmesg(format!("CID is {}", cid)); let handles = Handles::new( - EPHEMERAL_KEY_FILE.to_string(), - QUORUM_FILE.to_string(), - MANIFEST_FILE.to_string(), - PIVOT_FILE.to_string(), - ); - Reaper::execute( - &handles, - Box::new(Nsm), - SocketAddress::new_vsock(cid, 3, VMADDR_NO_FLAGS), - SocketAddress::new_unix(SEC_APP_SOCK), - None, + EPHEMERAL_KEY_FILE.to_string(), + QUORUM_FILE.to_string(), + MANIFEST_FILE.to_string(), + PIVOT_FILE.to_string(), ); + let start_port = 3; // used for qos-host only! others follow 4+ for the -host + let core_pool = AsyncStreamPool::new( + SocketAddress::new_vsock(cid, start_port, VMADDR_NO_FLAGS), + 1, // start at pool size 1, grow based on manifest/args as necessary (see Reaper) + ) + .expect("unable to create core pool"); + + let app_pool = AsyncStreamPool::new( + SocketAddress::new_unix(SEC_APP_SOCK), + 1, // start at pool size 1, grow based on manifest/args as necessary (see Reaper) + ) + .expect("unable to create app pool"); + + Reaper::execute(&handles, Box::new(Nsm), core_pool, app_pool, None); + reboot(); } diff --git a/src/integration/Cargo.toml b/src/integration/Cargo.toml index 618f04e56..a1f33695f 100644 --- a/src/integration/Cargo.toml +++ b/src/integration/Cargo.toml @@ -9,7 +9,7 @@ qos_core = { path = "../qos_core", features = ["mock"], default-features = false qos_nsm = { path = "../qos_nsm", features = ["mock"], default-features = false } qos_host = { path = "../qos_host", default-features = false } qos_client = { path = "../qos_client", default-features = false } -qos_net = { path = "../qos_net", default-features = false } +qos_net = { path = "../qos_net", features = ["proxy"], default-features = false } qos_crypto = { path = "../qos_crypto" } qos_hex = { path = "../qos_hex" } qos_p256 = { path = "../qos_p256", features = ["mock"] } @@ -17,8 +17,9 @@ qos_test_primitives = { path = "../qos_test_primitives" } tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"], default-features = false } borsh = { version = "1.0", features = ["std", "derive"] , default-features = false} -nix = { version = "0.26", features = ["socket"], default-features = false } +nix = { version = "0.29", features = ["socket"], default-features = false } rustls = { version = "0.23.5" } +tokio-rustls = { version = "0.26.2" } webpki-roots = { version = "0.26.1" } [dev-dependencies] diff --git a/src/integration/examples/boot_enclave.rs b/src/integration/examples/boot_enclave.rs new file mode 100644 index 000000000..84147e305 --- /dev/null +++ b/src/integration/examples/boot_enclave.rs @@ -0,0 +1,454 @@ +use std::{ + fs, + io::{BufRead, BufReader, Write}, + process::{Command, Stdio}, +}; + +use borsh::de::BorshDeserialize; +use integration::{LOCAL_HOST, PCR3_PRE_IMAGE_PATH, QOS_DIST_DIR}; +use qos_core::protocol::{ + services::{ + boot::{Approval, Manifest, ManifestSet, Namespace, ShareSet}, + genesis::{GenesisMemberOutput, GenesisOutput}, + }, + ProtocolPhase, QosHash, +}; +use qos_crypto::sha_256; +use qos_host::EnclaveInfo; +use qos_p256::P256Pair; +use qos_test_primitives::{ChildWrapper, PathWrapper}; + +#[tokio::main] +async fn main() { + let pivot_file_path = + std::env::args().nth(1).expect("No pivot file path provided"); + + const PIVOT_HASH_PATH: &str = "/tmp/enclave-example-pivot-hash.txt"; + + let host_port = 3001; + let tmp: PathWrapper = "/tmp/enclave-example".into(); + let _: PathWrapper = PIVOT_HASH_PATH.into(); + fs::create_dir_all(&*tmp).unwrap(); + + let usock: PathWrapper = "/tmp/enclave-example/example.sock".into(); + let app_usock: PathWrapper = "/tmp/enclave-example/example-app.sock".into(); + let secret_path: PathWrapper = "/tmp/enclave-example/example.secret".into(); + let pivot_path: PathWrapper = "/tmp/enclave-example/example.pivot".into(); + let manifest_path: PathWrapper = + "/tmp/enclave-example/example.manifest".into(); + let eph_path: PathWrapper = + "/tmp/enclave-example/ephemeral_key.secret".into(); + + let boot_dir: PathWrapper = "/tmp/enclave-example/boot-dir".into(); + fs::create_dir_all(&*boot_dir).unwrap(); + let attestation_dir: PathWrapper = + "/tmp/enclave-example/attestation-dir".into(); + fs::create_dir_all(&*attestation_dir).unwrap(); + let attestation_doc_path = format!("{}/attestation_doc", &*attestation_dir); + + let all_personal_dir = "./mock/boot-e2e/all-personal-dir"; + + let namespace = "quit-coding-to-vape"; + + let personal_dir = |user: &str| format!("{all_personal_dir}/{user}-dir"); + + let user1 = "user1"; + let user2 = "user2"; + let user3 = "user3"; + + // -- Create pivot-build-fingerprints.txt + let pivot = fs::read(&pivot_file_path).unwrap(); + let mock_pivot_hash = sha_256(&pivot); + let pivot_hash = qos_hex::encode_to_vec(&mock_pivot_hash); + std::fs::write(PIVOT_HASH_PATH, pivot_hash).unwrap(); + + // -- CLIENT create manifest. + let pivot_args = std::env::args().nth(2).expect("No pivot args provided"); + let cli_manifest_path = format!("{}/manifest", &*boot_dir); + + assert!(Command::new("../target/debug/qos_client") + .args([ + "generate-manifest", + "--nonce", + "2", + "--namespace", + namespace, + "--restart-policy", + "never", + "--pivot-hash-path", + PIVOT_HASH_PATH, + "--qos-release-dir", + QOS_DIST_DIR, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--manifest-path", + &cli_manifest_path, + "--pivot-args", + &pivot_args, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--share-set-dir", + "./mock/keys/share-set", + "--patch-set-dir", + "./mock/keys/manifest-set", + "--quorum-key-path", + "./mock/namespaces/quit-coding-to-vape/quorum_key.pub" + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + // Check the manifest written to file + let manifest = + Manifest::try_from_slice(&fs::read(&cli_manifest_path).unwrap()) + .unwrap(); + + let genesis_output = { + let contents = + fs::read("./mock/boot-e2e/genesis-dir/genesis_output").unwrap(); + GenesisOutput::try_from_slice(&contents).unwrap() + }; + // For simplicity sake, we use the same keys for the share set and manifest + // set. + let mut members: Vec<_> = genesis_output + .member_outputs + .iter() + .cloned() + .map(|GenesisMemberOutput { share_set_member, .. }| share_set_member) + .collect(); + members.sort(); + + let namespace_field = Namespace { + name: namespace.to_string(), + nonce: 2, + quorum_key: genesis_output.quorum_key, + }; + assert_eq!(manifest.namespace, namespace_field); + let manifest_set = ManifestSet { threshold: 2, members: members.clone() }; + assert_eq!(manifest.manifest_set, manifest_set); + let share_set = ShareSet { threshold: 2, members }; + assert_eq!(manifest.share_set, share_set); + + // -- CLIENT make sure each user can run `approve-manifest` + for alias in [user1, user2, user3] { + let approval_path = format!( + "{}/{}-{}-{}.approval", + &*boot_dir, alias, namespace, manifest.namespace.nonce, + ); + + let secret_path = format!("{}/{}.secret", &personal_dir(alias), alias); + + let mut child = Command::new("../target/debug/qos_client") + .args([ + "approve-manifest", + "--secret-path", + &*secret_path, + "--manifest-path", + &cli_manifest_path, + "--manifest-approvals-dir", + &*boot_dir, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--pivot-hash-path", + PIVOT_HASH_PATH, + "--qos-release-dir", + QOS_DIST_DIR, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--share-set-dir", + "./mock/keys/share-set", + "--patch-set-dir", + "./mock/keys/manifest-set", + "--quorum-key-path", + "./mock/namespaces/quit-coding-to-vape/quorum_key.pub", + "--alias", + alias, + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = child.stdin.take().expect("Failed to open stdin"); + + let mut stdout = { + let stdout = child.stdout.as_mut().unwrap(); + let stdout_reader = BufReader::new(stdout); + stdout_reader.lines() + }; + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace name: quit-coding-to-vape? (y/n)" + ); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace nonce: 2? (y/n)" + ); + // On purpose, try to input a bad value, neither yes or no + stdin + .write_all("maybe\n".as_bytes()) + .expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Please answer with either \"yes\" (y) or \"no\" (n)" + ); + // Try the longer option ("yes" rather than "y") + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct pivot restart policy: RestartPolicy::Never? (y/n)" + ); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Are these the correct pivot args:" + ); + stdout.next().unwrap().unwrap(); // pivot args confirm msg + assert_eq!(&stdout.next().unwrap().unwrap(), "(y/n)"); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + // Wait for the command to write the approval and exit + assert!(child.wait().unwrap().success()); + + // Read in the generated approval to check it was created correctly + let approval = + Approval::try_from_slice(&fs::read(approval_path).unwrap()) + .unwrap(); + let personal_pair = P256Pair::from_hex_file(format!( + "{}/{}.secret", + personal_dir(alias), + alias, + )) + .unwrap(); + + let signature = personal_pair.sign(&manifest.qos_hash()).unwrap(); + assert_eq!(approval.signature, signature); + + assert_eq!(approval.member.alias, alias); + assert_eq!( + approval.member.pub_key, + personal_pair.public_key().to_bytes(), + ); + } + + // -- ENCLAVE start enclave + let mut _enclave_child_process: ChildWrapper = + Command::new("../target/debug/qos_core") + .args([ + "--usock", + &*usock, + "--app-usock", + &*app_usock, + "--quorum-file", + &*secret_path, + "--pivot-file", + &*pivot_path, + "--ephemeral-file", + &*eph_path, + "--mock", + "--manifest-file", + &*manifest_path, + ]) + .spawn() + .unwrap() + .into(); + + // -- HOST start host + let mut _host_child_process: ChildWrapper = + Command::new("../target/debug/async_qos_host") + .args([ + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--usock", + &*usock, + ]) + .spawn() + .unwrap() + .into(); + + // -- Make sure the enclave and host have time to boot + qos_test_primitives::wait_until_port_is_bound(host_port); + + // -- CLIENT generate the manifest envelope + assert!(Command::new("../target/debug/qos_client") + .args([ + "generate-manifest-envelope", + "--manifest-approvals-dir", + &*boot_dir, + "--manifest-path", + &cli_manifest_path, + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + // -- CLIENT broadcast boot standard instruction + let manifest_envelope_path = format!("{}/manifest_envelope", &*boot_dir,); + assert!(Command::new("../target/debug/qos_client") + .args([ + "boot-standard", + "--manifest-envelope-path", + &manifest_envelope_path, + "--pivot-path", + &pivot_file_path, + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--pcr3-preimage-path", + "./mock/pcr3-preimage.txt", + "--unsafe-skip-attestation", + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + for user in [&user1, &user2] { + // Get attestation doc and manifest + assert!(Command::new("../target/debug/qos_client") + .args([ + "get-attestation-doc", + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--attestation-doc-path", + &*attestation_doc_path, + "--manifest-envelope-path", + "/tmp/dont_care" + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + let share_path = format!("{}/{}.share", &personal_dir(user), user); + let secret_path = format!("{}/{}.secret", &personal_dir(user), user); + let eph_wrapped_share_path: PathWrapper = + format!("{}/{}.eph_wrapped.share", &*tmp, user).into(); + let approval_path: PathWrapper = + format!("{}/{}.attestation.approval", &*tmp, user).into(); + // Encrypt share to ephemeral key + let mut child = Command::new("../target/debug/qos_client") + .args([ + "proxy-re-encrypt-share", + "--share-path", + &share_path, + "--secret-path", + &secret_path, + "--attestation-doc-path", + &*attestation_doc_path, + "--eph-wrapped-share-path", + &eph_wrapped_share_path, + "--approval-path", + &approval_path, + "--manifest-envelope-path", + &manifest_envelope_path, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--alias", + user, + "--unsafe-skip-attestation", + "--unsafe-eph-path-override", + &*eph_path, + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = child.stdin.take().expect("Failed to open stdin"); + + let mut stdout = { + let stdout = child.stdout.as_mut().unwrap(); + let stdout_reader = BufReader::new(stdout); + stdout_reader.lines() + }; + + // Skip over a log message + stdout.next(); + + // Answer prompts with yes + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace name: quit-coding-to-vape? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace nonce: 2? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Does this AWS IAM role belong to the intended organization: arn:aws:iam::123456789012:role/Webserver? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "The following manifest set members approved:" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + // Check that it finished successfully + assert!(child.wait().unwrap().success()); + + // Post the encrypted share + assert!(Command::new("../target/debug/qos_client") + .args([ + "post-share", + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--eph-wrapped-share-path", + &eph_wrapped_share_path, + "--approval-path", + &approval_path, + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + } + + // Give the enclave time to start the pivot + std::thread::sleep(std::time::Duration::from_secs(2)); + + let enclave_info_url = + format!("http://{LOCAL_HOST}:{}/qos/enclave-info", host_port); + let enclave_info: EnclaveInfo = + ureq::get(&enclave_info_url).call().unwrap().into_json().unwrap(); + assert_eq!(enclave_info.phase, ProtocolPhase::QuorumKeyProvisioned); + + eprintln!("=========ENCLAVE READY WITH PIVOT RUNNING!!=========="); + eprintln!("press ctrl+c to quit"); + + // drop(_host_child_process); + + match tokio::signal::ctrl_c().await { + Ok(()) => {} + Err(err) => panic!("{err}"), + } +} diff --git a/src/integration/src/bin/pivot_remote_tls.rs b/src/integration/src/bin/pivot_async_remote_tls.rs similarity index 56% rename from src/integration/src/bin/pivot_remote_tls.rs rename to src/integration/src/bin/pivot_async_remote_tls.rs index 2373d53e8..f22c19739 100644 --- a/src/integration/src/bin/pivot_remote_tls.rs +++ b/src/integration/src/bin/pivot_async_remote_tls.rs @@ -1,78 +1,74 @@ use core::panic; -use std::{ - io::{ErrorKind, Read, Write}, - sync::Arc, -}; +use std::{io::ErrorKind, sync::Arc}; use borsh::BorshDeserialize; use integration::PivotRemoteTlsMsg; use qos_core::{ - io::{SocketAddress, TimeVal}, - server::{RequestProcessor, SocketServer}, + async_server::{AsyncRequestProcessor, AsyncSocketServer}, + io::{AsyncStreamPool, SharedAsyncStreamPool, SocketAddress}, }; -use qos_net::proxy_stream::ProxyStream; +use qos_net::async_proxy_stream::AsyncProxyStream; use rustls::RootCertStore; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_rustls::TlsConnector; +#[derive(Clone)] struct Processor { - net_proxy: SocketAddress, + net_pool: SharedAsyncStreamPool, } impl Processor { - fn new(proxy_address: String) -> Self { - Processor { net_proxy: SocketAddress::new_unix(&proxy_address) } + fn new(net_pool: SharedAsyncStreamPool) -> Self { + Processor { net_pool } } } -impl RequestProcessor for Processor { - fn process(&mut self, request: Vec) -> Vec { +impl AsyncRequestProcessor for Processor { + async fn process(&self, request: Vec) -> Vec { let msg = PivotRemoteTlsMsg::try_from_slice(&request) .expect("Received invalid message - test is broken!"); match msg { PivotRemoteTlsMsg::RemoteTlsRequest { host, path } => { - let timeout = TimeVal::new(1, 0); - let mut stream = ProxyStream::connect_by_name( - &self.net_proxy, - timeout, + let pool = self.net_pool.read().await; + let mut stream = AsyncProxyStream::connect_by_name( + pool.get().await, host.clone(), 443, vec!["8.8.8.8".to_string()], 53, ) + .await .unwrap(); let root_store = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into(), }; - let server_name: rustls::pki_types::ServerName<'_> = host.clone().try_into().unwrap(); let config: rustls::ClientConfig = rustls::ClientConfig::builder() .with_root_certificates(root_store) .with_no_client_auth(); - let mut conn = rustls::ClientConnection::new( - Arc::new(config), - server_name, - ) - .unwrap(); - let mut tls = rustls::Stream::new(&mut conn, &mut stream); + let conn = TlsConnector::from(Arc::new(config)); + let mut tls = conn + .connect(server_name, &mut stream) + .await + .expect("tls unable to establish connection"); let http_request = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"); - tls.write_all(http_request.as_bytes()).unwrap(); + tls.write_all(http_request.as_bytes()).await.unwrap(); let mut response_bytes = Vec::new(); - let read_to_end_result = tls.read_to_end(&mut response_bytes); + let read_to_end_result = + tls.read_to_end(&mut response_bytes).await; match read_to_end_result { Ok(read_size) => { assert!(read_size > 0); - - // Assert the connection isn't closed yet, and close it. - assert!(!stream.is_closed()); - stream.close().expect("unable to close stream"); - assert!(stream.is_closed()); + // Refresh the connection for additional calls + stream.refresh().await.unwrap(); } Err(e) => { // Only EOF errors are expected. This means the @@ -99,7 +95,8 @@ impl RequestProcessor for Processor { } } -fn main() { +#[tokio::main] +async fn main() { // Parse args: // - first argument is the socket to bind to (normal server server) // - second argument is the socket to use for remote proxying @@ -108,9 +105,26 @@ fn main() { let socket_path: &String = &args[1]; let proxy_path: &String = &args[2]; - SocketServer::listen( - SocketAddress::new_unix(socket_path), - Processor::new(proxy_path.to_string()), + let enclave_pool = + AsyncStreamPool::new(SocketAddress::new_unix(socket_path), 1) + .expect("unable to create async stream pool"); + + let proxy_pool = + AsyncStreamPool::new(SocketAddress::new_unix(proxy_path), 1) + .expect("unable to create async stream pool") + .shared(); + + let server = AsyncSocketServer::listen_all( + enclave_pool, + &Processor::new(proxy_pool), ) .unwrap(); + + match tokio::signal::ctrl_c().await { + Ok(_) => { + eprintln!("pivot handling ctrl+c the tokio way"); + server.terminate(); + } + Err(err) => panic!("{err}"), + } } diff --git a/src/integration/src/bin/pivot_ok4.rs b/src/integration/src/bin/pivot_ok4.rs new file mode 100644 index 000000000..a235e1926 --- /dev/null +++ b/src/integration/src/bin/pivot_ok4.rs @@ -0,0 +1,5 @@ +use integration::PIVOT_OK4_SUCCESS_FILE; + +fn main() { + integration::Cli::execute(PIVOT_OK4_SUCCESS_FILE); +} diff --git a/src/integration/src/bin/pivot_ok5.rs b/src/integration/src/bin/pivot_ok5.rs new file mode 100644 index 000000000..5bf20e981 --- /dev/null +++ b/src/integration/src/bin/pivot_ok5.rs @@ -0,0 +1,5 @@ +use integration::PIVOT_OK5_SUCCESS_FILE; + +fn main() { + integration::Cli::execute(PIVOT_OK5_SUCCESS_FILE); +} diff --git a/src/integration/src/bin/pivot_proof.rs b/src/integration/src/bin/pivot_proof.rs index f4de0fdb4..7d6e679f3 100644 --- a/src/integration/src/bin/pivot_proof.rs +++ b/src/integration/src/bin/pivot_proof.rs @@ -3,17 +3,18 @@ use core::panic; use borsh::BorshDeserialize; use integration::{AdditionProof, AdditionProofPayload, PivotProofMsg}; use qos_core::{ + async_server::{AsyncRequestProcessor, AsyncSocketServer}, handles::EphemeralKeyHandle, - io::SocketAddress, - server::{RequestProcessor, SocketServer}, + io::{AsyncStreamPool, SocketAddress}, }; +#[derive(Clone)] struct Processor { ephemeral_key_handle: EphemeralKeyHandle, } -impl RequestProcessor for Processor { - fn process(&mut self, request: Vec) -> Vec { +impl AsyncRequestProcessor for Processor { + async fn process(&self, request: Vec) -> Vec { let msg = PivotProofMsg::try_from_slice(&request) .expect("Received invalid message - test is broken!"); @@ -48,17 +49,25 @@ impl RequestProcessor for Processor { } } -fn main() { +#[tokio::main] +async fn main() { let args: Vec = std::env::args().collect(); let socket_path: &String = &args[1]; - SocketServer::listen( - SocketAddress::new_unix(socket_path), - Processor { + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix(socket_path), 1) + .expect("unable to create app pool"); + + let server = AsyncSocketServer::listen_all( + app_pool, + &Processor { ephemeral_key_handle: EphemeralKeyHandle::new( "./mock/ephemeral_seed.secret.keep".to_string(), ), }, ) .unwrap(); + + tokio::signal::ctrl_c().await.unwrap(); + server.terminate(); } diff --git a/src/integration/src/bin/pivot_socket_stress.rs b/src/integration/src/bin/pivot_socket_stress.rs index 5b5780eab..c8e4b3e9f 100644 --- a/src/integration/src/bin/pivot_socket_stress.rs +++ b/src/integration/src/bin/pivot_socket_stress.rs @@ -3,15 +3,15 @@ use core::panic; use borsh::BorshDeserialize; use integration::PivotSocketStressMsg; use qos_core::{ - io::SocketAddress, - protocol::ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, - server::{RequestProcessor, SocketServer}, + async_server::{AsyncRequestProcessor, AsyncSocketServer}, + io::{AsyncStreamPool, SocketAddress}, }; +#[derive(Clone)] struct Processor; -impl RequestProcessor for Processor { - fn process(&mut self, request: Vec) -> Vec { +impl AsyncRequestProcessor for Processor { + async fn process(&self, request: Vec) -> Vec { // Simulate just some baseline lag for all requests std::thread::sleep(std::time::Duration::from_secs(1)); @@ -24,14 +24,16 @@ impl RequestProcessor for Processor { .expect("OkResponse is valid borsh") } PivotSocketStressMsg::PanicRequest => { - panic!( - "\"socket stress\" pivot app has received a PanicRequest" - ) + eprintln!("PIVOT: panic request received, panicing"); + // panic is not enough in tokio, we need process exit + std::process::exit(1) } - PivotSocketStressMsg::SlowRequest => { - std::thread::sleep(std::time::Duration::from_secs( - ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS as u64 + 1, - )); + PivotSocketStressMsg::SlowRequest(delay) => { + eprintln!( + "PIVOT: slow request received, sleeping for {delay}ms" + ); + tokio::time::sleep(std::time::Duration::from_millis(delay)) + .await; borsh::to_vec(&PivotSocketStressMsg::SlowResponse) .expect("OkResponse is valid borsh") } @@ -45,9 +47,17 @@ impl RequestProcessor for Processor { } } -fn main() { +#[tokio::main] +async fn main() { let args: Vec = std::env::args().collect(); let socket_path = &args[1]; - SocketServer::listen(SocketAddress::new_unix(socket_path), Processor) - .unwrap(); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix(socket_path), 1) + .expect("unable to create app pool"); + + let server = AsyncSocketServer::listen_all(app_pool, &Processor).unwrap(); + + tokio::signal::ctrl_c().await.unwrap(); + server.terminate(); } diff --git a/src/integration/src/lib.rs b/src/integration/src/lib.rs index 83aefe12a..b8a031426 100644 --- a/src/integration/src/lib.rs +++ b/src/integration/src/lib.rs @@ -4,8 +4,14 @@ #![deny(clippy::all)] #![warn(missing_docs)] +use std::time::Duration; + use borsh::{BorshDeserialize, BorshSerialize}; -use qos_core::parser::{GetParserForOptions, OptionsParser, Parser, Token}; +use qos_core::{ + async_client::AsyncClient, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, + parser::{GetParserForOptions, OptionsParser, Parser, Token}, +}; /// Path to the file `pivot_ok` writes on success for tests. pub const PIVOT_OK_SUCCESS_FILE: &str = "./pivot_ok_works"; @@ -13,12 +19,20 @@ pub const PIVOT_OK_SUCCESS_FILE: &str = "./pivot_ok_works"; pub const PIVOT_OK2_SUCCESS_FILE: &str = "./pivot_ok2_works"; /// Path to the file `pivot_ok3` writes on success for tests. pub const PIVOT_OK3_SUCCESS_FILE: &str = "./pivot_ok3_works"; +/// Path to the file `pivot_ok4` writes on success for tests. +pub const PIVOT_OK4_SUCCESS_FILE: &str = "./pivot_ok4_works"; +/// Path to the file `pivot_ok5` writes on success for tests. +pub const PIVOT_OK5_SUCCESS_FILE: &str = "./pivot_ok5_works"; /// Path to pivot_ok bin for tests. pub const PIVOT_OK_PATH: &str = "../target/debug/pivot_ok"; /// Path to pivot_ok2 bin for tests. pub const PIVOT_OK2_PATH: &str = "../target/debug/pivot_ok2"; /// Path to pivot_ok3 bin for tests. pub const PIVOT_OK3_PATH: &str = "../target/debug/pivot_ok3"; +/// Path to pivot_ok4 bin for tests. +pub const PIVOT_OK4_PATH: &str = "../target/debug/pivot_ok4"; +/// Path to pivot_ok5 bin for tests. +pub const PIVOT_OK5_PATH: &str = "../target/debug/pivot_ok5"; /// Path to pivot loop bin for tests. pub const PIVOT_LOOP_PATH: &str = "../target/debug/pivot_loop"; /// Path to pivot_abort bin for tests. @@ -28,6 +42,9 @@ pub const PIVOT_PANIC_PATH: &str = "../target/debug/pivot_panic"; /// Path to an enclave app that has routes to test remote connection features. pub const PIVOT_REMOTE_TLS_PATH: &str = "../target/debug/pivot_remote_tls"; /// Path to an enclave app that has routes to test remote connection features. +pub const PIVOT_ASYNC_REMOTE_TLS_PATH: &str = + "../target/debug/pivot_async_remote_tls"; +/// Path to an enclave app that has routes to test remote connection features. pub const QOS_NET_PATH: &str = "../target/debug/qos_net"; /// Path to an enclave app that has routes to stress our socket. pub const PIVOT_SOCKET_STRESS_PATH: &str = @@ -44,6 +61,7 @@ pub const QOS_DIST_DIR: &str = "./mock/dist"; pub const PCR3_PRE_IMAGE_PATH: &str = "./mock/namespaces/pcr3-preimage.txt"; const MSG: &str = "msg"; +const POOL_SIZE: &str = "pool-size"; /// Request/Response messages for "socket stress" pivot app. #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq)] @@ -54,9 +72,8 @@ pub enum PivotSocketStressMsg { OkResponse, /// Request the app to panic. Does not have a response. PanicRequest, - /// Request a response that will be slower then - /// `ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS`. - SlowRequest, + /// Request a response that will be slower than the provided `u64` value in milliseconds + SlowRequest(u64), // milliseconds /// Response to [`Self::SlowRequest`]. SlowResponse, } @@ -119,12 +136,39 @@ pub struct AdditionProofPayload { pub result: usize, } +/// Wait for a given usock file to exist and be connectible with a timeout of 5s. +/// +/// # Panics +/// Panics if fs::exists errors. +pub async fn wait_for_usock(path: &str) { + let addr = SocketAddress::new_unix(path); + let pool = AsyncStreamPool::new(addr, 1).unwrap().shared(); + let client = AsyncClient::new(pool, TimeVal::milliseconds(50)); + + for _ in 0..50 { + if std::fs::exists(path).unwrap() && client.try_connect().await.is_ok() + { + break; + } + + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + struct PivotParser; impl GetParserForOptions for PivotParser { fn parser() -> Parser { - Parser::new().token( - Token::new(MSG, "A msg to write").takes_value(true).required(true), - ) + Parser::new() + .token( + Token::new(MSG, "A msg to write") + .takes_value(true) + .required(true), + ) + .token( + Token::new(POOL_SIZE, "App pool size") + .takes_value(true) + .required(false), + ) } } diff --git a/src/integration/tests/async_boot.rs b/src/integration/tests/async_boot.rs new file mode 100644 index 000000000..03be96e4a --- /dev/null +++ b/src/integration/tests/async_boot.rs @@ -0,0 +1,470 @@ +use std::{ + fs, + io::{BufRead, BufReader, Write}, + path::Path, + process::{Command, Stdio}, +}; + +use borsh::de::BorshDeserialize; +use integration::{ + LOCAL_HOST, PCR3_PRE_IMAGE_PATH, PIVOT_OK5_PATH, PIVOT_OK5_SUCCESS_FILE, + QOS_DIST_DIR, +}; +use qos_core::protocol::{ + services::{ + boot::{ + Approval, Manifest, ManifestSet, Namespace, PivotConfig, + RestartPolicy, ShareSet, + }, + genesis::{GenesisMemberOutput, GenesisOutput}, + }, + ProtocolPhase, QosHash, +}; +use qos_crypto::sha_256; +use qos_host::EnclaveInfo; +use qos_p256::P256Pair; +use qos_test_primitives::{ChildWrapper, PathWrapper}; + +#[tokio::test] +async fn async_standard_boot_e2e() { + const PIVOT_HASH_PATH: &str = "/tmp/async_standard_boot_e2e-pivot-hash.txt"; + + let host_port = qos_test_primitives::find_free_port().unwrap(); + let tmp: PathWrapper = "/tmp/async-boot-e2e".into(); + let _: PathWrapper = PIVOT_OK5_SUCCESS_FILE.into(); + let _: PathWrapper = PIVOT_HASH_PATH.into(); + fs::create_dir_all(&*tmp).unwrap(); + + let usock: PathWrapper = "/tmp/async-boot-e2e/boot_e2e.sock".into(); + let secret_path: PathWrapper = "/tmp/async-boot-e2e/boot_e2e.secret".into(); + let pivot_path: PathWrapper = "/tmp/async-boot-e2e/boot_e2e.pivot".into(); + let manifest_path: PathWrapper = + "/tmp/async-boot-e2e/boot_e2e.manifest".into(); + let eph_path: PathWrapper = + "/tmp/async-boot-e2e/ephemeral_key.secret".into(); + + let boot_dir: PathWrapper = "/tmp/async-boot-e2e/boot-dir".into(); + fs::create_dir_all(&*boot_dir).unwrap(); + let attestation_dir: PathWrapper = + "/tmp/async-boot-e2e/attestation-dir".into(); + fs::create_dir_all(&*attestation_dir).unwrap(); + let attestation_doc_path = format!("{}/attestation_doc", &*attestation_dir); + + let all_personal_dir = "./mock/boot-e2e/all-personal-dir"; + + let namespace = "quit-coding-to-vape"; + + let personal_dir = |user: &str| format!("{all_personal_dir}/{user}-dir"); + + let user1 = "user1"; + let user2 = "user2"; + let user3 = "user3"; + + // -- Create pivot-build-fingerprints.txt + let pivot = fs::read(PIVOT_OK5_PATH).unwrap(); + let mock_pivot_hash = sha_256(&pivot); + let pivot_hash = qos_hex::encode_to_vec(&mock_pivot_hash); + std::fs::write(PIVOT_HASH_PATH, pivot_hash).unwrap(); + + // -- CLIENT create manifest. + let msg = "testing420"; + let pivot_args = format!("[--msg,{msg},--pool-size,20]"); + let cli_manifest_path = format!("{}/manifest", &*boot_dir); + + assert!(Command::new("../target/debug/qos_client") + .args([ + "generate-manifest", + "--nonce", + "2", + "--namespace", + namespace, + "--restart-policy", + "never", + "--pivot-hash-path", + PIVOT_HASH_PATH, + "--qos-release-dir", + QOS_DIST_DIR, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--manifest-path", + &cli_manifest_path, + "--pivot-args", + &pivot_args, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--share-set-dir", + "./mock/keys/share-set", + "--patch-set-dir", + "./mock/keys/manifest-set", + "--quorum-key-path", + "./mock/namespaces/quit-coding-to-vape/quorum_key.pub" + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + // Check the manifest written to file + let manifest = + Manifest::try_from_slice(&fs::read(&cli_manifest_path).unwrap()) + .unwrap(); + + let genesis_output = { + let contents = + fs::read("./mock/boot-e2e/genesis-dir/genesis_output").unwrap(); + GenesisOutput::try_from_slice(&contents).unwrap() + }; + // For simplicity sake, we use the same keys for the share set and manifest + // set. + let mut members: Vec<_> = genesis_output + .member_outputs + .iter() + .cloned() + .map(|GenesisMemberOutput { share_set_member, .. }| share_set_member) + .collect(); + members.sort(); + + let namespace_field = Namespace { + name: namespace.to_string(), + nonce: 2, + quorum_key: genesis_output.quorum_key, + }; + assert_eq!(manifest.namespace, namespace_field); + let pivot = PivotConfig { + hash: mock_pivot_hash, + restart: RestartPolicy::Never, + args: vec![ + "--msg".to_string(), + msg.to_string(), + "--pool-size".to_string(), + "20".to_string(), + ], + }; + assert_eq!(manifest.pivot, pivot); + let manifest_set = ManifestSet { threshold: 2, members: members.clone() }; + assert_eq!(manifest.manifest_set, manifest_set); + let share_set = ShareSet { threshold: 2, members }; + assert_eq!(manifest.share_set, share_set); + + // -- CLIENT make sure each user can run `approve-manifest` + for alias in [user1, user2, user3] { + let approval_path = format!( + "{}/{}-{}-{}.approval", + &*boot_dir, alias, namespace, manifest.namespace.nonce, + ); + + let secret_path = format!("{}/{}.secret", &personal_dir(alias), alias); + + let mut child = Command::new("../target/debug/qos_client") + .args([ + "approve-manifest", + "--secret-path", + &*secret_path, + "--manifest-path", + &cli_manifest_path, + "--manifest-approvals-dir", + &*boot_dir, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--pivot-hash-path", + PIVOT_HASH_PATH, + "--qos-release-dir", + QOS_DIST_DIR, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--share-set-dir", + "./mock/keys/share-set", + "--patch-set-dir", + "./mock/keys/manifest-set", + "--quorum-key-path", + "./mock/namespaces/quit-coding-to-vape/quorum_key.pub", + "--alias", + alias, + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = child.stdin.take().expect("Failed to open stdin"); + + let mut stdout = { + let stdout = child.stdout.as_mut().unwrap(); + let stdout_reader = BufReader::new(stdout); + stdout_reader.lines() + }; + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace name: quit-coding-to-vape? (y/n)" + ); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace nonce: 2? (y/n)" + ); + // On purpose, try to input a bad value, neither yes or no + stdin + .write_all("maybe\n".as_bytes()) + .expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Please answer with either \"yes\" (y) or \"no\" (n)" + ); + // Try the longer option ("yes" rather than "y") + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct pivot restart policy: RestartPolicy::Never? (y/n)" + ); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Are these the correct pivot args:" + ); + assert_eq!( + &stdout.next().unwrap().unwrap(), + "[\"--msg\", \"testing420\", \"--pool-size\", \"20\"]?" + ); + assert_eq!(&stdout.next().unwrap().unwrap(), "(y/n)"); + stdin.write_all("y\n".as_bytes()).expect("Failed to write to stdin"); + + // Wait for the command to write the approval and exit + assert!(child.wait().unwrap().success()); + + // Read in the generated approval to check it was created correctly + let approval = + Approval::try_from_slice(&fs::read(approval_path).unwrap()) + .unwrap(); + let personal_pair = P256Pair::from_hex_file(format!( + "{}/{}.secret", + personal_dir(alias), + alias, + )) + .unwrap(); + + let signature = personal_pair.sign(&manifest.qos_hash()).unwrap(); + assert_eq!(approval.signature, signature); + + assert_eq!(approval.member.alias, alias); + assert_eq!( + approval.member.pub_key, + personal_pair.public_key().to_bytes(), + ); + } + + // -- ENCLAVE start enclave + let mut _enclave_child_process: ChildWrapper = + Command::new("../target/debug/qos_core") + .args([ + "--usock", + &*usock, + "--quorum-file", + &*secret_path, + "--pivot-file", + &*pivot_path, + "--ephemeral-file", + &*eph_path, + "--mock", + "--manifest-file", + &*manifest_path, + ]) + .spawn() + .unwrap() + .into(); + + // -- HOST start host + let mut _host_child_process: ChildWrapper = + Command::new("../target/debug/qos_host") + .args([ + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--usock", + &*usock, + ]) + .spawn() + .unwrap() + .into(); + + // -- Make sure the enclave and host have time to boot + qos_test_primitives::wait_until_port_is_bound(host_port); + + // -- CLIENT generate the manifest envelope + assert!(Command::new("../target/debug/qos_client") + .args([ + "generate-manifest-envelope", + "--manifest-approvals-dir", + &*boot_dir, + "--manifest-path", + &cli_manifest_path, + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + // -- CLIENT broadcast boot standard instruction + let manifest_envelope_path = format!("{}/manifest_envelope", &*boot_dir,); + assert!(Command::new("../target/debug/qos_client") + .args([ + "boot-standard", + "--manifest-envelope-path", + &manifest_envelope_path, + "--pivot-path", + PIVOT_OK5_PATH, + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--pcr3-preimage-path", + "./mock/pcr3-preimage.txt", + "--unsafe-skip-attestation", + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + // For each user, post a share, + // and sanity check the pivot has not yet executed. + assert!(!Path::new(PIVOT_OK5_SUCCESS_FILE).exists()); + for user in [&user1, &user2] { + // Get attestation doc and manifest + assert!(Command::new("../target/debug/qos_client") + .args([ + "get-attestation-doc", + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--attestation-doc-path", + &*attestation_doc_path, + "--manifest-envelope-path", + "/tmp/dont_care" + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + + let share_path = format!("{}/{}.share", &personal_dir(user), user); + let secret_path = format!("{}/{}.secret", &personal_dir(user), user); + let eph_wrapped_share_path: PathWrapper = + format!("{}/{}.eph_wrapped.share", &*tmp, user).into(); + let approval_path: PathWrapper = + format!("{}/{}.attestation.approval", &*tmp, user).into(); + // Encrypt share to ephemeral key + let mut child = Command::new("../target/debug/qos_client") + .args([ + "proxy-re-encrypt-share", + "--share-path", + &share_path, + "--secret-path", + &secret_path, + "--attestation-doc-path", + &*attestation_doc_path, + "--eph-wrapped-share-path", + &eph_wrapped_share_path, + "--approval-path", + &approval_path, + "--manifest-envelope-path", + &manifest_envelope_path, + "--pcr3-preimage-path", + PCR3_PRE_IMAGE_PATH, + "--manifest-set-dir", + "./mock/keys/manifest-set", + "--alias", + user, + "--unsafe-skip-attestation", + "--unsafe-eph-path-override", + &*eph_path, + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stdin = child.stdin.take().expect("Failed to open stdin"); + + let mut stdout = { + let stdout = child.stdout.as_mut().unwrap(); + let stdout_reader = BufReader::new(stdout); + stdout_reader.lines() + }; + + // Skip over a log message + stdout.next(); + + // Answer prompts with yes + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace name: quit-coding-to-vape? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Is this the correct namespace nonce: 2? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "Does this AWS IAM role belong to the intended organization: arn:aws:iam::123456789012:role/Webserver? (y/n)" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + assert_eq!( + &stdout.next().unwrap().unwrap(), + "The following manifest set members approved:" + ); + stdin.write_all("yes\n".as_bytes()).expect("Failed to write to stdin"); + + // Check that it finished successfully + assert!(child.wait().unwrap().success()); + + // Post the encrypted share + assert!(Command::new("../target/debug/qos_client") + .args([ + "post-share", + "--host-port", + &host_port.to_string(), + "--host-ip", + LOCAL_HOST, + "--eph-wrapped-share-path", + &eph_wrapped_share_path, + "--approval-path", + &approval_path, + ]) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + } + + // Give the enclave time to start the pivot + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Check that the pivot executed + let contents = std::fs::read(PIVOT_OK5_SUCCESS_FILE).unwrap(); + assert_eq!(std::str::from_utf8(&contents).unwrap(), msg); + + let enclave_info_url = + format!("http://{LOCAL_HOST}:{}/qos/enclave-info", host_port); + let enclave_info: EnclaveInfo = + ureq::get(&enclave_info_url).call().unwrap().into_json().unwrap(); + assert_eq!(enclave_info.phase, ProtocolPhase::QuorumKeyProvisioned); + + fs::remove_file(PIVOT_OK5_SUCCESS_FILE).unwrap(); +} diff --git a/src/integration/tests/async_client.rs b/src/integration/tests/async_client.rs new file mode 100644 index 000000000..7a0564c74 --- /dev/null +++ b/src/integration/tests/async_client.rs @@ -0,0 +1,83 @@ +use qos_core::{ + async_client::AsyncClient, + async_server::SocketServerError, + async_server::{AsyncRequestProcessor, AsyncSocketServer}, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, +}; + +#[derive(Clone)] +struct EchoProcessor; + +impl AsyncRequestProcessor for EchoProcessor { + async fn process(&self, request: Vec) -> Vec { + request + } +} + +async fn run_echo_server( + socket_path: &str, +) -> Result { + let pool = AsyncStreamPool::new(SocketAddress::new_unix(socket_path), 1) + .expect("unable to create async pool"); + let server = AsyncSocketServer::listen_all(pool, &EchoProcessor)?; + + Ok(server) +} + +#[tokio::test] +async fn direct_connect_works() { + let socket_path = "/tmp/async_client_test_direct_connect_works.sock"; + let socket = SocketAddress::new_unix(socket_path); + let timeout = TimeVal::milliseconds(500); + let pool = AsyncStreamPool::new(socket, 1) + .expect("unable to create async pool") + .shared(); + + let client = AsyncClient::new(pool, timeout); + + let server = run_echo_server(socket_path).await.unwrap(); + + let r = client.call(&[0]).await; + assert!(r.is_ok()); + + server.terminate(); +} + +#[tokio::test] +async fn times_out_properly() { + let socket_path = "/tmp/async_client_test_times_out_properly.sock"; + let socket = SocketAddress::new_unix(socket_path); + let timeout = TimeVal::milliseconds(500); + let pool = AsyncStreamPool::new(socket, 1) + .expect("unable to create async pool") + .shared(); + let client = AsyncClient::new(pool, timeout); + + let r = client.call(&[0]).await; + assert!(r.is_err()); +} + +#[tokio::test] +async fn repeat_connect_works() { + let socket_path = "/tmp/async_client_test_repeat_connect_works.sock"; + let socket = SocketAddress::new_unix(socket_path); + let timeout = TimeVal::milliseconds(500); + let pool = AsyncStreamPool::new(socket, 1) + .expect("unable to create async pool") + .shared(); + let client = AsyncClient::new(pool, timeout); + + // server not running yet, expect a connection error + let r = client.call(&[0]).await; + assert!(r.is_err()); + + // start server + let server = run_echo_server(socket_path).await.unwrap(); + + // server running, expect success + let r = client.call(&[0]).await; + assert!(r.is_ok()); + + // cleanup + server.terminate(); +} diff --git a/src/integration/tests/async_qos_host.rs b/src/integration/tests/async_qos_host.rs new file mode 100644 index 000000000..9882b93ee --- /dev/null +++ b/src/integration/tests/async_qos_host.rs @@ -0,0 +1,65 @@ +use std::{process::Command, time::Duration}; + +use integration::PIVOT_OK_PATH; +use qos_test_primitives::{ChildWrapper, PathWrapper}; + +const TEST_ENCLAVE_SOCKET: &str = "/tmp/async_qos_host_test/enclave.sock"; + +#[tokio::test] +async fn connects_and_gets_info() { + // prep sock pool dir + std::fs::create_dir_all("/tmp/async_qos_host_test").unwrap(); + + let _qos_host: ChildWrapper = Command::new("../target/debug/qos_host") + .arg("--usock") + .arg(TEST_ENCLAVE_SOCKET) + .arg("--host-ip") + .arg("127.0.0.1") + .arg("--host-port") + .arg("3323") + .arg("--socket-timeout") + .arg("50") // ms + .spawn() + .unwrap() + .into(); + + tokio::time::sleep(Duration::from_millis(100)).await; // let the qos_host start + + let r = ureq::get("http://127.0.0.1:3323/qos/enclave-info").call(); + assert!(r.is_err()); // expect 500 here + + let enclave_socket = format!("{TEST_ENCLAVE_SOCKET}_0"); // manually pick the 1st one + let secret_path: PathWrapper = "./async_qos_host_test.secret".into(); + // let eph_path = "reaper_works.eph.key"; + let manifest_path: PathWrapper = "async_qos_host_test.manifest".into(); + + // For our sanity, ensure the secret does not yet exist + drop(std::fs::remove_file(&*secret_path)); + // Remove the socket file if it exists as well, in case of bad crashes + drop(std::fs::remove_file(&enclave_socket)); + + let mut _enclave_child_process: ChildWrapper = + Command::new("../target/debug/qos_core") + .args([ + "--usock", + TEST_ENCLAVE_SOCKET, + "--quorum-file", + &*secret_path, + "--pivot-file", + PIVOT_OK_PATH, + "--ephemeral-file", + "eph_path", + "--mock", + "--manifest-file", + &*manifest_path, + ]) + .spawn() + .unwrap() + .into(); + + // Give the enclave server time to bind to the socket + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let r = ureq::get("http://127.0.0.1:3323/qos/enclave-info").call(); + assert!(r.is_ok()); // expect 200 here +} diff --git a/src/integration/tests/remote_tls.rs b/src/integration/tests/async_remote_tls.rs similarity index 54% rename from src/integration/tests/remote_tls.rs rename to src/integration/tests/async_remote_tls.rs index 4e57415b3..e6eca108d 100644 --- a/src/integration/tests/remote_tls.rs +++ b/src/integration/tests/async_remote_tls.rs @@ -1,64 +1,54 @@ -use std::os::unix::net::UnixStream; -use std::time::{Duration, Instant}; -use std::{path::Path, process::Command, str}; +use std::{process::Command, time::Duration}; use borsh::BorshDeserialize; -use integration::{PivotRemoteTlsMsg, PIVOT_REMOTE_TLS_PATH, QOS_NET_PATH}; +use integration::{ + PivotRemoteTlsMsg, PIVOT_ASYNC_REMOTE_TLS_PATH, QOS_NET_PATH, +}; use qos_core::{ - client::Client, - io::{SocketAddress, TimeVal, TimeValLike}, + async_client::AsyncClient, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, protocol::ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, }; use qos_test_primitives::ChildWrapper; -const REMOTE_TLS_TEST_NET_PROXY_SOCKET: &str = "/tmp/remote_tls_test.net.sock"; +const REMOTE_TLS_TEST_NET_PROXY_SOCKET: &str = + "/tmp/async_remote_tls_test.net.sock"; const REMOTE_TLS_TEST_ENCLAVE_SOCKET: &str = - "/tmp/remote_tls_test.enclave.sock"; - -/// Waits for socket at `path` until it becomes ready. -/// If the socket isn't ready after `timeout`, this function panics. -fn wait_for_socket_ready>(path: P, timeout: Duration) { - let start = Instant::now(); - while start.elapsed() < timeout { - match UnixStream::connect(&path) { - Ok(_) => return, // socket is ready - Err(e) => { - // Error while connecting. Retry. - println!( - "[retrying] error while connecting at {}: {}", - path.as_ref().display(), - e - ) - } - } - std::thread::sleep(Duration::from_millis(50)); - } - panic!( - "Unable to connect to {}: timing out after retrying for {} seconds.", - path.as_ref().display(), - timeout.as_secs() - ); -} + "/tmp/async_remote_tls_test.enclave.sock"; +const POOL_SIZE: &str = "1"; -#[test] -fn fetch_remote_tls_content() { +#[tokio::test] +async fn fetch_async_remote_tls_content() { let _net_proxy: ChildWrapper = Command::new(QOS_NET_PATH) .arg("--usock") .arg(REMOTE_TLS_TEST_NET_PROXY_SOCKET) + .arg("--pool-size") + .arg(POOL_SIZE) .spawn() .unwrap() .into(); - let _enclave_app: ChildWrapper = Command::new(PIVOT_REMOTE_TLS_PATH) + let _enclave_app: ChildWrapper = Command::new(PIVOT_ASYNC_REMOTE_TLS_PATH) .arg(REMOTE_TLS_TEST_ENCLAVE_SOCKET) .arg(REMOTE_TLS_TEST_NET_PROXY_SOCKET) .spawn() .unwrap() .into(); - let enclave_client = Client::new( + // ensure the enclave socket is created by qos_net before proceeding + while !std::fs::exists(REMOTE_TLS_TEST_ENCLAVE_SOCKET).unwrap() { + tokio::time::sleep(Duration::from_millis(50)).await; + } + + let enclave_pool = AsyncStreamPool::new( SocketAddress::new_unix(REMOTE_TLS_TEST_ENCLAVE_SOCKET), + 1, + ) + .expect("unable to create enclave async pool"); + + let enclave_client = AsyncClient::new( + enclave_pool.shared(), TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ); @@ -68,16 +58,7 @@ fn fetch_remote_tls_content() { }) .unwrap(); - wait_for_socket_ready( - REMOTE_TLS_TEST_NET_PROXY_SOCKET, - Duration::from_secs(2), - ); - wait_for_socket_ready( - REMOTE_TLS_TEST_ENCLAVE_SOCKET, - Duration::from_secs(2), - ); - - let response = enclave_client.send(&app_request).unwrap(); + let response = enclave_client.call(&app_request).await.unwrap(); let response_text = match PivotRemoteTlsMsg::try_from_slice(&response).unwrap() { PivotRemoteTlsMsg::RemoteTlsResponse(s) => s, @@ -96,7 +77,7 @@ fn fetch_remote_tls_content() { }) .unwrap(); - let response = enclave_client.send(&app_request).unwrap(); + let response = enclave_client.call(&app_request).await.unwrap(); let response_text = match PivotRemoteTlsMsg::try_from_slice(&response).unwrap() { PivotRemoteTlsMsg::RemoteTlsResponse(s) => s, diff --git a/src/integration/tests/boot.rs b/src/integration/tests/boot.rs index 5b9938321..bb32cef29 100644 --- a/src/integration/tests/boot.rs +++ b/src/integration/tests/boot.rs @@ -25,10 +25,10 @@ use qos_host::EnclaveInfo; use qos_p256::P256Pair; use qos_test_primitives::{ChildWrapper, PathWrapper}; -const PIVOT_HASH_PATH: &str = "/tmp/standard_boot_e2e-pivot-hash.txt"; - #[tokio::test] async fn standard_boot_e2e() { + const PIVOT_HASH_PATH: &str = "/tmp/standard_boot_e2e-pivot-hash.txt"; + let host_port = qos_test_primitives::find_free_port().unwrap(); let tmp: PathWrapper = "/tmp/boot-e2e".into(); let _: PathWrapper = PIVOT_OK2_SUCCESS_FILE.into(); diff --git a/src/integration/tests/enclave_app_client_socket_stress.rs b/src/integration/tests/enclave_app_client_socket_stress.rs index aec6c196d..fd5829bcc 100644 --- a/src/integration/tests/enclave_app_client_socket_stress.rs +++ b/src/integration/tests/enclave_app_client_socket_stress.rs @@ -1,9 +1,11 @@ use borsh::BorshDeserialize; -use integration::{PivotSocketStressMsg, PIVOT_SOCKET_STRESS_PATH}; +use integration::{ + wait_for_usock, PivotSocketStressMsg, PIVOT_SOCKET_STRESS_PATH, +}; use qos_core::{ - client::Client, + async_client::AsyncClient, handles::Handles, - io::{SocketAddress, TimeVal, TimeValLike}, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, protocol::{ msg::ProtocolMsg, services::boot::{ @@ -22,8 +24,8 @@ const TEST_TMP: &str = "/tmp/enclave_app_client_socket_stress"; const ENCLAVE_SOCK: &str = "/tmp/enclave_app_client_socket_stress/enclave.sock"; const APP_SOCK: &str = "/tmp/enclave_app_client_socket_stress/app.sock"; -#[test] -fn enclave_app_client_socket_stress() { +#[tokio::test] +async fn enclave_app_client_socket_stress() { let _: PathWrapper = TEST_TMP.into(); std::fs::create_dir_all(TEST_TMP).unwrap(); @@ -73,12 +75,18 @@ fn enclave_app_client_socket_stress() { handles.put_manifest_envelope(&manifest_envelope).unwrap(); handles.put_quorum_key(&p256_pair).unwrap(); + let enclave_pool = + AsyncStreamPool::new(SocketAddress::new_unix(ENCLAVE_SOCK), 1).unwrap(); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix(APP_SOCK), 1).unwrap(); + std::thread::spawn(move || { Reaper::execute( &handles, Box::new(MockNsm), - SocketAddress::new_unix(ENCLAVE_SOCK), - SocketAddress::new_unix(APP_SOCK), + enclave_pool, + app_pool, // Force the phase to quorum key provisioned so message proxy-ing // works Some(ProtocolPhase::QuorumKeyProvisioned), @@ -86,11 +94,13 @@ fn enclave_app_client_socket_stress() { }); // Make sure the pivot has some time to start up - std::thread::sleep(std::time::Duration::from_secs(1)); + wait_for_usock(APP_SOCK).await; - let enclave_client = Client::new( - SocketAddress::new_unix(ENCLAVE_SOCK), - TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS + 1), + let enclave_client_pool = + AsyncStreamPool::new(SocketAddress::new_unix(ENCLAVE_SOCK), 1).unwrap(); + let enclave_client = AsyncClient::new( + enclave_client_pool.shared(), + TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS + 3), // needs to be bigger than the slow request below + some time for recovery ); let app_request = @@ -98,8 +108,9 @@ fn enclave_app_client_socket_stress() { let request = borsh::to_vec(&ProtocolMsg::ProxyRequest { data: app_request }) .unwrap(); - let raw_response = enclave_client.send(&request).unwrap(); + let raw_response = enclave_client.call(&request).await.unwrap(); let response = ProtocolMsg::try_from_slice(&raw_response).unwrap(); + assert_eq!( response, ProtocolMsg::ProtocolErrorResponse( @@ -107,15 +118,16 @@ fn enclave_app_client_socket_stress() { ) ); - std::thread::sleep(std::time::Duration::from_secs( + tokio::time::sleep(std::time::Duration::from_secs( REAPER_RESTART_DELAY_IN_SECONDS + 1, - )); + )) + .await; // The pivot panicked and should have been restarted. let app_request = borsh::to_vec(&PivotSocketStressMsg::OkRequest).unwrap(); let request = borsh::to_vec(&ProtocolMsg::ProxyRequest { data: app_request }) .unwrap(); - let raw_response = enclave_client.send(&request).unwrap(); + let raw_response = enclave_client.call(&request).await.unwrap(); let response = { let msg = ProtocolMsg::try_from_slice(&raw_response).unwrap(); let data = match msg { @@ -128,11 +140,11 @@ fn enclave_app_client_socket_stress() { // Send a request that the app will take too long to respond to let app_request = - borsh::to_vec(&PivotSocketStressMsg::SlowRequest).unwrap(); + borsh::to_vec(&PivotSocketStressMsg::SlowRequest(5500)).unwrap(); let request = borsh::to_vec(&ProtocolMsg::ProxyRequest { data: app_request }) .unwrap(); - let raw_response = enclave_client.send(&request).unwrap(); + let raw_response = enclave_client.call(&request).await.unwrap(); let response = ProtocolMsg::try_from_slice(&raw_response).unwrap(); assert_eq!( response, diff --git a/src/integration/tests/proofs.rs b/src/integration/tests/proofs.rs index 6ddfe105f..67af5a8b4 100644 --- a/src/integration/tests/proofs.rs +++ b/src/integration/tests/proofs.rs @@ -1,10 +1,10 @@ use std::{process::Command, str}; use borsh::BorshDeserialize; -use integration::{PivotProofMsg, PIVOT_PROOF_PATH}; +use integration::{wait_for_usock, PivotProofMsg, PIVOT_PROOF_PATH}; use qos_core::{ - client::Client, - io::{SocketAddress, TimeVal, TimeValLike}, + async_client::AsyncClient, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, protocol::ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, }; @@ -13,23 +13,31 @@ use qos_test_primitives::ChildWrapper; const PROOF_TEST_ENCLAVE_SOCKET: &str = "/tmp/proof_test.enclave.sock"; -#[test] -fn fetch_and_verify_app_proof() { +#[tokio::test] +async fn fetch_and_verify_app_proof() { let _enclave_app: ChildWrapper = Command::new(PIVOT_PROOF_PATH) .arg(PROOF_TEST_ENCLAVE_SOCKET) .spawn() .unwrap() .into(); - let enclave_client = Client::new( + wait_for_usock(PROOF_TEST_ENCLAVE_SOCKET).await; + + let enclave_pool = AsyncStreamPool::new( SocketAddress::new_unix(PROOF_TEST_ENCLAVE_SOCKET), + 1, + ) + .unwrap(); + + let enclave_client = AsyncClient::new( + enclave_pool.shared(), TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ); let app_request = borsh::to_vec(&PivotProofMsg::AdditionRequest { a: 2, b: 2 }).unwrap(); - let response = enclave_client.send(&app_request).unwrap(); + let response = enclave_client.call(&app_request).await.unwrap(); match PivotProofMsg::try_from_slice(&response).unwrap() { PivotProofMsg::AdditionResponse { result, proof } => { diff --git a/src/integration/tests/qos_host.rs b/src/integration/tests/qos_host.rs new file mode 100644 index 000000000..06f3a4310 --- /dev/null +++ b/src/integration/tests/qos_host.rs @@ -0,0 +1,60 @@ +use std::{process::Command, time::Duration}; + +use integration::PIVOT_OK_PATH; +use qos_test_primitives::{ChildWrapper, PathWrapper}; + +const TEST_ENCLAVE_SOCKET: &str = "/tmp/qos_host_test.enclave.sock"; + +#[test] +fn connects_and_gets_info() { + let _qos_host: ChildWrapper = Command::new("../target/debug/qos_host") + .arg("--usock") + .arg(TEST_ENCLAVE_SOCKET) + .arg("--host-ip") + .arg("127.0.0.1") + .arg("--host-port") + .arg("3323") + .arg("--socket-timeout") + .arg("50") // ms + .spawn() + .unwrap() + .into(); + + std::thread::sleep(Duration::from_millis(100)); // let the qos_host start + + let r = ureq::get("http://127.0.0.1:3323/qos/enclave-info").call(); + assert!(r.is_err()); // expect 500 here + + let secret_path: PathWrapper = "/tmp/qos_host_reaper_works.secret".into(); + // let eph_path = "reaper_works.eph.key"; + let manifest_path: PathWrapper = + "/tmp/qos_host_reaper_works.manifest".into(); + + // For our sanity, ensure the secret does not yet exist + drop(std::fs::remove_file(&*secret_path)); + + let mut _enclave_child_process: ChildWrapper = + Command::new("../target/debug/qos_core") + .args([ + "--usock", + TEST_ENCLAVE_SOCKET, + "--quorum-file", + &*secret_path, + "--pivot-file", + PIVOT_OK_PATH, + "--ephemeral-file", + "eph_path", + "--mock", + "--manifest-file", + &*manifest_path, + ]) + .spawn() + .unwrap() + .into(); + + // Give the enclave server time to bind to the socket + std::thread::sleep(std::time::Duration::from_millis(500)); + + let r = ureq::get("http://127.0.0.1:3323/qos/enclave-info").call(); + assert!(r.is_ok()); // expect 200 here +} diff --git a/src/integration/tests/reaper.rs b/src/integration/tests/reaper.rs index 420258925..cf9759d56 100644 --- a/src/integration/tests/reaper.rs +++ b/src/integration/tests/reaper.rs @@ -3,7 +3,7 @@ use std::fs; use integration::{PIVOT_ABORT_PATH, PIVOT_OK_PATH, PIVOT_PANIC_PATH}; use qos_core::{ handles::Handles, - io::SocketAddress, + io::{AsyncStreamPool, SocketAddress}, protocol::services::boot::ManifestEnvelope, reaper::{Reaper, REAPER_EXIT_DELAY_IN_SECONDS}, }; @@ -12,10 +12,10 @@ use qos_test_primitives::PathWrapper; #[test] fn reaper_works() { - let secret_path: PathWrapper = "./reaper_works.secret".into(); + let secret_path: PathWrapper = "/tmp/reaper_works.secret".into(); // let eph_path = "reaper_works.eph.key"; - let usock: PathWrapper = "./reaper_works/reaper_works.sock".into(); - let manifest_path: PathWrapper = "reaper_works.manifest".into(); + let usock: PathWrapper = "/tmp/reaper_works.sock".into(); + let manifest_path: PathWrapper = "/tmp/reaper_works.manifest".into(); let msg = "durp-a-durp"; // For our sanity, ensure the secret does not yet exist @@ -37,12 +37,19 @@ fn reaper_works() { handles.put_manifest_envelope(&manifest_envelope).unwrap(); assert!(handles.pivot_exists()); + let enclave_pool = + AsyncStreamPool::new(SocketAddress::new_unix(&usock), 1).unwrap(); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix("./never.sock"), 1) + .unwrap(); + let reaper_handle = std::thread::spawn(move || { Reaper::execute( &handles, Box::new(MockNsm), - SocketAddress::new_unix(&usock), - SocketAddress::new_unix("./never.sock"), + enclave_pool, + app_pool, None, ) }); @@ -68,10 +75,10 @@ fn reaper_works() { #[test] fn reaper_handles_non_zero_exits() { let secret_path: PathWrapper = - "./reaper_handles_non_zero_exits.secret".into(); - let usock: PathWrapper = "./reaper_handles_non_zero_exits.sock".into(); + "/tmp/reaper_handles_non_zero_exits.secret".into(); + let usock: PathWrapper = "/tmp/reaper_handles_non_zero_exits.sock".into(); let manifest_path: PathWrapper = - "./reaper_handles_non_zero_exits.manifest".into(); + "/tmp/reaper_handles_non_zero_exits.manifest".into(); // For our sanity, ensure the secret does not yet exist drop(fs::remove_file(&*secret_path)); @@ -88,12 +95,19 @@ fn reaper_handles_non_zero_exits() { handles.put_manifest_envelope(&Default::default()).unwrap(); assert!(handles.pivot_exists()); + let enclave_pool = + AsyncStreamPool::new(SocketAddress::new_unix(&usock), 1).unwrap(); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix("./never.sock"), 1) + .unwrap(); + let reaper_handle = std::thread::spawn(move || { Reaper::execute( &handles, Box::new(MockNsm), - SocketAddress::new_unix(&usock), - SocketAddress::new_unix("./never.sock"), + enclave_pool, + app_pool, None, ) }); @@ -120,9 +134,10 @@ fn reaper_handles_non_zero_exits() { #[test] fn reaper_handles_panic() { - let secret_path: PathWrapper = "./reaper_handles_panics.secret".into(); - let usock: PathWrapper = "./reaper_handles_panics.sock".into(); - let manifest_path: PathWrapper = "./reaper_handles_panics.manifest".into(); + let secret_path: PathWrapper = "/tmp/reaper_handles_panics.secret".into(); + let usock: PathWrapper = "/tmp/reaper_handles_panics.sock".into(); + let manifest_path: PathWrapper = + "/tmp/reaper_handles_panics.manifest".into(); // For our sanity, ensure the secret does not yet exist drop(fs::remove_file(&*secret_path)); @@ -139,12 +154,19 @@ fn reaper_handles_panic() { handles.put_manifest_envelope(&Default::default()).unwrap(); assert!(handles.pivot_exists()); + let enclave_pool = + AsyncStreamPool::new(SocketAddress::new_unix(&usock), 1).unwrap(); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix("./never.sock"), 1) + .unwrap(); + let reaper_handle = std::thread::spawn(move || { Reaper::execute( &handles, Box::new(MockNsm), - SocketAddress::new_unix(&usock), - SocketAddress::new_unix("./never.sock"), + enclave_pool, + app_pool, None, ) }); diff --git a/src/integration/tests/simple_socket_stress.rs b/src/integration/tests/simple_socket_stress.rs index 8d2e6f166..720e0bec6 100644 --- a/src/integration/tests/simple_socket_stress.rs +++ b/src/integration/tests/simple_socket_stress.rs @@ -1,55 +1,59 @@ use std::process::Command; -use integration::{PivotSocketStressMsg, PIVOT_SOCKET_STRESS_PATH}; +use integration::{ + wait_for_usock, PivotSocketStressMsg, PIVOT_SOCKET_STRESS_PATH, +}; use qos_core::{ - client::{Client, ClientError}, - io::{SocketAddress, TimeVal, TimeValLike}, + async_client::{AsyncClient, ClientError}, + io::{AsyncStreamPool, IOError, SocketAddress, TimeVal, TimeValLike}, protocol::ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, }; use qos_test_primitives::ChildWrapper; const SOCKET_STRESS_SOCK: &str = "/tmp/simple_socket_stress.sock"; -#[test] -fn simple_socket_stress() { +#[tokio::test] +async fn simple_socket_stress() { let _enclave_app: ChildWrapper = Command::new(PIVOT_SOCKET_STRESS_PATH) .arg(SOCKET_STRESS_SOCK) .spawn() .unwrap() .into(); - let enclave_client = Client::new( - SocketAddress::new_unix(SOCKET_STRESS_SOCK), - // The timeout of `PivotSocketStressMsg::SlowResponse` is relative to - // `ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS`. - TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), - ); + wait_for_usock(SOCKET_STRESS_SOCK).await; + + // needs to be long enough for process exit to register and not cause a timeout + let timeout = TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS); + + let app_pool = + AsyncStreamPool::new(SocketAddress::new_unix(SOCKET_STRESS_SOCK), 1) + .unwrap(); + + let enclave_client = AsyncClient::new(app_pool.shared(), timeout); let app_request = - borsh::to_vec(&PivotSocketStressMsg::SlowRequest).unwrap(); - let err = enclave_client.send(&app_request).unwrap_err(); + borsh::to_vec(&PivotSocketStressMsg::SlowRequest(5500)).unwrap(); + let err = enclave_client.call(&app_request).await.unwrap_err(); match err { ClientError::IOError(qos_core::io::IOError::RecvTimeout) => (), - e => panic!("did not get expected err {:?}", e), + e => panic!("slow pivot did not get expected err {:?}", e), }; let app_request = borsh::to_vec(&PivotSocketStressMsg::PanicRequest).unwrap(); - let err = enclave_client.send(&app_request).unwrap_err(); + let err = enclave_client.call(&app_request).await.unwrap_err(); match err { ClientError::IOError(qos_core::io::IOError::RecvConnectionClosed) => (), - e => panic!("did not get expected err {:?}", e), + e => panic!("panicing pivot did not get expected err {:?}", e), }; - std::thread::sleep(std::time::Duration::from_secs(1)); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; // The app has panic'ed and exited - so any proceeding request should fail. let app_request = borsh::to_vec(&PivotSocketStressMsg::OkRequest).unwrap(); - let err = enclave_client.send(&app_request).unwrap_err(); + let err = enclave_client.call(&app_request).await.unwrap_err(); match err { - ClientError::IOError(qos_core::io::IOError::ConnectNixError( - nix::Error::ENOENT, - )) => (), + ClientError::IOError(IOError::StdIoError(_)) => (), // for usock this is probably "no such file or directoy", vsock would differ e => panic!("did not get expected err {:?}", e), }; } diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 4fcbd4cf7..4127df37f 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -10,7 +10,7 @@ qos_hex = { path = "../qos_hex", features = ["serde"] } qos_p256 = { path = "../qos_p256" } qos_nsm = { path = "../qos_nsm", default-features = false } -nix = { version = "0.26", features = ["socket"], default-features = false } +nix = { version = "0.29", features = ["socket"], default-features = false } libc = "=0.2.172" borsh = { version = "1.0", features = ["std", "derive"] , default-features = false} vsss-rs = { version = "5.1", default-features = false, features = ["std", "zeroize"] } @@ -21,6 +21,10 @@ aws-nitro-enclaves-nsm-api = { version = "0.4", default-features = false } serde_bytes = { version = "0.11", default-features = false } serde = { version = "1", features = ["derive"], default-features = false } +futures = { version = "0.3.30" } +tokio = { version = "1.38.0", features = ["io-util", "macros", "net", "rt-multi-thread", "time", "signal"], default-features = false } +tokio-vsock = { version = "0.7.1" } + [dev-dependencies] qos_test_primitives = { path = "../qos_test_primitives" } qos_p256 = { path = "../qos_p256", features = ["mock"] } @@ -33,3 +37,7 @@ webpki-roots = { version = "0.26.1" } vm = [] # Never use in production - support for mock NSM mock = ["qos_nsm/mock"] + +[[bin]] +name = "qos_core" +path = "src/main.rs" diff --git a/src/qos_core/src/async_client.rs b/src/qos_core/src/async_client.rs new file mode 100644 index 000000000..d76889fe2 --- /dev/null +++ b/src/qos_core/src/async_client.rs @@ -0,0 +1,105 @@ +//! Streaming socket based client to connect with +//! [`crate::server::SocketServer`]. + +use std::time::Duration; + +use nix::sys::time::TimeVal; + +use crate::io::{IOError, SharedAsyncStreamPool}; + +/// Enclave client error. +#[derive(Debug)] +pub enum ClientError { + /// [`io::IOError`] wrapper. + IOError(IOError), + /// `borsh::io::Error` wrapper. + BorshError(borsh::io::Error), +} + +impl From for ClientError { + fn from(err: IOError) -> Self { + Self::IOError(err) + } +} + +impl From for ClientError { + fn from(err: borsh::io::Error) -> Self { + Self::BorshError(err) + } +} +/// Client for communicating with the enclave `crate::server::SocketServer`. +#[derive(Clone, Debug)] +pub struct AsyncClient { + pool: SharedAsyncStreamPool, + timeout: Duration, +} + +impl AsyncClient { + /// Create a new client. + #[must_use] + pub fn new(pool: SharedAsyncStreamPool, timeout: TimeVal) -> Self { + let timeout = timeval_to_duration(timeout); + Self { pool, timeout } + } + + /// Send raw bytes and wait for a response until the clients configured + /// timeout. + /// + /// # Panics + /// Does not. See comment bellow. + pub async fn call(&self, request: &[u8]) -> Result, ClientError> { + let pool = self.pool.read().await; + + // hold the stream if we got it before timeout, but errored out on timeout later + let mut maybe_stream = None; + + // timeout should apply to the entire operation + let timeout_result = tokio::time::timeout(self.timeout, async { + maybe_stream = Some(pool.get().await); + + maybe_stream + .as_deref_mut() + .expect("unreachable unwrap") // this can't happen, we just assigned it above + .call(request) + .await + }) + .await; + + let resp = match timeout_result { + Ok(result) => result?, + Err(_err) => { + // ensure we clean up the stream if we had it + if let Some(mut stream) = maybe_stream { + stream.reset(); + } + return Err(IOError::RecvTimeout.into()); + } + }; + + Ok(resp) + } + + /// Expands the underlaying `AsyncPool` to given `pool_size` + pub async fn expand_to( + &mut self, + pool_size: u32, + ) -> Result<(), ClientError> { + self.pool.write().await.expand_to(pool_size)?; + + Ok(()) + } + + /// Attempt a one-off connection, used for tests + pub async fn try_connect(&self) -> Result<(), IOError> { + let pool = self.pool.read().await; + let mut stream = pool.get().await; + + stream.connect().await + } +} + +fn timeval_to_duration(timeval: TimeVal) -> Duration { + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + Duration::new(timeval.tv_sec() as u64, timeval.tv_usec() as u32 * 1000) +} diff --git a/src/qos_core/src/async_server.rs b/src/qos_core/src/async_server.rs new file mode 100644 index 000000000..81831c935 --- /dev/null +++ b/src/qos_core/src/async_server.rs @@ -0,0 +1,148 @@ +//! Streaming socket based server for use in an enclave. Listens for connections +//! from [`crate::client::Client`]. + +use tokio::task::JoinHandle; + +use crate::io::{AsyncListener, AsyncStreamPool, IOError}; + +/// Error variants for [`SocketServer`] +#[derive(Debug)] +pub enum SocketServerError { + /// `io::IOError` wrapper. + IOError(IOError), +} + +impl From for SocketServerError { + fn from(err: IOError) -> Self { + Self::IOError(err) + } +} + +/// Something that can process requests in an async way. +pub trait AsyncRequestProcessor: Send { + /// Process an incoming request and return a response in async. + /// + /// The request and response are raw bytes. Likely this should be encoded + /// data and logic inside of this function should take care of decoding the + /// request and encoding a response. + fn process( + &self, + request: Vec, + ) -> impl std::future::Future> + Send; +} + +/// A bare bones, socket based server. +pub struct AsyncSocketServer { + /// `AsyncStreamPool` used to serve messages over. + pub pool: AsyncStreamPool, + /// List of tasks that are running on the server. + pub tasks: Vec>>, +} + +impl AsyncSocketServer { + /// Listen and respond to incoming requests on all the pool's addresses with the given `processor`. + /// This method returns a list of tasks that are running as part of this listener. `JoinHandle::abort()` + /// should be called on each when the program exists (e.g. on ctrl+c) + pub fn listen_all

( + pool: AsyncStreamPool, + processor: &P, + ) -> Result + where + P: AsyncRequestProcessor + 'static + Clone, + { + println!("`AsyncSocketServer` listening on pool size {}", pool.len()); + + let listeners = pool.listen()?; + let tasks = Self::spawn_tasks_for_listeners(listeners, processor); + + Ok(Self { pool, tasks }) + } + + fn spawn_tasks_for_listeners

( + listeners: Vec, + processor: &P, + ) -> Vec>> + where + P: AsyncRequestProcessor + 'static + Clone, + { + let mut tasks = Vec::new(); + for listener in listeners { + let p = processor.clone(); + let task = + tokio::spawn(async move { accept_loop(listener, p).await }); + + tasks.push(task); + } + + tasks + } + + /// Expand the server with listeners up to pool size. This adds new tasks as needed. + pub fn listen_to

( + &mut self, + pool_size: u32, + processor: &P, + ) -> Result<(), IOError> + where + P: AsyncRequestProcessor + 'static + Clone, + { + let listeners = self.pool.listen_to(pool_size)?; + let tasks = Self::spawn_tasks_for_listeners(listeners, processor); + + self.tasks.extend(tasks); + + Ok(()) + } + + /// Consume the socket server and terminate all running tasks. + pub fn terminate(self) { + for task in self.tasks { + task.abort(); + } + } +} + +async fn accept_loop

( + listener: AsyncListener, + processor: P, +) -> Result<(), SocketServerError> +where + P: AsyncRequestProcessor + Clone, +{ + loop { + eprintln!("AsyncServer: accepting"); + let mut stream = listener.accept().await?; + loop { + match stream.recv().await { + Ok(payload) => { + let response = processor.process(payload).await; + match stream.send(&response).await { + Ok(()) => {} + Err(err) => { + eprintln!( + "AsyncServer: error sending reply {err:?}, re-accepting" + ); + break; + } + } + } + Err(err) => { + if let IOError::StdIoError(err) = err { + eprintln!("AsyncServer: io error {err:?}"); + if err.kind() == std::io::ErrorKind::UnexpectedEof { + eprintln!( + "AsyncServer: unexpected eof, re-accepting" + ); + break; + } + } else { + eprintln!( + "AsyncServer: unknown error {err:?}, re-accepting" + ); + break; + } + } + } + } + } +} diff --git a/src/qos_core/src/cli.rs b/src/qos_core/src/cli.rs index e4526e5ce..8c96d8fd6 100644 --- a/src/qos_core/src/cli.rs +++ b/src/qos_core/src/cli.rs @@ -12,6 +12,8 @@ use crate::{ EPHEMERAL_KEY_FILE, MANIFEST_FILE, PIVOT_FILE, QUORUM_FILE, SEC_APP_SOCK, }; +use crate::io::{AsyncStreamPool, IOError}; + /// "cid" pub const CID: &str = "cid"; /// "port" @@ -28,6 +30,8 @@ pub const EPHEMERAL_FILE_OPT: &str = "ephemeral-file"; /// Name for the option to specify the manifest file. pub const MANIFEST_FILE_OPT: &str = "manifest-file"; const APP_USOCK: &str = "app-usock"; +/// Name for the option to specify the maximum `AsyncPool` size. +pub const POOL_SIZE: &str = "pool-size"; /// CLI options for starting up the enclave server. #[derive(Default, Clone, Debug, PartialEq)] @@ -44,11 +48,40 @@ impl EnclaveOpts { Self { parsed } } + /// Create a new [`AsyncPool`] of [`AsyncStream`] using the list of [`SocketAddress`] for the enclave server and + /// return the new [`AsyncPool`]. Analogous to [`Self::addr`] and [`Self::app_addr`] depending on the [`app`] parameter. + fn async_pool(&self, app: bool) -> Result { + let usock_param = if app { APP_USOCK } else { USOCK }; + + match ( + self.parsed.single(CID), + self.parsed.single(PORT), + self.parsed.single(usock_param), + ) { + #[cfg(feature = "vm")] + (Some(c), Some(p), None) => { + let c = + c.parse().map_err(|_| IOError::ConnectAddressInvalid)?; + let p = + p.parse().map_err(|_| IOError::ConnectAddressInvalid)?; + AsyncStreamPool::new( + SocketAddress::new_vsock(c, p, crate::io::VMADDR_NO_FLAGS), + 1, + ) + } + (None, None, Some(u)) => { + AsyncStreamPool::new(SocketAddress::new_unix(u), 1) + } + _ => panic!("Invalid socket opts"), + } + } + /// Get the `SocketAddress` for the enclave server. /// /// # Panics /// /// Panics if the opts are not valid for exactly one of unix or vsock. + #[allow(unused)] fn addr(&self) -> SocketAddress { match ( self.parsed.single(CID), @@ -66,6 +99,7 @@ impl EnclaveOpts { } } + #[allow(unused)] fn app_addr(&self) -> SocketAddress { SocketAddress::new_unix( self.parsed @@ -125,8 +159,11 @@ impl EnclaveOpts { /// Enclave server CLI. pub struct CLI; impl CLI { - /// Execute the enclave server CLI with the environment args. - pub fn execute() { + /// Execute the enclave server CLI with the environment args using tokio/async + /// + /// # Panics + /// If the socket pools cannot be created + pub async fn execute() { let mut args: Vec = env::args().collect(); let opts = EnclaveOpts::new(&mut args); @@ -135,18 +172,26 @@ impl CLI { } else if opts.parsed.help() { println!("{}", opts.parsed.info()); } else { - Reaper::execute( - &Handles::new( - opts.ephemeral_file(), - opts.quorum_file(), - opts.manifest_file(), - opts.pivot_file(), - ), - opts.nsm(), - opts.addr(), - opts.app_addr(), - None, - ); + // start reaper in a thread so we can terminate on ctrl+c properly + std::thread::spawn(move || { + Reaper::execute( + &Handles::new( + opts.ephemeral_file(), + opts.quorum_file(), + opts.manifest_file(), + opts.pivot_file(), + ), + opts.nsm(), + opts.async_pool(false) + .expect("Unable to create enclave socket pool"), + opts.async_pool(true) + .expect("Unable to create enclave app pool"), + None, + ); + }); + + eprintln!("qos_core: Reaper running, press ctrl+c to quit"); + let _ = tokio::signal::ctrl_c().await; } } } diff --git a/src/qos_core/src/client.rs b/src/qos_core/src/client.rs deleted file mode 100644 index 855dc2652..000000000 --- a/src/qos_core/src/client.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Streaming socket based client to connect with -//! [`crate::server::SocketServer`]. - -use crate::io::{self, SocketAddress, Stream, TimeVal}; - -/// Enclave client error. -#[derive(Debug)] -pub enum ClientError { - /// [`io::IOError`] wrapper. - IOError(io::IOError), - /// `borsh::io::Error` wrapper. - BorshError(borsh::io::Error), -} - -impl From for ClientError { - fn from(err: io::IOError) -> Self { - Self::IOError(err) - } -} - -impl From for ClientError { - fn from(err: borsh::io::Error) -> Self { - Self::BorshError(err) - } -} - -/// Client for communicating with the enclave [`crate::server::SocketServer`]. -#[derive(Debug, Clone)] -pub struct Client { - addr: SocketAddress, - timeout: TimeVal, -} - -impl Client { - /// Create a new client. - #[must_use] - pub fn new(addr: SocketAddress, timeout: TimeVal) -> Self { - Self { addr, timeout } - } - - /// Send raw bytes and wait for a response until the clients configured - /// timeout. - pub fn send(&self, request: &[u8]) -> Result, ClientError> { - let stream = Stream::connect(&self.addr, self.timeout)?; - stream.send(request)?; - stream.recv().map_err(Into::into) - } -} diff --git a/src/qos_core/src/io/async_pool.rs b/src/qos_core/src/io/async_pool.rs new file mode 100644 index 000000000..879f489e9 --- /dev/null +++ b/src/qos_core/src/io/async_pool.rs @@ -0,0 +1,301 @@ +use std::{path::Path, sync::Arc}; + +use nix::sys::socket::UnixAddr; +use tokio::sync::{Mutex, MutexGuard, RwLock}; + +use super::{AsyncListener, AsyncStream, IOError, SocketAddress}; + +/// Socket Pool Errors +#[derive(Debug)] +pub enum PoolError { + /// No addresses were provided in the pool constructor + NoAddressesSpecified, + /// Invalid source address specified for `next_address` call, usually due to `path` missing in `UnixSock`. + InvalidSourceAddress, +} + +/// Generic Async pool using tokio Mutex +#[derive(Debug)] +struct AsyncPool { + handles: Vec>, +} + +/// Specialization of `AsyncPool` with `AsyncStream` and connection/liste logic. +#[derive(Debug)] +pub struct AsyncStreamPool { + addresses: Vec, // local copy used for `listen` only TODO: refactor listeners out of pool + pool: AsyncPool, +} + +/// Helper type to wrap `AsyncStreamPool` in `Arc` and `RwLock`. Used to allow multiple processors to run across IO +/// await points without locking the whole set. +pub type SharedAsyncStreamPool = Arc>; + +impl AsyncStreamPool { + /// Create a new `AsyncStreamPool` with given starting `SocketAddress`, timout and number of addresses to populate. + pub fn new( + start_address: SocketAddress, + mut count: u32, + ) -> Result { + eprintln!( + "AsyncStreamPool start address: {:?}", + start_address.debug_info() + ); + + let mut addresses = Vec::new(); + let mut addr = start_address; + while count > 0 { + addresses.push(addr.clone()); + count -= 1; + + if count == 0 { + break; // early break to prevent needless address creation + } + addr = addr.next_address()?; + } + + Ok(Self::with_addresses(addresses)) + } + + /// Create a new `AsyncStreamPool` which will contain all the provided addresses but no connections yet. + #[must_use] + fn with_addresses( + addresses: impl IntoIterator, + ) -> Self { + let addresses: Vec = addresses.into_iter().collect(); + + let streams: Vec = + addresses.iter().map(AsyncStream::new).collect(); + + let pool = AsyncPool::from(streams); + + Self { addresses, pool } + } + + /// Helper function to get the Arc and Mutex wrapping + #[must_use] + pub fn shared(self) -> SharedAsyncStreamPool { + Arc::new(RwLock::new(self)) + } + + /// Returns number of expected sockets/connections + #[must_use] + pub fn len(&self) -> usize { + self.addresses.len() + } + + /// Returns true if pool is empty + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Gets the next available `AsyncStream` behind a `MutexGuard` + pub async fn get(&self) -> MutexGuard { + self.pool.get().await + } + + /// Create a new pool by listening new connection on all the addresses + pub fn listen(&self) -> Result, IOError> { + let mut listeners = Vec::new(); + + for addr in &self.addresses { + let listener = AsyncListener::listen(addr)?; + + listeners.push(listener); + } + + Ok(listeners) + } + + /// Expands the pool with new addresses using `SocketAddress::next_address` + pub fn expand_to(&mut self, size: u32) -> Result<(), IOError> { + eprintln!("expanding async pool to {size}"); + let size = size as usize; + + if let Some(last_address) = self.addresses.last().cloned() { + let mut next = last_address; + let count = self.addresses.len(); + for _ in count..size { + next = next.next_address()?; + + self.pool.push(AsyncStream::new(&next)); + self.addresses.push(next.clone()); + } + } + + Ok(()) + } + + /// Listen to new connections on added sockets on top of existing listeners, returning the list of new `AsyncListener` + pub fn listen_to( + &mut self, + size: u32, + ) -> Result, IOError> { + eprintln!("listening async pool to {size}"); + let size = size as usize; + let mut listeners = Vec::new(); + + if let Some(last_address) = self.addresses.last().cloned() { + let mut next = last_address; + let count = self.addresses.len(); + for _ in count..size { + next = next.next_address()?; + eprintln!("adding listener on {}", next.debug_info()); + + self.addresses.push(next.clone()); + let listener = AsyncListener::listen(&next)?; + + listeners.push(listener); + } + } + + Ok(listeners) + } +} + +impl AsyncPool { + /// Get a `AsyncStream` behind a `MutexGuard` for use in a `AsyncStream::call` + /// Will wait (async) if all connections are locked until one becomes available + async fn get(&self) -> MutexGuard { + // TODO: make this into an error + assert!( + !self.handles.is_empty(), + "empty handles in AsyncPool. Bad init?" + ); + + let iter = self.handles.iter().map(|h| { + let l = h.lock(); + Box::pin(l) + }); + + // find a unlock stream + let (stream, _, _) = futures::future::select_all(iter).await; + + stream + } + + fn push(&mut self, value: T) { + self.handles.push(Mutex::new(value)); + } +} + +impl From> for AsyncPool { + fn from(value: Vec) -> Self { + let handles: Vec> = + value.into_iter().map(|val| Mutex::new(val)).collect(); + + Self { handles } + } +} + +/// Provide the "next" usock path. Given a `"*_X"` where X is a number, this function +/// will return `"*_X+1"`. If there is no `"_X"` suffix a `"_0"` will be appended instead. +fn next_usock_path(path: &Path) -> Result { + let path = + path.as_os_str().to_str().ok_or(IOError::ConnectAddressInvalid)?; + if let Some(underscore_index) = path.rfind('_') { + let num_str = &path[underscore_index + 1..]; + let num = num_str.parse::(); + Ok(match num { + Ok(index) => { + format!("{}_{}", &path[0..underscore_index], index + 1) + } + Err(_) => format!("{path}_0"), // non-numerical _X, just add _0 + }) + } else { + Ok(format!("{path}_0")) + } +} + +impl SocketAddress { + /// Creates and returns the "following" `SocketAddress`. In case of VSOCK we increment the port from the source by 1. + /// In case of USOCK we increment the postfix of the path if present, or add a `"_0"` at the end. + /// + /// This is mostly used by the `AsyncSocketPool`. + pub(crate) fn next_address(&self) -> Result { + match self { + Self::Unix(usock) => match usock.path() { + Some(path) => { + let path: &str = &next_usock_path(path)?; + let addr = UnixAddr::new(path)?; + Ok(Self::Unix(addr)) + } + None => { + Err(IOError::PoolError(PoolError::InvalidSourceAddress)) + } + }, + #[cfg(feature = "vm")] + Self::Vsock(vsock) => Ok(Self::new_vsock( + vsock.cid(), + vsock.port() + 1, + super::stream::vsock_svm_flags(*vsock), + )), + } + } +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use super::*; + + // constructor for basic i32 with repeating 0 values for testing + impl AsyncPool { + fn test(count: usize) -> Self { + Self { + handles: std::iter::repeat(0) + .take(count) + .map(Mutex::new) + .collect(), + } + } + } + + // tests if basic pool works with still-available connections + #[tokio::test] + async fn test_async_pool_available() { + let pool = AsyncPool::test(2); + + let first = pool.get().await; + assert_eq!(*first, 0); + let second = pool.get().await; + assert_eq!(*second, 0); + + // this would hang (wait) if we didn't drop one of the previous ones + drop(first); + let third = pool.get().await; + assert_eq!(*third, 0); + } + + #[test] + fn next_usock_path_works() { + assert_eq!( + next_usock_path(&PathBuf::from("basic")).unwrap(), + "basic_0" + ); + assert_eq!(next_usock_path(&PathBuf::from("")).unwrap(), "_0"); + assert_eq!( + next_usock_path(&PathBuf::from("with_underscore_elsewhere")) + .unwrap(), + "with_underscore_elsewhere_0" + ); + assert_eq!( + next_usock_path(&PathBuf::from("with_underscore_at_end_")).unwrap(), + "with_underscore_at_end__0" + ); + assert_eq!( + next_usock_path(&PathBuf::from("good_num_2")).unwrap(), + "good_num_3" + ); + assert_eq!( + next_usock_path(&PathBuf::from("good_num_34")).unwrap(), + "good_num_35" + ); + assert_eq!( + next_usock_path(&PathBuf::from("good_num_999")).unwrap(), + "good_num_1000" + ); + } +} diff --git a/src/qos_core/src/io/async_stream.rs b/src/qos_core/src/io/async_stream.rs new file mode 100644 index 000000000..7416319bc --- /dev/null +++ b/src/qos_core/src/io/async_stream.rs @@ -0,0 +1,342 @@ +//! Abstractions to handle connection based socket streams. + +use std::{io::ErrorKind, pin::Pin}; + +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + net::{UnixListener, UnixSocket, UnixStream}, +}; +#[cfg(feature = "vm")] +use tokio_vsock::{VsockListener, VsockStream}; + +use super::{IOError, SocketAddress}; + +#[derive(Debug)] +enum InnerListener { + Unix(UnixListener), + #[cfg(feature = "vm")] + Vsock(VsockListener), +} + +#[derive(Debug)] +enum InnerStream { + Unix(UnixStream), + #[cfg(feature = "vm")] + Vsock(VsockStream), +} + +/// Handle on a stream +#[derive(Debug)] +pub struct AsyncStream { + address: Option, + inner: Option, +} + +impl AsyncStream { + // accept a new connection, used by server side + fn unix_accepted(stream: UnixStream) -> Self { + Self { address: None, inner: Some(InnerStream::Unix(stream)) } + } + + // accept a new connection, used by server side + #[cfg(feature = "vm")] + fn vsock_accepted(stream: VsockStream) -> Self { + Self { address: None, inner: Some(InnerStream::Vsock(stream)) } + } + + /// Create a new `AsyncStream` with known `SocketAddress` and `TimeVal`. The stream starts disconnected + /// and will connect on the first `call`. + #[must_use] + pub fn new(address: &SocketAddress) -> Self { + Self { address: Some(address.clone()), inner: None } + } + + /// Create a new `Stream` from a `SocketAddress` and a timeout and connect using async + /// Sets `inner` to the new stream. + pub async fn connect(&mut self) -> Result<(), IOError> { + let addr = self.address()?.clone(); + + match self.address()? { + SocketAddress::Unix(_uaddr) => { + let inner = unix_connect(addr).await?; + + self.inner = Some(InnerStream::Unix(inner)); + } + #[cfg(feature = "vm")] + SocketAddress::Vsock(_vaddr) => { + let inner = vsock_connect(addr).await?; + + self.inner = Some(InnerStream::Vsock(inner)); + } + } + + Ok(()) + } + + /// Reconnects this `AsyncStream` by calling `connect` again on the underlaying socket + pub async fn reconnect(&mut self) -> Result<(), IOError> { + let addr = self.address()?.clone(); + + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => { + *s = unix_connect(addr).await?; + } + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => { + *s = vsock_connect(addr).await?; + } + } + Ok(()) + } + + /// Sends a buffer over the underlying socket using async + pub async fn send(&mut self, buf: &[u8]) -> Result<(), IOError> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => send(s, buf).await, + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => send(s, buf).await, + } + } + + /// Receive from the underlying socket using async + pub async fn recv(&mut self) -> Result, IOError> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => recv(s).await, + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => recv(s).await, + } + } + + /// Perform a "call" by sending the `req_buf` bytes and waiting for reply on the same socket. + pub async fn call(&mut self, req_buf: &[u8]) -> Result, IOError> { + // first time? connect + if self.inner.is_none() { + self.connect().await?; + } + + let send_result = self.send(req_buf).await; + if send_result.is_err() { + self.reset(); + send_result?; + } + + let result = self.recv().await; + eprintln!("AsyncStream: received"); + if result.is_err() { + self.reset(); + } + + result + } + + fn address(&self) -> Result<&SocketAddress, IOError> { + self.address.as_ref().ok_or(IOError::ConnectAddressInvalid) + } + + fn inner_mut(&mut self) -> Result<&mut InnerStream, IOError> { + self.inner.as_mut().ok_or(IOError::DisconnectedStream) + } + + /// Resets the inner stream, forcing a re-connect next `call` + pub fn reset(&mut self) { + self.inner = None; + } +} + +async fn send( + stream: &mut S, + buf: &[u8], +) -> Result<(), IOError> { + let len = buf.len(); + // First, send the length of the buffer + let len_buf: [u8; size_of::()] = (len as u64).to_le_bytes(); + + // send the header + stream.write_all(&len_buf).await?; + // Send the actual contents of the buffer + stream.write_all(buf).await?; + + Ok(()) +} + +async fn recv( + stream: &mut S, +) -> Result, IOError> { + let length: usize = { + let mut buf = [0u8; size_of::()]; + + let r = stream.read_exact(&mut buf).await.map_err(|e| match e.kind() { + ErrorKind::UnexpectedEof => IOError::RecvConnectionClosed, + _ => IOError::StdIoError(e), + }); + + r?; + + u64::from_le_bytes(buf) + .try_into() + // Should only be possible if we are on 32bit architecture + .map_err(|_| IOError::ArithmeticSaturation)? + }; + + // Read the buffer + let mut buf = vec![0; length]; + stream.read_exact(&mut buf).await.map_err(|e| match e.kind() { + ErrorKind::UnexpectedEof => IOError::RecvConnectionClosed, + _ => IOError::StdIoError(e), + })?; + + Ok(buf) +} + +impl From for std::io::Error { + fn from(value: IOError) -> Self { + match value { + IOError::DisconnectedStream => std::io::Error::new( + std::io::ErrorKind::NotFound, + "connection not found", + ), + _ => { + std::io::Error::new(std::io::ErrorKind::Other, "unknown error") + } + } + } +} + +impl AsyncRead for AsyncStream { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => Pin::new(s).poll_read(cx, buf), + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => Pin::new(s).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for AsyncStream { + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => Pin::new(s).poll_write(cx, buf), + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => Pin::new(s).poll_write(cx, buf), + } + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => Pin::new(s).poll_flush(cx), + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => Pin::new(s).poll_flush(cx), + } + } + + fn poll_shutdown( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.inner_mut()? { + InnerStream::Unix(ref mut s) => Pin::new(s).poll_shutdown(cx), + #[cfg(feature = "vm")] + InnerStream::Vsock(ref mut s) => Pin::new(s).poll_shutdown(cx), + } + } +} + +/// Abstraction to listen for incoming stream connections. +pub struct AsyncListener { + inner: InnerListener, + // addr: SocketAddress, +} + +impl AsyncListener { + /// Bind and listen on the given address. + pub(crate) fn listen(addr: &SocketAddress) -> Result { + let listener = match *addr { + SocketAddress::Unix(uaddr) => { + let path = + uaddr.path().ok_or(IOError::ConnectAddressInvalid)?; + if path.exists() { + // attempt cleanup, this mostly happens from tests/panics + std::fs::remove_file(path)?; + } + let inner = InnerListener::Unix(UnixListener::bind(path)?); + Self { inner } + } + #[cfg(feature = "vm")] + SocketAddress::Vsock(vaddr) => { + let inner = InnerListener::Vsock(VsockListener::bind(vaddr)?); + Self { inner } + } + }; + + Ok(listener) + } + + /// Accept a new connection. + pub async fn accept(&self) -> Result { + let stream = match &self.inner { + InnerListener::Unix(l) => { + let (s, _) = l.accept().await?; + AsyncStream::unix_accepted(s) + } + #[cfg(feature = "vm")] + InnerListener::Vsock(l) => { + let (s, _) = l.accept().await?; + AsyncStream::vsock_accepted(s) + } + }; + + Ok(stream) + } +} + +impl Drop for AsyncListener { + fn drop(&mut self) { + match &mut self.inner { + InnerListener::Unix(usock) => match usock.local_addr() { + Ok(addr) => { + if let Some(path) = addr.as_pathname() { + _ = std::fs::remove_file(path); + } else { + eprintln!("unable to path the usock"); // do not crash in Drop + } + } + Err(e) => eprintln!("{e}"), // do not crash in Drop + }, + #[cfg(feature = "vm")] + InnerListener::Vsock(_vsock) => {} // vsock's drop will clear this + } + } +} + +async fn unix_connect( + addr: SocketAddress, +) -> Result { + let addr = addr.usock(); + let path = addr.path().ok_or(IOError::ConnectAddressInvalid)?; + + let socket = UnixSocket::new_stream()?; + eprintln!("Attempting USOCK connect to: {:?}", addr.path()); + socket.connect(path).await +} + +// raw vsock socket connect +#[cfg(feature = "vm")] +async fn vsock_connect( + addr: SocketAddress, +) -> Result { + let addr = addr.vsock(); + + eprintln!("Attempting VSOCK connect to: {:?}", addr); + VsockStream::connect(*addr).await +} diff --git a/src/qos_core/src/io/mod.rs b/src/qos_core/src/io/mod.rs index 8f1de7a09..17ef2d29a 100644 --- a/src/qos_core/src/io/mod.rs +++ b/src/qos_core/src/io/mod.rs @@ -3,22 +3,33 @@ //! NOTE TO MAINTAINERS: Interaction with any sys calls should be contained //! within this module. +mod async_pool; +mod async_stream; +pub use async_pool::*; +pub use async_stream::*; + mod stream; +pub use stream::{SocketAddress, VMADDR_FLAG_TO_HOST, VMADDR_NO_FLAGS}; -pub use stream::{ - Listener, SocketAddress, Stream, TimeVal, TimeValLike, MAX_PAYLOAD_SIZE, - VMADDR_FLAG_TO_HOST, VMADDR_NO_FLAGS, -}; +pub use nix::sys::time::{TimeVal, TimeValLike}; /// QOS I/O error #[derive(Debug)] pub enum IOError { + /// `std::io::Error` wrapper. + StdIoError(std::io::Error), /// `nix::Error` wrapper. NixError(nix::Error), /// Arithmetic operation saturated. ArithmeticSaturation, /// Unknown error. UnknownError, + /// Stream was not connected when expected to be connected. + DisconnectedStream, + /// Connect address invalid + ConnectAddressInvalid, + /// Timed out while claling `connect` over a socket. + ConnectTimeout, /// Timed out while calling `recv` over a socket. RecvTimeout, /// The `recv` system call was interrupted while receiving over a socket. @@ -33,6 +44,8 @@ pub enum IOError { RecvNixError(nix::Error), /// Reading the response size resulted in a size which exceeds the max payload size. OversizedPayload(usize), + /// A async socket pool error during pool operations. + PoolError(PoolError), } impl From for IOError { @@ -40,3 +53,15 @@ impl From for IOError { Self::NixError(err) } } + +impl From for IOError { + fn from(err: std::io::Error) -> Self { + Self::StdIoError(err) + } +} + +impl From for IOError { + fn from(value: PoolError) -> Self { + Self::PoolError(value) + } +} diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 49785305b..156f9d683 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -1,35 +1,10 @@ //! Abstractions to handle connection based socket streams. -use std::{ - io::{ErrorKind, Read, Write}, - mem::size_of, - os::unix::io::RawFd, -}; - #[cfg(feature = "vm")] use nix::sys::socket::VsockAddr; -pub use nix::sys::time::{TimeVal, TimeValLike}; -use nix::{ - sys::socket::{ - accept, bind, connect, listen, recv, send, shutdown, socket, sockopt, - AddressFamily, MsgFlags, SetSockOpt, Shutdown, SockFlag, SockType, - SockaddrLike, UnixAddr, - }, - unistd::close, -}; - -use super::IOError; +use nix::sys::socket::{AddressFamily, SockaddrLike, UnixAddr}; // 25(retries) x 10(milliseconds) = 1/4 a second of retrying -const MAX_RETRY: usize = 25; -const BACKOFF_MILLISECONDS: u64 = 10; -const BACKLOG: usize = 128; - -const MEGABYTE: usize = 1024 * 1024; - -/// Maximum payload size for a single recv / send call. We're being generous with 128MB. -/// The goal here is to avoid server crashes if the payload size exceeds the available system memory. -pub const MAX_PAYLOAD_SIZE: usize = 128 * MEGABYTE; /// Socket address. #[derive(Clone, Debug, PartialEq, Eq)] @@ -62,21 +37,17 @@ impl SocketAddress { /// /// For flags see: [Add flags field in the vsock address](). #[cfg(feature = "vm")] - #[allow(unsafe_code)] pub fn new_vsock(cid: u32, port: u32, flags: u8) -> Self { - #[repr(C)] - struct sockaddr_vm { - svm_family: libc::sa_family_t, - svm_reserved1: libc::c_ushort, - svm_port: libc::c_uint, - svm_cid: libc::c_uint, - // Field added [here](https://github.com/torvalds/linux/commit/3a9c049a81f6bd7c78436d7f85f8a7b97b0821e6) - // but not yet in a version of libc we can use. - svm_flags: u8, - svm_zero: [u8; 3], - } + Self::Vsock(Self::new_vsock_raw(cid, port, flags)) + } - let vsock_addr = sockaddr_vm { + /// Create a new raw VsockAddr. + /// + /// For flags see: [Add flags field in the vsock address](). + #[cfg(feature = "vm")] + #[allow(unsafe_code)] + pub fn new_vsock_raw(cid: u32, port: u32, flags: u8) -> VsockAddr { + let vsock_addr = SockAddrVm { svm_family: AddressFamily::Vsock as libc::sa_family_t, svm_reserved1: 0, svm_cid: cid, @@ -84,15 +55,15 @@ impl SocketAddress { svm_flags: flags, svm_zero: [0; 3], }; - let vsock_addr_len = size_of::() as libc::socklen_t; + let vsock_addr_len = size_of::() as libc::socklen_t; let addr = unsafe { VsockAddr::from_raw( - &vsock_addr as *const sockaddr_vm as *const libc::sockaddr, + &vsock_addr as *const SockAddrVm as *const libc::sockaddr, Some(vsock_addr_len), ) .unwrap() }; - Self::Vsock(addr) + addr } /// Get the `AddressFamily` of the socket. @@ -114,469 +85,69 @@ impl SocketAddress { Self::Unix(ua) => Box::new(ua), } } -} - -/// Handle on a stream -pub struct Stream { - fd: RawFd, -} - -impl Stream { - /// Create a new `Stream` from a `SocketAddress` and a timeout - pub fn connect( - addr: &SocketAddress, - timeout: TimeVal, - ) -> Result { - let mut err = IOError::UnknownError; - - for _ in 0..MAX_RETRY { - let fd = socket_fd(addr)?; - let stream = Self { fd }; - - // set `SO_RCVTIMEO` - let receive_timeout = sockopt::ReceiveTimeout; - receive_timeout.set(fd, &timeout)?; - - let send_timeout = sockopt::SendTimeout; - send_timeout.set(fd, &timeout)?; - - match connect(stream.fd, &*addr.addr()) { - Ok(()) => return Ok(stream), - Err(e) => err = IOError::ConnectNixError(e), - } - - std::thread::sleep(std::time::Duration::from_millis( - BACKOFF_MILLISECONDS, - )); - } - - Err(err) - } - - /// Sends a buffer over the underlying socket - pub fn send(&self, buf: &[u8]) -> Result<(), IOError> { - let len = buf.len(); - // First, send the length of the buffer - { - let len_buf: [u8; size_of::()] = (len as u64).to_le_bytes(); - - // First, send the length of the buffer - let mut sent_bytes = 0; - while sent_bytes < len_buf.len() { - sent_bytes += match send( - self.fd, - &len_buf[sent_bytes..len_buf.len()], - MsgFlags::empty(), - ) { - Ok(size) => size, - Err(err) => return Err(IOError::SendNixError(err)), - }; - } - } - - // Then, send the contents of the buffer - { - let mut sent_bytes = 0; - while sent_bytes < len { - sent_bytes += match send( - self.fd, - &buf[sent_bytes..len], - MsgFlags::empty(), - ) { - Ok(size) => size, - Err(err) => return Err(IOError::SendNixError(err)), - } - } - } - - Ok(()) - } - /// Receive from the underlying socket - pub fn recv(&self) -> Result, IOError> { - let length: usize = { - { - let mut buf = [0u8; size_of::()]; - let len = buf.len(); - std::debug_assert!(buf.len() == 8); - - let mut received_bytes = 0; - while received_bytes < len { - received_bytes += match recv( - self.fd, - &mut buf[received_bytes..len], - MsgFlags::empty(), - ) { - Ok(0) => { - return Err(IOError::RecvConnectionClosed); - } - Ok(size) => size, - Err(nix::Error::EINTR) => { - return Err(IOError::RecvInterrupted); - } - Err(nix::Error::EAGAIN) => { - return Err(IOError::RecvTimeout); - } - Err(err) => { - return Err(IOError::RecvNixError(err)); - } - }; - } - - u64::from_le_bytes(buf) - .try_into() - // Should only be possible if we are on 32bit architecture - .map_err(|_| IOError::ArithmeticSaturation)? + /// Shows socket debug info + #[must_use] + pub fn debug_info(&self) -> String { + match self { + #[cfg(feature = "vm")] + Self::Vsock(vsock) => { + format!("vsock cid: {} port: {}", vsock.cid(), vsock.port()) } - }; - - if length > MAX_PAYLOAD_SIZE { - return Err(IOError::OversizedPayload(length)); - } - - // Read the buffer - let mut buf = vec![0; length]; - { - let mut received_bytes = 0; - while received_bytes < length { - received_bytes += match recv( - self.fd, - &mut buf[received_bytes..length], - MsgFlags::empty(), - ) { - Ok(0) => { - return Err(IOError::RecvConnectionClosed); - } - Ok(size) => size, - Err(nix::Error::EINTR) => { - return Err(IOError::RecvInterrupted); - } - Err(nix::Error::EAGAIN) => { - return Err(IOError::RecvTimeout); - } - Err(err) => { - return Err(IOError::NixError(err)); - } - }; + Self::Unix(usock) => { + format!( + "usock path: {}", + usock + .path() + .unwrap_or(&std::path::PathBuf::from("unknown/error")) + .as_os_str() + .to_str() + .unwrap_or("unable to procure") + ) } } - Ok(buf) - } -} - -impl Read for Stream { - fn read(&mut self, buf: &mut [u8]) -> Result { - match recv(self.fd, buf, MsgFlags::empty()) { - Ok(0) => Err(std::io::Error::new( - ErrorKind::ConnectionAborted, - "read 0 bytes", - )), - Ok(size) => Ok(size), - Err(err) => Err(std::io::Error::from_raw_os_error(err as i32)), - } } -} -impl Write for Stream { - fn write(&mut self, buf: &[u8]) -> Result { - match send(self.fd, buf, MsgFlags::empty()) { - Ok(0) => Err(std::io::Error::new( - ErrorKind::ConnectionAborted, - "wrote 0 bytes", - )), - Ok(size) => Ok(size), - Err(err) => Err(std::io::Error::from_raw_os_error(err as i32)), + /// Returns the `UnixAddr` if this is a USOCK `SocketAddress`, panics otherwise + #[must_use] + pub fn usock(&self) -> &UnixAddr { + match self { + Self::Unix(usock) => usock, + #[cfg(feature = "vm")] + _ => panic!("invalid socket address requested"), } } - // No-op because we can't flush a socket. - fn flush(&mut self) -> Result<(), std::io::Error> { - Ok(()) - } -} - -impl Drop for Stream { - fn drop(&mut self) { - // Its ok if either of these error - likely means the other end of the - // connection has been shutdown - let _ = shutdown(self.fd, Shutdown::Both); - let _ = close(self.fd); - } -} - -/// Abstraction to listen for incoming stream connections. -pub struct Listener { - fd: RawFd, - addr: SocketAddress, -} - -impl Listener { - /// Bind and listen on the given address. - pub(crate) fn listen(addr: SocketAddress) -> Result { - // In case the last connection at this addr did not shutdown correctly - Self::clean(&addr); - - let fd = socket_fd(&addr)?; - bind(fd, &*addr.addr())?; - listen(fd, BACKLOG)?; - - Ok(Self { fd, addr }) - } - - fn accept(&self) -> Result { - let fd = accept(self.fd)?; - - Ok(Stream { fd }) - } - - /// Remove Unix socket if it exists - fn clean(addr: &SocketAddress) { - // Not irrefutable when "vm" is enabled - #[allow(irrefutable_let_patterns)] - if let SocketAddress::Unix(addr) = addr { - if let Some(path) = addr.path() { - if path.exists() { - drop(std::fs::remove_file(path)); - } - } + /// Returns the `UnixAddr` if this is a USOCK `SocketAddress`, panics otherwise + #[must_use] + #[cfg(feature = "vm")] + pub fn vsock(&self) -> &VsockAddr { + match self { + Self::Vsock(vsock) => vsock, + _ => panic!("invalid socket address requested"), } } } -impl Iterator for Listener { - type Item = Stream; - fn next(&mut self) -> Option { - self.accept().ok() - } -} - -impl Drop for Listener { - fn drop(&mut self) { - // Its ok if either of these error - likely means the other end of the - // connection has been shutdown - let _ = shutdown(self.fd, Shutdown::Both); - let _ = close(self.fd); - Self::clean(&self.addr); +/// Extract svm_flags field value from existing VSOCK. +#[cfg(feature = "vm")] +#[allow(unsafe_code)] +pub fn vsock_svm_flags(vsock: VsockAddr) -> u8 { + unsafe { + let cast: SockAddrVm = std::mem::transmute(vsock); + cast.svm_flags } } -fn socket_fd(addr: &SocketAddress) -> Result { - socket( - addr.family(), - // Type - sequenced, two way byte stream. (full duplexed). - // Stream must be in a connected state before send/receive. - SockType::Stream, - // Flags - SockFlag::empty(), - // Protocol - no protocol needs to be specified as SOCK_STREAM - // is both a type and protocol. - None, - ) - .map_err(IOError::NixError) -} - -#[cfg(test)] -mod test { - - use std::{ - os::{fd::AsRawFd, unix::net::UnixListener}, - path::Path, - str::from_utf8, - thread, - }; - - use super::*; - - fn timeval() -> TimeVal { - TimeVal::seconds(1) - } - - // A simple test socket server which says "PONG" when you send "PING". - // Then it kills itself. - pub struct HarakiriPongServer { - path: String, - fd: Option, - } - - impl HarakiriPongServer { - pub fn new(path: String) -> Self { - Self { path, fd: None } - } - pub fn start(&mut self) { - let listener = UnixListener::bind(&self.path).unwrap(); - self.fd = Some(listener.as_raw_fd()); - - let (mut stream, _peer_addr) = listener.accept().unwrap(); - - // Read 4 bytes ("PING") - let mut buf = [0u8; 4]; - stream.read_exact(&mut buf).unwrap(); - - // Send "PONG" if "PING" was sent - if from_utf8(&buf).unwrap() == "PING" { - let _ = stream.write(b"PONG").unwrap(); - } - } - } - - impl Drop for HarakiriPongServer { - fn drop(&mut self) { - if let Some(fd) = &self.fd { - // Cleanup server fd if we have access to one - let _ = shutdown(fd.to_owned(), Shutdown::Both); - let _ = close(fd.to_owned()); - - let server_socket = Path::new(&self.path); - if server_socket.exists() { - drop(std::fs::remove_file(server_socket)); - } - println!("HarakiriPongServer dropped successfully.") - } else { - println!( - "HarakiriPongServer dropped without a fd set. All done." - ) - } - } - } - - #[test] - fn stream_integration_test() { - // Ensure concurrent tests do not listen at the same path - let unix_addr = - nix::sys::socket::UnixAddr::new("./stream_integration_test.sock") - .unwrap(); - let addr: SocketAddress = SocketAddress::Unix(unix_addr); - let listener: Listener = Listener::listen(addr.clone()).unwrap(); - let client = Stream::connect(&addr, timeval()).unwrap(); - let server = listener.accept().unwrap(); - - let data = vec![1, 2, 3, 4, 5, 6, 6, 6]; - client.send(&data).unwrap(); - - let resp = server.recv().unwrap(); - - assert_eq!(data, resp); - } - - #[test] - fn stream_implements_read_write_traits() { - let socket_server_path = "./stream_implements_read_write_traits.sock"; - - // Start a simple socket server which replies "PONG" to any incoming - // request - let mut server = - HarakiriPongServer::new(socket_server_path.to_string()); - - // Start the server in its own thread - thread::spawn(move || { - server.start(); - }); - - // Now create a stream connecting to this mini-server - let unix_addr = - nix::sys::socket::UnixAddr::new(socket_server_path).unwrap(); - let addr = SocketAddress::Unix(unix_addr); - let mut pong_stream = Stream::connect(&addr, timeval()).unwrap(); - - // Write "PING" - let written = pong_stream.write(b"PING").unwrap(); - assert_eq!(written, 4); - - // Read, and expect "PONG" - let mut resp = [0u8; 4]; - let res = pong_stream.read(&mut resp).unwrap(); - assert_eq!(res, 4); - assert_eq!(from_utf8(&resp).unwrap(), "PONG"); - } - - #[test] - fn listener_iterator_test() { - // Ensure concurrent tests are not attempting to listen at the same - // address - let unix_addr = - nix::sys::socket::UnixAddr::new("./listener_iterator_test.sock") - .unwrap(); - let addr = SocketAddress::Unix(unix_addr); - - let mut listener = Listener::listen(addr.clone()).unwrap(); - - let handler = std::thread::spawn(move || { - if let Some(stream) = listener.next() { - let req = stream.recv().unwrap(); - stream.send(&req).unwrap(); - } - }); - - let client = Stream::connect(&addr, timeval()).unwrap(); - - let data = vec![1, 2, 3, 4, 5, 6, 6, 6]; - client.send(&data).unwrap(); - let resp = client.recv().unwrap(); - assert_eq!(data, resp); - - handler.join().unwrap(); - } - - #[test] - fn limit_sized_payload() { - // Ensure concurrent tests are not attempting to listen on the same socket - let unix_addr = - nix::sys::socket::UnixAddr::new("./limit_sized_payload.sock") - .unwrap(); - let addr = SocketAddress::Unix(unix_addr); - - let mut listener = Listener::listen(addr.clone()).unwrap(); - let handler = std::thread::spawn(move || { - if let Some(stream) = listener.next() { - let req = stream.recv().unwrap(); - stream.send(&req.clone()).unwrap(); - } - }); - - // Sending a request that is strictly less than the max size should work - // (the response will be exactly max size) - let client = Stream::connect(&addr, timeval()).unwrap(); - let req = vec![1u8; MAX_PAYLOAD_SIZE]; - client.send(&req).unwrap(); - let resp = client.recv().unwrap(); - assert_eq!(resp.len(), MAX_PAYLOAD_SIZE); - handler.join().unwrap(); - } - - #[test] - fn oversized_payload() { - // Ensure concurrent tests are not attempting to listen on the same socket - let unix_addr = - nix::sys::socket::UnixAddr::new("./oversized_payload.sock") - .unwrap(); - let addr = SocketAddress::Unix(unix_addr); - let mut listener = Listener::listen(addr.clone()).unwrap(); - - // Sneaky handler: adds one byte to the req, and returns this as a response - let _handler = std::thread::spawn(move || { - if let Some(stream) = listener.next() { - let req = stream.recv().unwrap(); - stream.send(&[req.clone(), vec![1u8]].concat()).unwrap(); - } - }); - - let client = Stream::connect(&addr, timeval()).unwrap(); - - // Sending with the limit payload size will fail to receive: our sneaky handler - // will add one byte and cause the response to be oversized. - let req = vec![1u8; MAX_PAYLOAD_SIZE]; - client.send(&req).unwrap(); - - match client.recv().unwrap_err() { - IOError::OversizedPayload(size) => { - assert_eq!(size, MAX_PAYLOAD_SIZE + 1); - } - other => { - panic!("test failed: unexpected error variant ({:?})", other); - } - } - - // N.B: we do not call _handler.join().unwrap() here, because the handler is blocking (indefinitely) on "send" - // Once the test exits, Rust/OS checks will pick up the slack and clean up this thread when this test exits. - } +#[cfg(feature = "vm")] +#[repr(C)] +struct SockAddrVm { + svm_family: libc::sa_family_t, + svm_reserved1: libc::c_ushort, + svm_port: libc::c_uint, + svm_cid: libc::c_uint, + // Field added [here](https://github.com/torvalds/linux/commit/3a9c049a81f6bd7c78436d7f85f8a7b97b0821e6) + // but not yet in a version of libc we can use. + svm_flags: u8, + svm_zero: [u8; 3], } diff --git a/src/qos_core/src/lib.rs b/src/qos_core/src/lib.rs index 42fdbacd9..46d60b6ca 100644 --- a/src/qos_core/src/lib.rs +++ b/src/qos_core/src/lib.rs @@ -17,14 +17,15 @@ compile_error!( "feature \"vm\" and feature \"mock\" cannot be enabled at the same time" ); +pub mod async_client; +pub mod async_server; + pub mod cli; -pub mod client; pub mod handles; pub mod io; pub mod parser; pub mod protocol; pub mod reaper; -pub mod server; /// Path to Quorum Key secret. #[cfg(not(feature = "vm"))] @@ -60,3 +61,5 @@ pub const SEC_APP_SOCK: &str = "./local-enclave/sec_app.sock"; /// Default socket for enclave <-> secure app communication. #[cfg(feature = "vm")] pub const SEC_APP_SOCK: &str = "/sec_app.sock"; +/// Default socket connect timeout in milliseconds +pub const DEFAULT_SOCKET_TIMEOUT: &str = "5000"; diff --git a/src/qos_core/src/main.rs b/src/qos_core/src/main.rs index 1e3d00c6d..b5e302571 100644 --- a/src/qos_core/src/main.rs +++ b/src/qos_core/src/main.rs @@ -1,5 +1,6 @@ use qos_core::cli::CLI; -pub fn main() { - CLI::execute(); +#[tokio::main] +async fn main() { + CLI::execute().await; } diff --git a/src/qos_core/src/protocol/async_processor.rs b/src/qos_core/src/protocol/async_processor.rs new file mode 100644 index 000000000..b44f5ddac --- /dev/null +++ b/src/qos_core/src/protocol/async_processor.rs @@ -0,0 +1,108 @@ +//! Quorum protocol processor +use std::sync::Arc; + +use crate::io::{TimeVal, TimeValLike}; +use borsh::BorshDeserialize; +use tokio::sync::Mutex; + +use super::{ + error::ProtocolError, msg::ProtocolMsg, state::ProtocolState, + ProtocolPhase, ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, +}; +use crate::{ + async_client::{AsyncClient, ClientError}, + async_server::AsyncRequestProcessor, + io::SharedAsyncStreamPool, +}; + +const MEGABYTE: usize = 1024 * 1024; +const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; + +/// Helper type to keep `ProtocolState` shared using `Arc>` +type SharedProtocolState = Arc>; + +impl ProtocolState { + /// Wrap this `ProtocolState` into a `Mutex` in an `Arc`. + pub fn shared(self) -> SharedProtocolState { + Arc::new(Mutex::new(self)) + } +} + +/// Enclave state machine that executes when given a `ProtocolMsg`. +#[derive(Clone)] +pub struct AsyncProcessor { + app_client: AsyncClient, + state: SharedProtocolState, +} + +impl AsyncProcessor { + /// Create a new `Self`. + #[must_use] + pub fn new( + state: SharedProtocolState, + app_pool: SharedAsyncStreamPool, + ) -> Self { + let app_client = AsyncClient::new( + app_pool, + TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), + ); + Self { app_client, state } + } + + /// Helper to get phase between locking the shared state + async fn get_phase(&self) -> ProtocolPhase { + self.state.lock().await.get_phase() + } + + /// Expands the app pool to given pool size + pub async fn expand_to( + &mut self, + pool_size: u32, + ) -> Result<(), ClientError> { + self.app_client.expand_to(pool_size).await + } +} + +impl AsyncRequestProcessor for AsyncProcessor { + async fn process(&self, req_bytes: Vec) -> Vec { + if req_bytes.len() > MAX_ENCODED_MSG_LEN { + return borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( + ProtocolError::OversizedPayload, + )) + .expect("ProtocolMsg can always be serialized. qed."); + } + + let Ok(msg_req) = ProtocolMsg::try_from_slice(&req_bytes) else { + return borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( + ProtocolError::ProtocolMsgDeserialization, + )) + .expect("ProtocolMsg can always be serialized. qed."); + }; + + // handle Proxy outside of the state + if let ProtocolMsg::ProxyRequest { data } = msg_req { + let phase = self.get_phase().await; + + if phase != ProtocolPhase::QuorumKeyProvisioned { + let err = ProtocolError::NoMatchingRoute(phase); + return borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse(err)) + .expect("ProtocolMsg can always be serialized. qed."); + } + + let result = self + .app_client + .call(&data) + .await + .map(|data| ProtocolMsg::ProxyResponse { data }) + .map_err(|e| ProtocolMsg::ProtocolErrorResponse(e.into())); + + match result { + Ok(msg_resp) | Err(msg_resp) => borsh::to_vec(&msg_resp) + .expect("ProtocolMsg can always be serialized. qed."), + } + } else { + // handle all the others here + self.state.lock().await.handle_msg(&msg_req) + } + } +} diff --git a/src/qos_core/src/protocol/error.rs b/src/qos_core/src/protocol/error.rs index 93fe6b612..e6de4549d 100644 --- a/src/qos_core/src/protocol/error.rs +++ b/src/qos_core/src/protocol/error.rs @@ -3,7 +3,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use qos_p256::P256Error; use crate::{ - client::{self, ClientError}, + async_client::ClientError, io::IOError, protocol::{services::boot, ProtocolPhase}, }; @@ -147,6 +147,8 @@ pub enum ProtocolError { DifferentManifest, /// Error from the qos crypto library. QosCrypto(String), + /// Error during expanding the `AsyncPool`. + PoolExpandError, } impl From for ProtocolError { @@ -155,8 +157,8 @@ impl From for ProtocolError { } } -impl From for ProtocolError { - fn from(err: client::ClientError) -> Self { +impl From for ProtocolError { + fn from(err: ClientError) -> Self { match err { ClientError::IOError(IOError::RecvTimeout) => { ProtocolError::AppClientRecvTimeout diff --git a/src/qos_core/src/protocol/mod.rs b/src/qos_core/src/protocol/mod.rs index 550b8711b..7785fc2ef 100644 --- a/src/qos_core/src/protocol/mod.rs +++ b/src/qos_core/src/protocol/mod.rs @@ -5,15 +5,15 @@ use qos_crypto::sha_256; mod error; pub mod msg; -mod processor; pub mod services; mod state; pub use error::ProtocolError; -pub use processor::Processor; -use state::ProtocolState; +pub(crate) use state::ProtocolState; pub use state::{ProtocolPhase, ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS}; +pub(crate) mod async_processor; + /// 256bit hash pub type Hash256 = [u8; 32]; diff --git a/src/qos_core/src/protocol/msg.rs b/src/qos_core/src/protocol/msg.rs index 9b1a3e984..142d44aac 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -140,6 +140,89 @@ pub enum ProtocolMsg { }, } +impl std::fmt::Display for ProtocolMsg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ProtocolErrorResponse(_) => { + write!(f, "ProtocolErrorResponse") + } + Self::StatusRequest => write!(f, "StatusRequest"), + Self::StatusResponse(_) => { + write!(f, "StatusResponse") + } + Self::BootStandardRequest { .. } => { + write!(f, "BootStandardRequest") + } + Self::BootStandardResponse { .. } => { + write!(f, "BootStandardResponse") + } + Self::BootGenesisRequest { .. } => { + write!(f, "BootGenesisRequest") + } + Self::BootGenesisResponse { .. } => { + write!(f, "BootGenesisResponse") + } + Self::ProvisionRequest { .. } => { + write!(f, "ProvisionRequest") + } + Self::ProvisionResponse { reconstructed } => { + write!( + f, + "ProvisionResponse{{ reconstructed: {reconstructed} }}" + ) + } + Self::ProxyRequest { .. } => { + write!(f, "ProxyRequest") + } + Self::ProxyResponse { .. } => { + write!(f, "ProxyResponse") + } + Self::LiveAttestationDocRequest { .. } => { + write!(f, "LiveAttestationDocRequest") + } + Self::LiveAttestationDocResponse { .. } => { + write!(f, "LiveAttestationDocResponse") + } + Self::BootKeyForwardRequest { .. } => { + write!(f, "BootKeyForwardRequest") + } + Self::BootKeyForwardResponse { nsm_response } => match nsm_response + { + NsmResponse::Attestation { .. } => write!( + f, + "BootKeyForwardResponse {{ nsm_response: Attestation }}" + ), + NsmResponse::Error(ecode) => write!( + f, + "BootKeyForwardResponse {{ nsm_response: Error({ecode:?}) }}" + ), + _ => write!( + f, + "BootKeyForwardResponse {{ nsm_response: Other }}" // this shouldn't really show up + ), + }, + Self::ExportKeyRequest { .. } => { + write!(f, "ExportKeyRequest") + } + Self::ExportKeyResponse { .. } => { + write!(f, "ExportKeyResponse") + } + Self::InjectKeyRequest { .. } => { + write!(f, "InjectKeyRequest") + } + Self::InjectKeyResponse { .. } => { + write!(f, "InjectKeyResponse") + } + Self::ManifestEnvelopeRequest { .. } => { + write!(f, "ManifestEnvelopeRequest") + } + Self::ManifestEnvelopeResponse { .. } => { + write!(f, "ManifestEnvelopeResponse") + } + } + } +} + #[cfg(test)] mod test { use borsh::BorshDeserialize; diff --git a/src/qos_core/src/protocol/processor.rs b/src/qos_core/src/protocol/processor.rs deleted file mode 100644 index ba43b58a3..000000000 --- a/src/qos_core/src/protocol/processor.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Quorum protocol processor -use borsh::BorshDeserialize; -use qos_nsm::NsmProvider; - -use super::{ - error::ProtocolError, msg::ProtocolMsg, state::ProtocolState, ProtocolPhase, -}; -use crate::io::MAX_PAYLOAD_SIZE; -use crate::{handles::Handles, io::SocketAddress, server}; -/// Enclave state machine that executes when given a `ProtocolMsg`. -pub struct Processor { - state: ProtocolState, -} - -impl Processor { - /// Create a new `Self`. - #[must_use] - pub fn new( - attestor: Box, - handles: Handles, - app_addr: SocketAddress, - test_only_init_phase_override: Option, - ) -> Self { - Self { - state: ProtocolState::new( - attestor, - handles, - app_addr, - test_only_init_phase_override, - ), - } - } -} - -impl server::RequestProcessor for Processor { - fn process(&mut self, req_bytes: Vec) -> Vec { - if req_bytes.len() > MAX_PAYLOAD_SIZE { - return borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( - ProtocolError::OversizedPayload, - )) - .expect("ProtocolMsg can always be serialized. qed."); - } - - let Ok(msg_req) = ProtocolMsg::try_from_slice(&req_bytes) else { - return borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( - ProtocolError::ProtocolMsgDeserialization, - )) - .expect("ProtocolMsg can always be serialized. qed."); - }; - - self.state.handle_msg(&msg_req) - } -} diff --git a/src/qos_core/src/protocol/services/boot.rs b/src/qos_core/src/protocol/services/boot.rs index 0341a4488..91646c65a 100644 --- a/src/qos_core/src/protocol/services/boot.rs +++ b/src/qos_core/src/protocol/services/boot.rs @@ -480,7 +480,7 @@ mod test { use qos_test_primitives::PathWrapper; use super::*; - use crate::{handles::Handles, io::SocketAddress}; + use crate::handles::Handles; fn get_manifest() -> (Manifest, Vec<(P256Pair, QuorumMember)>, Vec) { let quorum_pair = P256Pair::generate().unwrap(); @@ -580,12 +580,8 @@ mod test { manifest_file.clone(), pivot_file.clone(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); let _nsm_resposne = boot_standard(&mut protocol_state, &manifest_envelope, &pivot) @@ -638,12 +634,8 @@ mod test { manifest_file, pivot_file, ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); let nsm_resposne = boot_standard(&mut protocol_state, &manifest_envelope, &pivot); @@ -686,12 +678,8 @@ mod test { manifest_file, pivot_file, ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); let nsm_resposne = boot_standard(&mut protocol_state, &manifest_envelope, &pivot); @@ -736,12 +724,8 @@ mod test { (*manifest_file).to_string(), (*pivot_file).to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); let error = boot_standard(&mut protocol_state, &manifest_envelope, &pivot) @@ -796,12 +780,8 @@ mod test { (*manifest_file).to_string(), (*pivot_file).to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); let error = boot_standard(&mut protocol_state, &manifest_envelope, &pivot) diff --git a/src/qos_core/src/protocol/services/genesis.rs b/src/qos_core/src/protocol/services/genesis.rs index 95b9e6540..9e302fad9 100644 --- a/src/qos_core/src/protocol/services/genesis.rs +++ b/src/qos_core/src/protocol/services/genesis.rs @@ -195,7 +195,7 @@ mod test { use qos_p256::MASTER_SEED_LEN; use super::*; - use crate::{handles::Handles, io::SocketAddress}; + use crate::handles::Handles; #[test] fn boot_genesis_works() { @@ -205,12 +205,8 @@ mod test { "MAN".to_string(), "PIV".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); let member1_pair = P256Pair::generate().unwrap(); let member2_pair = P256Pair::generate().unwrap(); let member3_pair = P256Pair::generate().unwrap(); diff --git a/src/qos_core/src/protocol/services/key.rs b/src/qos_core/src/protocol/services/key.rs index eccb760bd..75e9a59b8 100644 --- a/src/qos_core/src/protocol/services/key.rs +++ b/src/qos_core/src/protocol/services/key.rs @@ -272,7 +272,6 @@ mod test { use super::{boot_key_forward, export_key_internal, validate_manifest}; use crate::{ handles::Handles, - io::SocketAddress, protocol::{ services::{ boot::{ @@ -420,12 +419,8 @@ mod test { manifest_file.deref().to_string(), pivot_file.deref().to_string(), ); - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); let response = boot_key_forward(&mut state, &manifest_envelope, &pivot) @@ -464,12 +459,8 @@ mod test { manifest_file.deref().to_string(), pivot_file.deref().to_string(), ); - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); // Remove an approval manifest_envelope.manifest_set_approvals.pop().unwrap(); @@ -506,12 +497,8 @@ mod test { manifest_file.deref().to_string(), pivot_file.deref().to_string(), ); - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); // Use a different pivot then what is referenced in the manifest let other_pivot = b"other pivot".to_vec(); @@ -547,12 +534,8 @@ mod test { manifest_file.deref().to_string(), pivot_file.deref().to_string(), ); - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); // Change the signature to something invalid manifest_envelope.manifest_set_approvals[0].signature = vec![1; 32]; @@ -604,12 +587,8 @@ mod test { manifest_file.deref().to_string(), pivot_file.deref().to_string(), ); - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles.clone(), - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = + ProtocolState::new(Box::new(MockNsm), handles.clone(), None); // Add an approval from a random key manifest_envelope.manifest_set_approvals.push(non_member_approval); @@ -894,7 +873,7 @@ mod test { &manifest_envelope, &att_doc ), - Err(ProtocolError::QosAttestError("DifferentPcr0".to_string())) + Err(ProtocolError::QosAttestError("DifferentPcr0(\"8080808080808080808080808080808080808080808080808080808080808080\", \"0404040404040404040404040404040404040404040404040404040404040404\")".to_string())) ); } @@ -1082,12 +1061,8 @@ mod test { "pivot".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); let EncryptedQuorumKey { encrypted_quorum_key, signature } = export_key_internal( &mut protocol_state, @@ -1147,12 +1122,8 @@ mod test { manifest_file.deref().to_string(), "pivot".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); protocol_state .transition(ProtocolPhase::WaitingForForwardedKey) .unwrap(); @@ -1209,12 +1180,8 @@ mod test { manifest_file.deref().to_string(), "pivot".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); assert_eq!( inject_key( @@ -1264,12 +1231,8 @@ mod test { manifest_file.deref().to_string(), "pivot".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); assert_eq!( inject_key( @@ -1319,12 +1282,8 @@ mod test { manifest_file.deref().to_string(), "pivot".to_string(), ); - let mut protocol_state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut protocol_state = + ProtocolState::new(Box::new(MockNsm), handles, None); assert_eq!( inject_key( diff --git a/src/qos_core/src/protocol/services/provision.rs b/src/qos_core/src/protocol/services/provision.rs index fe1594dc7..80e5fc2a3 100644 --- a/src/qos_core/src/protocol/services/provision.rs +++ b/src/qos_core/src/protocol/services/provision.rs @@ -134,7 +134,6 @@ mod test { use crate::{ handles::Handles, - io::SocketAddress, protocol::{ services::{ boot::{ @@ -236,12 +235,7 @@ mod test { handles.put_manifest_envelope(&manifest_envelope).unwrap(); // 3) Create state with eph key and manifest - let mut state = ProtocolState::new( - Box::new(MockNsm), - handles, - SocketAddress::new_unix("./never.sock"), - None, - ); + let mut state = ProtocolState::new(Box::new(MockNsm), handles, None); state.transition(ProtocolPhase::WaitingForQuorumShards).unwrap(); Setup { quorum_pair, eph_pair, threshold, state, approvals } diff --git a/src/qos_core/src/protocol/state.rs b/src/qos_core/src/protocol/state.rs index 6df388528..691bae4fb 100644 --- a/src/qos_core/src/protocol/state.rs +++ b/src/qos_core/src/protocol/state.rs @@ -1,11 +1,10 @@ //! Quorum protocol state machine -use nix::sys::time::{TimeVal, TimeValLike}; use qos_nsm::NsmProvider; use super::{ error::ProtocolError, msg::ProtocolMsg, services::provision::SecretBuilder, }; -use crate::{client::Client, handles::Handles, io::SocketAddress}; +use crate::handles::Handles; /// The timeout for the qos core when making requests to an enclave app. pub const ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS: i64 = 5; @@ -138,14 +137,6 @@ impl ProtocolRoute { ) } - pub fn proxy(current_phase: ProtocolPhase) -> Self { - ProtocolRoute::new( - Box::new(handlers::proxy), - current_phase, - current_phase, - ) - } - pub fn export_key(current_phase: ProtocolPhase) -> Self { ProtocolRoute::new( Box::new(handlers::export_key), @@ -175,7 +166,6 @@ impl ProtocolRoute { pub(crate) struct ProtocolState { pub provisioner: SecretBuilder, pub attestor: Box, - pub app_client: Client, pub handles: Handles, phase: ProtocolPhase, } @@ -184,8 +174,7 @@ impl ProtocolState { pub fn new( attestor: Box, handles: Handles, - app_addr: SocketAddress, - test_only_init_phase_override: Option, + #[allow(unused)] test_only_init_phase_override: Option, ) -> Self { let provisioner = SecretBuilder::new(); @@ -198,16 +187,7 @@ impl ProtocolState { #[cfg(not(any(feature = "mock", test)))] let init_phase = ProtocolPhase::WaitingForBootInstruction; - Self { - attestor, - provisioner, - phase: init_phase, - handles, - app_client: Client::new( - app_addr, - TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), - ), - } + Self { attestor, provisioner, phase: init_phase, handles } } pub fn get_phase(&self) -> ProtocolPhase { @@ -273,7 +253,6 @@ impl ProtocolState { ProtocolRoute::live_attestation_doc(self.phase), ProtocolRoute::manifest_envelope(self.phase), // phase specific routes - ProtocolRoute::proxy(self.phase), ProtocolRoute::export_key(self.phase), ] } @@ -376,22 +355,22 @@ mod handlers { } } - pub(super) fn proxy( - req: &ProtocolMsg, - state: &mut ProtocolState, - ) -> ProtocolRouteResponse { - if let ProtocolMsg::ProxyRequest { data: req_data } = req { - let result = state - .app_client - .send(req_data) - .map(|data| ProtocolMsg::ProxyResponse { data }) - .map_err(|e| ProtocolMsg::ProtocolErrorResponse(e.into())); - - Some(result) - } else { - None - } - } + // pub(super) fn proxy( + // req: &ProtocolMsg, + // state: &mut ProtocolState, + // ) -> ProtocolRouteResponse { + // if let ProtocolMsg::ProxyRequest { data: req_data } = req { + // let result = state + // .app_client + // .send(req_data) + // .map(|data| ProtocolMsg::ProxyResponse { data }) + // .map_err(|e| ProtocolMsg::ProtocolErrorResponse(e.into())); + + // Some(result) + // } else { + // None + // } + // } pub(super) fn provision( req: &ProtocolMsg, diff --git a/src/qos_core/src/reaper.rs b/src/qos_core/src/reaper.rs index 79148ec20..27201fb4f 100644 --- a/src/qos_core/src/reaper.rs +++ b/src/qos_core/src/reaper.rs @@ -4,18 +4,23 @@ //! //! The pivot is an executable the enclave runs to initialize the secure //! applications. -use std::process::Command; +use std::{ + process::Command, + sync::{Arc, RwLock}, + time::Duration, +}; use qos_nsm::NsmProvider; use crate::{ + async_server::AsyncSocketServer, handles::Handles, - io::SocketAddress, + io::AsyncStreamPool, protocol::{ + async_processor::AsyncProcessor, services::boot::{PivotConfig, RestartPolicy}, - Processor, ProtocolPhase, + ProtocolPhase, ProtocolState, }, - server::SocketServer, }; /// Delay for restarting the pivot app if the process exits. @@ -28,40 +33,114 @@ pub const REAPER_EXIT_DELAY_IN_SECONDS: u64 = 3; /// and pivot binary. pub struct Reaper; impl Reaper { - /// Run the Reaper. + /// Run the Reaper, with the given shutdown oneshot channel Receiver. If a signal is passed (regardless of value) + /// the Reaper will shut down and clean up the server. It is the responsibility of the caller to send the shutdown + /// signal. /// /// # Panics /// /// - If spawning the pivot errors. /// - If waiting for the pivot errors. + #[allow(dead_code)] + #[allow(clippy::too_many_lines)] pub fn execute( handles: &Handles, nsm: Box, - addr: SocketAddress, - app_addr: SocketAddress, + pool: AsyncStreamPool, + app_pool: AsyncStreamPool, test_only_init_phase_override: Option, ) { let handles2 = handles.clone(); + let inter_state = Arc::new(RwLock::new(InterState::Booting)); + let server_state = inter_state.clone(); + std::thread::spawn(move || { - let processor = Processor::new( - nsm, - handles2, - app_addr, - test_only_init_phase_override, - ); - SocketServer::listen(addr, processor).unwrap(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + // run the state processor inside a tokio runtime in this thread + // create the state + let protocol_state = ProtocolState::new( + nsm, + handles2.clone(), + test_only_init_phase_override, + ); + // send a shared version of state and the async pool to each processor + let mut processor = AsyncProcessor::new( + protocol_state.shared(), + app_pool.shared(), + ); + // listen_all will multiplex the processor accross all sockets + let mut server = + AsyncSocketServer::listen_all(pool, &processor) + .expect("unable to get listen task list"); + + loop { + // see if we got interrupted + if *server_state.read().unwrap() == InterState::Quitting + { + server.terminate(); + return; + } + + let (manifest_present, pool_size) = + get_pool_size_from_pivot_args(&handles2); + + if manifest_present { + let pool_size = pool_size.unwrap_or(1); + // expand server to pool_size + 1 (due to qos-host extra socket) + server.listen_to(pool_size + 1, &processor).expect( + "unable to listen_to on the running server", + ); + // expand app connections to pool_size + processor.expand_to(pool_size).await.expect( + "unable to expand_to on the processor app pool", + ); + + *server_state.write().unwrap() = + InterState::PivotReady; + eprintln!("Manifest is present, breaking out of server check loop"); + break; + } + + tokio::time::sleep(Duration::from_millis(100)).await; + } + + eprintln!( + "Reaper server post-expansion, waiting for shutdown" + ); + while *server_state.read().unwrap() != InterState::Quitting + { + tokio::time::sleep(Duration::from_millis(100)).await; + } + + eprintln!("Reaper server shutdown"); + server.terminate(); // ensure we cleanup the sockets + *server_state.write().unwrap() = InterState::Quitting; + }); }); loop { + let server_state = *inter_state.read().unwrap(); + // helper for integration tests and manual runs aka qos_core binary + if server_state == InterState::Quitting { + eprintln!("quit called by ctrl+c"); + std::process::exit(1); + } + if handles.quorum_key_exists() && handles.pivot_exists() && handles.manifest_envelope_exists() + && server_state == InterState::PivotReady { // The state required to pivot exists, so we can break this // holding pattern and start the pivot. break; } + eprintln!("Reaper looping"); std::thread::sleep(std::time::Duration::from_secs(1)); } @@ -99,15 +178,100 @@ impl Reaper { .expect("Failed to spawn") .wait() .expect("Pivot executable never started..."); - println!("Pivot exited with status: {status}"); + println!("Pivot (no restart) exited with status: {status}"); } } std::thread::sleep(std::time::Duration::from_secs( REAPER_EXIT_DELAY_IN_SECONDS, )); + println!("Reaper exiting ... "); } } -// See qos_test/tests/reaper for tests +// basic helper for x-thread comms in Reaper +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum InterState { + // We're booting, no pivot yet + Booting, + // We've booted and pivot is ready + PivotReady, + // We're quitting (ctrl+c for tests and such) + Quitting, +} + +// return if we have manifest and get pool_size args if present from it +fn get_pool_size_from_pivot_args(handles: &Handles) -> (bool, Option) { + if let Ok(envelope) = handles.get_manifest_envelope() { + (true, extract_pool_size_arg(&envelope.manifest.pivot.args)) + } else { + (false, None) + } +} + +// find the u32 value of --pool-size argument passed to the pivot if present +fn extract_pool_size_arg(args: &[String]) -> Option { + if let Some((i, _)) = + args.iter().enumerate().find(|(_, a)| *a == "--pool-size") + { + if let Some(pool_size_str) = args.get(i + 1) { + match pool_size_str.parse::() { + Ok(pool_size) => Some(pool_size), + Err(_) => None, + } + } else { + None + } + } else { + None + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn extract_pool_size_arg_works() { + // no arg + assert_eq!( + extract_pool_size_arg(&vec![ + "unrelated".to_owned(), + "--args".to_owned(), + ]), + None + ); + + // should work + assert_eq!( + extract_pool_size_arg(&vec![ + "--pool-size".to_owned(), + "8".to_owned(), + ]), + Some(8) + ); + + // wrong number, expect None + assert_eq!( + extract_pool_size_arg(&vec![ + "--pool-size".to_owned(), + "8a".to_owned(), + ]), + None + ); + + // duplicate arg, use 1st + assert_eq!( + extract_pool_size_arg(&vec![ + "--pool-size".to_owned(), + "8".to_owned(), + "--pool-size".to_owned(), + "9".to_owned(), + ]), + Some(8) + ); + } +} + +// See qos_test/tests/async_reaper for more tests diff --git a/src/qos_core/src/server.rs b/src/qos_core/src/server.rs deleted file mode 100644 index 238d0dbf1..000000000 --- a/src/qos_core/src/server.rs +++ /dev/null @@ -1,58 +0,0 @@ -//! Streaming socket based server for use in an enclave. Listens for connections -//! from [`crate::client::Client`]. - -use std::marker::PhantomData; - -use crate::io::{self, Listener, SocketAddress}; - -/// Error variants for [`SocketServer`] -#[derive(Debug)] -pub enum SocketServerError { - /// `io::IOError` wrapper. - IOError(io::IOError), -} - -impl From for SocketServerError { - fn from(err: io::IOError) -> Self { - Self::IOError(err) - } -} - -/// Something that can process requests. -pub trait RequestProcessor { - /// Process an incoming request and return a response. - /// - /// The request and response are raw bytes. Likely this should be encoded - /// data and logic inside of this function should take care of decoding the - /// request and encoding a response. - fn process(&mut self, request: Vec) -> Vec; -} - -/// A bare bones, socket based server. -pub struct SocketServer { - _phantom: PhantomData, -} - -impl SocketServer { - /// Listen and respond to incoming requests with the given `processor`. - pub fn listen( - addr: SocketAddress, - mut processor: R, - ) -> Result<(), SocketServerError> { - println!("`SocketServer` listening on {addr:?}"); - - let listener = Listener::listen(addr)?; - - for stream in listener { - match stream.recv() { - Ok(payload) => { - let response = processor.process(payload); - let _ = stream.send(&response); - } - Err(err) => eprintln!("Server::listen error: {err:?}"), - } - } - - Ok(()) - } -} diff --git a/src/qos_host/src/async_host.rs b/src/qos_host/src/async_host.rs new file mode 100644 index 000000000..9bee29c26 --- /dev/null +++ b/src/qos_host/src/async_host.rs @@ -0,0 +1,293 @@ +//! Enclave host implementation. The host primarily consists of a HTTP server +//! that proxies requests to the enclave by establishing a client connection +//! with the enclave. +//! +//! # IMPLEMENTERS NOTE +//! +//! The host HTTP server is currently implemented using the `axum` framework. +//! This may be swapped out in the the future in favor of a lighter package in +//! order to slim the dependency tree. In the mean time, these resources can +//! help familiarize you with the abstractions: +//! +//! * Request body extractors: +//! * Response: +//! * Responding with error: +#![forbid(unsafe_code)] +#![deny(clippy::all)] +#![warn(missing_docs, clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] + +use std::{net::SocketAddr, sync::Arc}; + +use axum::{ + body::Bytes, + extract::{DefaultBodyLimit, State}, + http::StatusCode, + response::{Html, IntoResponse}, + routing::{get, post}, + Json, Router, +}; +use borsh::BorshDeserialize; +use qos_core::{ + async_client::AsyncClient, + io::{SharedAsyncStreamPool, TimeVal}, + protocol::{msg::ProtocolMsg, ProtocolError, ProtocolPhase}, +}; + +use crate::{ + EnclaveInfo, EnclaveVitalStats, Error, ENCLAVE_HEALTH, ENCLAVE_INFO, + HOST_HEALTH, MAX_ENCODED_MSG_LEN, MESSAGE, +}; + +/// Resource shared across tasks in the `AsyncHostServer`. +#[derive(Debug)] +struct AsyncQosHostState { + enclave_client: AsyncClient, +} + +/// HTTP server for the host of the enclave; proxies requests to the enclave. +#[allow(clippy::module_name_repetitions)] +pub struct AsyncHostServer { + enclave_pool: SharedAsyncStreamPool, + timeout: TimeVal, + addr: SocketAddr, + base_path: Option, +} + +impl AsyncHostServer { + /// Create a new `HostServer`. See `Self::serve` for starting the + /// server. + #[must_use] + pub fn new( + enclave_pool: SharedAsyncStreamPool, + timeout: TimeVal, + addr: SocketAddr, + base_path: Option, + ) -> Self { + Self { enclave_pool, timeout, addr, base_path } + } + + fn path(&self, endpoint: &str) -> String { + if let Some(path) = self.base_path.as_ref() { + format!("/{path}{endpoint}") + } else { + format!("/qos{endpoint}") + } + } + + /// Start the server, running indefinitely. + /// + /// # Panics + /// + /// Panics if there is an issue starting the server. + // pub async fn serve(&self) -> Result<(), String> { + pub async fn serve(&self) { + let state = Arc::new(AsyncQosHostState { + enclave_client: AsyncClient::new( + self.enclave_pool.clone(), + self.timeout, + ), + }); + + let app = Router::new() + .route(&self.path(HOST_HEALTH), get(Self::host_health)) + .route(&self.path(ENCLAVE_HEALTH), get(Self::enclave_health)) + .route(&self.path(MESSAGE), post(Self::message)) + .route(&self.path(ENCLAVE_INFO), get(Self::enclave_info)) + .layer(DefaultBodyLimit::disable()) + .with_state(state); + + println!("AsyncHostServer listening on {}", self.addr); + + axum::Server::bind(&self.addr) + .serve(app.into_make_service()) + .await + .unwrap(); + } + + /// Health route handler. + #[allow(clippy::unused_async)] + async fn host_health( + _: State>, + ) -> impl IntoResponse { + println!("Host health..."); + Html("Ok!") + } + + /// Health route handler. + async fn enclave_health( + State(state): State>, + ) -> impl IntoResponse { + println!("Enclave health..."); + + let encoded_request = borsh::to_vec(&ProtocolMsg::StatusRequest) + .expect("ProtocolMsg can always serialize. qed."); + let encoded_response = match state + .enclave_client + .call(&encoded_request) + .await + { + Ok(encoded_response) => encoded_response, + Err(e) => { + let msg = format!("Error while trying to send socket request to enclave: {e:?}"); + eprintln!("{msg}"); + return (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)); + } + }; + + let response = match ProtocolMsg::try_from_slice(&encoded_response) { + Ok(r) => r, + Err(e) => { + let msg = format!("Error deserializing response from enclave, make sure qos_host version match qos_core: {e}"); + eprintln!("{msg}"); + return (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)); + } + }; + + match response { + ProtocolMsg::StatusResponse(phase) => { + let inner = format!("{phase:?}"); + let status = match phase { + ProtocolPhase::UnrecoverableError + | ProtocolPhase::WaitingForBootInstruction + | ProtocolPhase::WaitingForQuorumShards + | ProtocolPhase::WaitingForForwardedKey => StatusCode::SERVICE_UNAVAILABLE, + ProtocolPhase::QuorumKeyProvisioned + | ProtocolPhase::GenesisBooted => StatusCode::OK, + }; + + (status, Html(inner)) + } + other => { + let msg = format!("Unexpected response: Expected a ProtocolMsg::StatusResponse, but got: {other:?}"); + eprintln!("{msg}"); + (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)) + } + } + } + + async fn enclave_info( + State(state): State>, + ) -> Result, Error> { + println!("Enclave info..."); + + let enc_status_req = borsh::to_vec(&ProtocolMsg::StatusRequest) + .expect("ProtocolMsg can always serialize. qed."); + let enc_status_resp = + state.enclave_client.call(&enc_status_req).await.map_err(|e| { + Error(format!("error sending status request to enclave: {e:?}")) + })?; + + let status_resp = match ProtocolMsg::try_from_slice(&enc_status_resp) { + Ok(status_resp) => status_resp, + Err(e) => { + return Err(Error(format!("error deserializing status response from enclave, make sure qos_host version match qos_core: {e:?}"))); + } + }; + let phase = match status_resp { + ProtocolMsg::StatusResponse(phase) => phase, + other => { + return Err(Error(format!("unexpected response: expected a ProtocolMsg::StatusResponse, but got: {other:?}"))); + } + }; + + let enc_manifest_envelope_req = + borsh::to_vec(&ProtocolMsg::ManifestEnvelopeRequest) + .expect("ProtocolMsg can always serialize. qed."); + let enc_manifest_envelope_resp = state + .enclave_client + .call(&enc_manifest_envelope_req) + .await + .map_err(|e| { + Error(format!( + "error while trying to send manifest envelope socket request to enclave: {e:?}" + )) + })?; + + let manifest_envelope_resp = ProtocolMsg::try_from_slice( + &enc_manifest_envelope_resp, + ) + .map_err(|e| + Error(format!("error deserializing manifest envelope response from enclave, make sure qos_host version match qos_core: {e}")) + )?; + + let manifest_envelope = match manifest_envelope_resp { + ProtocolMsg::ManifestEnvelopeResponse { manifest_envelope } => { + *manifest_envelope + } + other => { + return Err( + Error(format!("unexpected response: expected a ProtocolMsg::ManifestEnvelopeResponse, but got: {other:?}")) + ); + } + }; + + let vitals_log = if let Some(m) = manifest_envelope.as_ref() { + serde_json::to_string(&EnclaveVitalStats { + phase, + namespace: m.manifest.namespace.name.clone(), + nonce: m.manifest.namespace.nonce, + pivot_hash: m.manifest.pivot.hash, + pcr0: m.manifest.enclave.pcr0.clone(), + pivot_args: m.manifest.pivot.args.clone(), + }) + .expect("always valid json. qed.") + } else { + serde_json::to_string(&phase).expect("always valid json. qed.") + }; + println!("{vitals_log}"); + + let info = EnclaveInfo { phase, manifest_envelope }; + + Ok(Json(info)) + } + + /// Message route handler. + async fn message( + State(state): State>, + encoded_request: Bytes, + ) -> impl IntoResponse { + if encoded_request.len() > MAX_ENCODED_MSG_LEN { + return ( + StatusCode::BAD_REQUEST, + borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( + ProtocolError::OversizeMsg, + )) + .expect("ProtocolMsg can always serialize. qed."), + ); + } + + // DEBUG: remove later + match ProtocolMsg::try_from_slice(&encoded_request) { + Ok(r) => eprintln!("Received message: {r}"), + Err(e) => eprintln!("Unable to decode request: {e}"), + } + + match state.enclave_client.call(&encoded_request).await { + Ok(encoded_response) => { + // DEBUG: remove later + match ProtocolMsg::try_from_slice(&encoded_response) { + Ok(r) => { + eprintln!("Enclave responded with: {r}"); + } + Err(e) => { + eprintln!("Error deserializing response from enclave, make sure qos_host version match qos_core: {e}"); + } + }; + + (StatusCode::OK, encoded_response) + } + Err(e) => { + eprintln!("Error while trying to send request over socket to enclave: {e:?}"); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( + ProtocolError::EnclaveClient, + )) + .expect("ProtocolMsg can always serialize. qed."), + ) + } + } + } +} diff --git a/src/qos_host/src/cli.rs b/src/qos_host/src/cli.rs index 818d2e309..bd82d26ac 100644 --- a/src/qos_host/src/cli.rs +++ b/src/qos_host/src/cli.rs @@ -8,16 +8,15 @@ use std::{ use qos_core::{ cli::{CID, PORT, USOCK}, - io::SocketAddress, + io::{AsyncStreamPool, SocketAddress, TimeVal, TimeValLike}, parser::{GetParserForOptions, OptionsParser, Parser, Token}, }; -use crate::HostServer; - const HOST_IP: &str = "host-ip"; const HOST_PORT: &str = "host-port"; const ENDPOINT_BASE_PATH: &str = "endpoint-base-path"; const VSOCK_TO_HOST: &str = "vsock-to-host"; +const SOCKET_TIMEOUT: &str = "socket-timeout"; struct HostParser; impl GetParserForOptions for HostParser { @@ -54,6 +53,11 @@ impl GetParserForOptions for HostParser { Token::new(ENDPOINT_BASE_PATH, "base path for all endpoints. e.g. /enclave-health") .takes_value(true) ) + .token( + Token::new(SOCKET_TIMEOUT, "maximum time in ms a connect to the USOCK/VSOCK will take") + .takes_value(true) + .default_value(qos_core::DEFAULT_SOCKET_TIMEOUT) + ) .token( Token::new(VSOCK_TO_HOST, "whether to add the to-host svm flag to the enclave vsock connection. Valid options are `true` or `false`") .takes_value(true) @@ -106,6 +110,48 @@ impl HostOpts { SocketAddr::new(IpAddr::V4(ip), port) } + pub(crate) fn socket_timeout(&self) -> TimeVal { + let default_timeout = &qos_core::DEFAULT_SOCKET_TIMEOUT.to_owned(); + let timeout_str = + self.parsed.single(SOCKET_TIMEOUT).unwrap_or(default_timeout); + TimeVal::milliseconds( + timeout_str.parse().expect("invalid timeout value"), + ) + } + + /// Create a new `AsyncPool` of `AsyncStream` using the list of `SocketAddress` for the enclave server and + /// return the new `AsyncPool`. + pub(crate) fn enclave_pool( + &self, + ) -> Result { + match ( + self.parsed.single(CID), + self.parsed.single(PORT), + self.parsed.single(USOCK), + ) { + #[cfg(feature = "vm")] + (Some(c), Some(p), None) => { + let c = c.parse().map_err(|_| { + qos_core::io::IOError::ConnectAddressInvalid + })?; + let p = p.parse().map_err(|_| { + qos_core::io::IOError::ConnectAddressInvalid + })?; + + let address = + SocketAddress::new_vsock(c, p, self.to_host_flag()); + + AsyncStreamPool::new(address, 1) // qos_host needs only 1 + } + (None, None, Some(u)) => { + let address = SocketAddress::new_unix(u); + + AsyncStreamPool::new(address, 1) + } + _ => panic!("Invalid socket opts"), + } + } + /// Get the `SocketAddress` for the enclave server. /// /// # Panics @@ -167,6 +213,8 @@ impl HostOpts { pub struct CLI; impl CLI { /// Execute the command line interface. + /// # Panics + /// If pool creation fails pub async fn execute() { let mut args: Vec = env::args().collect(); let options = HostOpts::new(&mut args); @@ -176,8 +224,12 @@ impl CLI { } else if options.parsed.help() { println!("{}", options.parsed.info()); } else { - HostServer::new( - options.enclave_addr(), + crate::async_host::AsyncHostServer::new( + options + .enclave_pool() + .expect("unable to create enclave pool") + .shared(), + options.socket_timeout(), options.host_addr(), options.base_path(), ) diff --git a/src/qos_host/src/lib.rs b/src/qos_host/src/lib.rs index 21bc6004f..9d77102ec 100644 --- a/src/qos_host/src/lib.rs +++ b/src/qos_host/src/lib.rs @@ -17,32 +17,20 @@ #![warn(missing_docs, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] -use std::{net::SocketAddr, sync::Arc}; - use axum::{ - body::Bytes, - extract::{DefaultBodyLimit, State}, http::StatusCode, - response::{Html, IntoResponse, Response}, - routing::{get, post}, - Json, Router, + response::{IntoResponse, Response}, + Json, }; -use borsh::BorshDeserialize; -use qos_core::{ - client::Client, - io::{SocketAddress, TimeVal, TimeValLike}, - protocol::{ - msg::ProtocolMsg, services::boot::ManifestEnvelope, Hash256, - ProtocolError, ProtocolPhase, ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, - }, +use qos_core::protocol::{ + services::boot::ManifestEnvelope, Hash256, ProtocolPhase, }; +pub mod async_host; pub mod cli; const MEGABYTE: usize = 1024 * 1024; const MAX_ENCODED_MSG_LEN: usize = 256 * MEGABYTE; -const QOS_SOCKET_CLIENT_TIMEOUT_SECS: i64 = - ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS + 2; /// Simple error that implements [`IntoResponse`] so it can /// be returned from handlers as an http response (and not get silently @@ -61,19 +49,6 @@ impl IntoResponse for Error { } } -/// Resource shared across tasks in the [`HostServer`]. -#[derive(Debug)] -struct QosHostState { - enclave_client: Client, -} - -/// HTTP server for the host of the enclave; proxies requests to the enclave. -pub struct HostServer { - enclave_addr: SocketAddress, - addr: SocketAddr, - base_path: Option, -} - const HOST_HEALTH: &str = "/host-health"; const ENCLAVE_HEALTH: &str = "/enclave-health"; const MESSAGE: &str = "/message"; @@ -110,221 +85,3 @@ pub struct JsonError { /// Error message. pub error: String, } - -impl HostServer { - /// Create a new [`HostServer`]. See [`Self::serve`] for starting the - /// server. - #[must_use] - pub fn new( - enclave_addr: SocketAddress, - addr: SocketAddr, - base_path: Option, - ) -> Self { - Self { enclave_addr, addr, base_path } - } - - fn path(&self, endpoint: &str) -> String { - if let Some(path) = self.base_path.as_ref() { - format!("/{path}{endpoint}") - } else { - format!("/qos{endpoint}") - } - } - - /// Start the server, running indefinitely. - /// - /// # Panics - /// - /// Panics if there is an issue starting the server. - // pub async fn serve(&self) -> Result<(), String> { - pub async fn serve(&self) { - let state = Arc::new(QosHostState { - enclave_client: Client::new( - self.enclave_addr.clone(), - TimeVal::seconds(QOS_SOCKET_CLIENT_TIMEOUT_SECS), - ), - }); - - let app = Router::new() - .route(&self.path(HOST_HEALTH), get(Self::host_health)) - .route(&self.path(ENCLAVE_HEALTH), get(Self::enclave_health)) - .route(&self.path(MESSAGE), post(Self::message)) - .route(&self.path(ENCLAVE_INFO), get(Self::enclave_info)) - .layer(DefaultBodyLimit::disable()) - .with_state(state); - - println!("HostServer listening on {}", self.addr); - - axum::Server::bind(&self.addr) - .serve(app.into_make_service()) - .await - .unwrap(); - } - - /// Health route handler. - #[allow(clippy::unused_async)] - async fn host_health(_: State>) -> impl IntoResponse { - println!("Host health..."); - Html("Ok!") - } - - /// Health route handler. - #[allow(clippy::unused_async)] - async fn enclave_health( - State(state): State>, - ) -> impl IntoResponse { - println!("Enclave health..."); - - let encoded_request = borsh::to_vec(&ProtocolMsg::StatusRequest) - .expect("ProtocolMsg can always serialize. qed."); - let encoded_response = match state.enclave_client.send(&encoded_request) - { - Ok(encoded_response) => encoded_response, - Err(e) => { - let msg = format!("Error while trying to send socket request to enclave: {e:?}"); - eprintln!("{msg}"); - return (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)); - } - }; - - let response = match ProtocolMsg::try_from_slice(&encoded_response) { - Ok(r) => r, - Err(e) => { - let msg = format!("Error deserializing response from enclave, make sure qos_host version match qos_core: {e}"); - eprintln!("{msg}"); - return (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)); - } - }; - - match response { - ProtocolMsg::StatusResponse(phase) => { - let inner = format!("{phase:?}"); - let status = match phase { - ProtocolPhase::UnrecoverableError - | ProtocolPhase::WaitingForBootInstruction - | ProtocolPhase::WaitingForQuorumShards - | ProtocolPhase::WaitingForForwardedKey => StatusCode::SERVICE_UNAVAILABLE, - ProtocolPhase::QuorumKeyProvisioned - | ProtocolPhase::GenesisBooted => StatusCode::OK, - }; - - (status, Html(inner)) - } - other => { - let msg = format!("Unexpected response: Expected a ProtocolMsg::StatusResponse, but got: {other:?}"); - eprintln!("{msg}"); - (StatusCode::INTERNAL_SERVER_ERROR, Html(msg)) - } - } - } - - #[allow(clippy::unused_async)] - async fn enclave_info( - State(state): State>, - ) -> Result, Error> { - println!("Enclave info..."); - - let enc_status_req = borsh::to_vec(&ProtocolMsg::StatusRequest) - .expect("ProtocolMsg can always serialize. qed."); - let enc_status_resp = - state.enclave_client.send(&enc_status_req).map_err(|e| { - Error(format!("error sending status request to enclave: {e:?}")) - })?; - - let status_resp = match ProtocolMsg::try_from_slice(&enc_status_resp) { - Ok(status_resp) => status_resp, - Err(e) => { - return Err(Error(format!("error deserializing status response from enclave, make sure qos_host version match qos_core: {e:?}"))); - } - }; - let phase = match status_resp { - ProtocolMsg::StatusResponse(phase) => phase, - other => { - return Err(Error(format!("unexpected response: expected a ProtocolMsg::StatusResponse, but got: {other:?}"))); - } - }; - - let enc_manifest_envelope_req = - borsh::to_vec(&ProtocolMsg::ManifestEnvelopeRequest) - .expect("ProtocolMsg can always serialize. qed."); - let enc_manifest_envelope_resp = state - .enclave_client - .send(&enc_manifest_envelope_req) - .map_err(|e| { - Error(format!( - "error while trying to send manifest envelope socket request to enclave: {e:?}" - )) - })?; - - let manifest_envelope_resp = ProtocolMsg::try_from_slice( - &enc_manifest_envelope_resp, - ) - .map_err(|e| - Error(format!("error deserializing manifest envelope response from enclave, make sure qos_host version match qos_core: {e}")) - )?; - - let manifest_envelope = match manifest_envelope_resp { - ProtocolMsg::ManifestEnvelopeResponse { manifest_envelope } => { - *manifest_envelope - } - other => { - return Err( - Error(format!("unexpected response: expected a ProtocolMsg::ManifestEnvelopeResponse, but got: {other:?}")) - ); - } - }; - - let vitals_log = if let Some(m) = manifest_envelope.as_ref() { - serde_json::to_string(&EnclaveVitalStats { - phase, - namespace: m.manifest.namespace.name.clone(), - nonce: m.manifest.namespace.nonce, - pivot_hash: m.manifest.pivot.hash, - pcr0: m.manifest.enclave.pcr0.clone(), - pivot_args: m.manifest.pivot.args.clone(), - }) - .expect("always valid json. qed.") - } else { - serde_json::to_string(&phase).expect("always valid json. qed.") - }; - println!("{vitals_log}"); - - let info = EnclaveInfo { phase, manifest_envelope }; - - Ok(Json(info)) - } - - /// Message route handler. - #[allow(clippy::unused_async)] - async fn message( - State(state): State>, - encoded_request: Bytes, - ) -> impl IntoResponse { - if encoded_request.len() > MAX_ENCODED_MSG_LEN { - return ( - StatusCode::BAD_REQUEST, - borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( - ProtocolError::OversizeMsg, - )) - .expect("ProtocolMsg can always serialize. qed."), - ); - } - - match state.enclave_client.send(&encoded_request) { - Ok(encoded_response) => (StatusCode::OK, encoded_response), - Err(e) => { - let msg = - format!("Error while trying to send request over socket to enclave: {e:?}"); - eprint!("{msg}"); - - ( - StatusCode::INTERNAL_SERVER_ERROR, - borsh::to_vec(&ProtocolMsg::ProtocolErrorResponse( - ProtocolError::EnclaveClient, - )) - .expect("ProtocolMsg can always serialize. qed."), - ) - } - } - } -} diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml index ba49e5c02..e6201c9e2 100644 --- a/src/qos_net/Cargo.toml +++ b/src/qos_net/Cargo.toml @@ -11,6 +11,7 @@ borsh = { version = "1.0", features = [ "std", "derive", ], default-features = false } +futures = { version = "0.3.30", optional = false } serde = { version = "1", features = ["derive"], default-features = false } hickory-resolver = { version = "0.25.2", features = [ "tokio", # for async @@ -19,19 +20,20 @@ hickory-resolver = { version = "0.25.2", features = [ rand = { version = "0.9.1", features = [ "thread_rng", ], default-features = false, optional = true } -tokio = { version = "1.38.0", default-features = false } +tokio = { version = "1.38.0", features = ["io-util", "macros", "net", "rt-multi-thread", "time"], default-features = false, optional = true } +tokio-rustls = { version = "0.26.2", optional = true } [dev-dependencies] qos_test_primitives = { path = "../qos_test_primitives" } httparse = { version = "1.9.4", default-features = false } chunked_transfer = { version = "1.5.0", default-features = false } +rustls = { version = "0.23.5" } serde_json = { version = "1.0.121", features = [ "std", ], default-features = false } -rustls = { version = "0.23.5" } webpki-roots = { version = "0.26.1" } [features] -default = ["proxy"] # keep this as a default feature ensures we lint by default -proxy = ["rand", "hickory-resolver"] -vm = [] +default = ["proxy"] # keep this as a default feature ensures we lint by default +proxy = ["hickory-resolver", "rand", "tokio", "tokio-rustls"] +vm = ["qos_core/vm"] diff --git a/src/qos_net/src/async_proxy.rs b/src/qos_net/src/async_proxy.rs new file mode 100644 index 000000000..194bcb294 --- /dev/null +++ b/src/qos_net/src/async_proxy.rs @@ -0,0 +1,185 @@ +//! Protocol proxy for our remote QOS net proxy +use borsh::BorshDeserialize; +use futures::Future; +use qos_core::{ + async_server::{AsyncSocketServer, SocketServerError}, + io::{AsyncListener, AsyncStream, AsyncStreamPool, IOError}, +}; + +use crate::{ + async_proxy_connection::AsyncProxyConnection, error::QosNetError, + proxy_msg::ProxyMsg, +}; + +const MEGABYTE: usize = 1024 * 1024; +const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; + +/// Socket<>TCP proxy to enable remote connections +pub struct AsyncProxy { + tcp_connection: Option, + sock_stream: AsyncStream, +} + +impl AsyncProxy { + /// Create a new AsyncProxy from the given AsyncStream, with empty tcp_connection + pub fn new(sock_stream: AsyncStream) -> Self { + Self { tcp_connection: None, sock_stream } + } + + /// Create a new connection by resolving a name into an IP + /// address. The TCP connection is opened and saved in internal state. + async fn connect_by_name( + &mut self, + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> ProxyMsg { + match AsyncProxyConnection::new_from_name( + hostname.clone(), + port, + dns_resolvers.clone(), + dns_port, + ) + .await + { + Ok(conn) => { + let connection_id = conn.id; + let remote_ip = conn.ip.clone(); + self.tcp_connection = Some(conn); + println!("Connection to {hostname} established"); + ProxyMsg::ConnectResponse { connection_id, remote_ip } + } + Err(e) => { + println!("error while establishing connection: {e:?}"); + ProxyMsg::ProxyError(e) + } + } + } + + /// Create a new connection, targeting an IP address directly. + /// address. The TCP connection is opened and saved in internal state. + async fn connect_by_ip(&mut self, ip: String, port: u16) -> ProxyMsg { + match AsyncProxyConnection::new_from_ip(ip.clone(), port).await { + Ok(conn) => { + let connection_id = conn.id; + let remote_ip = conn.ip.clone(); + self.tcp_connection = Some(conn); + println!("Connection to {ip} established and saved as ID {connection_id}"); + ProxyMsg::ConnectResponse { connection_id, remote_ip } + } + Err(e) => { + println!("error while establishing connection: {e:?}"); + ProxyMsg::ProxyError(e) + } + } + } + + // processes given `ProxyMsg` if it's a connection request or returns a `ProxyError` otherwise. + async fn process_req(&mut self, req_bytes: Vec) -> Vec { + if req_bytes.len() > MAX_ENCODED_MSG_LEN { + return borsh::to_vec(&ProxyMsg::ProxyError( + QosNetError::OversizedPayload, + )) + .expect("ProtocolMsg can always be serialized. qed."); + } + + let resp = match ProxyMsg::try_from_slice(&req_bytes) { + Ok(req) => match req { + // TODO: do we need this?? + ProxyMsg::StatusRequest => ProxyMsg::StatusResponse(0), + ProxyMsg::ConnectByNameRequest { + hostname, + port, + dns_resolvers, + dns_port, + } => { + self.connect_by_name( + hostname, + port, + dns_resolvers, + dns_port, + ) + .await + } + ProxyMsg::ConnectByIpRequest { ip, port } => { + self.connect_by_ip(ip, port).await + } + _ => ProxyMsg::ProxyError(QosNetError::InvalidMsg), + }, + Err(_) => ProxyMsg::ProxyError(QosNetError::InvalidMsg), + }; + + borsh::to_vec(&resp) + .expect("Protocol message can always be serialized. qed!") + } +} + +impl AsyncProxy { + async fn run(&mut self) -> Result<(), IOError> { + loop { + // Only try to process ProxyMsg content on the USOCK/VSOCK if we're not connected to TCP yet. + // If we're connected, we should be a "dumb pipe" using the `tokio::io::copy_bidirectional` + // which is handled in the connect functions above + if self.tcp_connection.is_none() { + let req_bytes = self.sock_stream.recv().await?; + let resp_bytes = self.process_req(req_bytes).await; + self.sock_stream.send(&resp_bytes).await?; + if let Some(tcp_connection) = &mut self.tcp_connection { + let (_, _) = tokio::io::copy_bidirectional( + &mut self.sock_stream, + &mut tcp_connection.tcp_stream, + ) + .await?; + + // Once the "dumb pipe" is closed we need to clear our tcp_connection and refresh + // the proxy socket stream by using shutdown + self.tcp_connection = None; + + break Ok(()); // return to the accept loop + } + } + } + } +} + +pub trait AsyncProxyServer { + fn listen_proxy( + pool: AsyncStreamPool, + ) -> impl Future, SocketServerError>> + Send; +} + +impl AsyncProxyServer for AsyncSocketServer { + /// Listen on a tcp proxy server in a way that allows the USOCK/VSOCK to be used as a + /// dumb pipe after getting the `connect*` calls. + async fn listen_proxy( + pool: AsyncStreamPool, + ) -> Result, SocketServerError> { + println!( + "`AsyncSocketServer` proxy listening on pool size {}", + pool.len() + ); + + let listeners = pool.listen()?; + + let mut tasks = Vec::new(); + for listener in listeners { + let task = + tokio::spawn(async move { accept_loop_proxy(listener).await }); + + tasks.push(task); + } + + Ok(Box::new(Self { pool, tasks })) + } +} + +async fn accept_loop_proxy( + listener: AsyncListener, +) -> Result<(), SocketServerError> { + loop { + let stream = listener.accept().await?; + let mut proxy = AsyncProxy::new(stream); + proxy.run().await?; + } +} diff --git a/src/qos_net/src/async_proxy_connection.rs b/src/qos_net/src/async_proxy_connection.rs new file mode 100644 index 000000000..ebee01394 --- /dev/null +++ b/src/qos_net/src/async_proxy_connection.rs @@ -0,0 +1,132 @@ +//! Contains logic for remote connection establishment: DNS resolution and TCP +//! connection. +use std::net::{AddrParseError, IpAddr, SocketAddr}; + +use hickory_resolver::{ + config::{NameServerConfigGroup, ResolverConfig}, + name_server::TokioConnectionProvider, + TokioResolver, +}; +use rand::Rng; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; + +use crate::error::QosNetError; + +/// Struct representing a TCP connection held on our proxy +pub struct AsyncProxyConnection { + /// Unsigned integer with the connection ID (random positive int) + pub id: u128, + /// IP address of the remote host + pub ip: String, + /// TCP stream object + pub(crate) tcp_stream: TcpStream, +} + +impl AsyncProxyConnection { + /// Create a new `ProxyConnection` from a name. This results in a DNS + /// request + TCP connection + pub async fn new_from_name( + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> Result { + let ip = resolve_hostname(hostname, dns_resolvers, dns_port).await?; + + // Generate a new random u32 to get an ID. We'll use it to name our + // socket. This will be our connection ID. + let connection_id = { + let mut rng = rand::rng(); + rng.random::() + }; + + let tcp_addr = SocketAddr::new(ip, port); + let tcp_stream = TcpStream::connect(tcp_addr).await?; + Ok(AsyncProxyConnection { + id: connection_id, + ip: ip.to_string(), + tcp_stream, + }) + } + + /// Create a new `ProxyConnection` from an IP address. This results in a + /// new TCP connection + pub async fn new_from_ip( + ip: String, + port: u16, + ) -> Result { + // Generate a new random u32 to get an ID. We'll use it to name our + // socket. This will be our connection ID. + let connection_id = { + let mut rng = rand::rng(); + rng.random::() + }; + + let ip_addr = ip.parse()?; + let tcp_addr = SocketAddr::new(ip_addr, port); + let tcp_stream = TcpStream::connect(tcp_addr).await?; + + Ok(AsyncProxyConnection { id: connection_id, ip, tcp_stream }) + } +} + +impl AsyncProxyConnection { + pub async fn read( + &mut self, + buf: &mut [u8], + ) -> Result { + self.tcp_stream.read(buf).await + } + + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.tcp_stream.write(buf).await + } + + pub async fn flush(&mut self) -> std::io::Result<()> { + self.tcp_stream.flush().await + } +} + +// Resolve a name into an IP address +pub async fn resolve_hostname( + hostname: String, + resolver_addrs: Vec, + port: u16, +) -> Result { + let resolver_parsed_addrs = resolver_addrs + .iter() + .map(|resolver_address| { + let ip_addr: Result = + resolver_address.parse(); + ip_addr + }) + .collect::, AddrParseError>>()?; + + let resolver_config = ResolverConfig::from_parts( + None, + vec![], + NameServerConfigGroup::from_ips_clear( + &resolver_parsed_addrs, + port, + true, + ), + ); + let resolver = TokioResolver::builder_with_config( + resolver_config, + TokioConnectionProvider::default(), + ) + .build(); + + let response = resolver + .lookup_ip(hostname.clone()) + .await + .map_err(QosNetError::from)?; + response.iter().next().ok_or_else(|| { + QosNetError::DNSResolutionError(format!( + "Empty response when querying for host {hostname}" + )) + }) +} diff --git a/src/qos_net/src/async_proxy_stream.rs b/src/qos_net/src/async_proxy_stream.rs new file mode 100644 index 000000000..a648f1d52 --- /dev/null +++ b/src/qos_net/src/async_proxy_stream.rs @@ -0,0 +1,148 @@ +//! Contains an abstraction to implement the standard library's Read/Write +//! traits with `ProxyMsg`s. +use std::pin::Pin; + +use borsh::BorshDeserialize; +use qos_core::io::AsyncStream; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + sync::MutexGuard, +}; + +use crate::{error::QosNetError, proxy_msg::ProxyMsg}; + +/// Struct representing a remote connection +/// This is going to be used by enclaves, on the other side of a socket +/// and plugged into the tokio-rustls via the AsyncWrite and AsyncRead traits +pub struct AsyncProxyStream<'pool> { + /// AsyncStream we hold for this connection + stream: MutexGuard<'pool, AsyncStream>, + /// Once a connection is established (successful `ConnectByName` or + /// ConnectByIp request), this connection ID is set the u32 in + /// `ConnectResponse`. + pub connection_id: u128, + /// The remote host this connection points to + pub remote_hostname: Option, + /// The remote IP this connection points to + pub remote_ip: String, +} + +impl<'pool> AsyncProxyStream<'pool> { + /// Create a new AsyncProxyStream by targeting a hostname + /// + /// # Arguments + /// + /// * `stream` - the `AsyncStream` picked from a `AsyncStreamPool` behind a `MutexGuard` (e.g. from `pool.get().await`) + /// * `hostname` - the hostname to connect to (the remote qos_net proxy will + /// resolve DNS) + /// * `port` - the port the remote qos_net proxy should connect to + /// (typically: 80 or 443 for http/https) + /// * `dns_resolvers` - array of resolvers to use to resolve `hostname` + /// * `dns_port` - DNS port to use while resolving DNS (typically: 53 or + /// 853) + pub async fn connect_by_name( + mut stream: MutexGuard<'pool, AsyncStream>, + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> Result { + let req = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { + hostname: hostname.clone(), + port, + dns_resolvers, + dns_port, + }) + .expect("ProtocolMsg can always be serialized."); + let resp_bytes = stream.call(&req).await?; + + match ProxyMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProxyMsg::ConnectResponse { connection_id, remote_ip } => { + Ok(Self { + stream, + connection_id, + remote_ip, + remote_hostname: Some(hostname), + }) + } + _ => Err(QosNetError::InvalidMsg), + }, + Err(_) => Err(QosNetError::InvalidMsg), + } + } + + /// Create a new ProxyStream by targeting an IP address directly. + /// + /// # Arguments + /// * `stream` - the `AsyncStream` picked from a `AsyncStreamPool` behind a `MutexGuard` (e.g. from `pool.get().await`) + /// * `ip` - the IP the remote qos_net proxy should connect to + /// * `port` - the port the remote qos_net proxy should connect to + /// (typically: 80 or 443 for http/https) + pub async fn connect_by_ip( + mut stream: MutexGuard<'pool, AsyncStream>, + ip: String, + port: u16, + ) -> Result { + let req = borsh::to_vec(&ProxyMsg::ConnectByIpRequest { ip, port }) + .expect("ProtocolMsg can always be serialized."); + let resp_bytes = stream.call(&req).await?; + + match ProxyMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProxyMsg::ConnectResponse { connection_id, remote_ip } => { + Ok(Self { + stream, + connection_id, + remote_ip, + remote_hostname: None, + }) + } + _ => Err(QosNetError::InvalidMsg), + }, + Err(_) => Err(QosNetError::InvalidMsg), + } + } + + /// Refresh this connection after a request has been completed. This MUST be called + /// after each successful rustls session. + pub async fn refresh(&mut self) -> Result<(), QosNetError> { + self.stream.reconnect().await?; + + Ok(()) + } +} + +impl AsyncRead for AsyncProxyStream<'_> { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + Pin::<&mut AsyncStream>::new(&mut self.stream).poll_read(cx, buf) + } +} + +impl AsyncWrite for AsyncProxyStream<'_> { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + Pin::<&mut AsyncStream>::new(&mut self.stream).poll_write(cx, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::<&mut AsyncStream>::new(&mut self.stream).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::<&mut AsyncStream>::new(&mut self.stream).poll_shutdown(cx) + } +} diff --git a/src/qos_net/src/cli.rs b/src/qos_net/src/cli.rs index 14cc9f997..cb8bc6c64 100644 --- a/src/qos_net/src/cli.rs +++ b/src/qos_net/src/cli.rs @@ -1,14 +1,13 @@ //! CLI for running a host proxy to provide remote connections. -use std::env; - use qos_core::{ io::SocketAddress, parser::{GetParserForOptions, OptionsParser, Parser, Token}, - server::SocketServer, }; -use crate::proxy::Proxy; +use qos_core::io::AsyncStreamPool; + +use crate::async_proxy::AsyncProxyServer; /// "cid" pub const CID: &str = "cid"; @@ -16,28 +15,66 @@ pub const CID: &str = "cid"; pub const PORT: &str = "port"; /// "usock" pub const USOCK: &str = "usock"; +/// "pool-size" +pub const POOL_SIZE: &str = "pool-size"; /// CLI options for starting up the proxy. #[derive(Default, Clone, Debug, PartialEq)] -struct ProxyOpts { - parsed: Parser, +pub(crate) struct ProxyOpts { + pub(crate) parsed: Parser, } impl ProxyOpts { /// Create a new instance of [`Self`] with some defaults. - fn new(args: &mut Vec) -> Self { + pub(crate) fn new(args: &mut Vec) -> Self { let parsed = OptionsParser::::parse(args) .expect("Entered invalid CLI args"); Self { parsed } } + /// Create a new `AsyncPool` of `AsyncStream` using the list of `SocketAddress` for the enclave server and + /// return the new `AsyncPool`. + pub(crate) fn async_pool( + &self, + ) -> Result { + let pool_size: u32 = self + .parsed + .single(POOL_SIZE) + .expect("invalid pool options") + .parse() + .expect("invalid pool_size specified"); + match ( + self.parsed.single(CID), + self.parsed.single(PORT), + self.parsed.single(USOCK), + ) { + #[cfg(feature = "vm")] + (Some(c), Some(p), None) => { + let c = c.parse::().unwrap(); + let p = p.parse::().unwrap(); + + let address = + SocketAddress::new_vsock(c, p, crate::io::VMADDR_NO_FLAGS); + + AsyncStreamPool::new(address, pool_size) + } + (None, None, Some(u)) => { + let address = SocketAddress::new_unix(u); + + AsyncStreamPool::new(address, pool_size) + } + _ => panic!("Invalid socket opts"), + } + } + /// Get the `SocketAddress` for the proxy server. /// /// # Panics /// /// Panics if the opts are not valid for exactly one of unix or vsock. - fn addr(&self) -> SocketAddress { + #[allow(unused)] + pub(crate) fn addr(&self) -> SocketAddress { match ( self.parsed.single(CID), self.parsed.single(PORT), @@ -47,7 +84,7 @@ impl ProxyOpts { (Some(c), Some(p), None) => SocketAddress::new_vsock( c.parse::().unwrap(), p.parse::().unwrap(), - crate::io::VMADDR_NO_FLAGS, + qos_core::io::VMADDR_NO_FLAGS, ), (None, None, Some(u)) => SocketAddress::new_unix(u), _ => panic!("Invalid socket opts"), @@ -57,10 +94,13 @@ impl ProxyOpts { /// Proxy CLI. pub struct CLI; + impl CLI { - /// Execute the enclave proxy CLI with the environment args. - pub fn execute() { - let mut args: Vec = env::args().collect(); + /// Execute the enclave proxy CLI with the environment args in an async way. + pub async fn execute() { + use qos_core::async_server::AsyncSocketServer; + + let mut args: Vec = std::env::args().collect(); let opts = ProxyOpts::new(&mut args); if opts.parsed.version() { @@ -68,7 +108,19 @@ impl CLI { } else if opts.parsed.help() { println!("{}", opts.parsed.info()); } else { - SocketServer::listen(opts.addr(), Proxy::new()).unwrap(); + let server = AsyncSocketServer::listen_proxy( + opts.async_pool().expect("unable to create async socket pool"), + ) + .await + .expect("unable to get listen join handles"); + + match tokio::signal::ctrl_c().await { + Ok(_) => { + eprintln!("handling ctrl+c the tokio way"); + server.terminate(); + } + Err(err) => panic!("{err}"), + } } } } @@ -98,6 +150,15 @@ impl GetParserForOptions for ProxyParser { .takes_value(true) .forbids(vec!["port", "cid"]), ) + .token( + Token::new( + POOL_SIZE, + "the pool size to use with all socket types.", + ) + .takes_value(true) + .forbids(vec!["port", "cid"]) + .default_value("1"), + ) } } @@ -127,6 +188,19 @@ mod test { assert_eq!(opts.addr(), SocketAddress::new_unix("./test.sock")); } + #[test] + fn parse_pool_size() { + let mut args: Vec<_> = + vec!["binary", "--usock", "./test.sock", "--pool-size", "7"] + .into_iter() + .map(String::from) + .collect(); + let opts = ProxyOpts::new(&mut args); + + let pool = opts.async_pool().unwrap(); + assert_eq!(pool.len(), 7); + } + #[test] #[should_panic = "Entered invalid CLI args: MutuallyExclusiveInput(\"cid\", \"usock\")"] fn panic_on_too_many_opts() { diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs index f903c091e..ea74616b8 100644 --- a/src/qos_net/src/lib.rs +++ b/src/qos_net/src/lib.rs @@ -5,12 +5,15 @@ #![deny(clippy::all, unsafe_code)] -#[cfg(feature = "proxy")] -pub mod cli; pub mod error; +pub mod proxy_msg; + #[cfg(feature = "proxy")] -pub mod proxy; +pub mod async_proxy; #[cfg(feature = "proxy")] -pub mod proxy_connection; -pub mod proxy_msg; -pub mod proxy_stream; +pub mod async_proxy_connection; +#[cfg(feature = "proxy")] +pub mod async_proxy_stream; + +#[cfg(feature = "proxy")] +pub mod cli; diff --git a/src/qos_net/src/main.rs b/src/qos_net/src/main.rs index 932fa1988..f7d970634 100644 --- a/src/qos_net/src/main.rs +++ b/src/qos_net/src/main.rs @@ -1,12 +1,11 @@ #[cfg(feature = "proxy")] -use qos_net::cli::CLI; - -#[cfg(feature = "proxy")] -pub fn main() { - CLI::execute(); +#[tokio::main] +pub async fn main() { + use qos_net::cli::CLI; + CLI::execute().await; } -#[cfg(not(feature = "proxy"))] +#[cfg(not(any(feature = "proxy")))] pub fn main() { panic!("Cannot run qos_net CLI without proxy feature enabled") } diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs deleted file mode 100644 index 9dcf2fce8..000000000 --- a/src/qos_net/src/proxy.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! Protocol proxy for our remote QOS net proxy -use std::{ - collections::HashMap, - io::{Read, Write}, -}; - -use borsh::BorshDeserialize; -use qos_core::server; - -use crate::{ - error::QosNetError, - proxy_connection::{self, ProxyConnection}, - proxy_msg::ProxyMsg, -}; -use rand::Rng; -use tokio::runtime::Runtime; - -const MEGABYTE: usize = 1024 * 1024; -const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; - -pub const DEFAULT_MAX_CONNECTION_SIZE: usize = 512; - -/// Socket<>TCP proxy to enable remote connections -pub struct Proxy { - connections: HashMap, - max_connections: usize, - tokio_runtime_context: Runtime, -} - -impl Default for Proxy { - fn default() -> Self { - Self::new() - } -} - -impl Proxy { - /// Create a new `Self`. - /// # Panics - /// Panics if Tokio setup fails - #[must_use] - pub fn new() -> Self { - Self { - connections: HashMap::new(), - max_connections: DEFAULT_MAX_CONNECTION_SIZE, - tokio_runtime_context: Runtime::new() - .expect("Failed to create tokio runtime"), - } - } - - #[must_use] - /// # Panics - /// Panics if Tokio setup fails - pub fn new_with_max_connections(max_connections: usize) -> Self { - Self { - connections: HashMap::new(), - max_connections, - tokio_runtime_context: Runtime::new() - .expect("Failed to create tokio runtime"), - } - } - - /// Save the connection in the proxy and assigns a connection ID - fn save_connection( - &mut self, - connection: ProxyConnection, - ) -> Result { - if self.connections.len() >= self.max_connections { - return Err(QosNetError::TooManyConnections(self.max_connections)); - } - let connection_id = self.next_id(); - if self.connections.contains_key(&connection_id) { - // This should never happen because "next_id" auto-increments - // Still, out of an abundance of caution, we error out here. - return Err(QosNetError::DuplicateConnectionId(connection_id)); - } - - match self.connections.insert(connection_id, connection) { - // Should never, ever happen because we checked above that the connection id was not present before proceeding. - // We explicitly handle this case here out of paranoia. If this happens, it means saving this connection - // overrode another. This is _very_ concerning. - Some(_) => Err(QosNetError::ConnectionOverridden(connection_id)), - // Normal case: no value was present before - None => Ok(connection_id), - } - } - - // Simple convenience method to get the next connection ID - // We use a simple strategy here: pick a random u128. - fn next_id(&mut self) -> u128 { - rand::rng().random::() - } - - fn remove_connection(&mut self, id: u128) -> Result<(), QosNetError> { - match self.get_connection(id) { - Some(_) => { - self.connections.remove(&id); - Ok(()) - } - None => Err(QosNetError::ConnectionIdNotFound(id)), - } - } - - fn get_connection(&mut self, id: u128) -> Option<&mut ProxyConnection> { - self.connections.get_mut(&id) - } - - /// Close a connection by its ID - pub fn close(&mut self, connection_id: u128) -> ProxyMsg { - match self.shutdown_and_remove_connection(connection_id) { - Ok(_) => ProxyMsg::CloseResponse { connection_id }, - Err(e) => ProxyMsg::ProxyError(e), - } - } - - fn shutdown_and_remove_connection( - &mut self, - id: u128, - ) -> Result<(), QosNetError> { - let conn = self - .get_connection(id) - .ok_or(QosNetError::ConnectionIdNotFound(id))?; - conn.shutdown()?; - self.remove_connection(id) - } - - /// Return the number of open remote connections - pub fn num_connections(&self) -> usize { - self.connections.len() - } - - /// Create a new connection by resolving a name into an IP - /// address. The TCP connection is opened and saved in internal state. - pub fn connect_by_name( - &mut self, - hostname: String, - port: u16, - dns_resolvers: Vec, - dns_port: u16, - ) -> ProxyMsg { - match proxy_connection::ProxyConnection::new_from_name( - hostname.clone(), - port, - dns_resolvers.clone(), - dns_port, - &self.tokio_runtime_context, - ) { - Ok(conn) => { - let remote_ip = conn.ip.clone(); - match self.save_connection(conn) { - Ok(connection_id) => { - println!( - "Connection to {hostname} established and saved as ID {connection_id}" - ); - ProxyMsg::ConnectResponse { connection_id, remote_ip } - } - Err(e) => { - println!("error saving connection: {e:?}"); - ProxyMsg::ProxyError(e) - } - } - } - Err(e) => { - println!("error while establishing connection: {e:?}"); - ProxyMsg::ProxyError(e) - } - } - } - - /// Create a new connection, targeting an IP address directly. - /// address. The TCP connection is opened and saved in internal state. - pub fn connect_by_ip(&mut self, ip: String, port: u16) -> ProxyMsg { - match proxy_connection::ProxyConnection::new_from_ip(ip.clone(), port) { - Ok(conn) => { - let remote_ip = conn.ip.clone(); - match self.save_connection(conn) { - Ok(connection_id) => { - println!("Connection to {ip} established and saved as ID {connection_id}"); - ProxyMsg::ConnectResponse { connection_id, remote_ip } - } - Err(e) => { - println!("error saving connection: {e:?}"); - ProxyMsg::ProxyError(e) - } - } - } - Err(e) => { - println!("error while establishing connection: {e:?}"); - ProxyMsg::ProxyError(e) - } - } - } - - /// Performs a Read on a connection - pub fn read(&mut self, connection_id: u128, size: usize) -> ProxyMsg { - if let Some(conn) = self.get_connection(connection_id) { - let mut buf: Vec = vec![0; size]; - match conn.read(&mut buf) { - Ok(0) => { - // A zero-sized read indicates a successful/graceful - // connection close. Close it on our side as well. - match self.shutdown_and_remove_connection(connection_id) { - Ok(_) => ProxyMsg::ReadResponse { - connection_id, - data: buf, - size: 0, - }, - Err(e) => ProxyMsg::ProxyError(e) - } - } - Ok(size) => { - ProxyMsg::ReadResponse { connection_id, data: buf, size } - } - Err(e) => match self.shutdown_and_remove_connection(connection_id) { - Ok(_) => ProxyMsg::ProxyError(e.into()), - // If we fail to shutdown / remove the connection we have 2 errors to communicate back up: the read error - // and the close error. We combine them under a single `IOError`, in the message. - Err(close_err) => ProxyMsg::ProxyError( - QosNetError::IOError( - format!( - "unable to read from connection: {}. Warning: unable to cleanly close to underlying connection: {:?}", - e, - close_err, - ) - ) - ), - } - } - } else { - ProxyMsg::ProxyError(QosNetError::ConnectionIdNotFound( - connection_id, - )) - } - } - - /// Performs a Write on an existing connection - pub fn write(&mut self, connection_id: u128, data: Vec) -> ProxyMsg { - if let Some(conn) = self.get_connection(connection_id) { - match conn.write(&data) { - Ok(size) => ProxyMsg::WriteResponse { connection_id, size }, - Err(e) => ProxyMsg::ProxyError(e.into()), - } - } else { - ProxyMsg::ProxyError(QosNetError::ConnectionIdNotFound( - connection_id, - )) - } - } - - /// Performs a Flush on an existing TCP connection - pub fn flush(&mut self, connection_id: u128) -> ProxyMsg { - if let Some(conn) = self.get_connection(connection_id) { - match conn.flush() { - Ok(_) => ProxyMsg::FlushResponse { connection_id }, - Err(e) => ProxyMsg::ProxyError(e.into()), - } - } else { - ProxyMsg::ProxyError(QosNetError::ConnectionIdNotFound( - connection_id, - )) - } - } -} - -impl server::RequestProcessor for Proxy { - fn process(&mut self, req_bytes: Vec) -> Vec { - if req_bytes.len() > MAX_ENCODED_MSG_LEN { - return borsh::to_vec(&ProxyMsg::ProxyError( - QosNetError::OversizedPayload, - )) - .expect("ProtocolMsg can always be serialized. qed."); - } - - let resp = match ProxyMsg::try_from_slice(&req_bytes) { - Ok(req) => match req { - ProxyMsg::StatusRequest => { - ProxyMsg::StatusResponse(self.connections.len()) - } - ProxyMsg::ConnectByNameRequest { - hostname, - port, - dns_resolvers, - dns_port, - } => self.connect_by_name( - hostname.clone(), - port, - dns_resolvers, - dns_port, - ), - ProxyMsg::ConnectByIpRequest { ip, port } => { - self.connect_by_ip(ip, port) - } - ProxyMsg::CloseRequest { connection_id } => { - self.close(connection_id) - } - ProxyMsg::ReadRequest { connection_id, size } => { - self.read(connection_id, size) - } - ProxyMsg::WriteRequest { connection_id, data } => { - self.write(connection_id, data) - } - ProxyMsg::FlushRequest { connection_id } => { - self.flush(connection_id) - } - ProxyMsg::ProxyError(_) => { - ProxyMsg::ProxyError(QosNetError::InvalidMsg) - } - ProxyMsg::StatusResponse(_) => { - ProxyMsg::ProxyError(QosNetError::InvalidMsg) - } - ProxyMsg::ConnectResponse { - connection_id: _, - remote_ip: _, - } => ProxyMsg::ProxyError(QosNetError::InvalidMsg), - ProxyMsg::CloseResponse { connection_id: _ } => { - ProxyMsg::ProxyError(QosNetError::InvalidMsg) - } - ProxyMsg::WriteResponse { connection_id: _, size: _ } => { - ProxyMsg::ProxyError(QosNetError::InvalidMsg) - } - ProxyMsg::FlushResponse { connection_id: _ } => { - ProxyMsg::ProxyError(QosNetError::InvalidMsg) - } - ProxyMsg::ReadResponse { - connection_id: _, - size: _, - data: _, - } => ProxyMsg::ProxyError(QosNetError::InvalidMsg), - }, - Err(_) => ProxyMsg::ProxyError(QosNetError::InvalidMsg), - }; - - borsh::to_vec(&resp) - .expect("Protocol message can always be serialized. qed!") - } -} - -#[cfg(test)] -mod test { - use std::str::from_utf8; - - use server::RequestProcessor; - - use super::*; - - #[test] - fn simple_status_request() { - let mut proxy = Proxy::new(); - let request = borsh::to_vec(&ProxyMsg::StatusRequest).unwrap(); - let response = proxy.process(request); - let msg = ProxyMsg::try_from_slice(&response).unwrap(); - assert_eq!(msg, ProxyMsg::StatusResponse(0)); - } - - #[test] - fn fetch_plaintext_http_from_api_turnkey_com() { - let mut proxy = Proxy::new(); - assert_eq!(proxy.num_connections(), 0); - - let request = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { - hostname: "api.turnkey.com".to_string(), - port: 443, - dns_resolvers: vec!["8.8.8.8".to_string()], - dns_port: 53, - }) - .unwrap(); - let response = proxy.process(request); - let msg = ProxyMsg::try_from_slice(&response).unwrap(); - let connection_id = match msg { - ProxyMsg::ConnectResponse { connection_id, remote_ip: _ } => { - connection_id - } - _ => { - panic!("test failure: msg is not ConnectResponse") - } - }; - let http_request = - "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n".to_string(); - - let request = borsh::to_vec(&ProxyMsg::WriteRequest { - connection_id, - data: http_request.as_bytes().to_vec(), - }) - .unwrap(); - let response = proxy.process(request); - let msg: ProxyMsg = ProxyMsg::try_from_slice(&response).unwrap(); - assert!(matches!( - msg, - ProxyMsg::WriteResponse { connection_id: _, size: _ } - )); - - // Check that we now have an active connection - assert_eq!(proxy.num_connections(), 1); - - let request = - borsh::to_vec(&ProxyMsg::ReadRequest { connection_id, size: 512 }) - .unwrap(); - let response = proxy.process(request); - let msg: ProxyMsg = ProxyMsg::try_from_slice(&response).unwrap(); - let data = match msg { - ProxyMsg::ReadResponse { connection_id: _, size: _, data } => data, - _ => { - panic!("test failure: msg is not ReadResponse") - } - }; - - let response = from_utf8(&data).unwrap(); - assert!(response.contains("HTTP/1.1 400 Bad Request")); - assert!(response.contains("plain HTTP request was sent to HTTPS port")); - } - - #[test] - fn error_when_connection_limit_is_reached() { - let mut proxy = Proxy::new_with_max_connections(2); - - let connect1 = proxy.connect_by_ip("8.8.8.8".to_string(), 53); - assert!(matches!( - connect1, - ProxyMsg::ConnectResponse { connection_id: _, remote_ip: _ } - )); - assert_eq!(proxy.num_connections(), 1); - - let connect2 = proxy.connect_by_ip("8.8.8.8".to_string(), 53); - assert!(matches!( - connect2, - ProxyMsg::ConnectResponse { connection_id: _, remote_ip: _ } - )); - assert_eq!(proxy.num_connections(), 2); - - let connect3 = proxy.connect_by_ip("8.8.8.8".to_string(), 53); - assert!(matches!( - connect3, - ProxyMsg::ProxyError(QosNetError::TooManyConnections(2)) - )); - } - - #[test] - fn closes_connections() { - let mut proxy = Proxy::new_with_max_connections(2); - - let connect = proxy.connect_by_ip("1.1.1.1".to_string(), 53); - assert_eq!(proxy.num_connections(), 1); - - match connect { - ProxyMsg::ConnectResponse { connection_id, remote_ip: _ } => { - assert_eq!( - proxy.close(connection_id), - ProxyMsg::CloseResponse { connection_id } - ); - assert_eq!(proxy.num_connections(), 0) - } - _ => panic!( - "test failure: expected ConnectResponse and got: {connect:?}" - ), - } - } - - /// Check how the upstream resolver deals with a known-bad DNSSEC protected domain - /// It does NOT actively test the security behavior of our local DNSSEC verification - #[test] - fn test_lookup_domain_bad_dnssec_record() { - let mut proxy = Proxy::new(); - assert_eq!(proxy.num_connections(), 0); - - let connect = proxy.connect_by_name( - "sigfail.ippacket.stream".to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ); - - assert_eq!(proxy.num_connections(), 0); - match connect { - ProxyMsg::ProxyError(qos_error) => { - // the upstream resolver lets us know with SERVFAIL that a DNSSEC check failed - assert_eq!( - qos_error, - QosNetError::DNSResolutionError("ResolveError { kind: Proto(ProtoError { kind: Message(\"\ - could not validate negative response missing SOA\") }) }".to_string()) - ); - } - _ => { - panic!("test failure: the resolution should fail: {connect:?}") - } - } - - // test domain seen in https://bind9.readthedocs.io/en/v9.18.14/dnssec-guide.html - let connect = proxy.connect_by_name( - "www.dnssec-failed.org".to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ); - - assert_eq!(proxy.num_connections(), 0); - match connect { - ProxyMsg::ProxyError(qos_error) => { - // the upstream resolver lets us know with SERVFAIL that a DNSSEC check failed - assert_eq!( - qos_error, - QosNetError::DNSResolutionError("ResolveError { kind: Proto(ProtoError { kind: Message(\"\ - could not validate negative response missing SOA\") }) }".to_string()) - ); - } - _ => { - panic!("test failure: the resolution should fail: {connect:?}") - } - } - } - - #[test] - /// Check how the upstream resolver deals with a known-good DNSSEC protected domain - /// It does NOT actively test the security behavior of our local DNSSEC verification - fn test_lookup_domain_good_dnssec_record() { - let mut proxy = Proxy::new(); - assert_eq!(proxy.num_connections(), 0); - - let connect = proxy.connect_by_name( - "sigok.ippacket.stream".to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ); - - assert_eq!(proxy.num_connections(), 1); - match connect { - ProxyMsg::ConnectResponse { connection_id: _, remote_ip } => { - assert_eq!(remote_ip, "195.201.14.36"); - } - _ => { - panic!( - "test failure: the resolution should succeed: {connect:?}" - ) - } - } - } - - /// Test that resolving a domain without DNSSEC records still works - #[test] - fn test_lookup_domain_no_dnssec_successful() { - let mut proxy = Proxy::new(); - assert_eq!(proxy.num_connections(), 0); - - // as of 6/2025, google.com doesn't have DNSSEC records - let connect = proxy.connect_by_name( - "google.com".to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ); - - assert_eq!(proxy.num_connections(), 1); - match connect { - ProxyMsg::ConnectResponse { connection_id: _, remote_ip: _ } => { - // any normal response is OK, we don't expect a specific IP - } - _ => { - panic!( - "test failure: the resolution should succeed: {connect:?}" - ) - } - } - } -} diff --git a/src/qos_net/src/proxy_connection.rs b/src/qos_net/src/proxy_connection.rs deleted file mode 100644 index 86dc8c1db..000000000 --- a/src/qos_net/src/proxy_connection.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Contains logic for remote connection establishment: DNS resolution and TCP -//! connection. -use crate::error::QosNetError; -use hickory_resolver::name_server::TokioConnectionProvider; -use hickory_resolver::{ - config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, - Resolver, -}; -use std::{ - io::{Read, Write}, - net::{AddrParseError, IpAddr, SocketAddr, TcpStream}, -}; -use tokio::runtime::Runtime; - -/// Struct representing a TCP connection held on our proxy -pub struct ProxyConnection { - /// IP address of the remote host - pub ip: String, - /// TCP stream object - tcp_stream: TcpStream, -} - -impl ProxyConnection { - /// Create a new `ProxyConnection` from a name. This results in a DNS - /// request + TCP connection - pub fn new_from_name( - hostname: String, - port: u16, - dns_resolvers: Vec, - dns_port: u16, - tokio_runtime_context: &Runtime, - ) -> Result { - let ip = resolve_hostname( - hostname, - dns_resolvers, - dns_port, - tokio_runtime_context, - )?; - let tcp_addr = SocketAddr::new(ip, port); - let tcp_stream = TcpStream::connect(tcp_addr)?; - - Ok(ProxyConnection { ip: ip.to_string(), tcp_stream }) - } - - /// Create a new `ProxyConnection` from an IP address. This results in a - /// new TCP connection - pub fn new_from_ip( - ip: String, - port: u16, - ) -> Result { - let ip_addr = ip.parse()?; - let tcp_addr = SocketAddr::new(ip_addr, port); - let tcp_stream = TcpStream::connect(tcp_addr)?; - - Ok(ProxyConnection { ip, tcp_stream }) - } - - /// Closes the underlying TCP connection (`Shutdown::Both`) - pub fn shutdown(&mut self) -> Result<(), QosNetError> { - if let Err(e) = self.tcp_stream.shutdown(std::net::Shutdown::Both) { - if e.kind() == std::io::ErrorKind::NotConnected { - return Ok(()); - } - return Err(QosNetError::from(e)); - } - Ok(()) - } -} - -impl Read for ProxyConnection { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.tcp_stream.read(buf) - } -} - -impl Write for ProxyConnection { - fn write(&mut self, buf: &[u8]) -> Result { - self.tcp_stream.write(buf) - } - fn flush(&mut self) -> std::io::Result<()> { - self.tcp_stream.flush() - } -} - -// Resolve a name into an IP address -fn resolve_hostname( - hostname: String, - resolver_addrs: Vec, - port: u16, - tokio_runtime_context: &Runtime, -) -> Result { - let resolver_parsed_addrs = resolver_addrs - .iter() - .map(|resolver_address| { - let ip_addr: Result = - resolver_address.parse(); - ip_addr - }) - .collect::, AddrParseError>>()?; - - let resolver_config = ResolverConfig::from_parts( - None, - vec![], - NameServerConfigGroup::from_ips_clear( - &resolver_parsed_addrs, - port, - true, - ), - ); - - let mut resolver_builder = Resolver::builder_with_config( - resolver_config, - TokioConnectionProvider::default(), - ); - let mut resolver_options = ResolverOpts::default(); - // this validates DNSSEC in responses if DNSSEC is present - // it still allows responses without DNSSEC to succeed, limiting its effectiveness - // against on-path MITM attackers, but is preferrable to not checking response validity - resolver_options.validate = true; - - // enable case randomization for improved security - // see https://developers.google.com/speed/public-dns/docs/security#randomize_case - resolver_options.case_randomization = true; - - // set our improved resolver options - *resolver_builder.options_mut() = resolver_options; - - let resolver = resolver_builder.build(); - - // needed for borrowing in async block - let cloned_hostname = hostname.clone(); - let response = tokio_runtime_context - .block_on(async move { resolver.lookup_ip(cloned_hostname).await }) - .map_err(QosNetError::from)?; - - response.iter().next().ok_or_else(|| { - QosNetError::DNSResolutionError(format!( - "Empty response when querying for host {hostname}" - )) - }) -} - -#[cfg(test)] -mod test { - - use std::{ - io::{ErrorKind, Read, Write}, - sync::Arc, - }; - - use rustls::{RootCertStore, SupportedCipherSuite}; - - use super::*; - - #[test] - fn can_fetch_tls_content_with_proxy_connection() { - let host = "api.turnkey.com"; - let path = "/health"; - - // manually set up a Tokio runtime - let runtime = Runtime::new().expect("Failed to create tokio runtime"); - - let mut remote_connection = ProxyConnection::new_from_name( - host.to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - &runtime, - ) - .unwrap(); - - drop(runtime); - - let root_store = - RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into() }; - - let server_name: rustls::pki_types::ServerName<'_> = - host.try_into().unwrap(); - let config: rustls::ClientConfig = rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - let mut conn = - rustls::ClientConnection::new(Arc::new(config), server_name) - .unwrap(); - let mut tls = rustls::Stream::new(&mut conn, &mut remote_connection); - - let http_request = format!( - "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" - ); - - tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - assert!(matches!(ciphersuite, SupportedCipherSuite::Tls13(_))); - - let mut response_bytes = Vec::new(); - let read_to_end_result = tls.read_to_end(&mut response_bytes); - - // Ignore eof errors: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof - assert!( - read_to_end_result.is_ok() - || (read_to_end_result - .is_err_and(|e| e.kind() == ErrorKind::UnexpectedEof)) - ); - - let response_text = std::str::from_utf8(&response_bytes).unwrap(); - assert!(response_text.contains("HTTP/1.1 200 OK")); - assert!(response_text.contains("currentTime")); - } -} diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs deleted file mode 100644 index 490206ff1..000000000 --- a/src/qos_net/src/proxy_stream.rs +++ /dev/null @@ -1,595 +0,0 @@ -//! Contains an abstraction to implement the standard library's Read/Write -//! traits with `ProxyMsg`s. -use std::io::{ErrorKind, Read, Write}; - -use borsh::BorshDeserialize; -use qos_core::io::{SocketAddress, Stream, TimeVal}; - -use crate::{error::QosNetError, proxy_msg::ProxyMsg}; - -/// Struct representing a remote connection -/// This is going to be used by enclaves, on the other side of a socket -pub struct ProxyStream { - /// socket address to create the underlying `Stream` over which we send - /// `ProxyMsg`s - addr: SocketAddress, - /// timeout to create the underlying `Stream` - timeout: TimeVal, - /// Whether the underlying stream has been closed - is_closed: bool, - /// Once a connection is established (successful `ConnectByName` or - /// `ConnectByIp` request), this connection ID is set to the u32 in - /// `ConnectResponse`. - pub connection_id: u128, - /// The remote host this connection points to - pub remote_hostname: Option, - /// The remote IP this connection points to - pub remote_ip: String, -} - -impl ProxyStream { - /// Create a new ProxyStream by targeting a hostname - /// - /// # Arguments - /// - /// * `addr` - the USOCK or VSOCK to connect to (this socket should be bound - /// to a qos_net proxy) `timeout` is the timeout applied to the socket - /// * `timeout` - the timeout to connect with - /// * `hostname` - the hostname to connect to (the remote qos_net proxy will - /// resolve DNS) - /// * `port` - the port the remote qos_net proxy should connect to - /// (typically: 80 or 443 for http/https) - /// * `dns_resolvers` - array of resolvers to use to resolve `hostname` - /// * `dns_port` - DNS port to use while resolving DNS (typically: 53 or - /// 853) - pub fn connect_by_name( - addr: &SocketAddress, - timeout: TimeVal, - hostname: String, - port: u16, - dns_resolvers: Vec, - dns_port: u16, - ) -> Result { - let stream = Stream::connect(addr, timeout)?; - let req = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { - hostname: hostname.clone(), - port, - dns_resolvers, - dns_port, - }) - .expect("ProtocolMsg can always be serialized."); - stream.send(&req)?; - let resp_bytes = stream.recv()?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::ConnectResponse { connection_id, remote_ip } => { - Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: Some(hostname), - is_closed: false, - }) - } - _ => Err(QosNetError::InvalidMsg), - }, - Err(_) => Err(QosNetError::InvalidMsg), - } - } - - /// Create a new ProxyStream by targeting an IP address directly. - /// - /// # Arguments - /// * `addr` - the USOCK or VSOCK to connect to (this socket should be bound - /// to a qos_net proxy) `timeout` is the timeout applied to the socket - /// * `timeout` - the timeout to connect with - /// * `ip` - the IP the remote qos_net proxy should connect to - /// * `port` - the port the remote qos_net proxy should connect to - /// (typically: 80 or 443 for http/https) - pub fn connect_by_ip( - addr: &SocketAddress, - timeout: TimeVal, - ip: String, - port: u16, - ) -> Result { - let stream: Stream = Stream::connect(addr, timeout)?; - let req = borsh::to_vec(&ProxyMsg::ConnectByIpRequest { ip, port }) - .expect("ProtocolMsg can always be serialized."); - stream.send(&req)?; - let resp_bytes = stream.recv()?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::ConnectResponse { connection_id, remote_ip } => { - Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: None, - is_closed: false, - }) - } - _ => Err(QosNetError::InvalidMsg), - }, - Err(_) => Err(QosNetError::InvalidMsg), - } - } - - /// Close the remote connection - pub fn close(&mut self) -> Result<(), QosNetError> { - if self.is_closed() { - return Ok(()); - } - - let stream: Stream = Stream::connect(&self.addr, self.timeout)?; - let req = borsh::to_vec(&ProxyMsg::CloseRequest { - connection_id: self.connection_id, - }) - .expect("ProtocolMsg can always be serialized."); - stream.send(&req)?; - let resp_bytes = stream.recv()?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::CloseResponse { connection_id: _ } => { - self.is_closed = true; - Ok(()) - } - ProxyMsg::ProxyError(e) => Err(e), - _ => Err(QosNetError::InvalidMsg), - }, - Err(_) => Err(QosNetError::InvalidMsg), - } - } - - /// Getter function for the internal `is_closed` boolean. Call `.close()` to close the underlying connection. - pub fn is_closed(&self) -> bool { - self.is_closed - } -} - -impl Read for ProxyStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - let stream: Stream = Stream::connect(&self.addr, self.timeout) - .map_err(|e| { - std::io::Error::new( - ErrorKind::NotConnected, - format!("Error while connecting to socket (sending read request): {:?}", e), - ) - })?; - - let req = borsh::to_vec(&ProxyMsg::ReadRequest { - connection_id: self.connection_id, - size: buf.len(), - }) - .expect("ProtocolMsg can always be serialized."); - stream.send(&req).map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError: {:?}", e), - ) - })?; - let resp_bytes = stream.recv().map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError: {:?}", e), - ) - })?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::ReadResponse { connection_id: _, size, data } => { - if data.len() > buf.len() { - return Err(std::io::Error::new( - ErrorKind::InvalidData, - format!( - "overflow: cannot read {} bytes into a buffer of {} bytes", - data.len(), - buf.len() - ), - )); - } - - // Copy data into buffer - for (i, b) in data.iter().enumerate() { - buf[i] = *b - } - - // A 0-sized read means that the remote server has closed the connection gracefully - // If this happens we're clear to consider this stream closed. - if size == 0 { - self.is_closed = true; - } - - Ok(size) - } - ProxyMsg::ProxyError(e) => Err(std::io::Error::new( - ErrorKind::InvalidData, - format!("Proxy error: {e:?}"), - )), - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } -} - -impl Write for ProxyStream { - fn write(&mut self, buf: &[u8]) -> Result { - let stream: Stream = Stream::connect(&self.addr, self.timeout) - .map_err(|e| { - std::io::Error::new( - ErrorKind::NotConnected, - format!("Error while connecting to socket (sending read request): {:?}", e), - ) - })?; - - let req = borsh::to_vec(&ProxyMsg::WriteRequest { - connection_id: self.connection_id, - data: buf.to_vec(), - }) - .expect("ProtocolMsg can always be serialized."); - stream.send(&req).map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError sending WriteRequest: {:?}", e), - ) - })?; - - let resp_bytes = stream.recv().map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError receiving bytes from stream after WriteRequest: {:?}", e), - ) - })?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::WriteResponse { connection_id: _, size } => { - if size == 0 { - return Err(std::io::Error::new( - ErrorKind::Interrupted, - "Write failed: 0 bytes written", - )); - } - Ok(size) - } - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } - - fn flush(&mut self) -> Result<(), std::io::Error> { - let stream: Stream = Stream::connect(&self.addr, self.timeout) - .map_err(|e| { - std::io::Error::new( - ErrorKind::NotConnected, - format!("Error while connecting to socket (sending read request): {:?}", e), - ) - })?; - - let req = borsh::to_vec(&ProxyMsg::FlushRequest { - connection_id: self.connection_id, - }) - .expect("ProtocolMsg can always be serialized."); - - stream.send(&req).map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError sending FlushRequest: {:?}", e), - ) - })?; - - let resp_bytes = stream.recv().map_err(|e| { - std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError receiving bytes from stream after FlushRequest: {:?}", e), - ) - })?; - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::FlushResponse { connection_id: _ } => Ok(()), - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } -} - -/// Implements drop. Clients are expected to call `close()` manually if error handling is needed. -/// Otherwise this implementation will catch non-closed connections and forcefully close them on drop. -impl Drop for ProxyStream { - fn drop(&mut self) { - if !self.is_closed() { - self.close().expect("unable to close the connection cleanly") - } - } -} - -#[cfg(test)] -mod test { - - use std::{io::ErrorKind, sync::Arc}; - - use chunked_transfer::Decoder; - use httparse::Response; - use qos_core::server::RequestProcessor; - use rustls::{RootCertStore, SupportedCipherSuite}; - use serde_json::Value; - - use super::*; - use crate::proxy::Proxy; - - #[test] - fn can_fetch_and_parse_chunked_json_over_tls_with_local_stream() { - let host = "www.googleapis.com"; - let path = "/oauth2/v3/certs"; - - let mut stream = LocalStream::new_by_name( - host.to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ) - .unwrap(); - assert_eq!(stream.num_connections(), 1); - - assert_eq!( - stream.remote_hostname, - Some("www.googleapis.com".to_string()) - ); - - let root_store = - RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into() }; - - let server_name: rustls::pki_types::ServerName<'_> = - host.try_into().unwrap(); - let config: rustls::ClientConfig = rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - let mut conn = - rustls::ClientConnection::new(Arc::new(config), server_name) - .unwrap(); - let mut tls = rustls::Stream::new(&mut conn, &mut stream); - - let http_request = format!( - "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" - ); - - tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - assert!(matches!(ciphersuite, SupportedCipherSuite::Tls13(_))); - - let mut response_bytes = Vec::new(); - let read_to_end_result = tls.read_to_end(&mut response_bytes); - - match read_to_end_result { - Ok(read_size) => { - assert!(read_size > 0); - // Close the connection - let closed = stream.close(); - assert!(closed.is_ok()); - } - Err(e) => { - // Only EOF errors are expected. This means the connection was - // closed by the remote server https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof - assert_eq!(e.kind(), ErrorKind::UnexpectedEof) - } - } - // We should be at 0 connections in our proxy: either the remote - // auto-closed (UnexpectedEof), or we did. - assert_eq!(stream.num_connections(), 0); - - // Parse headers with httparse - let mut headers = [httparse::EMPTY_HEADER; 16]; - let mut response = Response::new(&mut headers); - let res = httparse::ParserConfig::default() - .parse_response(&mut response, &response_bytes); - assert!(matches!(res, Ok(httparse::Status::Complete(..)))); - assert_eq!(response.code, Some(200)); - let header_byte_size = res.unwrap().unwrap(); - - // Assert that the response is chunk-encoded - let transfer_encoding_header = - response.headers.iter().find(|h| h.name == "Transfer-Encoding"); - assert!(transfer_encoding_header.is_some()); - assert_eq!( - transfer_encoding_header.unwrap().value, - "chunked".as_bytes() - ); - - // Decode the chunked content - let mut decoded = String::new(); - let mut decoder = Decoder::new(&response_bytes[header_byte_size..]); - let res = decoder.read_to_string(&mut decoded); - assert!(res.is_ok()); - - // Parse the JSON response body and make sure there is a proper "keys" - // array in it - let json_content: Value = serde_json::from_str(&decoded).unwrap(); - assert!(json_content["keys"].is_array()); - } - - /// Struct representing a stream, with direct access to the proxy. - /// Useful in tests! :) - struct LocalStream { - proxy: Box, - pub connection_id: u128, - pub remote_hostname: Option, - } - - impl LocalStream { - pub fn new_by_name( - hostname: String, - port: u16, - dns_resolvers: Vec, - dns_port: u16, - ) -> Result { - let req = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { - hostname: hostname.clone(), - port, - dns_resolvers, - dns_port, - }) - .expect("ProtocolMsg can always be serialized."); - let mut proxy = Box::new(Proxy::new()); - let resp_bytes = proxy.process(req); - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::ConnectResponse { - connection_id, - remote_ip: _, - } => Ok(Self { - proxy, - connection_id, - remote_hostname: Some(hostname), - }), - _ => Err(QosNetError::InvalidMsg), - }, - Err(_) => Err(QosNetError::InvalidMsg), - } - } - - pub fn close(&mut self) -> Result<(), QosNetError> { - match self.proxy.close(self.connection_id) { - ProxyMsg::CloseResponse { connection_id: _ } => Ok(()), - _ => Err(QosNetError::InvalidMsg), - } - } - - pub fn num_connections(&self) -> usize { - self.proxy.num_connections() - } - } - - impl Read for LocalStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - let req = borsh::to_vec(&ProxyMsg::ReadRequest { - connection_id: self.connection_id, - size: buf.len(), - }) - .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.proxy.process(req); - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::ReadResponse { connection_id: _, size, data } => { - if data.is_empty() { - return Err(std::io::Error::new( - ErrorKind::Interrupted, - "empty Read", - )); - } - if data.len() > buf.len() { - return Err(std::io::Error::new( - ErrorKind::InvalidData, - format!( - "overflow: cannot read {} bytes into a buffer of {} bytes", - data.len(), - buf.len() - ), - )); - } - - // Copy data into buffer - for (i, b) in data.iter().enumerate() { - buf[i] = *b - } - Ok(size) - } - ProxyMsg::ProxyError(e) => Err(std::io::Error::new( - ErrorKind::InvalidData, - format!("Proxy error: {e:?}"), - )), - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } - } - - impl Write for LocalStream { - fn write(&mut self, buf: &[u8]) -> Result { - let req = borsh::to_vec(&ProxyMsg::WriteRequest { - connection_id: self.connection_id, - data: buf.to_vec(), - }) - .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.proxy.process(req); - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::WriteResponse { connection_id: _, size } => { - if size == 0 { - return Err(std::io::Error::new( - ErrorKind::Interrupted, - "failed Write", - )); - } - Ok(size) - } - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } - - fn flush(&mut self) -> Result<(), std::io::Error> { - let req = borsh::to_vec(&ProxyMsg::FlushRequest { - connection_id: self.connection_id, - }) - .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.proxy.process(req); - - match ProxyMsg::try_from_slice(&resp_bytes) { - Ok(resp) => match resp { - ProxyMsg::FlushResponse { connection_id: _ } => Ok(()), - _ => Err(std::io::Error::new( - ErrorKind::InvalidData, - "unexpected response", - )), - }, - Err(_) => Err(std::io::Error::new( - ErrorKind::InvalidData, - "cannot deserialize message", - )), - } - } - } -} diff --git a/src/qos_nsm/src/nitro/error.rs b/src/qos_nsm/src/nitro/error.rs index 88639782f..6ada2a552 100644 --- a/src/qos_nsm/src/nitro/error.rs +++ b/src/qos_nsm/src/nitro/error.rs @@ -55,7 +55,7 @@ pub enum AttestError { /// The attestation doc does not contain a pcr0. MissingPcr0, /// The pcr3 in the attestation doc does not match. - DifferentPcr0, + DifferentPcr0(String, String), // TODO: DEBUG: ales - remove later /// The attestation doc does not have a pcr1. MissingPcr1, /// The attestation doc has a different pcr1. diff --git a/src/qos_nsm/src/nitro/mod.rs b/src/qos_nsm/src/nitro/mod.rs index e5153f23b..d44d57d78 100644 --- a/src/qos_nsm/src/nitro/mod.rs +++ b/src/qos_nsm/src/nitro/mod.rs @@ -87,15 +87,17 @@ pub fn verify_attestation_doc_against_user_input( return Err(AttestError::UnexpectedAttestationDocNonce); } - if pcr0 - != attestation_doc - .pcrs - .get(&0) - .ok_or(AttestError::MissingPcr0)? - .clone() - .into_vec() - { - return Err(AttestError::DifferentPcr0); + let doc_pcr0 = attestation_doc + .pcrs + .get(&0) + .ok_or(AttestError::MissingPcr0)? + .clone() + .into_vec(); + if pcr0 != doc_pcr0 { + return Err(AttestError::DifferentPcr0( + qos_hex::encode(pcr0), + qos_hex::encode(&doc_pcr0), + )); } // pcr1 matches @@ -707,7 +709,7 @@ mod test { .unwrap_err(); match err { - AttestError::DifferentPcr0 => (), + AttestError::DifferentPcr0(_, _) => (), _ => panic!(), } } diff --git a/src/qos_nsm/src/nsm.rs b/src/qos_nsm/src/nsm.rs index 360e6e948..7fe637a2f 100644 --- a/src/qos_nsm/src/nsm.rs +++ b/src/qos_nsm/src/nsm.rs @@ -8,7 +8,7 @@ use crate::{nitro, types}; /// generic so mock providers can be subbed in for testing. In production use /// [`Nsm`]. // https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md -pub trait NsmProvider { +pub trait NsmProvider: Send { /// Create a message with input data and output capacity from a given /// request, then send it to the NSM driver via `ioctl()` and wait /// for the driver's response. diff --git a/src/qos_system/src/lib.rs b/src/qos_system/src/lib.rs index 91d9f7f47..2279ef61e 100644 --- a/src/qos_system/src/lib.rs +++ b/src/qos_system/src/lib.rs @@ -148,10 +148,9 @@ pub fn socket_connect( pub fn check_hwrng(rng_expected: &str) -> Result<(), SystemError> { use std::fs::read_to_string; let filename: &str = "/sys/class/misc/hw_random/rng_current"; - let rng_current_raw = read_to_string(filename) - .map_err(|_| SystemError { - message: format!("Failed to read {}", &filename), - })?; + let rng_current_raw = read_to_string(filename).map_err(|_| { + SystemError { message: format!("Failed to read {}", &filename) } + })?; let rng_current = rng_current_raw.trim(); if rng_expected != rng_current { return Err(SystemError { @@ -159,27 +158,35 @@ pub fn check_hwrng(rng_expected: &str) -> Result<(), SystemError> { "Entropy source was {} instead of {}", rng_current, rng_expected ), - }) + }); }; Ok(()) } #[cfg(any(target_env = "musl"))] -type ioctl_num_type = ::libc::c_int; +type IoctlNumType = ::libc::c_int; #[cfg(not(any(target_env = "musl")))] -type ioctl_num_type = ::libc::c_ulong; +type IoctlNumType = ::libc::c_ulong; -const IOCTL_VM_SOCKETS_GET_LOCAL_CID: ioctl_num_type = 0x7b9; +const IOCTL_VM_SOCKETS_GET_LOCAL_CID: IoctlNumType = 0x7b9; pub fn get_local_cid() -> Result { use libc::ioctl; let f = match File::open("/dev/vsock") { Ok(f) => f, - Err(e) => return Err(SystemError{ message: format!("Failed to open /dev/vsock") }), + Err(e) => { + return Err(SystemError { + message: format!("Failed to open /dev/vsock: {}", e), + }) + } }; let mut cid = 0; - if unsafe { ioctl(f.as_raw_fd(), IOCTL_VM_SOCKETS_GET_LOCAL_CID, &mut cid) } == -1 { - return Err(SystemError{ message: "Failed to fetch local CID".to_string() }); + if unsafe { ioctl(f.as_raw_fd(), IOCTL_VM_SOCKETS_GET_LOCAL_CID, &mut cid) } + == -1 + { + return Err(SystemError { + message: "Failed to fetch local CID".to_string(), + }); } return Ok(cid); } diff --git a/src/qos_test_primitives/Cargo.toml b/src/qos_test_primitives/Cargo.toml index 754674de6..df77647c5 100644 --- a/src/qos_test_primitives/Cargo.toml +++ b/src/qos_test_primitives/Cargo.toml @@ -6,3 +6,4 @@ publish = false [dependencies] rand = "0.8" +nix = { version = "0.29", default-features = false, features = ["signal"] } diff --git a/src/qos_test_primitives/src/lib.rs b/src/qos_test_primitives/src/lib.rs index 08f5322f9..693d19231 100644 --- a/src/qos_test_primitives/src/lib.rs +++ b/src/qos_test_primitives/src/lib.rs @@ -27,6 +27,19 @@ impl From for ChildWrapper { impl Drop for ChildWrapper { fn drop(&mut self) { + #[cfg(unix)] + { + use nix::{sys::signal::Signal::SIGINT, unistd::Pid}; + let pid = Pid::from_raw(self.0.id() as i32); + match nix::sys::signal::kill(pid, SIGINT) { + Ok(_) => {} + Err(err) => eprintln!("error sending signal to child: {}", err), + } + + // allow clean exit + std::thread::sleep(Duration::from_millis(50)); + } + // Kill the process and explicitly ignore the result drop(self.0.kill()); }