diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8c33f23d..e3da98f8 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -2,8 +2,11 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +This project used to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) but we've since +moved out of versioning: we use submodule references to pull `qos` into repositories which depend on it (git SHA pointer). + +If you're changing QOS in a meaningful way, please add a changelog entry, but no need to group these in version sections. Types of changes: @@ -14,6 +17,15 @@ Removed: for now removed features. Fixed: for any bug fixes. Security: in case of vulnerabilities. +## Added: `qos_net` crate + +In PR #449 we introduce `qos_net`, a crate which contains a socket<>TCP proxy to let enclave application communicate with the outside world. + +This new crate contains: + +- a new CLI and associated binary containing the proxy logic, running outside enclaves (on the host side) +- a `ProxyStream` abstraction to let enclaves send `ProxyMsg` messages to `Open`, `Read`, `Write` or `Flush` proxy-held connections. + ## [0.4.0] 2024.4.9 ### Added diff --git a/src/Cargo.lock b/src/Cargo.lock index 0ed5424c..9e5d548b 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -52,6 +52,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -114,7 +123,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -123,6 +132,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-lc-rs" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "aws-nitro-enclaves-cose" version = "0.5.2" @@ -158,7 +194,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -237,12 +273,41 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.68", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "block-buffer" version = "0.10.4" @@ -272,7 +337,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "syn_derive", ] @@ -296,9 +361,23 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -337,6 +416,26 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -419,7 +518,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -430,7 +529,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -517,9 +616,15 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "ecdsa" version = "0.14.8" @@ -544,6 +649,12 @@ dependencies = [ "signature 2.0.0", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "elliptic-curve" version = "0.12.3" @@ -566,12 +677,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "ff" version = "0.12.1" @@ -603,6 +736,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.30" @@ -659,7 +798,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -729,6 +868,12 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.12.1" @@ -758,6 +903,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -776,6 +927,49 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -794,6 +988,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -880,6 +1083,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -933,13 +1146,31 @@ dependencies = [ "qos_crypto", "qos_hex", "qos_host", + "qos_net", "qos_nsm", "qos_p256", "qos_test_primitives", "rand", + "rustls", "serde", "tokio", "ureq", + "webpki-roots", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", ] [[package]] @@ -948,6 +1179,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -966,24 +1206,71 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1037,13 +1324,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "nix" 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", @@ -1210,6 +1503,35 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -1225,7 +1547,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45ed9d7f816b7d9ce9ddb0062dd2f393b3af31411a95a35411809b4b9116ea08" dependencies = [ - "bitflags", + "bitflags 1.3.2", "pcsc-sys", ] @@ -1270,7 +1592,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1337,6 +1659,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.68", +] + [[package]] name = "primeorder" version = "0.12.1" @@ -1424,8 +1756,10 @@ dependencies = [ "qos_nsm", "qos_p256", "qos_test_primitives", + "rustls", "serde", "serde_bytes", + "webpki-roots", ] [[package]] @@ -1457,6 +1791,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "qos_net" +version = "0.1.0" +dependencies = [ + "borsh", + "hickory-resolver", + "qos_core", + "qos_test_primitives", + "rand", + "rustls", + "serde", + "webpki-roots", +] + [[package]] name = "qos_nsm" version = "0.1.0" @@ -1466,7 +1814,6 @@ dependencies = [ "borsh", "hex-literal", "p384 0.12.0", - "qos_core", "qos_hex", "rand", "serde_bytes", @@ -1537,6 +1884,44 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rfc6979" version = "0.3.1" @@ -1611,6 +1996,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -1620,6 +2011,52 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1632,6 +2069,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sec1" version = "0.3.0" @@ -1666,9 +2109,9 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] @@ -1691,14 +2134,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "itoa", "ryu", @@ -1723,7 +2166,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1753,7 +2196,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1778,6 +2221,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "1.6.4" @@ -1847,9 +2296,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -1864,9 +2313,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.67" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -1882,7 +2331,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1920,7 +2369,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1956,9 +2405,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -1976,6 +2425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", + "bytes", "libc", "mio", "num_cpus", @@ -1993,7 +2443,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -2047,9 +2497,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2135,15 +2597,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "getrandom", ] @@ -2190,7 +2652,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -2212,7 +2674,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2233,6 +2695,27 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -2481,5 +2964,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] diff --git a/src/Cargo.toml b/src/Cargo.toml index deeea869..2ae5342c 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -6,12 +6,13 @@ members = [ "qos_crypto", "qos_host", "qos_hex", + "qos_net", "qos_test_primitives", "qos_p256", "qos_nsm", ] exclude = ["init", "qos_aws", "qos_system", "toolchain","qos_enclave", "eif_build"] -# We need this to avoid issues with the mock feature uinintentionally being +# We need this to avoid issues with the mock feature unintentionally being # enabled just because some tests need it. # https://nickb.dev/blog/cargo-workspace-and-the-feature-unification-pitfall/ resolver = "2" diff --git a/src/integration/Cargo.toml b/src/integration/Cargo.toml index a2c74209..3fa18f89 100644 --- a/src/integration/Cargo.toml +++ b/src/integration/Cargo.toml @@ -9,6 +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_crypto = { path = "../qos_crypto" } qos_hex = { path = "../qos_hex" } qos_p256 = { path = "../qos_p256", features = ["mock"] } @@ -17,6 +18,8 @@ qos_test_primitives = { path = "../qos_test_primitives" } tokio = { version = "1.33", 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 } +rustls = { version = "0.23.5" } +webpki-roots = { version = "0.26.1" } [dev-dependencies] qos_core = { path = "../qos_core", features = ["mock"], default-features = false } diff --git a/src/integration/src/bin/pivot_remote_tls.rs b/src/integration/src/bin/pivot_remote_tls.rs new file mode 100644 index 00000000..3d041bfd --- /dev/null +++ b/src/integration/src/bin/pivot_remote_tls.rs @@ -0,0 +1,115 @@ +use core::panic; +use std::{ + io::{ErrorKind, Read, Write}, + sync::Arc, +}; + +use borsh::BorshDeserialize; +use integration::PivotRemoteTlsMsg; +use qos_core::{ + io::{SocketAddress, TimeVal}, + server::{RequestProcessor, SocketServer}, +}; +use qos_net::proxy_stream::ProxyStream; +use rustls::RootCertStore; + +struct Processor { + net_proxy: SocketAddress, +} + +impl Processor { + fn new(proxy_address: String) -> Self { + Processor { net_proxy: SocketAddress::new_unix(&proxy_address) } + } +} + +impl RequestProcessor for Processor { + fn process(&mut 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, + host.clone(), + 443, + vec!["8.8.8.8".to_string()], + 53, + ) + .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 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 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(); + closed.unwrap(); + } + 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 + if e.kind() != ErrorKind::UnexpectedEof { + panic!( + "unexpected error trying to read_to_end: {e:?}" + ); + } + } + } + + let fetched_content = + std::str::from_utf8(&response_bytes).unwrap(); + borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsResponse(format!( + "Content fetched successfully: {fetched_content}" + ))) + .expect("RemoteTlsResponse is valid borsh") + } + PivotRemoteTlsMsg::RemoteTlsResponse(_) => { + panic!("Unexpected RemoteTlsResponse - test is broken") + } + } + } +} + +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 + let args: Vec = std::env::args().collect(); + + 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()), + ) + .unwrap(); +} diff --git a/src/integration/src/lib.rs b/src/integration/src/lib.rs index 4250b56f..3d074171 100644 --- a/src/integration/src/lib.rs +++ b/src/integration/src/lib.rs @@ -25,6 +25,10 @@ pub const PIVOT_LOOP_PATH: &str = "../target/debug/pivot_loop"; pub const PIVOT_ABORT_PATH: &str = "../target/debug/pivot_abort"; /// Path to pivot panic for tests. 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 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 = "../target/debug/pivot_socket_stress"; @@ -53,6 +57,23 @@ pub enum PivotSocketStressMsg { SlowResponse, } +/// Request/Response messages for "socket stress" pivot app. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq)] +pub enum PivotRemoteTlsMsg { + /// Request a remote host / port to be fetched over the socket. + /// We assume the port to be 443, and we use Google's servers to perform + /// DNS resolution (8.8.8.8) + RemoteTlsRequest { + /// Hostname (e.g. "api.turnkey.com") + host: String, + /// Path to fetch (e.g. "/health") + path: String, + }, + /// A successful response to [`Self::RemoteTlsRequest`] with the contents + /// of the response. + RemoteTlsResponse(String), +} + struct PivotParser; impl GetParserForOptions for PivotParser { fn parser() -> Parser { diff --git a/src/integration/tests/borsh_serialize.rs b/src/integration/tests/borsh_serialize.rs index 3f7f44f7..b4452f25 100644 --- a/src/integration/tests/borsh_serialize.rs +++ b/src/integration/tests/borsh_serialize.rs @@ -1,42 +1,50 @@ #[cfg(test)] mod tests { - use borsh::{BorshSerialize, BorshDeserialize}; + use borsh::{BorshDeserialize, BorshSerialize}; - #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] - struct TestSerializable { - a: u32, - b: String, - c: Vec, - } + #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] + struct TestSerializable { + a: u32, + b: String, + c: Vec, + } - #[test] - fn test_serializable_to_vec() { - let inst = TestSerializable { - a: 42, - b: "Hello, world!".to_string(), - c: vec![1, 2, 3, 4, 5], - }; + #[test] + fn test_serializable_to_vec() { + let inst = TestSerializable { + a: 42, + b: "Hello, world!".to_string(), + c: vec![1, 2, 3, 4, 5], + }; - // Expected serialized output - let expected_serialized: Vec = vec![ - 42, 0, 0, 0, // a: u32 (little-endian) - 13, 0, 0, 0, // Length of the string b (13) - 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, // "Hello, world!" as bytes - 5, 0, 0, 0, // Length of the vector c (5) - 1, 2, 3, 4, 5 // c: Vec - ]; + // Expected serialized output + let expected_serialized: Vec = vec![ + 42, 0, 0, 0, // a: u32 (little-endian) + 13, 0, 0, 0, // Length of the string b (13) + 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, + 33, // "Hello, world!" as bytes + 5, 0, 0, 0, // Length of the vector c (5) + 1, 2, 3, 4, 5, // c: Vec + ]; - // Serialize the instance - let serialized = borsh::to_vec(&inst).expect("Serialization failed"); + // Serialize the instance + let serialized = borsh::to_vec(&inst).expect("Serialization failed"); - // Assert that the serialized output matches the expected value - assert_eq!(serialized, expected_serialized, "Serialized bytes differ from the expected value"); + // Assert that the serialized output matches the expected value + assert_eq!( + serialized, expected_serialized, + "Serialized bytes differ from the expected value" + ); - // Deserialize the serialized data back to a new instance - let deserialized_inst: TestSerializable = borsh::BorshDeserialize::try_from_slice(&serialized) - .expect("Deserialization failed"); + // Deserialize the serialized data back to a new instance + let deserialized_inst: TestSerializable = + borsh::BorshDeserialize::try_from_slice(&serialized) + .expect("Deserialization failed"); - // Assert that the deserialized instance matches the original instance - assert_eq!(deserialized_inst, inst, "Deserialized instance differs from the original"); - } + // Assert that the deserialized instance matches the original instance + assert_eq!( + deserialized_inst, inst, + "Deserialized instance differs from the original" + ); + } } diff --git a/src/integration/tests/remote_tls.rs b/src/integration/tests/remote_tls.rs new file mode 100644 index 00000000..b468d989 --- /dev/null +++ b/src/integration/tests/remote_tls.rs @@ -0,0 +1,61 @@ +use std::{process::Command, str}; + +use integration::{PivotRemoteTlsMsg, PIVOT_REMOTE_TLS_PATH, QOS_NET_PATH}; +use qos_core::{ + client::Client, + io::{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_ENCLAVE_SOCKET: &str = + "/tmp/remote_tls_test.enclave.sock"; + +#[test] +fn fetch_remote_tls_content() { + let _net_proxy: ChildWrapper = Command::new(QOS_NET_PATH) + .arg("--usock") + .arg(REMOTE_TLS_TEST_NET_PROXY_SOCKET) + .spawn() + .unwrap() + .into(); + + let _enclave_app: ChildWrapper = Command::new(PIVOT_REMOTE_TLS_PATH) + .arg(REMOTE_TLS_TEST_ENCLAVE_SOCKET) + .arg(REMOTE_TLS_TEST_NET_PROXY_SOCKET) + .spawn() + .unwrap() + .into(); + + let enclave_client = Client::new( + SocketAddress::new_unix(REMOTE_TLS_TEST_ENCLAVE_SOCKET), + TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), + ); + + let app_request = borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsRequest { + host: "api.turnkey.com".to_string(), + path: "/health".to_string(), + }) + .unwrap(); + + let response = enclave_client.send(&app_request).unwrap(); + let response_text = str::from_utf8(&response).unwrap(); + + assert!(response_text.contains("Content fetched successfully")); + assert!(response_text.contains("HTTP/1.1 200 OK")); + assert!(response_text.contains("currentTime")); + + let app_request = borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsRequest { + host: "www.googleapis.com".to_string(), + path: "/oauth2/v3/certs".to_string(), + }) + .unwrap(); + + let response = enclave_client.send(&app_request).unwrap(); + let response_text = str::from_utf8(&response).unwrap(); + + assert!(response_text.contains("Content fetched successfully")); + assert!(response_text.contains("HTTP/1.1 200 OK")); + assert!(response_text.contains("keys")); +} diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index dc2984fc..5ecb72ef 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -16,14 +16,16 @@ borsh = { version = "1.0", features = ["std", "derive"] , default-features = fal # For AWS Nitro aws-nitro-enclaves-nsm-api = { version = "0.3", default-features = false } -serde_bytes = { version = "0.11", default-features = false } +serde_bytes = { version = "0.11", default-features = false } serde = { version = "1", features = ["derive"], default-features = false } [dev-dependencies] qos_test_primitives = { path = "../qos_test_primitives" } qos_p256 = { path = "../qos_p256", features = ["mock"] } qos_nsm = { path = "../qos_nsm", features = ["mock"], default-features = false } +rustls = { version = "0.23.5" } +webpki-roots = { version = "0.26.1" } [features] # Support for VSOCK diff --git a/src/qos_core/src/io/mod.rs b/src/qos_core/src/io/mod.rs index 61e03d85..323a75be 100644 --- a/src/qos_core/src/io/mod.rs +++ b/src/qos_core/src/io/mod.rs @@ -5,9 +5,9 @@ mod stream; -pub(crate) use stream::{Listener, Stream}; pub use stream::{ - SocketAddress, TimeVal, TimeValLike, VMADDR_FLAG_TO_HOST, VMADDR_NO_FLAGS, + Listener, SocketAddress, Stream, TimeVal, TimeValLike, VMADDR_FLAG_TO_HOST, + VMADDR_NO_FLAGS, }; /// QOS I/O error diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index f4e85cfe..a695694a 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -1,6 +1,10 @@ //! Abstractions to handle connection based socket streams. -use std::{mem::size_of, os::unix::io::RawFd}; +use std::{ + io::{ErrorKind, Read, Write}, + mem::size_of, + os::unix::io::RawFd, +}; #[cfg(feature = "vm")] use nix::sys::socket::VsockAddr; @@ -86,7 +90,8 @@ impl SocketAddress { } /// Get the `AddressFamily` of the socket. - fn family(&self) -> AddressFamily { + #[must_use] + pub fn family(&self) -> AddressFamily { match *self { #[cfg(feature = "vm")] Self::Vsock(_) => AddressFamily::Vsock, @@ -94,8 +99,9 @@ impl SocketAddress { } } - // Convenience method for accessing the wrapped address - fn addr(&self) -> Box { + /// Convenience method for accessing the wrapped address + #[must_use] + pub fn addr(&self) -> Box { match *self { #[cfg(feature = "vm")] Self::Vsock(vsa) => Box::new(vsa), @@ -105,12 +111,13 @@ impl SocketAddress { } /// Handle on a stream -pub(crate) struct Stream { +pub struct Stream { fd: RawFd, } impl Stream { - pub(crate) fn connect( + /// Create a new `Stream` from a `SocketAddress` and a timeout + pub fn connect( addr: &SocketAddress, timeout: TimeVal, ) -> Result { @@ -140,13 +147,14 @@ impl Stream { Err(err) } - pub(crate) fn send(&self, buf: &[u8]) -> Result<(), IOError> { + /// 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, sent the length of the buffer + // First, send the length of the buffer let mut sent_bytes = 0; while sent_bytes < len_buf.len() { sent_bytes += match send( @@ -178,7 +186,8 @@ impl Stream { Ok(()) } - pub(crate) fn recv(&self) -> Result, IOError> { + /// Receive from the underlying socket + pub fn recv(&self) -> Result, IOError> { let length: usize = { { let mut buf = [0u8; size_of::()]; @@ -192,7 +201,7 @@ impl Stream { &mut buf[received_bytes..len], MsgFlags::empty(), ) { - Ok(size) if size == 0 => { + Ok(0) => { return Err(IOError::RecvConnectionClosed); } Ok(size) => size, @@ -225,7 +234,7 @@ impl Stream { &mut buf[received_bytes..length], MsgFlags::empty(), ) { - Ok(size) if size == 0 => { + Ok(0) => { return Err(IOError::RecvConnectionClosed); } Ok(size) => size, @@ -245,6 +254,37 @@ impl Stream { } } +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)), + } + } + + // 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 @@ -255,7 +295,7 @@ impl Drop for Stream { } /// Abstraction to listen for incoming stream connections. -pub(crate) struct Listener { +pub struct Listener { fd: RawFd, addr: SocketAddress, } @@ -328,12 +368,56 @@ fn socket_fd(addr: &SocketAddress) -> Result { #[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, + } + + impl HarakiriPongServer { + pub fn new(path: String) -> Self { + Self { path } + } + pub fn start(&mut self) { + let listener = UnixListener::bind(&self.path).unwrap(); + let path = self.path.clone(); + thread::spawn(move || { + 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(); + } + + // Then shutdown the server + let _ = shutdown(listener.as_raw_fd(), Shutdown::Both); + let _ = close(listener.as_raw_fd()); + + let server_socket = Path::new(&path); + if server_socket.exists() { + drop(std::fs::remove_file(server_socket)); + } + }); + } + } + #[test] fn stream_integration_test() { // Ensure concurrent tests are not attempting to listen at the same @@ -341,8 +425,8 @@ mod test { let unix_addr = nix::sys::socket::UnixAddr::new("./stream_integration_test.sock") .unwrap(); - let addr = SocketAddress::Unix(unix_addr); - let listener = Listener::listen(addr.clone()).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(); @@ -354,6 +438,35 @@ mod test { 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()); + 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 diff --git a/src/qos_core/src/protocol/msg.rs b/src/qos_core/src/protocol/msg.rs index 855bb66e..9b1a3e98 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -66,7 +66,7 @@ pub enum ProtocolMsg { /// Proxy the encoded `data` to the secure app. ProxyRequest { - /// Encoded data that will be sent from the nitro enclave serverga to + /// Encoded data that will be sent from the nitro enclave server to /// the secure app. data: Vec, }, diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml new file mode 100644 index 00000000..ec09b49d --- /dev/null +++ b/src/qos_net/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "qos_net" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +qos_core = { path = "../qos_core", default-features = false } + +borsh = { version = "1.0", features = ["std", "derive"] , default-features = false} +serde = { version = "1", features = ["derive"], default-features = false } +hickory-resolver = { version = "0.24.1", features = ["tokio-runtime"], default-features = false, optional = true} +rand = { version = "0.8.5", default-features = false, optional = true } + +[dev-dependencies] +qos_test_primitives = { path = "../qos_test_primitives" } +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"] diff --git a/src/qos_net/README.MD b/src/qos_net/README.MD new file mode 100644 index 00000000..229e7554 --- /dev/null +++ b/src/qos_net/README.MD @@ -0,0 +1,13 @@ +# QOS Net + +This crate contains a proxy server and utilities to work with it. This server is a socket proxy: it listens on a socket (USOCK or VSOCK) and opens TCP connections to the outside. By sending `Proxy::*` messages over the socket, clients of the proxy can read/write/flush the TCP connections. + +When the proxy is run outside of an enclave and listening on a VSOCK port, the enclave process running on the inside can thus communicate with the outside and execute any protocol on top of a TCP connection by: +* Opening a connection to a target hostname (`Proxy::ConnectByName`) or IP (`ProxyMsg::ConnectByIp`): this returns a connection ID for subsequent messages. +* Sending `ProxyMsg::Read`, `ProxyMsg::Write` or `ProxyMsg::Flush` using the connection ID + +Libraries like [`rustls`](https://github.com/rustls/rustls) are built generically to let users run the TLS protocol over any struct which implements [`Read`](https://doc.rust-lang.org/std/io/trait.Read.html) and [`Write`](https://doc.rust-lang.org/std/io/trait.Write.html) traits. + +These traits are implemented in the `ProxyStream` struct: its `read`, `write`, and `flush` methods send `ProxyMsg` to a socket instead of manipulating a local socket or file descriptor. + +Binaries running in enclaves can thus open connections to the outside world by importing and using `ProxyStream`. See the following integration test: [src/integration/tests/remote_tls.rs](../integration/tests/remote_tls.rs). diff --git a/src/qos_net/src/cli.rs b/src/qos_net/src/cli.rs new file mode 100644 index 00000000..14cc9f99 --- /dev/null +++ b/src/qos_net/src/cli.rs @@ -0,0 +1,174 @@ +//! 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; + +/// "cid" +pub const CID: &str = "cid"; +/// "port" +pub const PORT: &str = "port"; +/// "usock" +pub const USOCK: &str = "usock"; + +/// CLI options for starting up the proxy. +#[derive(Default, Clone, Debug, PartialEq)] +struct ProxyOpts { + parsed: Parser, +} + +impl ProxyOpts { + /// Create a new instance of [`Self`] with some defaults. + fn new(args: &mut Vec) -> Self { + let parsed = OptionsParser::::parse(args) + .expect("Entered invalid CLI args"); + + Self { parsed } + } + + /// 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 { + match ( + self.parsed.single(CID), + self.parsed.single(PORT), + self.parsed.single(USOCK), + ) { + #[cfg(feature = "vm")] + (Some(c), Some(p), None) => SocketAddress::new_vsock( + c.parse::().unwrap(), + p.parse::().unwrap(), + crate::io::VMADDR_NO_FLAGS, + ), + (None, None, Some(u)) => SocketAddress::new_unix(u), + _ => panic!("Invalid socket opts"), + } + } +} + +/// 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(); + let opts = ProxyOpts::new(&mut args); + + if opts.parsed.version() { + println!("version: {}", env!("CARGO_PKG_VERSION")); + } else if opts.parsed.help() { + println!("{}", opts.parsed.info()); + } else { + SocketServer::listen(opts.addr(), Proxy::new()).unwrap(); + } + } +} + +/// Parser for proxy CLI +struct ProxyParser; +impl GetParserForOptions for ProxyParser { + fn parser() -> Parser { + Parser::new() + .token( + Token::new(CID, "cid of the VSOCK the proxy should listen on.") + .takes_value(true) + .forbids(vec![USOCK]) + .requires(PORT), + ) + .token( + Token::new( + PORT, + "port of the VSOCK the proxy should listen on.", + ) + .takes_value(true) + .forbids(vec![USOCK]) + .requires(CID), + ) + .token( + Token::new(USOCK, "unix socket (`.sock`) to listen on.") + .takes_value(true) + .forbids(vec!["port", "cid"]), + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_cid_and_port() { + let mut args: Vec<_> = vec!["binary", "--cid", "6", "--port", "3999"] + .into_iter() + .map(String::from) + .collect(); + let opts: ProxyOpts = ProxyOpts::new(&mut args); + + assert_eq!(*opts.parsed.single(CID).unwrap(), "6".to_string()); + assert_eq!(*opts.parsed.single(PORT).unwrap(), "3999".to_string()); + } + #[test] + fn parse_usock() { + let mut args: Vec<_> = vec!["binary", "--usock", "./test.sock"] + .into_iter() + .map(String::from) + .collect(); + let opts = ProxyOpts::new(&mut args); + + assert_eq!(opts.addr(), SocketAddress::new_unix("./test.sock")); + } + + #[test] + #[should_panic = "Entered invalid CLI args: MutuallyExclusiveInput(\"cid\", \"usock\")"] + fn panic_on_too_many_opts() { + let mut args: Vec<_> = vec![ + "binary", "--cid", "6", "--port", "3999", "--usock", "my.sock", + ] + .into_iter() + .map(String::from) + .collect(); + let _opts = ProxyOpts::new(&mut args); + } + + #[test] + #[should_panic = "Entered invalid CLI args: MissingInput(\"port\")"] + fn panic_on_not_enough_opts() { + let mut args: Vec<_> = vec!["binary", "--cid", "6"] + .into_iter() + .map(String::from) + .collect(); + let _opts = ProxyOpts::new(&mut args); + } + + #[test] + #[cfg(feature = "vm")] + fn build_vsock() { + let mut args: Vec<_> = vec!["binary", "--cid", "6", "--port", "3999"] + .into_iter() + .map(String::from) + .collect(); + let opts = EnclaveOpts::new(&mut args); + + assert_eq!( + opts.addr(), + SocketAddress::new_vsock(6, 3999, crate::io::VMADDR_NO_FLAGS) + ); + } + + #[test] + #[should_panic = "Entered invalid CLI args: UnexpectedInput(\"--derp\")"] + fn panic_when_mistyped_cid() { + let mut args: Vec<_> = + vec!["--derp"].into_iter().map(String::from).collect(); + let _opts = ProxyOpts::new(&mut args); + } +} diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs new file mode 100644 index 00000000..326437cc --- /dev/null +++ b/src/qos_net/src/error.rs @@ -0,0 +1,65 @@ +//! qos_net errors related to creating and using proxy connections. +use std::net::AddrParseError; + +use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "proxy")] +use hickory_resolver::error::ResolveError; + +/// Errors related to creating and using proxy connections +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub enum QosNetError { + /// Error variant encapsulating OS IO errors + IOError(String), + /// Error variant encapsulating OS IO errors + QOSIOError(String), + /// The message is too large. + OversizeMsg, + /// Payload is too big. See `MAX_ENCODED_MSG_LEN` for the upper bound on + /// message size. + OversizedPayload, + /// Message could not be deserialized + InvalidMsg, + /// Parsing error with a protocol message component + ParseError(String), + /// DNS Resolution error + DNSResolutionError(String), + /// Attempt to save a connection with a duplicate ID + DuplicateConnectionId(u32), + /// Attempt to send a message to a remote connection, but ID isn't found + ConnectionIdNotFound(u32), + /// Happens when a socket `read` returns too much data for the provided + /// buffer and the data doesn't fit. The first `usize` is the size of the + /// received data, the second `usize` is the size of the buffer. + ReadOverflow(usize, usize), + /// Happens when too many connections are opened in the proxy + TooManyConnections(usize), +} + +impl From for QosNetError { + fn from(err: std::io::Error) -> Self { + let msg = format!("{err:?}"); + Self::IOError(msg) + } +} + +impl From for QosNetError { + fn from(err: qos_core::io::IOError) -> Self { + let msg = format!("{err:?}"); + Self::QOSIOError(msg) + } +} + +impl From for QosNetError { + fn from(err: AddrParseError) -> Self { + let msg = format!("{err:?}"); + Self::ParseError(msg) + } +} + +#[cfg(feature = "proxy")] +impl From for QosNetError { + fn from(err: ResolveError) -> Self { + let msg = format!("{err:?}"); + Self::DNSResolutionError(msg) + } +} diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs new file mode 100644 index 00000000..f903c091 --- /dev/null +++ b/src/qos_net/src/lib.rs @@ -0,0 +1,16 @@ +//! This crate contains a simple proxy server which binds to a local socket and +//! opens TCP connection. +//! It exposes a simple protocol for remote clients who +//! connect to let them manipulate these connections (read/write/flush) + +#![deny(clippy::all, unsafe_code)] + +#[cfg(feature = "proxy")] +pub mod cli; +pub mod error; +#[cfg(feature = "proxy")] +pub mod proxy; +#[cfg(feature = "proxy")] +pub mod proxy_connection; +pub mod proxy_msg; +pub mod proxy_stream; diff --git a/src/qos_net/src/main.rs b/src/qos_net/src/main.rs new file mode 100644 index 00000000..76ebae57 --- /dev/null +++ b/src/qos_net/src/main.rs @@ -0,0 +1,5 @@ +use qos_net::cli::CLI; + +pub fn main() { + CLI::execute(); +} diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs new file mode 100644 index 00000000..6018ece2 --- /dev/null +++ b/src/qos_net/src/proxy.rs @@ -0,0 +1,405 @@ +//! Protocol proxy for our remote QOS net proxy +use std::io::{Read, Write}; + +use borsh::BorshDeserialize; +use qos_core::server; + +use crate::{ + error::QosNetError, + proxy_connection::{self, ProxyConnection}, + proxy_msg::ProxyMsg, +}; + +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: Vec, + max_connections: usize, +} + +impl Default for Proxy { + fn default() -> Self { + Self::new() + } +} + +impl Proxy { + /// Create a new `Self`. + #[must_use] + pub fn new() -> Self { + Self { + connections: vec![], + max_connections: DEFAULT_MAX_CONNECTION_SIZE, + } + } + + #[must_use] + pub fn new_with_max_connections(max_connections: usize) -> Self { + Self { connections: vec![], max_connections } + } + + fn save_connection( + &mut self, + connection: ProxyConnection, + ) -> Result<(), QosNetError> { + if self.connections.iter().any(|c| c.id == connection.id) { + Err(QosNetError::DuplicateConnectionId(connection.id)) + } else { + if self.connections.len() >= self.max_connections { + return Err(QosNetError::TooManyConnections( + self.max_connections, + )); + } + self.connections.push(connection); + Ok(()) + } + } + + fn remove_connection(&mut self, id: u32) -> Result<(), QosNetError> { + match self.connections.iter().position(|c| c.id == id) { + Some(i) => { + self.connections.remove(i); + Ok(()) + } + None => Err(QosNetError::ConnectionIdNotFound(id)), + } + } + + fn get_connection(&mut self, id: u32) -> Option<&mut ProxyConnection> { + self.connections.iter_mut().find(|c| c.id == id) + } + + /// Close a connection by its ID + pub fn close(&mut self, connection_id: u32) -> ProxyMsg { + match self.remove_connection(connection_id) { + Ok(_) => ProxyMsg::CloseResponse { connection_id }, + Err(e) => ProxyMsg::ProxyError(e), + } + } + + /// 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, + ) { + Ok(conn) => { + let connection_id = conn.id; + let remote_ip = conn.ip.clone(); + match self.save_connection(conn) { + Ok(()) => { + 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 connection_id = conn.id; + let remote_ip = conn.ip.clone(); + match self.save_connection(conn) { + Ok(()) => { + 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: u32, 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. So we can safely remove it. + match self.remove_connection(connection_id) { + Ok(_) => { + // Connection was successfully removed / closed + 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.remove_connection(connection_id) { + Ok(_) => ProxyMsg::ProxyError(e.into()), + Err(e) => ProxyMsg::ProxyError(e), + }, + } + } else { + ProxyMsg::ProxyError(QosNetError::ConnectionIdNotFound( + connection_id, + )) + } + } + + /// Performs a Write on an existing connection + pub fn write(&mut self, connection_id: u32, 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: u32) -> 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:?}" + ), + } + } +} diff --git a/src/qos_net/src/proxy_connection.rs b/src/qos_net/src/proxy_connection.rs new file mode 100644 index 00000000..c44a6e63 --- /dev/null +++ b/src/qos_net/src/proxy_connection.rs @@ -0,0 +1,179 @@ +//! Contains logic for remote connection establishment: DNS resolution and TCP +//! connection. +use std::{ + io::{Read, Write}, + net::{AddrParseError, IpAddr, SocketAddr, TcpStream}, +}; + +use hickory_resolver::{ + config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, + Resolver, +}; +use rand::Rng; + +use crate::error::QosNetError; + +/// Struct representing a TCP connection held on our proxy +pub struct ProxyConnection { + /// Unsigned integer with the connection ID (random positive int) + pub id: u32, + /// 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, + ) -> Result { + let ip = resolve_hostname(hostname, dns_resolvers, dns_port)?; + + // Generate a new random u32 to get an ID. We'll use it to name our + // socket. This will be our connection ID. + let mut rng = rand::thread_rng(); + let connection_id: u32 = rng.gen::(); + + let tcp_addr = SocketAddr::new(ip, port); + let tcp_stream = TcpStream::connect(tcp_addr)?; + Ok(ProxyConnection { + 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 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 mut rng = rand::thread_rng(); + let connection_id: u32 = rng.gen::(); + + let ip_addr = ip.parse()?; + let tcp_addr = SocketAddr::new(ip_addr, port); + let tcp_stream = TcpStream::connect(tcp_addr)?; + + Ok(ProxyConnection { id: connection_id, ip, tcp_stream }) + } +} + +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, +) -> 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 = Resolver::new(resolver_config, ResolverOpts::default())?; + let response = + resolver.lookup_ip(hostname.clone()).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"; + + let mut remote_connection = ProxyConnection::new_from_name( + host.to_string(), + 443, + vec!["8.8.8.8".to_string()], + 53, + ) + .unwrap(); + + 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_msg.rs b/src/qos_net/src/proxy_msg.rs new file mode 100644 index 00000000..23090cc7 --- /dev/null +++ b/src/qos_net/src/proxy_msg.rs @@ -0,0 +1,98 @@ +use crate::error::QosNetError; + +/// Message types to use with the remote proxy. +#[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] +pub enum ProxyMsg { + /// A error from executing the protocol. + ProxyError(QosNetError), + + /// Request the status of the proxy server. + StatusRequest, + /// Response for [`Self::StatusRequest`], contains the number of opened + /// connections + StatusResponse(usize), + + /// Request from the enclave app to open a TCP connection to a remote host, + /// by name This results in DNS resolution and new remote connection saved + /// in protocol state + ConnectByNameRequest { + /// The hostname to connect to, e.g. "www.googleapis.com" + hostname: String, + /// e.g. 443 + port: u16, + /// An array of DNS resolvers e.g. ["8.8.8.8", "8.8.4.4"] + dns_resolvers: Vec, + /// Port number to perform DNS resolution, e.g. 53 + dns_port: u16, + }, + /// Request from the enclave app to open a TCP connection to a remote host, + /// by IP This results in a new remote connection saved in protocol state + ConnectByIpRequest { + /// The IP to connect to, e.g. "1.2.3.4" + ip: String, + /// e.g. 443 + port: u16, + }, + /// Response for `ConnectByNameRequest` and `ConnectByIpRequest` + ConnectResponse { + /// Connection ID to reference the opened connection in later messages + /// (`Read`, `Write`, `Flush`) + connection_id: u32, + /// The remote host IP, e.g. "1.2.3.4" + remote_ip: String, + }, + /// Request from the enclave app to close the connection + CloseRequest { + /// Connection ID + connection_id: u32, + }, + /// Response for `CloseRequest` + CloseResponse { + /// Connection ID + connection_id: u32, + }, + /// Read from a remote connection + ReadRequest { + /// A connection ID from `ConnectResponse` + connection_id: u32, + /// number of bytes to read + size: usize, + }, + /// Response to `ReadRequest` containing read data + ReadResponse { + /// A connection ID from `ConnectResponse` + connection_id: u32, + /// number of bytes read + data: Vec, + /// buffer after mutation from `read`. The first `size` bytes contain + /// the result of the `read` call + size: usize, + }, + /// Write to a remote connection + WriteRequest { + /// A connection ID from `ConnectResponse` + connection_id: u32, + /// Data to be sent + data: Vec, + }, + /// Response to `WriteRequest` containing the number of successfully + /// written bytes. + WriteResponse { + /// Connection ID from `ConnectResponse` + connection_id: u32, + /// Number of bytes written successfully + size: usize, + }, + /// Write to a remote connection + FlushRequest { + /// A connection ID from `ConnectResponse` + connection_id: u32, + }, + /// Response to `FlushRequest` + /// The response only contains the connection ID. Success is implicit: if + /// the flush response fails, a `ProxyError` will be sent instead. + FlushResponse { + /// Connection ID from `ConnectResponse` + connection_id: u32, + }, +} diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs new file mode 100644 index 00000000..4d9a158d --- /dev/null +++ b/src/qos_net/src/proxy_stream.rs @@ -0,0 +1,526 @@ +//! 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, + /// Once a connection is established (successful `ConnectByName` or + /// ConnectByIp request), this connection ID is set the u32 in + /// `ConnectResponse`. + pub connection_id: u32, + /// 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), + }) + } + _ => 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, + }) + } + _ => Err(QosNetError::InvalidMsg), + }, + Err(_) => Err(QosNetError::InvalidMsg), + } + } + + /// Close the remote connection + pub fn close(&mut self) -> Result<(), QosNetError> { + 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: _ } => Ok(()), + _ => Err(QosNetError::InvalidMsg), + }, + Err(_) => Err(QosNetError::InvalidMsg), + } + } +} + +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.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 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", + )), + } + } +} + +#[cfg(test)] +mod test { + + use std::{io::ErrorKind, sync::Arc}; + + use qos_core::server::RequestProcessor; + use rustls::{RootCertStore, SupportedCipherSuite}; + + use super::*; + use crate::proxy::Proxy; + + #[test] + fn can_fetch_tls_content_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); + + let response_text = std::str::from_utf8(&response_bytes).unwrap(); + assert!(response_text.contains("HTTP/1.1 200 OK")); + assert!(response_text.contains("keys")); + } + + /// Struct representing a stream, with direct access to the proxy. + /// Useful in tests! :) + struct LocalStream { + proxy: Box, + pub connection_id: u32, + 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/Cargo.toml b/src/qos_nsm/Cargo.toml index a35fb63a..162cf307 100644 --- a/src/qos_nsm/Cargo.toml +++ b/src/qos_nsm/Cargo.toml @@ -18,9 +18,8 @@ x509-cert = { version = "=0.1.0", features = ["pem"], default-features = false } [dev-dependencies] hex-literal = "0.4" rand = "0.8" -qos_core = { path = "../qos_core", features = ["mock"] } [features] # Never use in production - support for mock NSM mock = [] -mock_realtime = [] \ No newline at end of file +mock_realtime = [] diff --git a/src/rust-toolchain.toml b/src/rust-toolchain.toml new file mode 100644 index 00000000..8287f65c --- /dev/null +++ b/src/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.76" +components = [ "rustfmt", "cargo", "clippy", "rust-analyzer" ] +profile = "minimal"